@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
package/src/fixtures.ts CHANGED
@@ -1,453 +1,10 @@
1
- import { createElement, Fragment } from 'react'
2
- import { Duration, Effect, Layer, Option, Schema as S } from 'effect'
3
- import {
4
- Channel,
5
- Composition,
6
- type CapturedScene,
7
- type DerivedContract,
8
- Engine,
9
- Event,
10
- type EventOf,
11
- Procedure,
12
- Reducer,
13
- State,
14
- StateGroup,
15
- Ui,
16
- each,
17
- mount,
18
- one,
19
- provide,
20
- scene,
21
- slot,
22
- type SlotService,
23
- ui,
24
- } from '@playfast/reform'
25
- import {
26
- type CompositionChildSummary,
27
- type EngineServices,
28
- type WiredUi,
29
- } from '@playfast/reform/internal'
30
-
31
- const CountBase: State.StateDefinition<'count', number, typeof S.Number> = State.make(
32
- 'count',
33
- S.Number,
34
- )
35
- class Count extends CountBase {}
36
- class Counters extends StateGroup.make(Count) {}
37
- class Bumped extends Event.make('Bumped', S.Struct({ by: S.Number })) {}
38
- class Bump extends Reducer.make('Bump', {
39
- states: [Count],
40
- events: [Bumped],
41
- }) {}
42
- const CounterUiBase: WiredUi<
43
- DerivedContract<{
44
- props: S.Struct<{ count: typeof S.Number }>
45
- events: { bump: S.Struct<{ by: typeof S.Number }> }
46
- }>,
47
- 'Counter'
48
- > = ui('Counter', {
49
- props: S.Struct({ count: S.Number }),
50
- events: { bump: S.Struct({ by: S.Number }) },
51
- })
52
- export class CounterUi extends CounterUiBase {}
53
-
54
- class Counter extends Composition.make('Counter', {
55
- title: 'Counter',
56
- ui: CounterUi,
57
- states: [Count],
58
- })<Counter>() {}
59
-
60
- type CounterScene = CapturedScene<
61
- typeof CounterUi.Contract,
62
- readonly [typeof Count],
63
- | EngineServices
64
- | State.StateStore<'count', number>
65
- | Ui.UiService<'Counter'>
66
- | Composition.CompositionId<'Counter', Counter, never>,
67
- unknown,
68
- 'Counter',
69
- Counter
70
- >
71
-
72
- export const bumpedBy = (amount: number): EventOf<'Bumped', { by: number }> =>
73
- Event.construct(Bumped, { by: amount })
74
-
75
- const makeCounterScene = (
76
- boot?: ReadonlyArray<EventOf<'Bumped', { by: number }>>,
77
- ): CounterScene => {
78
- const presentation = Layer.mergeAll(
79
- provide(
80
- CounterUi,
81
- Ui.make(CounterUi, ({ count }) => `count:${count}`),
82
- ),
83
- StateGroup.live(Counters, { count: 0 }),
84
- )
85
- const app = Layer.mergeAll(
86
- Composition.live(Counter, function* () {
87
- const count = yield* StateGroup.select(Counters, 'count')
88
- const bump = yield* Event.trigger(Bumped)
89
- return mount({ props: { count }, slots: {}, events: { bump } })
90
- }),
91
- Reducer.live(Bump, (count, event) => count + event.by),
92
- ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
93
- return scene(Counter, boot === undefined ? { provide: [app] } : { provide: [app], boot })
94
- }
95
- export const counterScene: typeof makeCounterScene = makeCounterScene
96
-
97
- const makeStructureCounterScene = (): CounterScene => {
98
- const presentation = Layer.mergeAll(
99
- provide(
100
- CounterUi,
101
- Ui.make(CounterUi, ({ count }) => `count:${count}`),
102
- ),
103
- StateGroup.live(Counters, { count: 0 }),
104
- )
105
- const app = Layer.mergeAll(
106
- Composition.live(Counter, function* () {
107
- const count = yield* StateGroup.select(Counters, 'count')
108
- const bump = yield* Event.trigger(Bumped)
109
- return mount({ props: { count }, slots: {}, events: { bump } })
110
- }),
111
- Reducer.live(Bump, (count, event) => count + event.by),
112
- ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
113
- return scene(Counter, { provide: [app] })
114
- }
115
- export const structureCounterScene: typeof makeStructureCounterScene = makeStructureCounterScene
116
-
117
- const LabelBase: State.StateDefinition<
118
- 'label',
119
- Option.Option<string>,
120
- S.OptionFromSelf<typeof S.String>
121
- > = State.make('label', S.OptionFromSelf(S.String))
122
- class Label extends LabelBase {}
123
- class Labels extends StateGroup.make(Label) {}
124
- const OptionalUiBase: WiredUi<
125
- DerivedContract<{ props: S.Struct<{ label: S.Option<typeof S.String> }> }>,
126
- 'Optional'
127
- > = ui('Optional', { props: S.Struct({ label: S.Option(S.String) }) })
128
- export class OptionalUi extends OptionalUiBase {}
129
-
130
- class Optional extends Composition.make('Optional', {
131
- title: 'Optional',
132
- ui: OptionalUi,
133
- states: [Label],
134
- })<Optional>() {}
135
-
136
- type OptionalScene = CapturedScene<
137
- typeof OptionalUi.Contract,
138
- readonly [typeof Label],
139
- | EngineServices
140
- | State.StateStore<'label', Option.Option<string>>
141
- | Ui.UiService<'Optional'>
142
- | Composition.CompositionId<'Optional', Optional, never>,
143
- unknown,
144
- 'Optional',
145
- Optional
146
- >
147
-
148
- const makeOptionScene = (initial: Option.Option<string> = Option.some('hi')): OptionalScene => {
149
- const presentation = Layer.mergeAll(
150
- provide(
151
- OptionalUi,
152
- Ui.make(OptionalUi, () => null),
153
- ),
154
- StateGroup.live(Labels, { label: initial }),
155
- )
156
- const app = Layer.mergeAll(
157
- Composition.live(Optional, function* () {
158
- const label = yield* StateGroup.select(Labels, 'label')
159
- return mount({ props: { label }, slots: {} })
160
- }),
161
- ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
162
- return scene(Optional, { provide: [app] })
163
- }
164
- export const optionScene: typeof makeOptionScene = makeOptionScene
165
-
166
- const GreetingBase: State.StateDefinition<'greeting', string, typeof S.String> = State.make(
167
- 'greeting',
168
- S.String,
169
- )
170
- class Greeting extends GreetingBase {}
171
- class Greeted extends Event.make('Greeted', S.Struct({ text: S.String })) {}
172
- class Cleared extends Event.make('Cleared', S.Struct({})) {}
173
- class SetGreeting extends Reducer.make('SetGreeting', {
174
- states: [Greeting],
175
- events: [Greeted],
176
- }) {}
177
- class ClearGreeting extends Reducer.make('ClearGreeting', {
178
- states: [Greeting],
179
- events: [Cleared],
180
- }) {}
181
- const PanelUiBase: WiredUi<
182
- DerivedContract<{
183
- props: S.Struct<{ greeting: typeof S.String }>
184
- events: {
185
- greet: S.Struct<{ text: typeof S.String }>
186
- clear: S.Struct<{}>
187
- }
188
- }>,
189
- 'Panel'
190
- > = ui('Panel', {
191
- props: S.Struct({ greeting: S.String }),
192
- events: { greet: S.Struct({ text: S.String }), clear: S.Struct({}) },
193
- })
194
- class PanelUi extends PanelUiBase {}
195
-
196
- class Panel extends Composition.make('Panel', {
197
- title: 'Panel',
198
- ui: PanelUi,
199
- states: [Greeting],
200
- })<Panel>() {}
201
-
202
- class MainSlot extends slot('Main')<MainSlot, typeof Panel>() {}
203
- const ShellUiBase: Ui.UiClass<{ props: {}; slots: { Main: MainSlot } }, 'Shell'> = ui('Shell')<{
204
- props: {}
205
- slots: { Main: MainSlot }
206
- }>()
207
- class ShellUi extends ShellUiBase {}
208
-
209
- class Shell extends Composition.make('Shell', {
210
- title: 'Shell',
211
- slots: { Main: MainSlot },
212
- ui: ShellUi,
213
- })<Shell>() {}
214
-
215
- type SlottedScene = CapturedScene<
216
- typeof ShellUi.Contract,
217
- readonly [],
218
- | EngineServices
219
- | State.StateStore<'greeting', string>
220
- | Ui.UiService<'Panel'>
221
- | Ui.UiService<'Shell'>
222
- | Composition.CompositionId<'Shell', Shell, MainSlot>
223
- | Composition.CompositionId<'Panel', Panel, never>
224
- | SlotService<'Main', MainSlot, CompositionChildSummary<Panel>>,
225
- unknown,
226
- 'Shell',
227
- Shell
228
- >
229
-
230
- const makeSlottedScene = (): SlottedScene => {
231
- const presentation = Layer.mergeAll(
232
- provide(
233
- ShellUi,
234
- Ui.make(ShellUi, (_props, slots) => createElement(slots.Main, {})),
235
- ),
236
- provide(
237
- PanelUi,
238
- Ui.make(PanelUi, ({ greeting }) => greeting),
239
- ),
240
- provide(MainSlot, Panel),
241
- State.live(Greeting, 'hi'),
242
- )
243
- const app = Layer.mergeAll(
244
- Composition.live(Shell, function* () {
245
- yield* Effect.void
246
- return mount({ props: {}, slots: { Main: one({}) } })
247
- }),
248
- Composition.live(Panel, function* () {
249
- const greeting = yield* Greeting
250
- const greet = yield* Event.trigger(Greeted)
251
- const clearTrigger = yield* Event.trigger(Cleared)
252
- const clear = (): void => clearTrigger({})
253
- return mount({
254
- props: { greeting },
255
- slots: {},
256
- events: { greet, clear },
257
- })
258
- }),
259
- Reducer.live(SetGreeting, (_greeting, event) => event.text),
260
- Reducer.live(ClearGreeting, () => ''),
261
- ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
262
- return scene(Shell, { provide: [app] })
263
- }
264
- export const slottedScene: typeof makeSlottedScene = makeSlottedScene
265
-
266
- const ListItem: S.Struct<{ id: typeof S.String; label: typeof S.String }> = S.Struct({
267
- id: S.String,
268
- label: S.String,
269
- })
270
- const ItemsBase: State.StateDefinition<
271
- 'items',
272
- ReadonlyArray<{ readonly id: string; readonly label: string }>,
273
- S.Array$<typeof ListItem>
274
- > = State.make('items', S.Array(ListItem))
275
- class Items extends ItemsBase {}
276
- class Added extends Event.make('Added', ListItem) {}
277
- class Removed extends Event.make('Removed', S.Struct({ id: S.String })) {}
278
- class AddItem extends Reducer.make('AddItem', {
279
- states: [Items],
280
- events: [Added],
281
- }) {}
282
- class RemoveItem extends Reducer.make('RemoveItem', {
283
- states: [Items],
284
- events: [Removed],
285
- }) {}
286
-
287
- const BarUiBase: WiredUi<
288
- DerivedContract<{
289
- props: S.Struct<{}>
290
- events: { add: typeof ListItem }
291
- }>,
292
- 'Bar'
293
- > = ui('Bar', {
294
- props: S.Struct({}),
295
- events: { add: ListItem },
296
- })
297
- class BarUi extends BarUiBase {}
298
-
299
- class Bar extends Composition.make('Bar', { title: 'Bar', ui: BarUi })<Bar>() {}
300
- const ItemUiBase: WiredUi<
301
- DerivedContract<{
302
- props: S.Struct<{ label: typeof S.String }>
303
- events: { remove: S.Struct<{}> }
304
- }>,
305
- 'Item'
306
- > = ui('Item', {
307
- props: S.Struct({ label: S.String }),
308
- events: { remove: S.Struct({}) },
309
- })
310
- class ItemUi extends ItemUiBase {}
311
-
312
- class ItemComp extends Composition.make('Item', {
313
- title: 'Item',
314
- ui: ItemUi,
315
- props: ListItem,
316
- })<ItemComp>() {}
317
-
318
- class BarSlot extends slot('Bar')<BarSlot, typeof Bar>() {}
319
-
320
- class ItemSlot extends slot('Item')<ItemSlot, typeof ItemComp>() {}
321
- const ListUiBase: Ui.UiClass<
322
- {
323
- props: { items: ReadonlyArray<{ id: string; label: string }> }
324
- slots: { Bar: BarSlot; Item: ItemSlot }
325
- },
326
- 'List'
327
- > = ui('List')<{
328
- props: { items: ReadonlyArray<{ id: string; label: string }> }
329
- slots: { Bar: BarSlot; Item: ItemSlot }
330
- }>()
331
- class ListUi extends ListUiBase {}
332
-
333
- class ListComp extends Composition.make('List', {
334
- title: 'List',
335
- slots: { Bar: BarSlot, Item: ItemSlot },
336
- ui: ListUi,
337
- states: [Items],
338
- })<ListComp>() {}
339
-
340
- type ListScene = CapturedScene<
341
- typeof ListUi.Contract,
342
- readonly [typeof Items],
343
- | EngineServices
344
- | Ui.UiService<'Item'>
345
- | State.StateStore<'items', ReadonlyArray<{ readonly id: string; readonly label: string }>>
346
- | Ui.UiService<'Bar'>
347
- | Ui.UiService<'List'>
348
- | Composition.CompositionId<'List', ListComp, BarSlot | ItemSlot>
349
- | Composition.CompositionId<'Bar', Bar, never>
350
- | Composition.CompositionId<'Item', ItemComp, never>
351
- | SlotService<'Bar', BarSlot, CompositionChildSummary<Bar>>
352
- | SlotService<'Item', ItemSlot, CompositionChildSummary<ItemComp>>,
353
- unknown,
354
- 'List',
355
- ListComp
356
- >
357
-
358
- const makeListScene = (
359
- initial: ReadonlyArray<{ id: string; label: string }> = [
360
- { id: 'a', label: 'A' },
361
- { id: 'b', label: 'B' },
362
- ],
363
- ): ListScene => {
364
- const presentation = Layer.mergeAll(
365
- provide(
366
- ListUi,
367
- Ui.make(ListUi, (_props, slots) =>
368
- createElement(Fragment, null, createElement(slots.Bar, {}), createElement(slots.Item, {})),
369
- ),
370
- ),
371
- provide(
372
- BarUi,
373
- Ui.make(BarUi, () => null),
374
- ),
375
- provide(
376
- ItemUi,
377
- Ui.make(ItemUi, ({ label }) => label),
378
- ),
379
- provide(BarSlot, Bar),
380
- provide(ItemSlot, ItemComp),
381
- State.live(Items, initial),
382
- )
383
- const app = Layer.mergeAll(
384
- Composition.live(ListComp, function* () {
385
- const listItems = yield* Items
386
- return mount({
387
- props: { items: listItems },
388
- slots: {
389
- Bar: one({}),
390
- Item: each(listItems, {
391
- key: (entry) => entry.id,
392
- props: (entry) => ({ id: entry.id, label: entry.label }),
393
- }),
394
- },
395
- })
396
- }),
397
- Composition.live(Bar, function* () {
398
- const add = yield* Event.trigger(Added)
399
- return mount({ props: {}, slots: {}, events: { add } })
400
- }),
401
- Composition.live(ItemComp, function* () {
402
- const itemProps = yield* ItemComp.props
403
- const removeTrigger = yield* Event.trigger(Removed)
404
- // The item binds its id so the wire event carries no payload.
405
- const remove = (): void => removeTrigger({ id: itemProps.id })
406
- return mount({
407
- props: { label: itemProps.label },
408
- slots: {},
409
- events: { remove },
410
- })
411
- }),
412
- Reducer.live(AddItem, (current, event) => [...current, { id: event.id, label: event.label }]),
413
- Reducer.live(RemoveItem, (current, event) => current.filter((entry) => entry.id !== event.id)),
414
- ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
415
- return scene(ListComp, { provide: [app] })
416
- }
417
- export const listScene: typeof makeListScene = makeListScene
418
-
419
- class Kick extends Event.make('Kick', S.Struct({ to: S.Number })) {}
420
- class Loader extends Channel.make('Loader', { policy: { _tag: 'latest' } }) {}
421
- class DelayedBump extends Procedure.make('DelayedBump', {
422
- channel: Loader,
423
- events: [Kick],
424
- }) {}
425
-
426
- const makeAsyncCounterScene = (delay: Duration.DurationInput = '40 millis'): CounterScene => {
427
- const presentation = Layer.mergeAll(
428
- provide(
429
- CounterUi,
430
- Ui.make(CounterUi, ({ count }) => `count:${count}`),
431
- ),
432
- StateGroup.live(Counters, { count: 0 }),
433
- )
434
- const app = Layer.mergeAll(
435
- Composition.live(Counter, function* () {
436
- const count = yield* StateGroup.select(Counters, 'count')
437
- const bump = yield* Event.trigger(Bumped)
438
- return mount({ props: { count }, slots: {}, events: { bump } })
439
- }),
440
- Reducer.live(Bump, (count, event) => count + event.by),
441
- Procedure.live(DelayedBump, function* (event) {
442
- yield* Effect.sleep(delay)
443
- yield* Event.dispatch(Bumped, { by: event.to })
444
- }),
445
- // Without Channel.live the boot Kick is dropped.
446
- Channel.live(Loader),
447
- ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
448
- return scene(Counter, {
449
- provide: [app],
450
- boot: [Event.construct(Kick, { to: 7 })],
451
- })
452
- }
453
- export const asyncCounterScene: typeof makeAsyncCounterScene = makeAsyncCounterScene
1
+ export {
2
+ asyncCounterScene,
3
+ bumpedBy,
4
+ CounterUi,
5
+ counterScene,
6
+ structureCounterScene,
7
+ } from './counterFixtures'
8
+ export { OptionalUi, optionScene } from './optionalFixtures'
9
+ export { slottedScene } from './slottedFixtures'
10
+ export { listScene } from './listFixtures'
@@ -0,0 +1,174 @@
1
+ import { createElement, Fragment } from 'react'
2
+ import { Layer, Schema as S } from 'effect'
3
+ import {
4
+ Composition,
5
+ Engine,
6
+ Event,
7
+ Reducer,
8
+ State,
9
+ Ui,
10
+ each,
11
+ mount,
12
+ one,
13
+ provide,
14
+ scene,
15
+ slot,
16
+ type CapturedScene,
17
+ type DerivedContract,
18
+ type SlotService,
19
+ ui,
20
+ } from '@playfast/reform'
21
+ import { type CompositionChildSummary, type EngineServices, type WiredUi } from '@playfast/reform/internal'
22
+
23
+ const ListItem: S.Struct<{ id: typeof S.String; label: typeof S.String }> = S.Struct({
24
+ id: S.String,
25
+ label: S.String,
26
+ })
27
+ const ItemsBase: State.StateDefinition<
28
+ 'items',
29
+ ReadonlyArray<{ readonly id: string; readonly label: string }>,
30
+ S.Array$<typeof ListItem>
31
+ > = State.make('items', S.Array(ListItem))
32
+ class Items extends ItemsBase {}
33
+ class Added extends Event.make('Added', ListItem) {}
34
+ class Removed extends Event.make('Removed', S.Struct({ id: S.String })) {}
35
+ class AddItem extends Reducer.make('AddItem', {
36
+ states: [Items],
37
+ events: [Added],
38
+ }) {}
39
+ class RemoveItem extends Reducer.make('RemoveItem', {
40
+ states: [Items],
41
+ events: [Removed],
42
+ }) {}
43
+
44
+ const BarUiBase: WiredUi<
45
+ DerivedContract<{
46
+ props: S.Struct<{}>
47
+ events: { add: typeof ListItem }
48
+ }>,
49
+ 'Bar'
50
+ > = ui('Bar', {
51
+ props: S.Struct({}),
52
+ events: { add: ListItem },
53
+ })
54
+ class BarUi extends BarUiBase {}
55
+
56
+ class Bar extends Composition.make('Bar', { title: 'Bar', ui: BarUi })<Bar>() {}
57
+ const ItemUiBase: WiredUi<
58
+ DerivedContract<{
59
+ props: S.Struct<{ label: typeof S.String }>
60
+ events: { remove: S.Struct<{}> }
61
+ }>,
62
+ 'Item'
63
+ > = ui('Item', {
64
+ props: S.Struct({ label: S.String }),
65
+ events: { remove: S.Struct({}) },
66
+ })
67
+ class ItemUi extends ItemUiBase {}
68
+
69
+ class ItemComp extends Composition.make('Item', {
70
+ title: 'Item',
71
+ ui: ItemUi,
72
+ props: ListItem,
73
+ })<ItemComp>() {}
74
+
75
+ class BarSlot extends slot('Bar')<BarSlot, typeof Bar>() {}
76
+
77
+ class ItemSlot extends slot('Item')<ItemSlot, typeof ItemComp>() {}
78
+ const ListUiBase: Ui.UiClass<
79
+ {
80
+ props: { items: ReadonlyArray<{ id: string; label: string }> }
81
+ slots: { Bar: BarSlot; Item: ItemSlot }
82
+ },
83
+ 'List'
84
+ > = ui('List')<{
85
+ props: { items: ReadonlyArray<{ id: string; label: string }> }
86
+ slots: { Bar: BarSlot; Item: ItemSlot }
87
+ }>()
88
+ class ListUi extends ListUiBase {}
89
+
90
+ class ListComp extends Composition.make('List', {
91
+ title: 'List',
92
+ slots: { Bar: BarSlot, Item: ItemSlot },
93
+ ui: ListUi,
94
+ states: [Items],
95
+ })<ListComp>() {}
96
+
97
+ type ListScene = CapturedScene<
98
+ typeof ListUi.Contract,
99
+ readonly [typeof Items],
100
+ | EngineServices
101
+ | Ui.UiService<'Item'>
102
+ | State.StateStore<'items', ReadonlyArray<{ readonly id: string; readonly label: string }>>
103
+ | Ui.UiService<'Bar'>
104
+ | Ui.UiService<'List'>
105
+ | Composition.CompositionId<'List', ListComp, BarSlot | ItemSlot>
106
+ | Composition.CompositionId<'Bar', Bar, never>
107
+ | Composition.CompositionId<'Item', ItemComp, never>
108
+ | SlotService<'Bar', BarSlot, CompositionChildSummary<Bar>>
109
+ | SlotService<'Item', ItemSlot, CompositionChildSummary<ItemComp>>,
110
+ unknown,
111
+ 'List',
112
+ ListComp
113
+ >
114
+
115
+ const makeListScene = (
116
+ initial: ReadonlyArray<{ id: string; label: string }> = [
117
+ { id: 'a', label: 'A' },
118
+ { id: 'b', label: 'B' },
119
+ ],
120
+ ): ListScene => {
121
+ const presentation = Layer.mergeAll(
122
+ provide(
123
+ ListUi,
124
+ Ui.make(ListUi, (_props, slots) =>
125
+ createElement(Fragment, null, createElement(slots.Bar, {}), createElement(slots.Item, {})),
126
+ ),
127
+ ),
128
+ provide(
129
+ BarUi,
130
+ Ui.make(BarUi, () => null),
131
+ ),
132
+ provide(
133
+ ItemUi,
134
+ Ui.make(ItemUi, ({ label }) => label),
135
+ ),
136
+ provide(BarSlot, Bar),
137
+ provide(ItemSlot, ItemComp),
138
+ State.live(Items, initial),
139
+ )
140
+ const app = Layer.mergeAll(
141
+ Composition.live(ListComp, function* () {
142
+ const listItems = yield* Items
143
+ return mount({
144
+ props: { items: listItems },
145
+ slots: {
146
+ Bar: one({}),
147
+ Item: each(listItems, {
148
+ key: (entry) => entry.id,
149
+ props: (entry) => ({ id: entry.id, label: entry.label }),
150
+ }),
151
+ },
152
+ })
153
+ }),
154
+ Composition.live(Bar, function* () {
155
+ const add = yield* Event.trigger(Added)
156
+ return mount({ props: {}, slots: {}, events: { add } })
157
+ }),
158
+ Composition.live(ItemComp, function* () {
159
+ const itemProps = yield* ItemComp.props
160
+ const removeTrigger = yield* Event.trigger(Removed)
161
+ // The item binds its id so the wire event carries no payload.
162
+ const remove = (): void => removeTrigger({ id: itemProps.id })
163
+ return mount({
164
+ props: { label: itemProps.label },
165
+ slots: {},
166
+ events: { remove },
167
+ })
168
+ }),
169
+ Reducer.live(AddItem, (current, event) => [...current, { id: event.id, label: event.label }]),
170
+ Reducer.live(RemoveItem, (current, event) => current.filter((entry) => entry.id !== event.id)),
171
+ ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
172
+ return scene(ListComp, { provide: [app] })
173
+ }
174
+ export const listScene: typeof makeListScene = makeListScene
@@ -0,0 +1,64 @@
1
+ import { Layer, Option, Schema as S } from 'effect'
2
+ import {
3
+ Composition,
4
+ Engine,
5
+ State,
6
+ StateGroup,
7
+ Ui,
8
+ mount,
9
+ provide,
10
+ scene,
11
+ type CapturedScene,
12
+ type DerivedContract,
13
+ ui,
14
+ } from '@playfast/reform'
15
+ import { type EngineServices, type WiredUi } from '@playfast/reform/internal'
16
+
17
+ const LabelBase: State.StateDefinition<
18
+ 'label',
19
+ Option.Option<string>,
20
+ S.OptionFromSelf<typeof S.String>
21
+ > = State.make('label', S.OptionFromSelf(S.String))
22
+ class Label extends LabelBase {}
23
+ class Labels extends StateGroup.make(Label) {}
24
+ const OptionalUiBase: WiredUi<
25
+ DerivedContract<{ props: S.Struct<{ label: S.Option<typeof S.String> }> }>,
26
+ 'Optional'
27
+ > = ui('Optional', { props: S.Struct({ label: S.Option(S.String) }) })
28
+ export class OptionalUi extends OptionalUiBase {}
29
+
30
+ class Optional extends Composition.make('Optional', {
31
+ title: 'Optional',
32
+ ui: OptionalUi,
33
+ states: [Label],
34
+ })<Optional>() {}
35
+
36
+ type OptionalScene = CapturedScene<
37
+ typeof OptionalUi.Contract,
38
+ readonly [typeof Label],
39
+ | EngineServices
40
+ | State.StateStore<'label', Option.Option<string>>
41
+ | Ui.UiService<'Optional'>
42
+ | Composition.CompositionId<'Optional', Optional, never>,
43
+ unknown,
44
+ 'Optional',
45
+ Optional
46
+ >
47
+
48
+ const makeOptionScene = (initial: Option.Option<string> = Option.some('hi')): OptionalScene => {
49
+ const presentation = Layer.mergeAll(
50
+ provide(
51
+ OptionalUi,
52
+ Ui.make(OptionalUi, () => null),
53
+ ),
54
+ StateGroup.live(Labels, { label: initial }),
55
+ )
56
+ const app = Layer.mergeAll(
57
+ Composition.live(Optional, function* () {
58
+ const label = yield* StateGroup.select(Labels, 'label')
59
+ return mount({ props: { label }, slots: {} })
60
+ }),
61
+ ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
62
+ return scene(Optional, { provide: [app] })
63
+ }
64
+ export const optionScene: typeof makeOptionScene = makeOptionScene