@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
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 } = {}
|
package/src/server.ts
CHANGED
|
@@ -1,241 +1,180 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Array as Arr,
|
|
3
2
|
Effect,
|
|
4
|
-
|
|
3
|
+
Layer,
|
|
4
|
+
ManagedRuntime,
|
|
5
5
|
Option,
|
|
6
6
|
type ParseResult,
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
PubSub,
|
|
8
|
+
Queue,
|
|
9
9
|
} from 'effect'
|
|
10
|
+
import { type CapturedScene, forceSync, publish, type UiContract } from '@playfast/reform'
|
|
10
11
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
isFeatureBinding,
|
|
15
|
-
isStructure,
|
|
16
|
-
type RenderEnv,
|
|
17
|
-
type Scene,
|
|
18
|
-
type SlotChild,
|
|
19
|
-
type SlotClass,
|
|
20
|
-
type SlotFill,
|
|
21
|
-
type Structure,
|
|
22
|
-
type Trigger,
|
|
12
|
+
type AnyScene,
|
|
13
|
+
Bus,
|
|
14
|
+
Triggers,
|
|
23
15
|
type TriggerRegistryApi,
|
|
24
|
-
|
|
25
|
-
type
|
|
26
|
-
type WireNode,
|
|
27
|
-
type WireProp,
|
|
16
|
+
Wire,
|
|
17
|
+
type WirePatch,
|
|
28
18
|
type WireTree,
|
|
29
|
-
} from '@playfast/reform'
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
interface Mounted {
|
|
39
|
-
readonly comp: CompositionClass<unknown>
|
|
40
|
-
readonly props: unknown
|
|
41
|
-
readonly id: string
|
|
42
|
-
readonly parentId: Option.Option<string>
|
|
43
|
-
readonly slot: Option.Option<string>
|
|
44
|
-
readonly childIndex: number
|
|
45
|
-
readonly key: Option.Option<string>
|
|
46
|
-
}
|
|
19
|
+
} from '@playfast/reform/internal'
|
|
20
|
+
import {
|
|
21
|
+
capturedSlotProvider,
|
|
22
|
+
renderCapturedComposition,
|
|
23
|
+
type RenderSceneToWireOptions,
|
|
24
|
+
renderSceneWith,
|
|
25
|
+
serviceFromContext,
|
|
26
|
+
} from './wireRender'
|
|
27
|
+
import { handlesOf, settleDrain } from './wireNode'
|
|
47
28
|
|
|
48
|
-
|
|
49
|
-
isFeatureBinding(child) ? child.composition : child
|
|
29
|
+
export type { RenderSceneToWireOptions } from './wireRender'
|
|
50
30
|
|
|
51
|
-
//
|
|
52
|
-
const
|
|
31
|
+
// oxlint-disable-next-line reform-rules/prefer-effect-fn -- exported binding: Effect.fn's inferred type isn't portable under isolatedDeclarations
|
|
32
|
+
export const renderSceneToWire = <
|
|
33
|
+
C extends UiContract,
|
|
34
|
+
S extends ReadonlyArray<unknown>,
|
|
35
|
+
Services,
|
|
36
|
+
P,
|
|
37
|
+
N extends string,
|
|
38
|
+
Identity,
|
|
39
|
+
>(
|
|
40
|
+
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
41
|
+
options?: RenderSceneToWireOptions,
|
|
42
|
+
): Effect.Effect<WireTree, ParseResult.ParseError, Services> =>
|
|
43
|
+
Effect.context<Services>().pipe(
|
|
44
|
+
Effect.flatMap((context) =>
|
|
45
|
+
renderSceneWith(
|
|
46
|
+
scene,
|
|
47
|
+
options,
|
|
48
|
+
(slotDefinition) => capturedSlotProvider(context, slotDefinition),
|
|
49
|
+
(composition, props) => renderCapturedComposition(context, composition, props),
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
export interface RemoteServer {
|
|
55
|
+
readonly render: () => Promise<WireTree>
|
|
56
|
+
readonly renderDiff: () => Promise<ReadonlyArray<WirePatch>>
|
|
57
|
+
readonly invoke: (handle: string, encodedPayload: unknown) => Promise<void>
|
|
58
|
+
readonly subscribe: (listener: () => void) => () => void
|
|
59
|
+
readonly currentTree: () => WireTree
|
|
60
|
+
readonly dispose: () => Promise<void>
|
|
61
|
+
}
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
const makeCapturedRemoteServer = <
|
|
64
|
+
C extends UiContract,
|
|
65
|
+
S extends ReadonlyArray<unknown>,
|
|
66
|
+
Services,
|
|
67
|
+
P,
|
|
68
|
+
N extends string,
|
|
69
|
+
Identity,
|
|
70
|
+
>(
|
|
71
|
+
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
72
|
+
): RemoteServer => {
|
|
73
|
+
const layer = scene.provide.reduce((acc, provided) => Layer.merge(acc, provided))
|
|
74
|
+
const runtime = ManagedRuntime.make(layer)
|
|
75
|
+
const registry: TriggerRegistryApi = forceSync(() => runtime.runSync(Triggers.make))
|
|
76
|
+
const context = forceSync(() => runtime.runSync(Effect.context<Services>()))
|
|
77
|
+
const busFromContext = serviceFromContext(
|
|
78
|
+
context,
|
|
79
|
+
Bus,
|
|
80
|
+
'reform-remote: scene does not provide Bus',
|
|
68
81
|
)
|
|
69
82
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return Option.some<WireProp>({ _tag: 'Event', name: eventName, handle })
|
|
81
|
-
}),
|
|
82
|
-
).pipe(Effect.map(Arr.getSomes))
|
|
83
|
+
const renderEffect = settleDrain.pipe(
|
|
84
|
+
Effect.zipRight(
|
|
85
|
+
renderSceneWith(
|
|
86
|
+
scene,
|
|
87
|
+
{ register: registry.register },
|
|
88
|
+
(slotDefinition) => capturedSlotProvider(context, slotDefinition),
|
|
89
|
+
(composition, props) => renderCapturedComposition(context, composition, props),
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
)
|
|
83
93
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
parentId: Option.getOrNull(mounted.parentId),
|
|
88
|
-
childIndex: mounted.childIndex,
|
|
89
|
-
slot: Option.getOrNull(mounted.slot),
|
|
90
|
-
key: Option.getOrNull(mounted.key),
|
|
91
|
-
props: [...dataProps, ...eventProps],
|
|
94
|
+
const changeListeners = new Set<() => void>()
|
|
95
|
+
const notifyChange = (): void => {
|
|
96
|
+
changeListeners.forEach((listener) => listener())
|
|
92
97
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
runtime.runFork(
|
|
99
|
+
Effect.scoped(
|
|
100
|
+
Effect.gen(function* () {
|
|
101
|
+
const bus = yield* busFromContext
|
|
102
|
+
const subscription = yield* PubSub.subscribe(bus)
|
|
103
|
+
// The transport debounces; settling here would perturb the drain's scheduling.
|
|
104
|
+
yield* Queue.take(subscription).pipe(
|
|
105
|
+
Effect.zipRight(Effect.sync(notifyChange)),
|
|
106
|
+
Effect.forever,
|
|
107
|
+
)
|
|
108
|
+
}),
|
|
109
|
+
),
|
|
110
|
+
)
|
|
101
111
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
runtime.runSync(
|
|
113
|
+
Effect.forEach(
|
|
114
|
+
Option.getOrElse(Option.fromNullable(scene.boot), () => []),
|
|
115
|
+
(event) =>
|
|
116
|
+
busFromContext.pipe(
|
|
117
|
+
Effect.flatMap((bus) => publish('High', event).pipe(Effect.provideService(Bus, bus))),
|
|
118
|
+
),
|
|
106
119
|
),
|
|
107
120
|
)
|
|
108
121
|
|
|
109
|
-
const
|
|
110
|
-
Option.getOrElse(Option.fromNullable(comp.manifest.slots), () => ({}))
|
|
122
|
+
const frame: { tree: WireTree } = { tree: [] }
|
|
111
123
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
each.items.map(
|
|
121
|
-
(entry, index): Mounted => ({
|
|
122
|
-
comp: child,
|
|
123
|
-
props: entry.props,
|
|
124
|
-
id: `${parent.id}.${slotName}.${index}`,
|
|
125
|
-
parentId: Option.some(parent.id),
|
|
126
|
-
slot: Option.some(slotName),
|
|
127
|
-
childIndex: index,
|
|
128
|
-
key: Option.fromNullable(entry.key),
|
|
129
|
-
}),
|
|
124
|
+
const render = (): Promise<WireTree> =>
|
|
125
|
+
runtime.runPromise(
|
|
126
|
+
renderEffect.pipe(
|
|
127
|
+
Effect.tap((tree) =>
|
|
128
|
+
Effect.sync(() => {
|
|
129
|
+
frame.tree = tree
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
130
132
|
),
|
|
131
|
-
)
|
|
132
|
-
Match.tag('One', (single) => [
|
|
133
|
-
{
|
|
134
|
-
comp: child,
|
|
135
|
-
props: single.props,
|
|
136
|
-
id: `${parent.id}.${slotName}.0`,
|
|
137
|
-
parentId: Option.some(parent.id),
|
|
138
|
-
slot: Option.some(slotName),
|
|
139
|
-
childIndex: 0,
|
|
140
|
-
key: Option.none(),
|
|
141
|
-
} satisfies Mounted,
|
|
142
|
-
]),
|
|
143
|
-
Match.tag('Absent', () => []),
|
|
144
|
-
Match.exhaustive,
|
|
145
|
-
)
|
|
133
|
+
)
|
|
146
134
|
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
): Effect.fn.Return<Map<SlotClass, CompositionClass<unknown>>, never, SlotChild> {
|
|
150
|
-
const bindings = new Map<SlotClass, CompositionClass<unknown>>()
|
|
151
|
-
// Explicit type so recursive walk references itself cast-free.
|
|
152
|
-
const walk: (comp: CompositionClass<unknown>) => Effect.Effect<void, never, SlotChild> = Effect.fn(
|
|
153
|
-
'walk',
|
|
154
|
-
)(function* (comp: CompositionClass<unknown>): Effect.fn.Return<void, never, SlotChild> {
|
|
155
|
-
yield* Effect.forEach(Rec.values(slotsOf(comp)), (slotClass) =>
|
|
135
|
+
const renderDiff = (): Promise<ReadonlyArray<WirePatch>> =>
|
|
136
|
+
runtime.runPromise(
|
|
156
137
|
Effect.gen(function* () {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
138
|
+
const next = yield* renderEffect
|
|
139
|
+
const previous = frame.tree
|
|
140
|
+
const patches = Wire.diff(previous, next)
|
|
141
|
+
frame.tree = next
|
|
142
|
+
const nextIds = new Set(next.map((node) => node.id))
|
|
143
|
+
const staleHandles = previous.filter((node) => !nextIds.has(node.id)).flatMap(handlesOf)
|
|
144
|
+
yield* Effect.forEach(staleHandles, (handle) => registry.revoke(handle))
|
|
145
|
+
return patches
|
|
163
146
|
}),
|
|
164
147
|
)
|
|
165
|
-
})
|
|
166
|
-
yield* walk(root)
|
|
167
|
-
return bindings
|
|
168
|
-
})
|
|
169
148
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
|
|
149
|
+
return {
|
|
150
|
+
render,
|
|
151
|
+
renderDiff,
|
|
152
|
+
invoke: (handle, encodedPayload) =>
|
|
153
|
+
runtime.runPromise(
|
|
154
|
+
Effect.gen(function* () {
|
|
155
|
+
yield* registry.invoke(handle, encodedPayload)
|
|
156
|
+
yield* settleDrain
|
|
157
|
+
}),
|
|
158
|
+
),
|
|
159
|
+
subscribe: (listener) => {
|
|
160
|
+
changeListeners.add(listener)
|
|
161
|
+
return () => void changeListeners.delete(listener)
|
|
162
|
+
},
|
|
163
|
+
currentTree: () => frame.tree,
|
|
164
|
+
dispose: () => runtime.dispose(),
|
|
184
165
|
}
|
|
185
|
-
const rendered = yield* Effect.forEach(frontier, (mounted) =>
|
|
186
|
-
Effect.gen(function* () {
|
|
187
|
-
const service = yield* mounted.comp.tag
|
|
188
|
-
const env: RenderEnv = { props: mounted.props, tracker: { add: () => {} } }
|
|
189
|
-
const frame = yield* Composition.render(service, env)
|
|
190
|
-
if (!isStructure(frame)) {
|
|
191
|
-
return yield* Effect.dieMessage(
|
|
192
|
-
`reform-remote server: composition ${mounted.comp.manifest.name} did not return a Structure`,
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
const node = yield* toWireNodeFromStructure(mounted, frame, register)
|
|
196
|
-
const fills = structureFills(frame)
|
|
197
|
-
const children = Rec.toEntries(slotsOf(mounted.comp)).flatMap(([slotName, slotClass]) => {
|
|
198
|
-
const child = bindings.get(slotClass)
|
|
199
|
-
if (child === undefined) {
|
|
200
|
-
return []
|
|
201
|
-
}
|
|
202
|
-
const fill = fills[slotName]
|
|
203
|
-
if (fill === undefined) {
|
|
204
|
-
return []
|
|
205
|
-
}
|
|
206
|
-
return enqueueStructureSlot(mounted, slotName, child, fill)
|
|
207
|
-
})
|
|
208
|
-
return { node, children }
|
|
209
|
-
}),
|
|
210
|
-
)
|
|
211
|
-
const nodes = rendered.map((entry) => entry.node)
|
|
212
|
-
const next = rendered.flatMap((entry) => entry.children)
|
|
213
|
-
const rest = yield* renderLevel(next, bindings, register)
|
|
214
|
-
return [...nodes, ...rest]
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
export interface RenderSceneToWireOptions {
|
|
218
|
-
readonly register: TriggerRegistryApi['register']
|
|
219
166
|
}
|
|
220
167
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
slot: Option.none(),
|
|
235
|
-
childIndex: 0,
|
|
236
|
-
key: Option.none(),
|
|
237
|
-
}
|
|
238
|
-
return yield* renderLevel([root], bindings, register)
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
export { makeRemoteServer, type RemoteServer } from './remote-server'
|
|
168
|
+
export const makeRemoteServer = (scene: AnyScene): RemoteServer =>
|
|
169
|
+
scene.captureAny<RemoteServer>(
|
|
170
|
+
<
|
|
171
|
+
C extends UiContract,
|
|
172
|
+
S extends ReadonlyArray<unknown>,
|
|
173
|
+
Services,
|
|
174
|
+
P,
|
|
175
|
+
N extends string,
|
|
176
|
+
Identity,
|
|
177
|
+
>(
|
|
178
|
+
exactScene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
179
|
+
) => makeCapturedRemoteServer(exactScene),
|
|
180
|
+
)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createElement } from 'react'
|
|
2
|
+
import { Effect, Layer, Schema as S } from 'effect'
|
|
3
|
+
import {
|
|
4
|
+
Composition,
|
|
5
|
+
Engine,
|
|
6
|
+
Event,
|
|
7
|
+
Reducer,
|
|
8
|
+
State,
|
|
9
|
+
Ui,
|
|
10
|
+
mount,
|
|
11
|
+
one,
|
|
12
|
+
provide,
|
|
13
|
+
scene,
|
|
14
|
+
slot,
|
|
15
|
+
type CapturedScene,
|
|
16
|
+
type DerivedContract,
|
|
17
|
+
type SlotService,
|
|
18
|
+
ui,
|
|
19
|
+
} from '@playfast/reform'
|
|
20
|
+
import { type CompositionChildSummary, type EngineServices, type WiredUi } from '@playfast/reform/internal'
|
|
21
|
+
|
|
22
|
+
const GreetingBase: State.StateDefinition<'greeting', string, typeof S.String> = State.make(
|
|
23
|
+
'greeting',
|
|
24
|
+
S.String,
|
|
25
|
+
)
|
|
26
|
+
class Greeting extends GreetingBase {}
|
|
27
|
+
class Greeted extends Event.make('Greeted', S.Struct({ text: S.String })) {}
|
|
28
|
+
class Cleared extends Event.make('Cleared', S.Struct({})) {}
|
|
29
|
+
class SetGreeting extends Reducer.make('SetGreeting', {
|
|
30
|
+
states: [Greeting],
|
|
31
|
+
events: [Greeted],
|
|
32
|
+
}) {}
|
|
33
|
+
class ClearGreeting extends Reducer.make('ClearGreeting', {
|
|
34
|
+
states: [Greeting],
|
|
35
|
+
events: [Cleared],
|
|
36
|
+
}) {}
|
|
37
|
+
const PanelUiBase: WiredUi<
|
|
38
|
+
DerivedContract<{
|
|
39
|
+
props: S.Struct<{ greeting: typeof S.String }>
|
|
40
|
+
events: {
|
|
41
|
+
greet: S.Struct<{ text: typeof S.String }>
|
|
42
|
+
clear: S.Struct<{}>
|
|
43
|
+
}
|
|
44
|
+
}>,
|
|
45
|
+
'Panel'
|
|
46
|
+
> = ui('Panel', {
|
|
47
|
+
props: S.Struct({ greeting: S.String }),
|
|
48
|
+
events: { greet: S.Struct({ text: S.String }), clear: S.Struct({}) },
|
|
49
|
+
})
|
|
50
|
+
class PanelUi extends PanelUiBase {}
|
|
51
|
+
|
|
52
|
+
class Panel extends Composition.make('Panel', {
|
|
53
|
+
title: 'Panel',
|
|
54
|
+
ui: PanelUi,
|
|
55
|
+
states: [Greeting],
|
|
56
|
+
})<Panel>() {}
|
|
57
|
+
|
|
58
|
+
class MainSlot extends slot('Main')<MainSlot, typeof Panel>() {}
|
|
59
|
+
const ShellUiBase: Ui.UiClass<{ props: {}; slots: { Main: MainSlot } }, 'Shell'> = ui('Shell')<{
|
|
60
|
+
props: {}
|
|
61
|
+
slots: { Main: MainSlot }
|
|
62
|
+
}>()
|
|
63
|
+
class ShellUi extends ShellUiBase {}
|
|
64
|
+
|
|
65
|
+
class Shell extends Composition.make('Shell', {
|
|
66
|
+
title: 'Shell',
|
|
67
|
+
slots: { Main: MainSlot },
|
|
68
|
+
ui: ShellUi,
|
|
69
|
+
})<Shell>() {}
|
|
70
|
+
|
|
71
|
+
type SlottedScene = CapturedScene<
|
|
72
|
+
typeof ShellUi.Contract,
|
|
73
|
+
readonly [],
|
|
74
|
+
| EngineServices
|
|
75
|
+
| State.StateStore<'greeting', string>
|
|
76
|
+
| Ui.UiService<'Panel'>
|
|
77
|
+
| Ui.UiService<'Shell'>
|
|
78
|
+
| Composition.CompositionId<'Shell', Shell, MainSlot>
|
|
79
|
+
| Composition.CompositionId<'Panel', Panel, never>
|
|
80
|
+
| SlotService<'Main', MainSlot, CompositionChildSummary<Panel>>,
|
|
81
|
+
unknown,
|
|
82
|
+
'Shell',
|
|
83
|
+
Shell
|
|
84
|
+
>
|
|
85
|
+
|
|
86
|
+
const makeSlottedScene = (): SlottedScene => {
|
|
87
|
+
const presentation = Layer.mergeAll(
|
|
88
|
+
provide(
|
|
89
|
+
ShellUi,
|
|
90
|
+
Ui.make(ShellUi, (_props, slots) => createElement(slots.Main, {})),
|
|
91
|
+
),
|
|
92
|
+
provide(
|
|
93
|
+
PanelUi,
|
|
94
|
+
Ui.make(PanelUi, ({ greeting }) => greeting),
|
|
95
|
+
),
|
|
96
|
+
provide(MainSlot, Panel),
|
|
97
|
+
State.live(Greeting, 'hi'),
|
|
98
|
+
)
|
|
99
|
+
const app = Layer.mergeAll(
|
|
100
|
+
Composition.live(Shell, function* () {
|
|
101
|
+
yield* Effect.void
|
|
102
|
+
return mount({ props: {}, slots: { Main: one({}) } })
|
|
103
|
+
}),
|
|
104
|
+
Composition.live(Panel, function* () {
|
|
105
|
+
const greeting = yield* Greeting
|
|
106
|
+
const greet = yield* Event.trigger(Greeted)
|
|
107
|
+
const clearTrigger = yield* Event.trigger(Cleared)
|
|
108
|
+
const clear = (): void => clearTrigger({})
|
|
109
|
+
return mount({
|
|
110
|
+
props: { greeting },
|
|
111
|
+
slots: {},
|
|
112
|
+
events: { greet, clear },
|
|
113
|
+
})
|
|
114
|
+
}),
|
|
115
|
+
Reducer.live(SetGreeting, (_greeting, event) => event.text),
|
|
116
|
+
Reducer.live(ClearGreeting, () => ''),
|
|
117
|
+
).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
|
|
118
|
+
return scene(Shell, { provide: [app] })
|
|
119
|
+
}
|
|
120
|
+
export const slottedScene: typeof makeSlottedScene = makeSlottedScene
|
package/src/transport.test.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { inMemoryTransportPair } from './memory'
|
|
|
9
9
|
import { remoteViews } from './client'
|
|
10
10
|
import { asyncCounterScene, CounterUi, counterScene } from './fixtures'
|
|
11
11
|
|
|
12
|
-
const draw = (node: ReactNode): void =>
|
|
12
|
+
const draw = (node: ReactNode): void =>
|
|
13
|
+
void renderToStaticMarkup(createElement(Fragment, null, node))
|
|
13
14
|
|
|
14
15
|
interface Probe {
|
|
15
16
|
props?: Record<string, unknown>
|