import { Context, Effect, Match, Option, type ParseResult, Record as Rec } from 'effect' import { Composition, type CompositionClass, isStructure, type SlotFill, type Structure, type UiContract, } from '@playfast/reform' import { type AnyComposition, type AnyScene, type AnySlot, type AnySlotChild, type TriggerRegistryApi, type WireNode, type WireTree, } from '@playfast/reform/internal' import { childComposition, type Mounted, noRegister, slotsOf, structureFills, toWireNodeFromStructure, } from './wireNode' export interface RenderSceneToWireOptions { readonly register: TriggerRegistryApi['register'] } export const serviceFromContext = ( context: Context.Context, tag: Context.Tag, missing: string, ): Effect.Effect => Option.match(Context.getOption(context, tag), { onNone: () => Effect.dieMessage(missing), onSome: Effect.succeed, }) export const capturedSlotProvider = ( context: Context.Context, slotDefinition: AnySlot, ): Effect.Effect => slotDefinition.capture((exact) => serviceFromContext( context, exact.provider, 'reform-remote: scene does not provide a declared slot', ), ) export const renderCapturedComposition = ( context: Context.Context, composition: AnyComposition, props: unknown, ): Effect.Effect, never, never> => composition.capture, never, never>>( , N extends string, Identity>( exact: CompositionClass, ): Effect.Effect, never, never> => Option.match(exact.parseProps(props), { onNone: () => Effect.dieMessage( `reform-remote: invalid props for composition ${composition.manifest.name}`, ), onSome: (parsed) => serviceFromContext( context, exact.tag, `reform-remote: scene does not provide composition ${composition.manifest.name}`, ).pipe( Effect.flatMap((service) => Composition.render(service, { props: parsed, tracker: { add: () => {} }, }), ), Effect.flatMap((frame) => { if (isStructure(frame)) { return Effect.succeed(frame) } return Effect.dieMessage( `reform-remote: composition ${composition.manifest.name} did not return a Structure`, ) }), ), }), ) export const enqueueStructureSlot = ( parent: Mounted, slotName: string, child: AnyComposition, fill: SlotFill, ): ReadonlyArray => Match.value(fill).pipe( Match.tag('Each', (each) => { // Keyed, not positional: a trigger handle embeds the node id, so an id built from // the index silently re-points a handle the client already holds at whatever row // slid into that slot. An accidental duplicate key falls back to the index rather // than minting two nodes with one id. const used = new Set() // The disambiguator is retried rather than assumed free: a key that literally ends // in `#` can already hold the string a later repeat would mint, and two nodes // sharing an id costs a whole row on the wire. const unusedId = (base: string): string => { const attempt = (round: number): string => { const candidate = round === 0 ? base : `${base}#${round}` return used.has(candidate) ? attempt(round + 1) : candidate } return attempt(0) } return each.items.map((entry, index): Mounted => { const id = unusedId(`${parent.id}.${slotName}.${entry.key ?? index}`) used.add(id) return { comp: child, props: entry.props, id, parentId: Option.some(parent.id), slot: Option.some(slotName), childIndex: index, key: Option.fromNullable(entry.key), } }) }), Match.tag('One', (single) => [ { comp: child, props: single.props, id: `${parent.id}.${slotName}.0`, parentId: Option.some(parent.id), slot: Option.some(slotName), childIndex: 0, key: Option.none(), } satisfies Mounted, ]), Match.tag('Absent', () => []), Match.exhaustive, ) export type SlotProvider = ( slotDefinition: AnySlot, ) => Effect.Effect export const collect: ( root: AnyComposition, provideSlot: SlotProvider, ) => Effect.Effect, never, Services> = Effect.fn('collect')(function* < Services, >( root: AnyComposition, provideSlot: SlotProvider, ): Effect.fn.Return, never, Services> { const bindings = new Map() // Explicit type keeps the recursive reference cast-free. const walk: (comp: AnyComposition) => Effect.Effect = Effect.fn('walk')( function* (comp: AnyComposition): Effect.fn.Return { yield* Effect.forEach(Rec.values(slotsOf(comp)), (slotClass) => Effect.gen(function* () { if (bindings.has(slotClass)) { return } const child = childComposition(yield* provideSlot(slotClass)) bindings.set(slotClass, child) yield* walk(child) }), ) }, ) yield* walk(root) return bindings }) export type CompositionRenderer = ( composition: AnyComposition, props: unknown, ) => Effect.Effect, never, Services> export interface RenderLevel { ( frontier: ReadonlyArray, bindings: ReadonlyMap, register: TriggerRegistryApi['register'], render: CompositionRenderer, ): Effect.Effect, ParseResult.ParseError, Services> } export const renderLevel: RenderLevel = Effect.fn('renderLevel')(function* ( frontier: ReadonlyArray, bindings: ReadonlyMap, register: TriggerRegistryApi['register'], render: CompositionRenderer, ): Effect.fn.Return, ParseResult.ParseError, Services> { if (frontier.length === 0) { return [] } const rendered = yield* Effect.forEach(frontier, (mounted) => Effect.gen(function* () { const frame = yield* render(mounted.comp, mounted.props) const node = yield* toWireNodeFromStructure(mounted, frame, register) const fills = structureFills(frame) const children = Rec.toEntries(slotsOf(mounted.comp)).flatMap(([slotName, slotClass]) => { const child = bindings.get(slotClass) if (child === undefined) { return [] } const fill = fills[slotName] if (fill === undefined) { return [] } return enqueueStructureSlot(mounted, slotName, child, fill) }) return { node, children } }), ) const nodes = rendered.map((entry) => entry.node) const next = rendered.flatMap((entry) => entry.children) const rest = yield* renderLevel(next, bindings, register, render) return [...nodes, ...rest] }) export const renderSceneWith: ( scene: AnyScene, options: RenderSceneToWireOptions | undefined, provideSlot: SlotProvider, render: CompositionRenderer, ) => Effect.Effect = Effect.fn('renderSceneWith')(function* ( scene: AnyScene, options: RenderSceneToWireOptions | undefined, provideSlot: SlotProvider, render: CompositionRenderer, ): Effect.fn.Return { const register = options?.register ?? noRegister const bindings = yield* collect(scene.composition, provideSlot) const root: Mounted = { comp: scene.composition, props: {}, id: '0', parentId: Option.none(), slot: Option.none(), childIndex: 0, key: Option.none(), } return yield* renderLevel([root], bindings, register, render) })