@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/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
- class Count extends State.make('count', S.Number) {}
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', { states: [Count], events: [Bumped] }) {}
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
- class Counter extends Composition.make('Counter', { title: 'Counter', ui: CounterUi, states: [Count] }) {}
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
- export const counterScene = (boot?: ReadonlyArray<EventOf<'Bumped', { by: number }>>): Scene => {
75
+ const makeCounterScene = (
76
+ boot?: ReadonlyArray<EventOf<'Bumped', { by: number }>>,
77
+ ): CounterScene => {
47
78
  const presentation = Layer.mergeAll(
48
- provide(CounterUi, Ui.make(CounterUi, ({ count }) => `count:${count}`)),
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
- export const structureCounterScene = (): Scene => {
97
+ const makeStructureCounterScene = (): CounterScene => {
63
98
  const presentation = Layer.mergeAll(
64
- provide(CounterUi, Ui.make(CounterUi, ({ count }) => `count:${count}`)),
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
- class Label extends State.make('label', S.OptionFromSelf(S.String)) {}
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
- export const optionScene = (initial: Option.Option<string> = Option.some('hi')): Scene => {
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(OptionalUi, Ui.make(OptionalUi, () => null)),
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
- class Greeting extends State.make('greeting', S.String) {}
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', { states: [Greeting], events: [Greeted] }) {}
104
- class ClearGreeting extends Reducer.make('ClearGreeting', { states: [Greeting], events: [Cleared] }) {}
105
- class PanelUi extends ui('Panel', {
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 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 }) {}
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
- export const slottedScene = (): Scene => {
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(ShellUi, Ui.make(ShellUi, (_props, slots) => createElement(slots.Main, {}))),
117
- provide(PanelUi, Ui.make(PanelUi, ({ greeting }) => greeting)),
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({ props: { greeting }, slots: {}, events: { greet, clear } })
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 = S.Struct({ id: S.String, label: S.String })
140
- class Items extends State.make('items', S.Array(ListItem)) {}
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', { states: [Items], events: [Added] }) {}
144
- class RemoveItem extends Reducer.make('RemoveItem', { states: [Items], events: [Removed] }) {}
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
- class BarSlot extends slot('Bar')<typeof Bar>() {}
155
- class ItemSlot extends slot('Item')<typeof ItemComp>() {}
156
- class ListUi extends ui('List')<{
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
- export const listScene = (
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
- ): Scene => {
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(BarUi, Ui.make(BarUi, () => null)),
186
- provide(ItemUi, Ui.make(ItemUi, ({ label }) => label)),
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 = S.decodeUnknownSync(ListItem)(yield* Props)
402
+ const itemProps = yield* ItemComp.props
211
403
  const removeTrigger = yield* Event.trigger(Removed)
212
- // Item binds its own id into remove so the wire event has empty payload.
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({ props: { label: itemProps.label }, slots: {}, events: { remove } })
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', { channel: Loader, events: [Kick] }) {}
421
+ class DelayedBump extends Procedure.make('DelayedBump', {
422
+ channel: Loader,
423
+ events: [Kick],
424
+ }) {}
226
425
 
227
- export const asyncCounterScene = (delay: Duration.DurationInput = '40 millis'): Scene => {
426
+ const makeAsyncCounterScene = (delay: Duration.DurationInput = '40 millis'): CounterScene => {
228
427
  const presentation = Layer.mergeAll(
229
- provide(CounterUi, Ui.make(CounterUi, ({ count }) => `count:${count}`)),
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, { provide: [app], boot: [Event.construct(Kick, { to: 7 })] })
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
- 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, {}))
@@ -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, Wire } from '@playfast/reform'
6
- import type { WireNode, WirePatch, WireProp, WireTree } from '@playfast/reform'
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 => void renderToStaticMarkup(createElement(Fragment, null, node))
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((prop): prop is Extract<WireProp, { _tag: 'Event' }> => prop._tag === 'Event' && prop.name === name)
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 => tree.find((node) => node.id === id)
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: { props?: Record<string, unknown>; events?: Record<string, (p: { by: number }) => void> } = {}
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({ value: 'hello' })
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({ value: '' })
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 } = {}