@playfast/reform-remote 0.1.0 → 1.0.2
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/package.json +1 -1
- package/src/client-keyed-slot.test.ts +0 -22
- package/src/client-remount.test.ts +0 -23
- package/src/client.ts +10 -152
- package/src/connect.ts +44 -0
- package/src/contract.ts +3 -0
- package/src/fixtures.ts +4 -34
- package/src/index.ts +0 -10
- package/src/memory.ts +0 -9
- package/src/react.ts +0 -26
- package/src/remote-server.ts +109 -0
- package/src/remote-view.typecheck.ts +0 -15
- package/src/server.test.ts +2 -20
- package/src/server.ts +6 -209
- package/src/transport.test.ts +0 -24
- package/src/transport.ts +20 -162
- package/src/transport.types.ts +29 -0
package/src/transport.test.ts
CHANGED
|
@@ -9,18 +9,10 @@ import { inMemoryTransportPair } from './memory'
|
|
|
9
9
|
import { remoteViews } from './client'
|
|
10
10
|
import { asyncCounterScene, CounterUi, counterScene } from './fixtures'
|
|
11
11
|
|
|
12
|
-
// The client renders views as React components (so view-body hooks work), so a test
|
|
13
|
-
// must actually RENDER `client.node()` to run the views — `draw` forces that pass
|
|
14
|
-
// (node, no DOM needed) and the probe captures what each view received.
|
|
15
12
|
const draw = (node: ReactNode): void => void renderToStaticMarkup(createElement(Fragment, null, node))
|
|
16
13
|
|
|
17
|
-
// A presentation that records the props/events handed to it, so a test can read
|
|
18
|
-
// what the client reconstructed and fire the callbacks back at the server. It is a
|
|
19
|
-
// regular `Ui.make` view (the only client authoring form), bound to the contract.
|
|
20
14
|
interface Probe {
|
|
21
15
|
props?: Record<string, unknown>
|
|
22
|
-
// The counter contract's only event, `bump`, takes `{ by: number }`; typing the
|
|
23
|
-
// record to that payload lets the strongly-typed `Ui.make` events assign in.
|
|
24
16
|
events?: Record<string, (payload: { by: number }) => void>
|
|
25
17
|
}
|
|
26
18
|
const probeView = (probe: Probe) =>
|
|
@@ -64,8 +56,6 @@ test('server-initiated state change streams to the client with NO invoke (backgr
|
|
|
64
56
|
InvokeMessage
|
|
65
57
|
>()
|
|
66
58
|
|
|
67
|
-
// Count outbound client→server messages: the whole point is that the new frame arrives
|
|
68
|
-
// without the client invoking anything. A non-zero count would mean the test cheated.
|
|
69
59
|
const invokes = { count: 0 }
|
|
70
60
|
const countingClientTransport = {
|
|
71
61
|
...clientTransport,
|
|
@@ -80,8 +70,6 @@ test('server-initiated state change streams to the client with NO invoke (backgr
|
|
|
80
70
|
transport: countingClientTransport,
|
|
81
71
|
views: remoteViews<{ Counter: typeof CounterUi }>({ Counter: probeView(probe) }),
|
|
82
72
|
})
|
|
83
|
-
// The scene's procedure sleeps, THEN bumps to 7 — so the snapshot is count:0 and the
|
|
84
|
-
// change can only reach the client via a server-pushed diff.
|
|
85
73
|
const server = serve({ scene: asyncCounterScene('30 millis'), transport: serverTransport })
|
|
86
74
|
try {
|
|
87
75
|
await server.start()
|
|
@@ -92,7 +80,6 @@ test('server-initiated state change streams to the client with NO invoke (backgr
|
|
|
92
80
|
draw(client.node())
|
|
93
81
|
expect(probe.props).toEqual({ count: 7 })
|
|
94
82
|
})
|
|
95
|
-
// The client never sent a single Invoke — the frame was entirely server-initiated.
|
|
96
83
|
expect(invokes.count).toBe(0)
|
|
97
84
|
} finally {
|
|
98
85
|
client.dispose()
|
|
@@ -127,12 +114,10 @@ test('a fresh Snapshot replaces the client tree — stale nodes are dropped (rec
|
|
|
127
114
|
props: [],
|
|
128
115
|
})
|
|
129
116
|
|
|
130
|
-
// First session: two roots.
|
|
131
117
|
serverTransport.send({ _tag: 'Snapshot', tree: [node('a', 'A'), node('b', 'B')] })
|
|
132
118
|
draw(client.node())
|
|
133
119
|
expect(rendered).toEqual(['A', 'B'])
|
|
134
120
|
|
|
135
|
-
// Reconnect: a fresh snapshot with only one root must drop the stale 'B'.
|
|
136
121
|
rendered.length = 0
|
|
137
122
|
serverTransport.send({ _tag: 'Snapshot', tree: [node('a', 'A')] })
|
|
138
123
|
draw(client.node())
|
|
@@ -160,7 +145,6 @@ test('two clients each get their own server runtime — state is isolated', asyn
|
|
|
160
145
|
try {
|
|
161
146
|
await serverOne.start()
|
|
162
147
|
await serverTwo.start()
|
|
163
|
-
// Render once so each probe captures its event callbacks.
|
|
164
148
|
draw(clientOne.node())
|
|
165
149
|
draw(clientTwo.node())
|
|
166
150
|
|
|
@@ -170,7 +154,6 @@ test('two clients each get their own server runtime — state is isolated', asyn
|
|
|
170
154
|
expect(probeOne.props).toEqual({ count: 9 })
|
|
171
155
|
})
|
|
172
156
|
|
|
173
|
-
// The second client's runtime never saw that bump.
|
|
174
157
|
draw(clientTwo.node())
|
|
175
158
|
expect(probeTwo.props).toEqual({ count: 0 })
|
|
176
159
|
} finally {
|
|
@@ -196,12 +179,10 @@ test('serveShared: two clients share ONE runtime — one client drives state for
|
|
|
196
179
|
views: remoteViews<{ Counter: typeof CounterUi }>({ Counter: probeView(probeTwo) }),
|
|
197
180
|
})
|
|
198
181
|
|
|
199
|
-
// One shared server; both transports attach to the same runtime.
|
|
200
182
|
const shared = serveShared({ scene: counterScene() })
|
|
201
183
|
const handleOne = shared.addClient(one.server)
|
|
202
184
|
const handleTwo = shared.addClient(two.server)
|
|
203
185
|
try {
|
|
204
|
-
// Both clients receive the opening snapshot off the shared baseline.
|
|
205
186
|
await vi.waitFor(() => {
|
|
206
187
|
draw(clientOne.node())
|
|
207
188
|
draw(clientTwo.node())
|
|
@@ -209,7 +190,6 @@ test('serveShared: two clients share ONE runtime — one client drives state for
|
|
|
209
190
|
expect(probeTwo.props).toEqual({ count: 0 })
|
|
210
191
|
})
|
|
211
192
|
|
|
212
|
-
// A bump from client ONE is broadcast to BOTH — they share the same state.
|
|
213
193
|
probeOne.events?.['bump']?.({ by: 5 })
|
|
214
194
|
await vi.waitFor(() => {
|
|
215
195
|
draw(clientOne.node())
|
|
@@ -218,7 +198,6 @@ test('serveShared: two clients share ONE runtime — one client drives state for
|
|
|
218
198
|
expect(probeTwo.props).toEqual({ count: 5 })
|
|
219
199
|
})
|
|
220
200
|
|
|
221
|
-
// And a bump from client TWO lands on the same shared counter.
|
|
222
201
|
probeTwo.events?.['bump']?.({ by: 3 })
|
|
223
202
|
await vi.waitFor(() => {
|
|
224
203
|
draw(clientOne.node())
|
|
@@ -258,7 +237,6 @@ test('serveShared: a client connecting mid-stream snapshots the current shared s
|
|
|
258
237
|
expect(probeOne.props).toEqual({ count: 4 })
|
|
259
238
|
})
|
|
260
239
|
|
|
261
|
-
// A late joiner must see the CURRENT shared count (4), not a fresh 0.
|
|
262
240
|
const two = inMemoryTransportPair<ServerMessage, InvokeMessage>()
|
|
263
241
|
const probeTwo: Probe = {}
|
|
264
242
|
const clientTwo = connect({
|
|
@@ -300,7 +278,5 @@ test('dispose detaches both ends: a post-dispose invoke no longer reaches the se
|
|
|
300
278
|
await server.dispose()
|
|
301
279
|
client.dispose()
|
|
302
280
|
|
|
303
|
-
// After dispose the server's onMessage handler is detached; firing the stale
|
|
304
|
-
// callback is a no-op rather than a throw.
|
|
305
281
|
expect(() => probe.events?.['bump']?.({ by: 1 })).not.toThrow()
|
|
306
282
|
})
|
package/src/transport.ts
CHANGED
|
@@ -1,65 +1,20 @@
|
|
|
1
|
-
import { Effect, Fiber,
|
|
2
|
-
import type { ReactNode } from 'react'
|
|
3
|
-
import { Wire, type WirePatch, type WireTree } from '@playfast/reform'
|
|
1
|
+
import { Effect, Fiber, Option } from 'effect'
|
|
4
2
|
import type { Scene } from '@playfast/reform'
|
|
5
3
|
import { makeRemoteServer } from './server'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* empty, so it can only *add* nodes; a snapshot is what lets a reconnecting
|
|
19
|
-
* client drop the stale tree it accumulated on the previous socket.
|
|
20
|
-
*/
|
|
21
|
-
export interface SnapshotMessage {
|
|
22
|
-
readonly _tag: 'Snapshot'
|
|
23
|
-
readonly tree: WireTree
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Server → client: a batch of tree patches to fold with `Wire.apply`. */
|
|
27
|
-
export interface PatchesMessage {
|
|
28
|
-
readonly _tag: 'Patches'
|
|
29
|
-
readonly patches: ReadonlyArray<WirePatch>
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Everything the server sends the client: a fresh snapshot or incremental patches. */
|
|
33
|
-
export type ServerMessage = SnapshotMessage | PatchesMessage
|
|
34
|
-
|
|
35
|
-
/** Client → server: fire the trigger behind `handle` with an encoded payload. */
|
|
36
|
-
export interface InvokeMessage {
|
|
37
|
-
readonly _tag: 'Invoke'
|
|
38
|
-
readonly handle: string
|
|
39
|
-
readonly payload: unknown
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** A duplex channel: send `Out`, receive `In`. */
|
|
43
|
-
export interface RemoteTransport<Out, In> {
|
|
44
|
-
readonly send: (message: Out) => void
|
|
45
|
-
readonly onMessage: (handler: (message: In) => void) => () => void
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface ServerBinding {
|
|
49
|
-
/** Render the first frame and push it. Call once the client is connected. */
|
|
50
|
-
readonly start: () => Promise<void>
|
|
51
|
-
readonly dispose: () => Promise<void>
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Debounce window for SERVER-initiated frame pushes (background loads, scheduler ticks).
|
|
56
|
-
* Long enough that the engine drain has folded the change into state and a burst of facts
|
|
57
|
-
* coalesces into one render; short enough to feel live. Client invokes don't wait on this —
|
|
58
|
-
* they push immediately.
|
|
59
|
-
*/
|
|
4
|
+
export { connect, type ClientBinding, type ConnectOptions } from './connect'
|
|
5
|
+
import type { InvokeMessage, RemoteTransport, ServerBinding, ServerMessage } from './transport.types'
|
|
6
|
+
export type {
|
|
7
|
+
InvokeMessage,
|
|
8
|
+
PatchesMessage,
|
|
9
|
+
RemoteTransport,
|
|
10
|
+
ServerBinding,
|
|
11
|
+
ServerMessage,
|
|
12
|
+
SnapshotMessage,
|
|
13
|
+
} from './transport.types'
|
|
14
|
+
|
|
15
|
+
// Debounce for server-initiated pushes: drain settles + burst coalesces; client invokes push immediately.
|
|
60
16
|
const BACKGROUND_FLUSH_MS = 16
|
|
61
17
|
|
|
62
|
-
/** Options for {@link serve} — a single named object so every reform-remote binding shares one shape. */
|
|
63
18
|
export interface ServeOptions {
|
|
64
19
|
readonly scene: Scene
|
|
65
20
|
readonly transport: RemoteTransport<ServerMessage, InvokeMessage>
|
|
@@ -68,22 +23,8 @@ export interface ServeOptions {
|
|
|
68
23
|
export const serve = (options: ServeOptions): ServerBinding => {
|
|
69
24
|
const { scene, transport } = options
|
|
70
25
|
const server = makeRemoteServer(scene)
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
// reference what the client actually holds. Diffs are emitted both after a client invoke
|
|
74
|
-
// AND whenever the server's OWN state changes (a background load resolving, a scheduler
|
|
75
|
-
// tick) — without the latter, anything the user didn't directly trigger would never reach
|
|
76
|
-
// the client (it would sit on the snapshot's loading state forever).
|
|
77
|
-
//
|
|
78
|
-
// `started` gates pushes until the opening snapshot is sent: a diff that raced ahead of
|
|
79
|
-
// the snapshot would reference a tree the client has not received.
|
|
80
|
-
//
|
|
81
|
-
// `push` is a COALESCING single-flight render: at most one `renderDiff` runs at a time
|
|
82
|
-
// (concurrent ones would interleave the sink captures and corrupt the frame baseline). If
|
|
83
|
-
// a change arrives WHILE a render is in flight — e.g. an async procedure folds its result
|
|
84
|
-
// mid-render — `dirty` is set and a follow-up render runs when the current one settles, so
|
|
85
|
-
// a late state change is never dropped. Both a client invoke and a server-side change
|
|
86
|
-
// (the bus subscriber) funnel through here, so there is exactly one push path.
|
|
26
|
+
// Snapshot first; then diffs. `started` gates races before snapshot.
|
|
27
|
+
// Single-flight push + dirty re-run: concurrent renderDiff would corrupt frame baseline.
|
|
87
28
|
const started = { value: false }
|
|
88
29
|
const flight = { promise: Option.none<Promise<void>>(), dirty: false }
|
|
89
30
|
const push = (): Promise<void> => {
|
|
@@ -94,8 +35,7 @@ export const serve = (options: ServeOptions): ServerBinding => {
|
|
|
94
35
|
flight.dirty = true
|
|
95
36
|
return flight.promise.value
|
|
96
37
|
}
|
|
97
|
-
//
|
|
98
|
-
// (the old `.finally`), so the single-flight slot is never left stuck.
|
|
38
|
+
// ensuring so single-flight slot clears even on render failure.
|
|
99
39
|
const run = Effect.runPromise(
|
|
100
40
|
Effect.gen(function* () {
|
|
101
41
|
const patches = yield* Effect.promise(() => server.renderDiff())
|
|
@@ -117,12 +57,7 @@ export const serve = (options: ServeOptions): ServerBinding => {
|
|
|
117
57
|
flight.promise = Option.some(run)
|
|
118
58
|
return run
|
|
119
59
|
}
|
|
120
|
-
//
|
|
121
|
-
// coalesces a burst (a procedure that dispatches several facts) into one render, and the
|
|
122
|
-
// delay lets the drain fold the change into state before we read it — so the diff reflects
|
|
123
|
-
// the settled result, and the background flush never races the synchronous invoke push
|
|
124
|
-
// above. A client invoke still pushes immediately (its own settle path), so user actions
|
|
125
|
-
// stay snappy; this path only carries updates no interaction triggered.
|
|
60
|
+
// Background debounce so drain folds state before read; never races invoke's immediate push.
|
|
126
61
|
const debounce = { fiber: Option.none<Fiber.RuntimeFiber<void>>() }
|
|
127
62
|
const scheduleFlush = (): void => {
|
|
128
63
|
if (!started.value || Option.isSome(debounce.fiber)) {
|
|
@@ -144,7 +79,6 @@ export const serve = (options: ServeOptions): ServerBinding => {
|
|
|
144
79
|
const tree = await server.render()
|
|
145
80
|
transport.send({ _tag: 'Snapshot', tree })
|
|
146
81
|
started.value = true
|
|
147
|
-
// Flush anything that changed during the (async) opening render window.
|
|
148
82
|
scheduleFlush()
|
|
149
83
|
}
|
|
150
84
|
const off = transport.onMessage((message) => {
|
|
@@ -168,40 +102,19 @@ export const serve = (options: ServeOptions): ServerBinding => {
|
|
|
168
102
|
}
|
|
169
103
|
}
|
|
170
104
|
|
|
171
|
-
/** A client's membership in a {@link SharedServerBinding}; `remove` detaches it. */
|
|
172
105
|
export interface SharedClientHandle {
|
|
173
|
-
/** Stop sending this client frames and drop its invoke listener. Call on disconnect. */
|
|
174
106
|
readonly remove: () => void
|
|
175
107
|
}
|
|
176
108
|
|
|
177
109
|
export interface SharedServerBinding {
|
|
178
|
-
/**
|
|
179
|
-
* Attach a freshly-connected transport to the ONE shared runtime: send it a
|
|
180
|
-
* Snapshot of the current shared tree, then fold it into the broadcast set so
|
|
181
|
-
* every later frame — and every other client's invoke result — reaches it too.
|
|
182
|
-
* Returns a handle to detach the client when its socket closes.
|
|
183
|
-
*/
|
|
184
110
|
readonly addClient: (transport: RemoteTransport<ServerMessage, InvokeMessage>) => SharedClientHandle
|
|
185
|
-
/** Tear down the shared runtime and drop every client. */
|
|
186
111
|
readonly dispose: () => Promise<void>
|
|
187
112
|
}
|
|
188
113
|
|
|
189
|
-
/** Options for {@link serveShared} — one scene, run once, shared by every client. */
|
|
190
114
|
export interface ServeSharedOptions {
|
|
191
115
|
readonly scene: Scene
|
|
192
116
|
}
|
|
193
117
|
|
|
194
|
-
/**
|
|
195
|
-
* The single-instance counterpart to {@link serve}: ONE `makeRemoteServer(scene)` —
|
|
196
|
-
* one runtime, one trigger registry, one frame baseline — shared by every connected
|
|
197
|
-
* client. A client's invoke fires on the shared runtime and the resulting diff is
|
|
198
|
-
* broadcast to ALL clients, so every connection sees and drives the same state.
|
|
199
|
-
*
|
|
200
|
-
* Sync is structural: all clients hold the same baseline, so one render's diff applies
|
|
201
|
-
* to all. A client connecting mid-stream snapshots off the live `currentTree()` (after
|
|
202
|
-
* the opening render settles) and joins the broadcast set in the same tick — it never
|
|
203
|
-
* misses or double-applies a frame.
|
|
204
|
-
*/
|
|
205
118
|
export const serveShared = (options: ServeSharedOptions): SharedServerBinding => {
|
|
206
119
|
const { scene } = options
|
|
207
120
|
const server = makeRemoteServer(scene)
|
|
@@ -210,9 +123,7 @@ export const serveShared = (options: ServeSharedOptions): SharedServerBinding =>
|
|
|
210
123
|
clients.forEach((client) => client.send(message))
|
|
211
124
|
}
|
|
212
125
|
|
|
213
|
-
//
|
|
214
|
-
// patches fan out to EVERY client. `started` gates pushes until the opening render has
|
|
215
|
-
// set the baseline; `dirty` re-runs a render that a late change raced into mid-flight.
|
|
126
|
+
// Same single-flight + dirty as serve; patches broadcast to every client.
|
|
216
127
|
const started = { value: false }
|
|
217
128
|
const flight = { promise: Option.none<Promise<void>>(), dirty: false }
|
|
218
129
|
const push = (): Promise<void> => {
|
|
@@ -263,9 +174,7 @@ export const serveShared = (options: ServeSharedOptions): SharedServerBinding =>
|
|
|
263
174
|
debounce.fiber = Option.some(fiber)
|
|
264
175
|
}
|
|
265
176
|
|
|
266
|
-
//
|
|
267
|
-
// it. Clients `await ready`, so they never capture the empty pre-render tree. Anything
|
|
268
|
-
// that changed during the (async) opening render is flushed by the trailing scheduleFlush.
|
|
177
|
+
// Baseline before any client snapshots; clients await ready so they never see empty tree.
|
|
269
178
|
const ready = (async (): Promise<void> => {
|
|
270
179
|
await server.render()
|
|
271
180
|
started.value = true
|
|
@@ -284,9 +193,7 @@ export const serveShared = (options: ServeSharedOptions): SharedServerBinding =>
|
|
|
284
193
|
),
|
|
285
194
|
)
|
|
286
195
|
})
|
|
287
|
-
//
|
|
288
|
-
// broadcast set in the SAME tick — no async push can interleave between the two, so
|
|
289
|
-
// the client's first frame and every subsequent diff stay in lockstep.
|
|
196
|
+
// Snapshot + join broadcast in same tick so no push interleaves between them.
|
|
290
197
|
void Effect.runPromise(
|
|
291
198
|
Effect.promise(() => ready).pipe(
|
|
292
199
|
Effect.zipRight(
|
|
@@ -317,52 +224,3 @@ export const serveShared = (options: ServeSharedOptions): SharedServerBinding =>
|
|
|
317
224
|
},
|
|
318
225
|
}
|
|
319
226
|
}
|
|
320
|
-
|
|
321
|
-
export interface ClientBinding {
|
|
322
|
-
/** The current rendered node — re-read on each `subscribe` notification. */
|
|
323
|
-
readonly node: () => ReactNode
|
|
324
|
-
/** Notified whenever applied patches change the tree. */
|
|
325
|
-
readonly subscribe: (listener: () => void) => () => void
|
|
326
|
-
/**
|
|
327
|
-
* The current wire tree — a stable reference that changes only when a frame is
|
|
328
|
-
* applied, so it is a valid `useSyncExternalStore` snapshot (the React binding
|
|
329
|
-
* builds on this).
|
|
330
|
-
*/
|
|
331
|
-
readonly snapshot: () => WireTree
|
|
332
|
-
readonly dispose: () => void
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/** Options for {@link connect} — a single named object, mirroring {@link ServeOptions}. */
|
|
336
|
-
export interface ConnectOptions<C extends RemoteContract> {
|
|
337
|
-
readonly transport: RemoteTransport<InvokeMessage, ServerMessage>
|
|
338
|
-
readonly views: RemoteViewSet<C>
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
export const connect = <C extends RemoteContract>(options: ConnectOptions<C>): ClientBinding => {
|
|
342
|
-
const { transport, views } = options
|
|
343
|
-
const state: { tree: WireTree } = { tree: [] }
|
|
344
|
-
const listeners = new Set<() => void>()
|
|
345
|
-
const config: ClientConfig<C> = {
|
|
346
|
-
views,
|
|
347
|
-
invoke: (handle, payload) => transport.send({ _tag: 'Invoke', handle, payload }),
|
|
348
|
-
}
|
|
349
|
-
const off = transport.onMessage((message) => {
|
|
350
|
-
// A snapshot replaces the tree wholesale (a fresh or reconnected session);
|
|
351
|
-
// patches fold into it. So a reconnect re-syncs without leaking stale nodes.
|
|
352
|
-
state.tree = Match.value(message).pipe(
|
|
353
|
-
Match.tag('Snapshot', ({ tree }) => tree),
|
|
354
|
-
Match.tag('Patches', ({ patches }) => Wire.apply(state.tree, patches)),
|
|
355
|
-
Match.exhaustive,
|
|
356
|
-
)
|
|
357
|
-
listeners.forEach((listener) => listener())
|
|
358
|
-
})
|
|
359
|
-
return {
|
|
360
|
-
node: () => renderWireTree(state.tree, config),
|
|
361
|
-
subscribe: (listener) => {
|
|
362
|
-
listeners.add(listener)
|
|
363
|
-
return () => void listeners.delete(listener)
|
|
364
|
-
},
|
|
365
|
-
snapshot: () => state.tree,
|
|
366
|
-
dispose: off,
|
|
367
|
-
}
|
|
368
|
-
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { WirePatch, WireTree } from '@playfast/reform'
|
|
2
|
+
|
|
3
|
+
export interface SnapshotMessage {
|
|
4
|
+
readonly _tag: 'Snapshot'
|
|
5
|
+
readonly tree: WireTree
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PatchesMessage {
|
|
9
|
+
readonly _tag: 'Patches'
|
|
10
|
+
readonly patches: ReadonlyArray<WirePatch>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ServerMessage = SnapshotMessage | PatchesMessage
|
|
14
|
+
|
|
15
|
+
export interface InvokeMessage {
|
|
16
|
+
readonly _tag: 'Invoke'
|
|
17
|
+
readonly handle: string
|
|
18
|
+
readonly payload: unknown
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RemoteTransport<Out, In> {
|
|
22
|
+
readonly send: (message: Out) => void
|
|
23
|
+
readonly onMessage: (handler: (message: In) => void) => () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ServerBinding {
|
|
27
|
+
readonly start: () => Promise<void>
|
|
28
|
+
readonly dispose: () => Promise<void>
|
|
29
|
+
}
|