import { Effect, Layer, ManagedRuntime, Option, type ParseResult, PubSub, Queue, } from 'effect' import { type CapturedScene, forceSync, publish, type UiContract } from '@playfast/reform' import { type AnyScene, Bus, Triggers, type TriggerRegistryApi, Wire, type WirePatch, type WireTree, } from '@playfast/reform/internal' import { capturedSlotProvider, renderCapturedComposition, type RenderSceneToWireOptions, renderSceneWith, serviceFromContext, } from './wireRender' import { handlesOf, settleDrain } from './wireNode' export type { RenderSceneToWireOptions } from './wireRender' // oxlint-disable-next-line reform-rules/prefer-effect-fn -- exported binding: Effect.fn's inferred type isn't portable under isolatedDeclarations export const renderSceneToWire = < C extends UiContract, S extends ReadonlyArray, Services, P, N extends string, Identity, >( scene: CapturedScene, options?: RenderSceneToWireOptions, ): Effect.Effect => Effect.context().pipe( Effect.flatMap((context) => renderSceneWith( scene, options, (slotDefinition) => capturedSlotProvider(context, slotDefinition), (composition, props) => renderCapturedComposition(context, composition, props), ), ), ) export interface RemoteServer { readonly render: () => Promise readonly renderDiff: () => Promise> readonly invoke: (handle: string, encodedPayload: unknown) => Promise readonly subscribe: (listener: () => void) => () => void readonly currentTree: () => WireTree readonly dispose: () => Promise } const makeCapturedRemoteServer = < C extends UiContract, S extends ReadonlyArray, Services, P, N extends string, Identity, >( scene: CapturedScene, ): RemoteServer => { const layer = scene.provide.reduce((acc, provided) => Layer.merge(acc, provided)) const runtime = ManagedRuntime.make(layer) const registry: TriggerRegistryApi = forceSync(() => runtime.runSync(Triggers.make)) const context = forceSync(() => runtime.runSync(Effect.context())) const busFromContext = serviceFromContext( context, Bus, 'reform-remote: scene does not provide Bus', ) const renderEffect = settleDrain.pipe( Effect.zipRight( renderSceneWith( scene, { register: registry.register }, (slotDefinition) => capturedSlotProvider(context, slotDefinition), (composition, props) => renderCapturedComposition(context, composition, props), ), ), ) const changeListeners = new Set<() => void>() const notifyChange = (): void => { changeListeners.forEach((listener) => listener()) } runtime.runFork( Effect.scoped( Effect.gen(function* () { const bus = yield* busFromContext const subscription = yield* PubSub.subscribe(bus) // The transport debounces; settling here would perturb the drain's scheduling. yield* Queue.take(subscription).pipe( Effect.zipRight(Effect.sync(notifyChange)), Effect.forever, ) }), ), ) runtime.runSync( Effect.forEach( Option.getOrElse(Option.fromNullable(scene.boot), () => []), (event) => busFromContext.pipe( Effect.flatMap((bus) => publish('High', event).pipe(Effect.provideService(Bus, bus))), ), ), ) const frame: { tree: WireTree } = { tree: [] } const render = (): Promise => runtime.runPromise( renderEffect.pipe( Effect.tap((tree) => Effect.sync(() => { frame.tree = tree }), ), ), ) const renderDiff = (): Promise> => runtime.runPromise( Effect.gen(function* () { const next = yield* renderEffect const previous = frame.tree const patches = Wire.diff(previous, next) frame.tree = next const nextIds = new Set(next.map((node) => node.id)) const staleHandles = previous.filter((node) => !nextIds.has(node.id)).flatMap(handlesOf) yield* Effect.forEach(staleHandles, (handle) => registry.revoke(handle)) return patches }), ) return { render, renderDiff, invoke: (handle, encodedPayload) => runtime.runPromise( Effect.gen(function* () { yield* registry.invoke(handle, encodedPayload) yield* settleDrain }), ), subscribe: (listener) => { changeListeners.add(listener) return () => void changeListeners.delete(listener) }, currentTree: () => frame.tree, dispose: () => runtime.dispose(), } } export const makeRemoteServer = (scene: AnyScene): RemoteServer => scene.captureAny( < C extends UiContract, S extends ReadonlyArray, Services, P, N extends string, Identity, >( exactScene: CapturedScene, ) => makeCapturedRemoteServer(exactScene), )