@playfast/reform-remote 1.0.2 → 1.2.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/README.md +16 -16
  2. package/dist/client.d.ts +33 -0
  3. package/dist/client.d.ts.map +1 -0
  4. package/dist/client.js +80 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/index.d.ts +11 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +11 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/memory.d.ts +7 -0
  11. package/dist/memory.d.ts.map +1 -0
  12. package/dist/memory.js +21 -0
  13. package/dist/memory.js.map +1 -0
  14. package/dist/react.d.ts +15 -0
  15. package/dist/react.d.ts.map +1 -0
  16. package/dist/react.js +11 -0
  17. package/dist/react.js.map +1 -0
  18. package/dist/server.d.ts +16 -0
  19. package/dist/server.d.ts.map +1 -0
  20. package/dist/server.js +56 -0
  21. package/dist/server.js.map +1 -0
  22. package/dist/transport.d.ts +53 -0
  23. package/dist/transport.d.ts.map +1 -0
  24. package/dist/transport.js +171 -0
  25. package/dist/transport.js.map +1 -0
  26. package/dist/wireNode.d.ts +24 -0
  27. package/dist/wireNode.d.ts.map +1 -0
  28. package/dist/wireNode.js +80 -0
  29. package/dist/wireNode.js.map +1 -0
  30. package/dist/wireRender.d.ts +20 -0
  31. package/dist/wireRender.d.ts.map +1 -0
  32. package/dist/wireRender.js +97 -0
  33. package/dist/wireRender.js.map +1 -0
  34. package/package.json +34 -22
  35. package/src/client-keyed-slot.test.ts +19 -6
  36. package/src/client-remount.test.ts +14 -5
  37. package/src/client.ts +74 -41
  38. package/src/counterFixtures.ts +142 -0
  39. package/src/fixtures.ts +10 -247
  40. package/src/listFixtures.ts +174 -0
  41. package/src/optionalFixtures.ts +64 -0
  42. package/src/react.ts +1 -6
  43. package/src/remote-view.typecheck.ts +3 -2
  44. package/src/server.test.ts +26 -12
  45. package/src/server.ts +150 -211
  46. package/src/slottedFixtures.ts +120 -0
  47. package/src/transport.test.ts +2 -1
  48. package/src/transport.ts +88 -24
  49. package/src/wireNode.ts +153 -0
  50. package/src/wireRender.ts +235 -0
  51. package/src/connect.ts +0 -44
  52. package/src/contract.ts +0 -3
  53. package/src/remote-server.ts +0 -109
  54. package/src/transport.types.ts +0 -29
