import { Array as Arr, Effect, Option, type ParseResult, Record as Rec, Schema } from 'effect' import { isFeatureBinding, isSlot, type SlotFill, type Structure, type Trigger, type UiContract, } from '@playfast/reform' import { type AnyComposition, type AnySlot, type AnySlotChild, type TriggerRegistryApi, type UiManifest, type WireNode, type WireProp, } from '@playfast/reform/internal' export const SETTLE_DRAIN = 30 export const settleDrain: Effect.Effect = Effect.yieldNow().pipe(Effect.repeatN(SETTLE_DRAIN)) // The erased event map's never values remain assignable to Trigger without a cast. export const eventsOf = (structure: Structure): Record> => Option.match(Option.fromNullable(structure.events), { onNone: () => ({}), onSome: (events) => events, }) export interface Mounted { readonly comp: AnyComposition readonly props: unknown readonly id: string readonly parentId: Option.Option readonly slot: Option.Option readonly childIndex: number readonly key: Option.Option } export const childComposition = (child: AnySlotChild): AnyComposition => { if (!isFeatureBinding(child)) { return child } return child.capture((binding): AnyComposition => binding.composition) } export const isRecord = (candidate: unknown): candidate is Record => typeof candidate === 'object' && candidate !== null && !Array.isArray(candidate) export const handlesOf = (node: WireNode): ReadonlyArray => node.props.flatMap((prop) => (prop._tag === 'Event' ? [prop.handle] : [])) // One-shot renders still emit deterministic handles but have no client registry. export const noRegister: TriggerRegistryApi['register'] = () => Effect.void export const toWireNodeFromStructure: ( mounted: Mounted, structure: Structure, register: TriggerRegistryApi['register'], ) => Effect.Effect = Effect.fn('toWireNodeFromStructure')( function* ( mounted: Mounted, structure: Structure, register: TriggerRegistryApi['register'], ): Effect.fn.Return { const manifest: UiManifest = mounted.comp.manifest.ui.manifest const name = manifest.name // The manifest stores schema AST only; reconstruct an unknown-safe schema at this wire seam. const encodedProps = yield* Option.match(Option.fromNullable(manifest.props), { onNone: () => Effect.succeed({}), onSome: (reflection) => Schema.encodeUnknown(Schema.make(reflection.ast))(structure.props).pipe( Effect.flatMap((encoded) => { if (isRecord(encoded)) { return Effect.succeed(encoded) } return Effect.dieMessage( `reform-remote: ${manifest.name} props did not encode to a record`, ) }), ), }) const dataProps: ReadonlyArray = Rec.toEntries(encodedProps).map( ([propName, propValue]): WireProp => ({ _tag: 'Data', name: propName, value: propValue, }), ) const eventSchemas = Option.fromNullable(manifest.events) const events = eventsOf(structure) const eventProps: ReadonlyArray = yield* Effect.forEach( Rec.toEntries(events), ([eventName, trigger]) => Effect.gen(function* () { const schema = Option.flatMap(eventSchemas, (schemas) => Rec.get(schemas, eventName)) if (Option.isNone(schema)) { return Option.none() } const handle = `${mounted.id}:${eventName}` yield* register(handle, trigger, Schema.make(schema.value.ast)) return Option.some({ _tag: 'Event', name: eventName, handle, }) }), ).pipe(Effect.map(Arr.getSomes)) return { id: mounted.id, name, parentId: Option.getOrNull(mounted.parentId), childIndex: mounted.childIndex, slot: Option.getOrNull(mounted.slot), key: Option.getOrNull(mounted.key), props: [...dataProps, ...eventProps], } }) // Structure erases per-slot fill types; recover SlotFill structurally without `as`. export const isSlotFill = (candidate: unknown): candidate is SlotFill => typeof candidate === 'object' && candidate !== null && '_tag' in candidate && (candidate._tag === 'Each' || candidate._tag === 'One' || candidate._tag === 'Absent') export const structureFills = (structure: Structure): Record> => Rec.fromEntries( Rec.toEntries(structure.slots).flatMap( ([slotName, fillValue]): ReadonlyArray]> => isSlotFill(fillValue) ? [[slotName, fillValue]] : [], ), ) export const slotsOf = (comp: AnyComposition): Record => comp.capture>((exact) => { if (!('slots' in exact.manifest)) { return {} } const candidate: unknown = exact.manifest.slots if (!isRecord(candidate)) { return {} } return Rec.fromEntries( Rec.toEntries(candidate).flatMap( ([name, slotDefinition]): ReadonlyArray => isSlot(slotDefinition) ? [[name, slotDefinition]] : [], ), ) })