@playfast/reform-remote 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/client.d.ts +33 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +97 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/clientBinding.d.ts +18 -0
  6. package/dist/clientBinding.d.ts.map +1 -0
  7. package/dist/clientBinding.js +70 -0
  8. package/dist/clientBinding.js.map +1 -0
  9. package/dist/index.d.ts +11 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +11 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/memory.d.ts +7 -0
  14. package/dist/memory.d.ts.map +1 -0
  15. package/dist/memory.js +21 -0
  16. package/dist/memory.js.map +1 -0
  17. package/dist/react.d.ts +15 -0
  18. package/dist/react.d.ts.map +1 -0
  19. package/dist/react.js +16 -0
  20. package/dist/react.js.map +1 -0
  21. package/dist/server.d.ts +16 -0
  22. package/dist/server.d.ts.map +1 -0
  23. package/dist/server.js +56 -0
  24. package/dist/server.js.map +1 -0
  25. package/dist/transport.d.ts +41 -0
  26. package/dist/transport.d.ts.map +1 -0
  27. package/dist/transport.js +157 -0
  28. package/dist/transport.js.map +1 -0
  29. package/dist/wireNode.d.ts +24 -0
  30. package/dist/wireNode.d.ts.map +1 -0
  31. package/dist/wireNode.js +80 -0
  32. package/dist/wireNode.js.map +1 -0
  33. package/dist/wireRender.d.ts +20 -0
  34. package/dist/wireRender.d.ts.map +1 -0
  35. package/dist/wireRender.js +118 -0
  36. package/dist/wireRender.js.map +1 -0
  37. package/package.json +17 -5
  38. package/src/client-prop-decode.test.ts +98 -0
  39. package/src/client.ts +25 -9
  40. package/src/clientBinding.ts +103 -0
  41. package/src/counterFixtures.ts +142 -0
  42. package/src/fixtures.ts +10 -453
  43. package/src/listFixtures.ts +174 -0
  44. package/src/optionalFixtures.ts +64 -0
  45. package/src/react.ts +14 -3
  46. package/src/server.test.ts +6 -6
  47. package/src/server.ts +11 -349
  48. package/src/slottedFixtures.ts +120 -0
  49. package/src/transport.ts +15 -47
  50. package/src/wire-id-uniqueness.test.ts +287 -0
  51. package/src/wire-identity.test.ts +369 -0
  52. package/src/wire-keyed-identity.test.ts +434 -0
  53. package/src/wireNode.ts +153 -0
  54. package/src/wireRender.ts +249 -0
