@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.
- package/README.md +16 -16
- package/dist/client.d.ts +33 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +80 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/memory.d.ts +7 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +21 -0
- package/dist/memory.js.map +1 -0
- package/dist/react.d.ts +15 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +11 -0
- package/dist/react.js.map +1 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +56 -0
- package/dist/server.js.map +1 -0
- package/dist/transport.d.ts +53 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +171 -0
- package/dist/transport.js.map +1 -0
- package/dist/wireNode.d.ts +24 -0
- package/dist/wireNode.d.ts.map +1 -0
- package/dist/wireNode.js +80 -0
- package/dist/wireNode.js.map +1 -0
- package/dist/wireRender.d.ts +20 -0
- package/dist/wireRender.d.ts.map +1 -0
- package/dist/wireRender.js +97 -0
- package/dist/wireRender.js.map +1 -0
- package/package.json +34 -22
- 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/counterFixtures.ts +142 -0
- package/src/fixtures.ts +10 -247
- package/src/listFixtures.ts +174 -0
- package/src/optionalFixtures.ts +64 -0
- 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 +150 -211
- package/src/slottedFixtures.ts +120 -0
- package/src/transport.test.ts +2 -1
- package/src/transport.ts +88 -24
- package/src/wireNode.ts +153 -0
- package/src/wireRender.ts +235 -0
- 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
|
@@ -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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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, {}))
|