@playfast/reform-remote 1.0.2 → 1.1.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.
- package/README.md +16 -16
- package/package.json +18 -18
- package/src/client-keyed-slot.test.ts +19 -6
- package/src/client-remount.test.ts +14 -5
- package/src/client.ts +74 -41
- package/src/fixtures.ts +266 -60
- package/src/react.ts +1 -6
- package/src/remote-view.typecheck.ts +3 -2
- package/src/server.test.ts +26 -12
- package/src/server.ts +362 -85
- package/src/transport.test.ts +2 -1
- package/src/transport.ts +88 -24
- package/src/connect.ts +0 -44
- package/src/contract.ts +0 -3
- package/src/remote-server.ts +0 -109
- package/src/transport.types.ts +0 -29
package/src/fixtures.ts
CHANGED
|
@@ -3,49 +3,83 @@ import { Duration, Effect, Layer, Option, Schema as S } from 'effect'
|
|
|
3
3
|
import {
|
|
4
4
|
Channel,
|
|
5
5
|
Composition,
|
|
6
|
+
type CapturedScene,
|
|
6
7
|
type DerivedContract,
|
|
7
8
|
Engine,
|
|
8
9
|
Event,
|
|
9
10
|
type EventOf,
|
|
10
11
|
Procedure,
|
|
11
|
-
Props,
|
|
12
12
|
Reducer,
|
|
13
|
-
type Scene,
|
|
14
13
|
State,
|
|
15
14
|
StateGroup,
|
|
16
15
|
Ui,
|
|
17
|
-
type WiredUi,
|
|
18
16
|
each,
|
|
19
17
|
mount,
|
|
20
18
|
one,
|
|
21
19
|
provide,
|
|
22
20
|
scene,
|
|
23
21
|
slot,
|
|
22
|
+
type SlotService,
|
|
24
23
|
ui,
|
|
25
24
|
} from '@playfast/reform'
|
|
25
|
+
import {
|
|
26
|
+
type CompositionChildSummary,
|
|
27
|
+
type EngineServices,
|
|
28
|
+
type WiredUi,
|
|
29
|
+
} from '@playfast/reform/internal'
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
const CountBase: State.StateDefinition<'count', number, typeof S.Number> = State.make(
|
|
32
|
+
'count',
|
|
33
|
+
S.Number,
|
|
34
|
+
)
|
|
35
|
+
class Count extends CountBase {}
|
|
28
36
|
class Counters extends StateGroup.make(Count) {}
|
|
29
37
|
class Bumped extends Event.make('Bumped', S.Struct({ by: S.Number })) {}
|
|
30
|
-
class Bump extends Reducer.make('Bump', {
|
|
38
|
+
class Bump extends Reducer.make('Bump', {
|
|
39
|
+
states: [Count],
|
|
40
|
+
events: [Bumped],
|
|
41
|
+
}) {}
|
|
31
42
|
const CounterUiBase: WiredUi<
|
|
32
43
|
DerivedContract<{
|
|
33
44
|
props: S.Struct<{ count: typeof S.Number }>
|
|
34
45
|
events: { bump: S.Struct<{ by: typeof S.Number }> }
|
|
35
|
-
}
|
|
46
|
+
}>,
|
|
47
|
+
'Counter'
|
|
36
48
|
> = ui('Counter', {
|
|
37
49
|
props: S.Struct({ count: S.Number }),
|
|
38
50
|
events: { bump: S.Struct({ by: S.Number }) },
|
|
39
51
|
})
|
|
40
52
|
export class CounterUi extends CounterUiBase {}
|
|
41
|
-
|
|
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
|
+
>
|
|
42
71
|
|
|
43
72
|
export const bumpedBy = (amount: number): EventOf<'Bumped', { by: number }> =>
|
|
44
73
|
Event.construct(Bumped, { by: amount })
|
|
45
74
|
|
|
46
|
-
|
|
75
|
+
const makeCounterScene = (
|
|
76
|
+
boot?: ReadonlyArray<EventOf<'Bumped', { by: number }>>,
|
|
77
|
+
): CounterScene => {
|
|
47
78
|
const presentation = Layer.mergeAll(
|
|
48
|
-
provide(
|
|
79
|
+
provide(
|
|
80
|
+
CounterUi,
|
|
81
|
+
Ui.make(CounterUi, ({ count }) => `count:${count}`),
|
|
82
|
+
),
|
|
49
83
|
StateGroup.live(Counters, { count: 0 }),
|
|
50
84
|
)
|
|
51
85
|
const app = Layer.mergeAll(
|
|
@@ -58,10 +92,14 @@ export const counterScene = (boot?: ReadonlyArray<EventOf<'Bumped', { by: number
|
|
|
58
92
|
).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
|
|
59
93
|
return scene(Counter, boot === undefined ? { provide: [app] } : { provide: [app], boot })
|
|
60
94
|
}
|
|
95
|
+
export const counterScene: typeof makeCounterScene = makeCounterScene
|
|
61
96
|
|
|
62
|
-
|
|
97
|
+
const makeStructureCounterScene = (): CounterScene => {
|
|
63
98
|
const presentation = Layer.mergeAll(
|
|
64
|
-
provide(
|
|
99
|
+
provide(
|
|
100
|
+
CounterUi,
|
|
101
|
+
Ui.make(CounterUi, ({ count }) => `count:${count}`),
|
|
102
|
+
),
|
|
65
103
|
StateGroup.live(Counters, { count: 0 }),
|
|
66
104
|
)
|
|
67
105
|
const app = Layer.mergeAll(
|
|
@@ -74,18 +112,45 @@ export const structureCounterScene = (): Scene => {
|
|
|
74
112
|
).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
|
|
75
113
|
return scene(Counter, { provide: [app] })
|
|
76
114
|
}
|
|
115
|
+
export const structureCounterScene: typeof makeStructureCounterScene = makeStructureCounterScene
|
|
77
116
|
|
|
78
|
-
|
|
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 {}
|
|
79
123
|
class Labels extends StateGroup.make(Label) {}
|
|
80
124
|
const OptionalUiBase: WiredUi<
|
|
81
|
-
DerivedContract<{ props: S.Struct<{ label: S.Option<typeof S.String> }> }
|
|
125
|
+
DerivedContract<{ props: S.Struct<{ label: S.Option<typeof S.String> }> }>,
|
|
126
|
+
'Optional'
|
|
82
127
|
> = ui('Optional', { props: S.Struct({ label: S.Option(S.String) }) })
|
|
83
128
|
export class OptionalUi extends OptionalUiBase {}
|
|
84
|
-
class Optional extends Composition.make('Optional', { title: 'Optional', ui: OptionalUi, states: [Label] }) {}
|
|
85
129
|
|
|
86
|
-
|
|
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 => {
|
|
87
149
|
const presentation = Layer.mergeAll(
|
|
88
|
-
provide(
|
|
150
|
+
provide(
|
|
151
|
+
OptionalUi,
|
|
152
|
+
Ui.make(OptionalUi, () => null),
|
|
153
|
+
),
|
|
89
154
|
StateGroup.live(Labels, { label: initial }),
|
|
90
155
|
)
|
|
91
156
|
const app = Layer.mergeAll(
|
|
@@ -96,25 +161,82 @@ export const optionScene = (initial: Option.Option<string> = Option.some('hi')):
|
|
|
96
161
|
).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
|
|
97
162
|
return scene(Optional, { provide: [app] })
|
|
98
163
|
}
|
|
164
|
+
export const optionScene: typeof makeOptionScene = makeOptionScene
|
|
99
165
|
|
|
100
|
-
|
|
166
|
+
const GreetingBase: State.StateDefinition<'greeting', string, typeof S.String> = State.make(
|
|
167
|
+
'greeting',
|
|
168
|
+
S.String,
|
|
169
|
+
)
|
|
170
|
+
class Greeting extends GreetingBase {}
|
|
101
171
|
class Greeted extends Event.make('Greeted', S.Struct({ text: S.String })) {}
|
|
102
172
|
class Cleared extends Event.make('Cleared', S.Struct({})) {}
|
|
103
|
-
class SetGreeting extends Reducer.make('SetGreeting', {
|
|
104
|
-
|
|
105
|
-
|
|
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', {
|
|
106
191
|
props: S.Struct({ greeting: S.String }),
|
|
107
192
|
events: { greet: S.Struct({ text: S.String }), clear: S.Struct({}) },
|
|
108
|
-
})
|
|
109
|
-
class
|
|
110
|
-
|
|
111
|
-
class
|
|
112
|
-
|
|
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 {}
|
|
113
208
|
|
|
114
|
-
|
|
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 => {
|
|
115
231
|
const presentation = Layer.mergeAll(
|
|
116
|
-
provide(
|
|
117
|
-
|
|
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
|
+
),
|
|
118
240
|
provide(MainSlot, Panel),
|
|
119
241
|
State.live(Greeting, 'hi'),
|
|
120
242
|
)
|
|
@@ -128,62 +250,132 @@ export const slottedScene = (): Scene => {
|
|
|
128
250
|
const greet = yield* Event.trigger(Greeted)
|
|
129
251
|
const clearTrigger = yield* Event.trigger(Cleared)
|
|
130
252
|
const clear = (): void => clearTrigger({})
|
|
131
|
-
return mount({
|
|
253
|
+
return mount({
|
|
254
|
+
props: { greeting },
|
|
255
|
+
slots: {},
|
|
256
|
+
events: { greet, clear },
|
|
257
|
+
})
|
|
132
258
|
}),
|
|
133
259
|
Reducer.live(SetGreeting, (_greeting, event) => event.text),
|
|
134
260
|
Reducer.live(ClearGreeting, () => ''),
|
|
135
261
|
).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
|
|
136
262
|
return scene(Shell, { provide: [app] })
|
|
137
263
|
}
|
|
264
|
+
export const slottedScene: typeof makeSlottedScene = makeSlottedScene
|
|
138
265
|
|
|
139
|
-
const ListItem
|
|
140
|
-
|
|
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 {}
|
|
141
276
|
class Added extends Event.make('Added', ListItem) {}
|
|
142
277
|
class Removed extends Event.make('Removed', S.Struct({ id: S.String })) {}
|
|
143
|
-
class AddItem extends Reducer.make('AddItem', {
|
|
144
|
-
|
|
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 {}
|
|
145
311
|
|
|
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
312
|
class ItemComp extends Composition.make('Item', {
|
|
150
313
|
title: 'Item',
|
|
151
314
|
ui: ItemUi,
|
|
152
315
|
props: ListItem,
|
|
153
|
-
}) {}
|
|
154
|
-
|
|
155
|
-
class
|
|
156
|
-
|
|
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')<{
|
|
157
328
|
props: { items: ReadonlyArray<{ id: string; label: string }> }
|
|
158
329
|
slots: { Bar: BarSlot; Item: ItemSlot }
|
|
159
|
-
}>()
|
|
330
|
+
}>()
|
|
331
|
+
class ListUi extends ListUiBase {}
|
|
332
|
+
|
|
160
333
|
class ListComp extends Composition.make('List', {
|
|
161
334
|
title: 'List',
|
|
162
335
|
slots: { Bar: BarSlot, Item: ItemSlot },
|
|
163
336
|
ui: ListUi,
|
|
164
337
|
states: [Items],
|
|
165
|
-
}) {}
|
|
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
|
+
>
|
|
166
357
|
|
|
167
|
-
|
|
358
|
+
const makeListScene = (
|
|
168
359
|
initial: ReadonlyArray<{ id: string; label: string }> = [
|
|
169
360
|
{ id: 'a', label: 'A' },
|
|
170
361
|
{ id: 'b', label: 'B' },
|
|
171
362
|
],
|
|
172
|
-
):
|
|
363
|
+
): ListScene => {
|
|
173
364
|
const presentation = Layer.mergeAll(
|
|
174
365
|
provide(
|
|
175
366
|
ListUi,
|
|
176
367
|
Ui.make(ListUi, (_props, slots) =>
|
|
177
|
-
createElement(
|
|
178
|
-
Fragment,
|
|
179
|
-
null,
|
|
180
|
-
createElement(slots.Bar, {}),
|
|
181
|
-
createElement(slots.Item, {}),
|
|
182
|
-
),
|
|
368
|
+
createElement(Fragment, null, createElement(slots.Bar, {}), createElement(slots.Item, {})),
|
|
183
369
|
),
|
|
184
370
|
),
|
|
185
|
-
provide(
|
|
186
|
-
|
|
371
|
+
provide(
|
|
372
|
+
BarUi,
|
|
373
|
+
Ui.make(BarUi, () => null),
|
|
374
|
+
),
|
|
375
|
+
provide(
|
|
376
|
+
ItemUi,
|
|
377
|
+
Ui.make(ItemUi, ({ label }) => label),
|
|
378
|
+
),
|
|
187
379
|
provide(BarSlot, Bar),
|
|
188
380
|
provide(ItemSlot, ItemComp),
|
|
189
381
|
State.live(Items, initial),
|
|
@@ -207,26 +399,36 @@ export const listScene = (
|
|
|
207
399
|
return mount({ props: {}, slots: {}, events: { add } })
|
|
208
400
|
}),
|
|
209
401
|
Composition.live(ItemComp, function* () {
|
|
210
|
-
const itemProps =
|
|
402
|
+
const itemProps = yield* ItemComp.props
|
|
211
403
|
const removeTrigger = yield* Event.trigger(Removed)
|
|
212
|
-
//
|
|
404
|
+
// The item binds its id so the wire event carries no payload.
|
|
213
405
|
const remove = (): void => removeTrigger({ id: itemProps.id })
|
|
214
|
-
return mount({
|
|
406
|
+
return mount({
|
|
407
|
+
props: { label: itemProps.label },
|
|
408
|
+
slots: {},
|
|
409
|
+
events: { remove },
|
|
410
|
+
})
|
|
215
411
|
}),
|
|
216
412
|
Reducer.live(AddItem, (current, event) => [...current, { id: event.id, label: event.label }]),
|
|
217
413
|
Reducer.live(RemoveItem, (current, event) => current.filter((entry) => entry.id !== event.id)),
|
|
218
414
|
).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
|
|
219
415
|
return scene(ListComp, { provide: [app] })
|
|
220
416
|
}
|
|
417
|
+
export const listScene: typeof makeListScene = makeListScene
|
|
221
418
|
|
|
222
|
-
// Boot Kick → DelayedBump sleeps past opening snapshot, then Bumped — only path for server-pushed diffs.
|
|
223
419
|
class Kick extends Event.make('Kick', S.Struct({ to: S.Number })) {}
|
|
224
420
|
class Loader extends Channel.make('Loader', { policy: { _tag: 'latest' } }) {}
|
|
225
|
-
class DelayedBump extends Procedure.make('DelayedBump', {
|
|
421
|
+
class DelayedBump extends Procedure.make('DelayedBump', {
|
|
422
|
+
channel: Loader,
|
|
423
|
+
events: [Kick],
|
|
424
|
+
}) {}
|
|
226
425
|
|
|
227
|
-
|
|
426
|
+
const makeAsyncCounterScene = (delay: Duration.DurationInput = '40 millis'): CounterScene => {
|
|
228
427
|
const presentation = Layer.mergeAll(
|
|
229
|
-
provide(
|
|
428
|
+
provide(
|
|
429
|
+
CounterUi,
|
|
430
|
+
Ui.make(CounterUi, ({ count }) => `count:${count}`),
|
|
431
|
+
),
|
|
230
432
|
StateGroup.live(Counters, { count: 0 }),
|
|
231
433
|
)
|
|
232
434
|
const app = Layer.mergeAll(
|
|
@@ -243,5 +445,9 @@ export const asyncCounterScene = (delay: Duration.DurationInput = '40 millis'):
|
|
|
243
445
|
// Without Channel.live the boot Kick is dropped.
|
|
244
446
|
Channel.live(Loader),
|
|
245
447
|
).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
|
|
246
|
-
return scene(Counter, {
|
|
448
|
+
return scene(Counter, {
|
|
449
|
+
provide: [app],
|
|
450
|
+
boot: [Event.construct(Kick, { to: 7 })],
|
|
451
|
+
})
|
|
247
452
|
}
|
|
453
|
+
export const asyncCounterScene: typeof makeAsyncCounterScene = makeAsyncCounterScene
|
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
|
-
|
|
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, {}))
|
package/src/server.test.ts
CHANGED
|
@@ -2,8 +2,14 @@ import { createElement, Fragment, type ReactNode } from 'react'
|
|
|
2
2
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
3
|
import { expect, test } from 'vitest'
|
|
4
4
|
import { Option } from 'effect'
|
|
5
|
-
import { Ui
|
|
6
|
-
import
|
|
5
|
+
import { Ui } from '@playfast/reform'
|
|
6
|
+
import {
|
|
7
|
+
Wire,
|
|
8
|
+
type WireNode,
|
|
9
|
+
type WirePatch,
|
|
10
|
+
type WireProp,
|
|
11
|
+
type WireTree,
|
|
12
|
+
} from '@playfast/reform/internal'
|
|
7
13
|
import { makeRemoteServer } from './server'
|
|
8
14
|
import { remoteViews, renderWireTree } from './client'
|
|
9
15
|
import {
|
|
@@ -17,15 +23,20 @@ import {
|
|
|
17
23
|
structureCounterScene,
|
|
18
24
|
} from './fixtures'
|
|
19
25
|
|
|
20
|
-
const draw = (node: ReactNode): void =>
|
|
26
|
+
const draw = (node: ReactNode): void =>
|
|
27
|
+
void renderToStaticMarkup(createElement(Fragment, null, node))
|
|
21
28
|
|
|
22
29
|
const dataOf = (node: WireNode, name: string): WireProp | undefined =>
|
|
23
30
|
node.props.find((prop) => prop._tag === 'Data' && prop.name === name)
|
|
24
31
|
|
|
25
32
|
const eventOf = (node: WireNode, name: string): Extract<WireProp, { _tag: 'Event' }> | undefined =>
|
|
26
|
-
node.props.find(
|
|
33
|
+
node.props.find(
|
|
34
|
+
(prop): prop is Extract<WireProp, { _tag: 'Event' }> =>
|
|
35
|
+
prop._tag === 'Event' && prop.name === name,
|
|
36
|
+
)
|
|
27
37
|
|
|
28
|
-
const byId = (tree: WireTree, id: string): WireNode | undefined =>
|
|
38
|
+
const byId = (tree: WireTree, id: string): WireNode | undefined =>
|
|
39
|
+
tree.find((node) => node.id === id)
|
|
29
40
|
|
|
30
41
|
const upsertIds = (patches: ReadonlyArray<WirePatch>): ReadonlyArray<string> =>
|
|
31
42
|
patches.flatMap((patch) => (patch._tag === 'Upsert' ? [patch.node.id] : []))
|
|
@@ -62,7 +73,6 @@ test('invoking a trigger handle dispatches into the runtime and the next render
|
|
|
62
73
|
}
|
|
63
74
|
})
|
|
64
75
|
|
|
65
|
-
|
|
66
76
|
test('a mount(...)-returning body registers its event from structure.events and invoking it dispatches', async () => {
|
|
67
77
|
const server = makeRemoteServer(structureCounterScene())
|
|
68
78
|
try {
|
|
@@ -110,7 +120,10 @@ test('renderDiff streams patches a client folds back to the current tree', async
|
|
|
110
120
|
|
|
111
121
|
test('end to end: client reconstructs props + event callbacks that drive the server', async () => {
|
|
112
122
|
const server = makeRemoteServer(counterScene())
|
|
113
|
-
const captured: {
|
|
123
|
+
const captured: {
|
|
124
|
+
props?: Record<string, unknown>
|
|
125
|
+
events?: Record<string, (p: { by: number }) => void>
|
|
126
|
+
} = {}
|
|
114
127
|
const sent: Array<[string, unknown]> = []
|
|
115
128
|
try {
|
|
116
129
|
const tree = await server.render()
|
|
@@ -149,7 +162,6 @@ test('boot events are applied before the first render', async () => {
|
|
|
149
162
|
}
|
|
150
163
|
})
|
|
151
164
|
|
|
152
|
-
|
|
153
165
|
test('a slotted child renders nested with its own encoded props and handle', async () => {
|
|
154
166
|
const server = makeRemoteServer(slottedScene())
|
|
155
167
|
try {
|
|
@@ -197,16 +209,19 @@ test('two events on one node register distinct handles and each fires independen
|
|
|
197
209
|
expect(eventOf(before, 'clear')?.handle).toBe('0.Main.0:clear')
|
|
198
210
|
|
|
199
211
|
await server.invoke('0.Main.0:greet', { text: 'hello' })
|
|
200
|
-
expect(dataOf(byId(await server.render(), '0.Main.0')!, 'greeting')).toMatchObject({
|
|
212
|
+
expect(dataOf(byId(await server.render(), '0.Main.0')!, 'greeting')).toMatchObject({
|
|
213
|
+
value: 'hello',
|
|
214
|
+
})
|
|
201
215
|
|
|
202
216
|
await server.invoke('0.Main.0:clear', {})
|
|
203
|
-
expect(dataOf(byId(await server.render(), '0.Main.0')!, 'greeting')).toMatchObject({
|
|
217
|
+
expect(dataOf(byId(await server.render(), '0.Main.0')!, 'greeting')).toMatchObject({
|
|
218
|
+
value: '',
|
|
219
|
+
})
|
|
204
220
|
} finally {
|
|
205
221
|
await server.dispose()
|
|
206
222
|
}
|
|
207
223
|
})
|
|
208
224
|
|
|
209
|
-
|
|
210
225
|
test('a list renders one child per item under a single slot, in order', async () => {
|
|
211
226
|
const server = makeRemoteServer(listScene())
|
|
212
227
|
try {
|
|
@@ -255,7 +270,6 @@ test("a removed node's handle is revoked — a stale client invocation rejects",
|
|
|
255
270
|
}
|
|
256
271
|
})
|
|
257
272
|
|
|
258
|
-
|
|
259
273
|
test('an Option prop round-trips through JSON as a real Option (symmetric schema decode)', async () => {
|
|
260
274
|
const server = makeRemoteServer(optionScene(Option.some('hi')))
|
|
261
275
|
const captured: { label?: unknown } = {}
|