@@ -0,0 +1,142 @@
1
+ import { Duration, Effect, Layer, Schema as S } from 'effect'
2
+ import {
3
+ Channel,
4
+ Composition,
5
+ Engine,
6
+ Event,
7
+ Procedure,
8
+ Reducer,
9
+ State,
10
+ StateGroup,
11
+ Ui,
12
+ mount,
13
+ provide,
14
+ scene,
15
+ type CapturedScene,
16
+ type DerivedContract,
17
+ type EventOf,
18
+ ui,
19
+ } from '@playfast/reform'
20
+ import { type EngineServices, type WiredUi } from '@playfast/reform/internal'
21
+
22
+ const CountBase: State.StateDefinition<'count', number, typeof S.Number> = State.make(
23
+ 'count',
24
+ S.Number,
25
+ )
26
+ class Count extends CountBase {}
27
+ class Counters extends StateGroup.make(Count) {}
28
+ class Bumped extends Event.make('Bumped', S.Struct({ by: S.Number })) {}
29
+ class Bump extends Reducer.make('Bump', {
30
+ states: [Count],
31
+ events: [Bumped],
32
+ }) {}
33
+ const CounterUiBase: WiredUi<
34
+ DerivedContract<{
35
+ props: S.Struct<{ count: typeof S.Number }>
36
+ events: { bump: S.Struct<{ by: typeof S.Number }> }
37
+ }>,
38
+ 'Counter'
39
+ > = ui('Counter', {
40
+ props: S.Struct({ count: S.Number }),
41
+ events: { bump: S.Struct({ by: S.Number }) },
42
+ })
43
+ export class CounterUi extends CounterUiBase {}
44
+
45
+ class Counter extends Composition.make('Counter', {
46
+ title: 'Counter',
47
+ ui: CounterUi,
48
+ states: [Count],
49
+ })<Counter>() {}
50
+
51
+ type CounterScene = CapturedScene<
52
+ typeof CounterUi.Contract,
53
+ readonly [typeof Count],
54
+ | EngineServices
55
+ | State.StateStore<'count', number>
56
+ | Ui.UiService<'Counter'>
57
+ | Composition.CompositionId<'Counter', Counter, never>,
58
+ unknown,
59
+ 'Counter',
60
+ Counter
61
+ >
62
+
63
+ export const bumpedBy = (amount: number): EventOf<'Bumped', { by: number }> =>
64
+ Event.construct(Bumped, { by: amount })
65
+
66
+ const makeCounterScene = (
67
+ boot?: ReadonlyArray<EventOf<'Bumped', { by: number }>>,
68
+ ): CounterScene => {
69
+ const presentation = Layer.mergeAll(
70
+ provide(
71
+ CounterUi,
72
+ Ui.make(CounterUi, ({ count }) => `count:${count}`),
73
+ ),
74
+ StateGroup.live(Counters, { count: 0 }),
75
+ )
76
+ const app = Layer.mergeAll(
77
+ Composition.live(Counter, function* () {
78
+ const count = yield* StateGroup.select(Counters, 'count')
79
+ const bump = yield* Event.trigger(Bumped)
80
+ return mount({ props: { count }, slots: {}, events: { bump } })
81
+ }),
82
+ Reducer.live(Bump, (count, event) => count + event.by),
83
+ ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
84
+ return scene(Counter, boot === undefined ? { provide: [app] } : { provide: [app], boot })
85
+ }
86
+ export const counterScene: typeof makeCounterScene = makeCounterScene
87
+
88
+ const makeStructureCounterScene = (): CounterScene => {
89
+ const presentation = Layer.mergeAll(
90
+ provide(
91
+ CounterUi,
92
+ Ui.make(CounterUi, ({ count }) => `count:${count}`),
93
+ ),
94
+ StateGroup.live(Counters, { count: 0 }),
95
+ )
96
+ const app = Layer.mergeAll(
97
+ Composition.live(Counter, function* () {
98
+ const count = yield* StateGroup.select(Counters, 'count')
99
+ const bump = yield* Event.trigger(Bumped)
100
+ return mount({ props: { count }, slots: {}, events: { bump } })
101
+ }),
102
+ Reducer.live(Bump, (count, event) => count + event.by),
103
+ ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
104
+ return scene(Counter, { provide: [app] })
105
+ }
106
+ export const structureCounterScene: typeof makeStructureCounterScene = makeStructureCounterScene
107
+
108
+ class Kick extends Event.make('Kick', S.Struct({ to: S.Number })) {}
109
+ class Loader extends Channel.make('Loader', { policy: { _tag: 'latest' } }) {}
110
+ class DelayedBump extends Procedure.make('DelayedBump', {
111
+ channel: Loader,
112
+ events: [Kick],
113
+ }) {}
114
+
115
+ const makeAsyncCounterScene = (delay: Duration.DurationInput = '40 millis'): CounterScene => {
116
+ const presentation = Layer.mergeAll(
117
+ provide(
118
+ CounterUi,
119
+ Ui.make(CounterUi, ({ count }) => `count:${count}`),
120
+ ),
121
+ StateGroup.live(Counters, { count: 0 }),
122
+ )
123
+ const app = Layer.mergeAll(
124
+ Composition.live(Counter, function* () {
125
+ const count = yield* StateGroup.select(Counters, 'count')
126
+ const bump = yield* Event.trigger(Bumped)
127
+ return mount({ props: { count }, slots: {}, events: { bump } })
128
+ }),
129
+ Reducer.live(Bump, (count, event) => count + event.by),
130
+ Procedure.live(DelayedBump, function* (event) {
131
+ yield* Effect.sleep(delay)
132
+ yield* Event.dispatch(Bumped, { by: event.to })
133
+ }),
134
+ // Without Channel.live the boot Kick is dropped.
135
+ Channel.live(Loader),
136
+ ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
137
+ return scene(Counter, {
138
+ provide: [app],
139
+ boot: [Event.construct(Kick, { to: 7 })],
140
+ })
141
+ }
142
+ export const asyncCounterScene: typeof makeAsyncCounterScene = makeAsyncCounterScene
package/src/fixtures.ts CHANGED
@@ -1,247 +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 DerivedContract,
7
- Engine,
8
- Event,
9
- type EventOf,
10
- Procedure,
11
- Props,
12
- Reducer,
13
- type Scene,
14
- State,
15
- StateGroup,
16
- Ui,
17
- type WiredUi,
18
- each,
19
- mount,
20
- one,
21
- provide,
22
- scene,
23
- slot,
24
- ui,
25
- } from '@playfast/reform'
26
-
27
- class Count extends State.make('count', S.Number) {}
28
- class Counters extends StateGroup.make(Count) {}
29
- class Bumped extends Event.make('Bumped', S.Struct({ by: S.Number })) {}
30
- class Bump extends Reducer.make('Bump', { states: [Count], events: [Bumped] }) {}
31
- const CounterUiBase: WiredUi<
32
- DerivedContract<{
33
- props: S.Struct<{ count: typeof S.Number }>
34
- events: { bump: S.Struct<{ by: typeof S.Number }> }
35
- }>
36
- > = ui('Counter', {
37
- props: S.Struct({ count: S.Number }),
38
- events: { bump: S.Struct({ by: S.Number }) },
39
- })
40
- export class CounterUi extends CounterUiBase {}
41
- class Counter extends Composition.make('Counter', { title: 'Counter', ui: CounterUi, states: [Count] }) {}
42
-
43
- export const bumpedBy = (amount: number): EventOf<'Bumped', { by: number }> =>
44
- Event.construct(Bumped, { by: amount })
45
-
46
- export const counterScene = (boot?: ReadonlyArray<EventOf<'Bumped', { by: number }>>): Scene => {
47
- const presentation = Layer.mergeAll(
48
- provide(CounterUi, Ui.make(CounterUi, ({ count }) => `count:${count}`)),
49
- StateGroup.live(Counters, { count: 0 }),
50
- )
51
- const app = Layer.mergeAll(
52
- Composition.live(Counter, function* () {
53
- const count = yield* StateGroup.select(Counters, 'count')
54
- const bump = yield* Event.trigger(Bumped)
55
- return mount({ props: { count }, slots: {}, events: { bump } })
56
- }),
57
- Reducer.live(Bump, (count, event) => count + event.by),
58
- ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
59
- return scene(Counter, boot === undefined ? { provide: [app] } : { provide: [app], boot })
60
- }
61
-
62
- export const structureCounterScene = (): Scene => {
63
- const presentation = Layer.mergeAll(
64
- provide(CounterUi, Ui.make(CounterUi, ({ count }) => `count:${count}`)),
65
- StateGroup.live(Counters, { count: 0 }),
66
- )
67
- const app = Layer.mergeAll(
68
- Composition.live(Counter, function* () {
69
- const count = yield* StateGroup.select(Counters, 'count')
70
- const bump = yield* Event.trigger(Bumped)
71
- return mount({ props: { count }, slots: {}, events: { bump } })
72
- }),
73
- Reducer.live(Bump, (count, event) => count + event.by),
74
- ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
75
- return scene(Counter, { provide: [app] })
76
- }
77
-
78
- class Label extends State.make('label', S.OptionFromSelf(S.String)) {}
79
- class Labels extends StateGroup.make(Label) {}
80
- const OptionalUiBase: WiredUi<
81
- DerivedContract<{ props: S.Struct<{ label: S.Option<typeof S.String> }> }>
82
- > = ui('Optional', { props: S.Struct({ label: S.Option(S.String) }) })
83
- export class OptionalUi extends OptionalUiBase {}
84
- class Optional extends Composition.make('Optional', { title: 'Optional', ui: OptionalUi, states: [Label] }) {}
85
-
86
- export const optionScene = (initial: Option.Option<string> = Option.some('hi')): Scene => {
87
- const presentation = Layer.mergeAll(
88
- provide(OptionalUi, Ui.make(OptionalUi, () => null)),
89
- StateGroup.live(Labels, { label: initial }),
90
- )
91
- const app = Layer.mergeAll(
92
- Composition.live(Optional, function* () {
93
- const label = yield* StateGroup.select(Labels, 'label')
94
- return mount({ props: { label }, slots: {} })
95
- }),
96
- ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
97
- return scene(Optional, { provide: [app] })
98
- }
99
-
100
- class Greeting extends State.make('greeting', S.String) {}
101
- class Greeted extends Event.make('Greeted', S.Struct({ text: S.String })) {}
102
- class Cleared extends Event.make('Cleared', S.Struct({})) {}
103
- class SetGreeting extends Reducer.make('SetGreeting', { states: [Greeting], events: [Greeted] }) {}
104
- class ClearGreeting extends Reducer.make('ClearGreeting', { states: [Greeting], events: [Cleared] }) {}
105
- class PanelUi extends ui('Panel', {
106
- props: S.Struct({ greeting: S.String }),
107
- events: { greet: S.Struct({ text: S.String }), clear: S.Struct({}) },
108
- }) {}
109
- class Panel extends Composition.make('Panel', { title: 'Panel', ui: PanelUi, states: [Greeting] }) {}
110
- class MainSlot extends slot('Main')<typeof Panel>() {}
111
- class ShellUi extends ui('Shell')<{ props: {}; slots: { Main: MainSlot } }>() {}
112
- class Shell extends Composition.make('Shell', { title: 'Shell', slots: { Main: MainSlot }, ui: ShellUi }) {}
113
-
114
- export const slottedScene = (): Scene => {
115
- const presentation = Layer.mergeAll(
116
- provide(ShellUi, Ui.make(ShellUi, (_props, slots) => createElement(slots.Main, {}))),
117
- provide(PanelUi, Ui.make(PanelUi, ({ greeting }) => greeting)),
118
- provide(MainSlot, Panel),
119
- State.live(Greeting, 'hi'),
120
- )
121
- const app = Layer.mergeAll(
122
- Composition.live(Shell, function* () {
123
- yield* Effect.void
124
- return mount({ props: {}, slots: { Main: one({}) } })
125
- }),
126
- Composition.live(Panel, function* () {
127
- const greeting = yield* Greeting
128
- const greet = yield* Event.trigger(Greeted)
129
- const clearTrigger = yield* Event.trigger(Cleared)
130
- const clear = (): void => clearTrigger({})
131
- return mount({ props: { greeting }, slots: {}, events: { greet, clear } })
132
- }),
133
- Reducer.live(SetGreeting, (_greeting, event) => event.text),
134
- Reducer.live(ClearGreeting, () => ''),
135
- ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
136
- return scene(Shell, { provide: [app] })
137
- }
138
-
139
- const ListItem = S.Struct({ id: S.String, label: S.String })
140
- class Items extends State.make('items', S.Array(ListItem)) {}
141
- class Added extends Event.make('Added', ListItem) {}
142
- class Removed extends Event.make('Removed', S.Struct({ id: S.String })) {}
143
- class AddItem extends Reducer.make('AddItem', { states: [Items], events: [Added] }) {}
144
- class RemoveItem extends Reducer.make('RemoveItem', { states: [Items], events: [Removed] }) {}
145
-
146
- class BarUi extends ui('Bar', { props: S.Struct({}), events: { add: ListItem } }) {}
147
- class Bar extends Composition.make('Bar', { title: 'Bar', ui: BarUi }) {}
148
- class ItemUi extends ui('Item', { props: S.Struct({ label: S.String }), events: { remove: S.Struct({}) } }) {}
149
- class ItemComp extends Composition.make('Item', {
150
- title: 'Item',
151
- ui: ItemUi,
152
- props: ListItem,
153
- }) {}
154
- class BarSlot extends slot('Bar')<typeof Bar>() {}
155
- class ItemSlot extends slot('Item')<typeof ItemComp>() {}
156
- class ListUi extends ui('List')<{
157
- props: { items: ReadonlyArray<{ id: string; label: string }> }
158
- slots: { Bar: BarSlot; Item: ItemSlot }
159
- }>() {}
160
- class ListComp extends Composition.make('List', {
161
- title: 'List',
162
- slots: { Bar: BarSlot, Item: ItemSlot },
163
- ui: ListUi,
164
- states: [Items],
165
- }) {}
166
-
167
- export const listScene = (
168
- initial: ReadonlyArray<{ id: string; label: string }> = [
169
- { id: 'a', label: 'A' },
170
- { id: 'b', label: 'B' },
171
- ],
172
- ): Scene => {
173
- const presentation = Layer.mergeAll(
174
- provide(
175
- ListUi,
176
- Ui.make(ListUi, (_props, slots) =>
177
- createElement(
178
- Fragment,
179
- null,
180
- createElement(slots.Bar, {}),
181
- createElement(slots.Item, {}),
182
- ),
183
- ),
184
- ),
185
- provide(BarUi, Ui.make(BarUi, () => null)),
186
- provide(ItemUi, Ui.make(ItemUi, ({ label }) => label)),
187
- provide(BarSlot, Bar),
188
- provide(ItemSlot, ItemComp),
189
- State.live(Items, initial),
190
- )
191
- const app = Layer.mergeAll(
192
- Composition.live(ListComp, function* () {
193
- const listItems = yield* Items
194
- return mount({
195
- props: { items: listItems },
196
- slots: {
197
- Bar: one({}),
198
- Item: each(listItems, {
199
- key: (entry) => entry.id,
200
- props: (entry) => ({ id: entry.id, label: entry.label }),
201
- }),
202
- },
203
- })
204
- }),
205
- Composition.live(Bar, function* () {
206
- const add = yield* Event.trigger(Added)
207
- return mount({ props: {}, slots: {}, events: { add } })
208
- }),
209
- Composition.live(ItemComp, function* () {
210
- const itemProps = S.decodeUnknownSync(ListItem)(yield* Props)
211
- const removeTrigger = yield* Event.trigger(Removed)
212
- // Item binds its own id into remove so the wire event has empty payload.
213
- const remove = (): void => removeTrigger({ id: itemProps.id })
214
- return mount({ props: { label: itemProps.label }, slots: {}, events: { remove } })
215
- }),
216
- Reducer.live(AddItem, (current, event) => [...current, { id: event.id, label: event.label }]),
217
- Reducer.live(RemoveItem, (current, event) => current.filter((entry) => entry.id !== event.id)),
218
- ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
219
- return scene(ListComp, { provide: [app] })
220
- }
221
-
222
- // Boot Kick → DelayedBump sleeps past opening snapshot, then Bumped — only path for server-pushed diffs.
223
- class Kick extends Event.make('Kick', S.Struct({ to: S.Number })) {}
224
- class Loader extends Channel.make('Loader', { policy: { _tag: 'latest' } }) {}
225
- class DelayedBump extends Procedure.make('DelayedBump', { channel: Loader, events: [Kick] }) {}
226
-
227
- export const asyncCounterScene = (delay: Duration.DurationInput = '40 millis'): Scene => {
228
- const presentation = Layer.mergeAll(
229
- provide(CounterUi, Ui.make(CounterUi, ({ count }) => `count:${count}`)),
230
- StateGroup.live(Counters, { count: 0 }),
231
- )
232
- const app = Layer.mergeAll(
233
- Composition.live(Counter, function* () {
234
- const count = yield* StateGroup.select(Counters, 'count')
235
- const bump = yield* Event.trigger(Bumped)
236
- return mount({ props: { count }, slots: {}, events: { bump } })
237
- }),
238
- Reducer.live(Bump, (count, event) => count + event.by),
239
- Procedure.live(DelayedBump, function* (event) {
240
- yield* Effect.sleep(delay)
241
- yield* Event.dispatch(Bumped, { by: event.to })
242
- }),
243
- // Without Channel.live the boot Kick is dropped.
244
- Channel.live(Loader),
245
- ).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
246
- return scene(Counter, { provide: [app], boot: [Event.construct(Kick, { to: 7 })] })
247
- }
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
package/src/react.ts CHANGED
@@ -1,10 +1,5 @@
1
1
  import { type ReactNode, useEffect, useState, useSyncExternalStore } from 'react'
2
- import {
3
- connect,
4
- type InvokeMessage,
5
- type RemoteTransport,
6
- type ServerMessage,
7
- } from './transport'
2
+ import { connect, type InvokeMessage, type RemoteTransport, type ServerMessage } from './transport'
8
3
  import type { RemoteContract, RemoteViewSet } from './client'
9
4
 
10
5
  export const useRemoteUI = <C extends RemoteContract>(
@@ -31,8 +31,9 @@ Ui.make(FooUi, (_props, _slots, events) => {
31
31
  return null
32
32
  })
33
33
 
34
- class FooComp extends Composition.make('FooComp', { title: 'Foo', ui: FooUi }) {}
35
- class FooSlot extends slot('Foo')<typeof FooComp>() {}
34
+ class FooComp extends Composition.make('FooComp', { title: 'Foo', ui: FooUi })<FooComp>() {}
35
+
36
+ class FooSlot extends slot('Foo')<FooSlot, typeof FooComp>() {}
36
37
  class ShellUi extends ui('Shell', { props: S.Struct({}), slots: { Main: FooSlot } }) {}
37
38
 
38
39
  const shellView = Ui.make(ShellUi, (_props, slots) => createElement(slots.Main, {}))