@@ -0,0 +1,157 @@
1
+ import { Effect, Fiber, Option } from 'effect';
2
+ import {} from '@playfast/reform/internal';
3
+ import { makeRemoteServer } from './server.js';
4
+ const BACKGROUND_FLUSH_MS = 16;
5
+ export const serve = (options) => {
6
+ const { scene, transport } = options;
7
+ const server = makeRemoteServer(scene);
8
+ // Snapshot gates diffs; single-flight rendering protects the shared frame baseline.
9
+ const started = { value: false };
10
+ const flight = { promise: Option.none(), dirty: false };
11
+ const push = () => {
12
+ if (!started.value) {
13
+ return Effect.runPromise(Effect.void);
14
+ }
15
+ if (Option.isSome(flight.promise)) {
16
+ flight.dirty = true;
17
+ return flight.promise.value;
18
+ }
19
+ // ensuring clears the single-flight slot even when rendering fails.
20
+ const run = Effect.runPromise(Effect.gen(function* () {
21
+ const patches = yield* Effect.promise(() => server.renderDiff());
22
+ if (patches.length > 0) {
23
+ transport.send({ _tag: 'Patches', patches });
24
+ }
25
+ }).pipe(Effect.ensuring(Effect.sync(() => {
26
+ flight.promise = Option.none();
27
+ if (flight.dirty) {
28
+ flight.dirty = false;
29
+ void push();
30
+ }
31
+ }))));
32
+ flight.promise = Option.some(run);
33
+ return run;
34
+ };
35
+ // Background debounce lets the drain settle and coalesces bursts without delaying invokes.
36
+ const debounce = { fiber: Option.none() };
37
+ const scheduleFlush = () => {
38
+ if (!started.value || Option.isSome(debounce.fiber)) {
39
+ return;
40
+ }
41
+ const fiber = Effect.runFork(Effect.sleep(BACKGROUND_FLUSH_MS).pipe(Effect.zipRight(Effect.sync(() => {
42
+ debounce.fiber = Option.none();
43
+ void push();
44
+ }))));
45
+ debounce.fiber = Option.some(fiber);
46
+ };
47
+ const start = async () => {
48
+ const tree = await server.render();
49
+ transport.send({ _tag: 'Snapshot', tree });
50
+ started.value = true;
51
+ scheduleFlush();
52
+ };
53
+ const off = transport.onMessage((message) => {
54
+ void Effect.runPromise(Effect.promise(() => server.invoke(message.handle, message.payload)).pipe(Effect.zipRight(Effect.promise(push)),
55
+ // A stale handle (its node was deleted by a patch still in flight) fails by
56
+ // design. Left unhandled that escapes as a rejected promise and, under Node's
57
+ // default --unhandled-rejections=throw, takes the server down for every client.
58
+ Effect.catchAllCause((cause) => Effect.logError(`reform-remote: invoke '${message.handle}' failed`, cause))));
59
+ });
60
+ const offChange = server.subscribe(scheduleFlush);
61
+ return {
62
+ start,
63
+ dispose: async () => {
64
+ off();
65
+ offChange();
66
+ if (Option.isSome(debounce.fiber)) {
67
+ Effect.runFork(Fiber.interrupt(debounce.fiber.value));
68
+ }
69
+ await server.dispose();
70
+ },
71
+ };
72
+ };
73
+ export const serveShared = (options) => {
74
+ const { scene } = options;
75
+ const server = makeRemoteServer(scene);
76
+ const clients = new Set();
77
+ const broadcast = (message) => {
78
+ clients.forEach((client) => client.send(message));
79
+ };
80
+ // One shared render baseline requires the same single-flight discipline as serve.
81
+ const started = { value: false };
82
+ const flight = { promise: Option.none(), dirty: false };
83
+ const push = () => {
84
+ if (!started.value) {
85
+ return Effect.runPromise(Effect.void);
86
+ }
87
+ if (Option.isSome(flight.promise)) {
88
+ flight.dirty = true;
89
+ return flight.promise.value;
90
+ }
91
+ const run = Effect.runPromise(Effect.gen(function* () {
92
+ const patches = yield* Effect.promise(() => server.renderDiff());
93
+ if (patches.length > 0) {
94
+ broadcast({ _tag: 'Patches', patches });
95
+ }
96
+ }).pipe(Effect.ensuring(Effect.sync(() => {
97
+ flight.promise = Option.none();
98
+ if (flight.dirty) {
99
+ flight.dirty = false;
100
+ void push();
101
+ }
102
+ }))));
103
+ flight.promise = Option.some(run);
104
+ return run;
105
+ };
106
+ const debounce = { fiber: Option.none() };
107
+ const scheduleFlush = () => {
108
+ if (!started.value || Option.isSome(debounce.fiber)) {
109
+ return;
110
+ }
111
+ const fiber = Effect.runFork(Effect.sleep(BACKGROUND_FLUSH_MS).pipe(Effect.zipRight(Effect.sync(() => {
112
+ debounce.fiber = Option.none();
113
+ void push();
114
+ }))));
115
+ debounce.fiber = Option.some(fiber);
116
+ };
117
+ // Clients await the initial baseline so none snapshot an empty tree.
118
+ const ready = (async () => {
119
+ await server.render();
120
+ started.value = true;
121
+ scheduleFlush();
122
+ })();
123
+ const offChange = server.subscribe(scheduleFlush);
124
+ const addClient = (transport) => {
125
+ const off = transport.onMessage((message) => {
126
+ void Effect.runPromise(Effect.promise(() => server.invoke(message.handle, message.payload)).pipe(Effect.zipRight(Effect.promise(push)),
127
+ // A stale handle (its node was deleted by a patch still in flight) fails by
128
+ // design. Left unhandled that escapes as a rejected promise and, under Node's
129
+ // default --unhandled-rejections=throw, takes the server down for every client.
130
+ Effect.catchAllCause((cause) => Effect.logError(`reform-remote: invoke '${message.handle}' failed`, cause))));
131
+ });
132
+ // Snapshot and broadcast membership must change in one tick so no diff interleaves.
133
+ void Effect.runPromise(Effect.promise(() => ready).pipe(Effect.zipRight(Effect.sync(() => {
134
+ transport.send({ _tag: 'Snapshot', tree: server.currentTree() });
135
+ clients.add(transport);
136
+ }))));
137
+ return {
138
+ remove: () => {
139
+ off();
140
+ clients.delete(transport);
141
+ },
142
+ };
143
+ };
144
+ return {
145
+ addClient,
146
+ dispose: async () => {
147
+ offChange();
148
+ if (Option.isSome(debounce.fiber)) {
149
+ Effect.runFork(Fiber.interrupt(debounce.fiber.value));
150
+ }
151
+ clients.clear();
152
+ await server.dispose();
153
+ },
154
+ };
155
+ };
156
+ export { connect } from './clientBinding.js';
157
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import { Effect, Option, type ParseResult } from 'effect';
2
+ import { type SlotFill, type Structure, type Trigger, type UiContract } from '@playfast/reform';
3
+ import { type AnyComposition, type AnySlot, type AnySlotChild, type TriggerRegistryApi, type WireNode } from '@playfast/reform/internal';
4
+ export declare const SETTLE_DRAIN = 30;
5
+ export declare const settleDrain: Effect.Effect<void>;
6
+ export declare const eventsOf: (structure: Structure<UiContract>) => Record<string, Trigger<unknown>>;
7
+ export interface Mounted {
8
+ readonly comp: AnyComposition;
9
+ readonly props: unknown;
10
+ readonly id: string;
11
+ readonly parentId: Option.Option<string>;
12
+ readonly slot: Option.Option<string>;
13
+ readonly childIndex: number;
14
+ readonly key: Option.Option<string>;
15
+ }
16
+ export declare const childComposition: (child: AnySlotChild) => AnyComposition;
17
+ export declare const isRecord: (candidate: unknown) => candidate is Record<string, unknown>;
18
+ export declare const handlesOf: (node: WireNode) => ReadonlyArray<string>;
19
+ export declare const noRegister: TriggerRegistryApi['register'];
20
+ export declare const toWireNodeFromStructure: (mounted: Mounted, structure: Structure<UiContract>, register: TriggerRegistryApi['register']) => Effect.Effect<WireNode, ParseResult.ParseError>;
21
+ export declare const isSlotFill: (candidate: unknown) => candidate is SlotFill<unknown>;
22
+ export declare const structureFills: (structure: Structure<UiContract>) => Record<string, SlotFill<unknown>>;
23
+ export declare const slotsOf: (comp: AnyComposition) => Record<string, AnySlot>;
24
+ //# sourceMappingURL=wireNode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wireNode.d.ts","sourceRoot":"","sources":["../src/wireNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,MAAM,EAAE,MAAM,EAAE,KAAK,WAAW,EAAyB,MAAM,QAAQ,CAAA;AAC9F,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,OAAO,EACZ,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,kBAAkB,EAEvB,KAAK,QAAQ,EAEd,MAAM,2BAA2B,CAAA;AAElC,eAAO,MAAM,YAAY,KAAK,CAAA;AAC9B,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAwD,CAAA;AAGpG,eAAO,MAAM,QAAQ,GAAI,WAAW,SAAS,CAAC,UAAU,CAAC,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,CAIvF,CAAA;AAEJ,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;CACpC;AAED,eAAO,MAAM,gBAAgB,GAAI,OAAO,YAAY,KAAG,cAKtD,CAAA;AAED,eAAO,MAAM,QAAQ,GAAI,WAAW,OAAO,KAAG,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CACC,CAAA;AAElF,eAAO,MAAM,SAAS,GAAI,MAAM,QAAQ,KAAG,aAAa,CAAC,MAAM,CACa,CAAA;AAG5E,eAAO,MAAM,UAAU,EAAE,kBAAkB,CAAC,UAAU,CAAqB,CAAA;AAE3E,eAAO,MAAM,uBAAuB,EAAE,CACpC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,SAAS,CAAC,UAAU,CAAC,EAChC,QAAQ,EAAE,kBAAkB,CAAC,UAAU,CAAC,KACrC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,UAAU,CA6DjD,CAAA;AAGF,eAAO,MAAM,UAAU,GAAI,WAAW,OAAO,KAAG,SAAS,IAAI,QAAQ,CAAC,OAAO,CAIW,CAAA;AAExF,eAAO,MAAM,cAAc,GAAI,WAAW,SAAS,CAAC,UAAU,CAAC,KAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,CAM/F,CAAA;AAEH,eAAO,MAAM,OAAO,GAAI,MAAM,cAAc,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAejE,CAAA"}
@@ -0,0 +1,80 @@
1
+ import { Array as Arr, Effect, Option, Record as Rec, Schema } from 'effect';
2
+ import { isFeatureBinding, isSlot, } from '@playfast/reform';
3
+ import {} from '@playfast/reform/internal';
4
+ export const SETTLE_DRAIN = 30;
5
+ export const settleDrain = Effect.yieldNow().pipe(Effect.repeatN(SETTLE_DRAIN));
6
+ // The erased event map's never values remain assignable to Trigger<unknown> without a cast.
7
+ export const eventsOf = (structure) => Option.match(Option.fromNullable(structure.events), {
8
+ onNone: () => ({}),
9
+ onSome: (events) => events,
10
+ });
11
+ export const childComposition = (child) => {
12
+ if (!isFeatureBinding(child)) {
13
+ return child;
14
+ }
15
+ return child.capture((binding) => binding.composition);
16
+ };
17
+ export const isRecord = (candidate) => typeof candidate === 'object' && candidate !== null && !Array.isArray(candidate);
18
+ export const handlesOf = (node) => node.props.flatMap((prop) => (prop._tag === 'Event' ? [prop.handle] : []));
19
+ // One-shot renders still emit deterministic handles but have no client registry.
20
+ export const noRegister = () => Effect.void;
21
+ export const toWireNodeFromStructure = Effect.fn('toWireNodeFromStructure')(function* (mounted, structure, register) {
22
+ const manifest = mounted.comp.manifest.ui.manifest;
23
+ const name = manifest.name;
24
+ // The manifest stores schema AST only; reconstruct an unknown-safe schema at this wire seam.
25
+ const encodedProps = yield* Option.match(Option.fromNullable(manifest.props), {
26
+ onNone: () => Effect.succeed({}),
27
+ onSome: (reflection) => Schema.encodeUnknown(Schema.make(reflection.ast))(structure.props).pipe(Effect.flatMap((encoded) => {
28
+ if (isRecord(encoded)) {
29
+ return Effect.succeed(encoded);
30
+ }
31
+ return Effect.dieMessage(`reform-remote: ${manifest.name} props did not encode to a record`);
32
+ })),
33
+ });
34
+ const dataProps = Rec.toEntries(encodedProps).map(([propName, propValue]) => ({
35
+ _tag: 'Data',
36
+ name: propName,
37
+ value: propValue,
38
+ }));
39
+ const eventSchemas = Option.fromNullable(manifest.events);
40
+ const events = eventsOf(structure);
41
+ const eventProps = yield* Effect.forEach(Rec.toEntries(events), ([eventName, trigger]) => Effect.gen(function* () {
42
+ const schema = Option.flatMap(eventSchemas, (schemas) => Rec.get(schemas, eventName));
43
+ if (Option.isNone(schema)) {
44
+ return Option.none();
45
+ }
46
+ const handle = `${mounted.id}:${eventName}`;
47
+ yield* register(handle, trigger, Schema.make(schema.value.ast));
48
+ return Option.some({
49
+ _tag: 'Event',
50
+ name: eventName,
51
+ handle,
52
+ });
53
+ })).pipe(Effect.map(Arr.getSomes));
54
+ return {
55
+ id: mounted.id,
56
+ name,
57
+ parentId: Option.getOrNull(mounted.parentId),
58
+ childIndex: mounted.childIndex,
59
+ slot: Option.getOrNull(mounted.slot),
60
+ key: Option.getOrNull(mounted.key),
61
+ props: [...dataProps, ...eventProps],
62
+ };
63
+ });
64
+ // Structure erases per-slot fill types; recover SlotFill structurally without `as`.
65
+ export const isSlotFill = (candidate) => typeof candidate === 'object' &&
66
+ candidate !== null &&
67
+ '_tag' in candidate &&
68
+ (candidate._tag === 'Each' || candidate._tag === 'One' || candidate._tag === 'Absent');
69
+ export const structureFills = (structure) => Rec.fromEntries(Rec.toEntries(structure.slots).flatMap(([slotName, fillValue]) => isSlotFill(fillValue) ? [[slotName, fillValue]] : []));
70
+ export const slotsOf = (comp) => comp.capture((exact) => {
71
+ if (!('slots' in exact.manifest)) {
72
+ return {};
73
+ }
74
+ const candidate = exact.manifest.slots;
75
+ if (!isRecord(candidate)) {
76
+ return {};
77
+ }
78
+ return Rec.fromEntries(Rec.toEntries(candidate).flatMap(([name, slotDefinition]) => isSlot(slotDefinition) ? [[name, slotDefinition]] : []));
79
+ });
80
+ //# sourceMappingURL=wireNode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wireNode.js","sourceRoot":"","sources":["../src/wireNode.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { Context, Effect, type ParseResult } from 'effect';
2
+ import { type SlotFill, type Structure, type UiContract } from '@playfast/reform';
3
+ import { type AnyComposition, type AnyScene, type AnySlot, type AnySlotChild, type TriggerRegistryApi, type WireNode, type WireTree } from '@playfast/reform/internal';
4
+ import { type Mounted } from './wireNode.js';
5
+ export interface RenderSceneToWireOptions {
6
+ readonly register: TriggerRegistryApi['register'];
7
+ }
8
+ export declare const serviceFromContext: <Services, Identifier, Service>(context: Context.Context<Services>, tag: Context.Tag<Identifier, Service>, missing: string) => Effect.Effect<Service, never, never>;
9
+ export declare const capturedSlotProvider: <Services>(context: Context.Context<Services>, slotDefinition: AnySlot) => Effect.Effect<AnySlotChild, never, never>;
10
+ export declare const renderCapturedComposition: <Services>(context: Context.Context<Services>, composition: AnyComposition, props: unknown) => Effect.Effect<Structure<UiContract>, never, never>;
11
+ export declare const enqueueStructureSlot: (parent: Mounted, slotName: string, child: AnyComposition, fill: SlotFill<unknown>) => ReadonlyArray<Mounted>;
12
+ export type SlotProvider<Services> = (slotDefinition: AnySlot) => Effect.Effect<AnySlotChild, never, Services>;
13
+ export declare const collect: <Services>(root: AnyComposition, provideSlot: SlotProvider<Services>) => Effect.Effect<Map<AnySlot, AnyComposition>, never, Services>;
14
+ export type CompositionRenderer<Services> = (composition: AnyComposition, props: unknown) => Effect.Effect<Structure<UiContract>, never, Services>;
15
+ export interface RenderLevel {
16
+ <Services>(frontier: ReadonlyArray<Mounted>, bindings: ReadonlyMap<AnySlot, AnyComposition>, register: TriggerRegistryApi['register'], render: CompositionRenderer<Services>): Effect.Effect<ReadonlyArray<WireNode>, ParseResult.ParseError, Services>;
17
+ }
18
+ export declare const renderLevel: RenderLevel;
19
+ export declare const renderSceneWith: <SlotServices, CompositionServices>(scene: AnyScene, options: RenderSceneToWireOptions | undefined, provideSlot: SlotProvider<SlotServices>, render: CompositionRenderer<CompositionServices>) => Effect.Effect<WireTree, ParseResult.ParseError, SlotServices | CompositionServices>;
20
+ //# sourceMappingURL=wireRender.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wireRender.d.ts","sourceRoot":"","sources":["../src/wireRender.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,118 @@
1
+ import { Context, Effect, Match, Option, Record as Rec } from 'effect';
2
+ import { Composition, isStructure, } from '@playfast/reform';
3
+ import {} from '@playfast/reform/internal';
4
+ import { childComposition, noRegister, slotsOf, structureFills, toWireNodeFromStructure, } from './wireNode.js';
5
+ export const serviceFromContext = (context, tag, missing) => Option.match(Context.getOption(context, tag), {
6
+ onNone: () => Effect.dieMessage(missing),
7
+ onSome: Effect.succeed,
8
+ });
9
+ export const capturedSlotProvider = (context, slotDefinition) => slotDefinition.capture((exact) => serviceFromContext(context, exact.provider, 'reform-remote: scene does not provide a declared slot'));
10
+ export const renderCapturedComposition = (context, composition, props) => composition.capture((exact) => Option.match(exact.parseProps(props), {
11
+ onNone: () => Effect.dieMessage(`reform-remote: invalid props for composition ${composition.manifest.name}`),
12
+ onSome: (parsed) => serviceFromContext(context, exact.tag, `reform-remote: scene does not provide composition ${composition.manifest.name}`).pipe(Effect.flatMap((service) => Composition.render(service, {
13
+ props: parsed,
14
+ tracker: { add: () => { } },
15
+ })), Effect.flatMap((frame) => {
16
+ if (isStructure(frame)) {
17
+ return Effect.succeed(frame);
18
+ }
19
+ return Effect.dieMessage(`reform-remote: composition ${composition.manifest.name} did not return a Structure`);
20
+ })),
21
+ }));
22
+ export const enqueueStructureSlot = (parent, slotName, child, fill) => Match.value(fill).pipe(Match.tag('Each', (each) => {
23
+ // Keyed, not positional: a trigger handle embeds the node id, so an id built from
24
+ // the index silently re-points a handle the client already holds at whatever row
25
+ // slid into that slot. An accidental duplicate key falls back to the index rather
26
+ // than minting two nodes with one id.
27
+ const used = new Set();
28
+ // The disambiguator is retried rather than assumed free: a key that literally ends
29
+ // in `#<n>` can already hold the string a later repeat would mint, and two nodes
30
+ // sharing an id costs a whole row on the wire.
31
+ const unusedId = (base) => {
32
+ const attempt = (round) => {
33
+ const candidate = round === 0 ? base : `${base}#${round}`;
34
+ return used.has(candidate) ? attempt(round + 1) : candidate;
35
+ };
36
+ return attempt(0);
37
+ };
38
+ return each.items.map((entry, index) => {
39
+ const id = unusedId(`${parent.id}.${slotName}.${entry.key ?? index}`);
40
+ used.add(id);
41
+ return {
42
+ comp: child,
43
+ props: entry.props,
44
+ id,
45
+ parentId: Option.some(parent.id),
46
+ slot: Option.some(slotName),
47
+ childIndex: index,
48
+ key: Option.fromNullable(entry.key),
49
+ };
50
+ });
51
+ }), Match.tag('One', (single) => [
52
+ {
53
+ comp: child,
54
+ props: single.props,
55
+ id: `${parent.id}.${slotName}.0`,
56
+ parentId: Option.some(parent.id),
57
+ slot: Option.some(slotName),
58
+ childIndex: 0,
59
+ key: Option.none(),
60
+ },
61
+ ]), Match.tag('Absent', () => []), Match.exhaustive);
62
+ export const collect = Effect.fn('collect')(function* (root, provideSlot) {
63
+ const bindings = new Map();
64
+ // Explicit type keeps the recursive reference cast-free.
65
+ const walk = Effect.fn('walk')(function* (comp) {
66
+ yield* Effect.forEach(Rec.values(slotsOf(comp)), (slotClass) => Effect.gen(function* () {
67
+ if (bindings.has(slotClass)) {
68
+ return;
69
+ }
70
+ const child = childComposition(yield* provideSlot(slotClass));
71
+ bindings.set(slotClass, child);
72
+ yield* walk(child);
73
+ }));
74
+ });
75
+ yield* walk(root);
76
+ return bindings;
77
+ });
78
+ export const renderLevel = Effect.fn('renderLevel')(function* (frontier, bindings, register, render) {
79
+ if (frontier.length === 0) {
80
+ return [];
81
+ }
82
+ const rendered = yield* Effect.forEach(frontier, (mounted) => Effect.gen(function* () {
83
+ const frame = yield* render(mounted.comp, mounted.props);
84
+ const node = yield* toWireNodeFromStructure(mounted, frame, register);
85
+ const fills = structureFills(frame);
86
+ const children = Rec.toEntries(slotsOf(mounted.comp)).flatMap(([slotName, slotClass]) => {
87
+ const child = bindings.get(slotClass);
88
+ if (child === undefined) {
89
+ return [];
90
+ }
91
+ const fill = fills[slotName];
92
+ if (fill === undefined) {
93
+ return [];
94
+ }
95
+ return enqueueStructureSlot(mounted, slotName, child, fill);
96
+ });
97
+ return { node, children };
98
+ }));
99
+ const nodes = rendered.map((entry) => entry.node);
100
+ const next = rendered.flatMap((entry) => entry.children);
101
+ const rest = yield* renderLevel(next, bindings, register, render);
102
+ return [...nodes, ...rest];
103
+ });
104
+ export const renderSceneWith = Effect.fn('renderSceneWith')(function* (scene, options, provideSlot, render) {
105
+ const register = options?.register ?? noRegister;
106
+ const bindings = yield* collect(scene.composition, provideSlot);
107
+ const root = {
108
+ comp: scene.composition,
109
+ props: {},
110
+ id: '0',
111
+ parentId: Option.none(),
112
+ slot: Option.none(),
113
+ childIndex: 0,
114
+ key: Option.none(),
115
+ };
116
+ return yield* renderLevel([root], bindings, register, render);
117
+ });
118
+ //# sourceMappingURL=wireRender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wireRender.js","sourceRoot":"","sources":["../src/wireRender.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playfast/reform-remote",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Run a reform scene's logic on the server and stream its rendered UI to a thin client over any duplex transport.",
5
5
  "keywords": [
6
6
  "effect",
@@ -20,15 +20,26 @@
20
20
  "directory": "packages/reform-remote"
21
21
  },
22
22
  "files": [
23
+ "dist",
23
24
  "src",
24
25
  "README.md"
25
26
  ],
26
27
  "type": "module",
27
28
  "sideEffects": false,
29
+ "main": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
28
31
  "exports": {
29
32
  "./package.json": "./package.json",
30
- ".": "./src/index.ts",
31
- "./*": "./src/*.ts"
33
+ ".": {
34
+ "playfast-src": "./src/index.ts",
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./*": {
39
+ "playfast-src": "./src/*.ts",
40
+ "types": "./dist/*.d.ts",
41
+ "default": "./dist/*.js"
42
+ }
32
43
  },
33
44
  "publishConfig": {
34
45
  "access": "public"
@@ -36,12 +47,13 @@
36
47
  "scripts": {
37
48
  "clean": "rm -rf dist .tsbuildinfo",
38
49
  "check": "tsc --noEmit",
39
- "build": "tsc -p tsconfig.build.json",
50
+ "build": "rm -rf dist .tsbuildinfo && tsc -p tsconfig.build.json && bun ../../scripts/fix-esm-extensions.ts dist",
40
51
  "test": "vitest run",
41
52
  "test:watch": "vitest",
42
53
  "coverage": "vitest run --coverage",
43
54
  "lint": "oxlint src",
44
- "lint:fix": "oxlint --fix src"
55
+ "lint:fix": "oxlint --fix src",
56
+ "prepack": "bun run build"
45
57
  },
46
58
  "peerDependencies": {
47
59
  "@playfast/reform": "*",
@@ -0,0 +1,98 @@
1
+ import { createElement } from 'react'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { expect, test } from 'vitest'
4
+ import { Either, Schema as S } from 'effect'
5
+ import { Ui, ui } from '@playfast/reform'
6
+ import type { WireNode } from '@playfast/reform/internal'
7
+ import { remoteViews, renderWireTree, type RemoteViews } from './client'
8
+
9
+ /**
10
+ * Round-4 adversarial audit — the transport-decode fix, sibling half.
11
+ *
12
+ * `reform-remote-web/src/transport.ts` moved `decodeServerMessage` from
13
+ * `Schema.decodeUnknownSync` to `Schema.decodeUnknownOption` so a frame this
14
+ * client cannot parse costs that frame and nothing else. That fix is sound for
15
+ * the *envelope*.
16
+ *
17
+ * It does not reach the second `decodeUnknownSync` on the same path. The frame
18
+ * schema types a data prop as `value: Schema.Unknown` (transport.ts, WirePropSchema),
19
+ * so ANY JSON value passes the envelope decode and is handed to the tree. The
20
+ * per-view props schema is applied later, in `client.ts`:
21
+ *
22
+ * view: (encoded, slots, events) =>
23
+ * exact(Schema.decodeUnknownSync(propsSchema)(encoded), ...)
24
+ *
25
+ * and `registered.view(...)` is the return statement of `WireNodeView`, a React
26
+ * component. A prop the client's schema rejects — version skew against a newer
27
+ * server, a hostile endpoint, a hand-rolled server — therefore throws out of
28
+ * React's render phase, where the blast radius is not one node but every sibling
29
+ * in the tree: with no error boundary React unmounts the whole root.
30
+ *
31
+ * The two halves of the same frame path now answer the same untrusted input in
32
+ * two different ways.
33
+ */
34
+
35
+ class AuditItemUi extends ui('audit4.Item', { props: S.Struct({ label: S.String }) }) {}
36
+
37
+ const AuditItemView = Ui.make(AuditItemUi, ({ label }) =>
38
+ createElement('span', { 'data-item': label }, label),
39
+ )
40
+
41
+ const node = (over: Partial<WireNode> & Pick<WireNode, 'id' | 'name'>): WireNode => ({
42
+ parentId: null,
43
+ childIndex: 0,
44
+ slot: null,
45
+ key: null,
46
+ props: [],
47
+ ...over,
48
+ })
49
+
50
+ type AuditContract = { Item: typeof AuditItemUi }
51
+
52
+ const views: RemoteViews<AuditContract> = { Item: AuditItemView }
53
+
54
+ const render = (tree: ReadonlyArray<WireNode>): string =>
55
+ renderToStaticMarkup(
56
+ renderWireTree(tree, {
57
+ views: remoteViews<AuditContract>(views),
58
+ invoke: () => undefined,
59
+ }),
60
+ )
61
+
62
+ // The frame the server actually put on the wire. `label` is a string on the
63
+ // server's schema and a number here — the plainest form of version skew, and
64
+ // exactly what `WirePropSchema`'s `value: Schema.Unknown` lets through.
65
+ const wellFormed = node({
66
+ id: 'ok',
67
+ name: 'audit4.Item',
68
+ childIndex: 0,
69
+ props: [{ _tag: 'Data', name: 'label', value: 'still-here' }],
70
+ })
71
+ const skewed = node({
72
+ id: 'skewed',
73
+ name: 'audit4.Item',
74
+ childIndex: 1,
75
+ props: [{ _tag: 'Data', name: 'label', value: 7 }],
76
+ })
77
+
78
+ // The render is what is under test, so its outcome is data, not an exception.
79
+ const attempt = (tree: ReadonlyArray<WireNode>): Either.Either<string, unknown> =>
80
+ Either.try(() => render(tree))
81
+
82
+ test('a wire prop this client cannot decode costs that node, not the whole render', () => {
83
+ // CONTROL — the harness renders, the view is registered, and a well-formed
84
+ // node produces its markup.
85
+ expect(Either.getOrElse(attempt([wellFormed]), () => '')).toContain('still-here')
86
+
87
+ // CONTROL — a node whose NAME this client does not know is already handled the
88
+ // way the fix asks for: `WireNodeView` returns null for it and its siblings
89
+ // render. Rejecting a prop is the same class of skew, one field deeper.
90
+ const unknownView = node({ id: 'unknown', name: 'audit4.NotRegistered', childIndex: 1 })
91
+ expect(Either.getOrElse(attempt([wellFormed, unknownView]), () => '')).toContain('still-here')
92
+
93
+ // THE DEFECT — one node the client's props schema rejects takes the render
94
+ // down with it, so the sibling that decoded fine is never painted either.
95
+ const skewedRender = attempt([wellFormed, skewed])
96
+ expect(Either.isRight(skewedRender)).toBe(true)
97
+ expect(Either.getOrElse(skewedRender, () => '')).toContain('still-here')
98
+ })