@playfast/reform-remote 0.0.2 → 0.0.4
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 +2 -2
- package/src/client-keyed-slot.test.ts +136 -0
- package/src/client-remount.test.ts +127 -0
- package/src/client.ts +284 -0
- package/src/fixtures.ts +276 -0
- package/src/index.ts +49 -0
- package/src/memory.ts +36 -0
- package/src/react.ts +62 -0
- package/src/remote-view.typecheck.ts +75 -0
- package/src/server.test.ts +331 -0
- package/src/server.ts +343 -0
- package/src/transport.test.ts +205 -0
- package/src/transport.ts +192 -0
- package/dist/client.d.ts +0 -127
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -127
- package/dist/client.js.map +0 -1
- package/dist/index.d.ts +0 -11
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -20
- package/dist/index.js.map +0 -1
- package/dist/memory.d.ts +0 -15
- package/dist/memory.d.ts.map +0 -1
- package/dist/memory.js +0 -21
- package/dist/memory.js.map +0 -1
- package/dist/react.d.ts +0 -38
- package/dist/react.d.ts.map +0 -1
- package/dist/react.js +0 -29
- package/dist/react.js.map +0 -1
- package/dist/server.d.ts +0 -26
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -206
- package/dist/server.js.map +0 -1
- package/dist/transport.d.ts +0 -69
- package/dist/transport.d.ts.map +0 -1
- package/dist/transport.js +0 -116
- package/dist/transport.js.map +0 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { createElement, Fragment, type ReactNode } from 'react'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { expect, test, vi } from 'vitest'
|
|
4
|
+
import { Schema as S } from 'effect'
|
|
5
|
+
import { Ui, ui } from '@playfast/reform'
|
|
6
|
+
import type { InvokeMessage, ServerMessage } from './transport'
|
|
7
|
+
import { connect, serve } from './transport'
|
|
8
|
+
import { inMemoryTransportPair } from './memory'
|
|
9
|
+
import { remoteViews } from './client'
|
|
10
|
+
import { asyncCounterScene, CounterUi, counterScene } from './fixtures'
|
|
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
|
+
const draw = (node: ReactNode): void => void renderToStaticMarkup(createElement(Fragment, null, node))
|
|
16
|
+
|
|
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
|
+
interface Probe {
|
|
21
|
+
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
|
+
events?: Record<string, (payload: { by: number }) => void>
|
|
25
|
+
}
|
|
26
|
+
const probeView = (probe: Probe) =>
|
|
27
|
+
Ui.make(CounterUi, (props, _slots, events) => {
|
|
28
|
+
probe.props = props
|
|
29
|
+
probe.events = events
|
|
30
|
+
return null
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('remote UI over a transport: client renders server state and drives it back', async () => {
|
|
34
|
+
const { server: serverTransport, client: clientTransport } = inMemoryTransportPair<
|
|
35
|
+
ServerMessage,
|
|
36
|
+
InvokeMessage
|
|
37
|
+
>()
|
|
38
|
+
|
|
39
|
+
const probe: Probe = {}
|
|
40
|
+
const client = connect({
|
|
41
|
+
transport: clientTransport,
|
|
42
|
+
views: remoteViews<{ Counter: typeof CounterUi }>({ Counter: probeView(probe) }),
|
|
43
|
+
})
|
|
44
|
+
const server = serve({ scene: counterScene(), transport: serverTransport })
|
|
45
|
+
try {
|
|
46
|
+
await server.start()
|
|
47
|
+
draw(client.node())
|
|
48
|
+
expect(probe.props).toEqual({ count: 0 })
|
|
49
|
+
|
|
50
|
+
probe.events?.['bump']?.({ by: 6 })
|
|
51
|
+
await vi.waitFor(() => {
|
|
52
|
+
draw(client.node())
|
|
53
|
+
expect(probe.props).toEqual({ count: 6 })
|
|
54
|
+
})
|
|
55
|
+
} finally {
|
|
56
|
+
client.dispose()
|
|
57
|
+
await server.dispose()
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('server-initiated state change streams to the client with NO invoke (background load)', async () => {
|
|
62
|
+
const { server: serverTransport, client: clientTransport } = inMemoryTransportPair<
|
|
63
|
+
ServerMessage,
|
|
64
|
+
InvokeMessage
|
|
65
|
+
>()
|
|
66
|
+
|
|
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
|
+
const invokes = { count: 0 }
|
|
70
|
+
const countingClientTransport = {
|
|
71
|
+
...clientTransport,
|
|
72
|
+
send: (message: InvokeMessage) => {
|
|
73
|
+
invokes.count += 1
|
|
74
|
+
clientTransport.send(message)
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const probe: Probe = {}
|
|
79
|
+
const client = connect({
|
|
80
|
+
transport: countingClientTransport,
|
|
81
|
+
views: remoteViews<{ Counter: typeof CounterUi }>({ Counter: probeView(probe) }),
|
|
82
|
+
})
|
|
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
|
+
const server = serve({ scene: asyncCounterScene('30 millis'), transport: serverTransport })
|
|
86
|
+
try {
|
|
87
|
+
await server.start()
|
|
88
|
+
draw(client.node())
|
|
89
|
+
expect(probe.props).toEqual({ count: 0 })
|
|
90
|
+
|
|
91
|
+
await vi.waitFor(() => {
|
|
92
|
+
draw(client.node())
|
|
93
|
+
expect(probe.props).toEqual({ count: 7 })
|
|
94
|
+
})
|
|
95
|
+
// The client never sent a single Invoke — the frame was entirely server-initiated.
|
|
96
|
+
expect(invokes.count).toBe(0)
|
|
97
|
+
} finally {
|
|
98
|
+
client.dispose()
|
|
99
|
+
await server.dispose()
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('a fresh Snapshot replaces the client tree — stale nodes are dropped (reconnect semantics)', () => {
|
|
104
|
+
const { server: serverTransport, client: clientTransport } = inMemoryTransportPair<
|
|
105
|
+
ServerMessage,
|
|
106
|
+
unknown
|
|
107
|
+
>()
|
|
108
|
+
|
|
109
|
+
const rendered: Array<string> = []
|
|
110
|
+
class AUi extends ui('A', { props: S.Struct({}) }) {}
|
|
111
|
+
class BUi extends ui('B', { props: S.Struct({}) }) {}
|
|
112
|
+
const client = connect({
|
|
113
|
+
transport: clientTransport,
|
|
114
|
+
views: remoteViews<{ A: typeof AUi; B: typeof BUi }>({
|
|
115
|
+
A: Ui.make(AUi, () => (rendered.push('A'), null)),
|
|
116
|
+
B: Ui.make(BUi, () => (rendered.push('B'), null)),
|
|
117
|
+
}),
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const node = (id: string, name: string) => ({
|
|
121
|
+
id,
|
|
122
|
+
name,
|
|
123
|
+
parentId: null,
|
|
124
|
+
childIndex: 0,
|
|
125
|
+
slot: null,
|
|
126
|
+
key: null,
|
|
127
|
+
props: [],
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// First session: two roots.
|
|
131
|
+
serverTransport.send({ _tag: 'Snapshot', tree: [node('a', 'A'), node('b', 'B')] })
|
|
132
|
+
draw(client.node())
|
|
133
|
+
expect(rendered).toEqual(['A', 'B'])
|
|
134
|
+
|
|
135
|
+
// Reconnect: a fresh snapshot with only one root must drop the stale 'B'.
|
|
136
|
+
rendered.length = 0
|
|
137
|
+
serverTransport.send({ _tag: 'Snapshot', tree: [node('a', 'A')] })
|
|
138
|
+
draw(client.node())
|
|
139
|
+
expect(rendered).toEqual(['A'])
|
|
140
|
+
|
|
141
|
+
client.dispose()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('two clients each get their own server runtime — state is isolated', async () => {
|
|
145
|
+
const one = inMemoryTransportPair<ServerMessage, InvokeMessage>()
|
|
146
|
+
const two = inMemoryTransportPair<ServerMessage, InvokeMessage>()
|
|
147
|
+
|
|
148
|
+
const probeOne: Probe = {}
|
|
149
|
+
const probeTwo: Probe = {}
|
|
150
|
+
const clientOne = connect({
|
|
151
|
+
transport: one.client,
|
|
152
|
+
views: remoteViews<{ Counter: typeof CounterUi }>({ Counter: probeView(probeOne) }),
|
|
153
|
+
})
|
|
154
|
+
const clientTwo = connect({
|
|
155
|
+
transport: two.client,
|
|
156
|
+
views: remoteViews<{ Counter: typeof CounterUi }>({ Counter: probeView(probeTwo) }),
|
|
157
|
+
})
|
|
158
|
+
const serverOne = serve({ scene: counterScene(), transport: one.server })
|
|
159
|
+
const serverTwo = serve({ scene: counterScene(), transport: two.server })
|
|
160
|
+
try {
|
|
161
|
+
await serverOne.start()
|
|
162
|
+
await serverTwo.start()
|
|
163
|
+
// Render once so each probe captures its event callbacks.
|
|
164
|
+
draw(clientOne.node())
|
|
165
|
+
draw(clientTwo.node())
|
|
166
|
+
|
|
167
|
+
probeOne.events?.['bump']?.({ by: 9 })
|
|
168
|
+
await vi.waitFor(() => {
|
|
169
|
+
draw(clientOne.node())
|
|
170
|
+
expect(probeOne.props).toEqual({ count: 9 })
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// The second client's runtime never saw that bump.
|
|
174
|
+
draw(clientTwo.node())
|
|
175
|
+
expect(probeTwo.props).toEqual({ count: 0 })
|
|
176
|
+
} finally {
|
|
177
|
+
clientOne.dispose()
|
|
178
|
+
clientTwo.dispose()
|
|
179
|
+
await serverOne.dispose()
|
|
180
|
+
await serverTwo.dispose()
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('dispose detaches both ends: a post-dispose invoke no longer reaches the server', async () => {
|
|
185
|
+
const { server: serverTransport, client: clientTransport } = inMemoryTransportPair<
|
|
186
|
+
ServerMessage,
|
|
187
|
+
InvokeMessage
|
|
188
|
+
>()
|
|
189
|
+
|
|
190
|
+
const probe: Probe = {}
|
|
191
|
+
const client = connect({
|
|
192
|
+
transport: clientTransport,
|
|
193
|
+
views: remoteViews<{ Counter: typeof CounterUi }>({ Counter: probeView(probe) }),
|
|
194
|
+
})
|
|
195
|
+
const server = serve({ scene: counterScene(), transport: serverTransport })
|
|
196
|
+
await server.start()
|
|
197
|
+
draw(client.node())
|
|
198
|
+
|
|
199
|
+
await server.dispose()
|
|
200
|
+
client.dispose()
|
|
201
|
+
|
|
202
|
+
// After dispose the server's onMessage handler is detached; firing the stale
|
|
203
|
+
// callback is a no-op rather than a throw.
|
|
204
|
+
expect(() => probe.events?.['bump']?.({ by: 1 })).not.toThrow()
|
|
205
|
+
})
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Match } from 'effect'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { Wire, type WirePatch, type WireTree } from '@playfast/reform'
|
|
4
|
+
import type { Scene } from '@playfast/reform'
|
|
5
|
+
import { makeRemoteServer } from './server'
|
|
6
|
+
import { renderWireTree, type RemoteContract, type RemoteViewSet } from './client'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Binds the server driver and the client renderer to a transport. The wire
|
|
10
|
+
* carries only serializable data — patches one way, trigger invocations the
|
|
11
|
+
* other — so any duplex channel (WebSocket, postMessage, in-memory) works; a
|
|
12
|
+
* concrete adapter just implements `RemoteTransport`. See REMOTE_UI.md §5.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Server → client: the full current tree, replacing whatever the client holds.
|
|
17
|
+
* The first frame of every (re)connection — a fresh server runtime diffs from
|
|
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
|
+
*/
|
|
60
|
+
const BACKGROUND_FLUSH_MS = 16
|
|
61
|
+
|
|
62
|
+
/** Options for {@link serve} — a single named object so every reform-remote binding shares one shape. */
|
|
63
|
+
export interface ServeOptions {
|
|
64
|
+
readonly scene: Scene
|
|
65
|
+
readonly transport: RemoteTransport<ServerMessage, InvokeMessage>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const serve = (options: ServeOptions): ServerBinding => {
|
|
69
|
+
const { scene, transport } = options
|
|
70
|
+
const server = makeRemoteServer(scene)
|
|
71
|
+
// The first frame is a full snapshot (replace); every frame after is the diff since the
|
|
72
|
+
// last (apply). Both keep the server's `frame` baseline in step, so the diffs always
|
|
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.
|
|
87
|
+
const started = { value: false }
|
|
88
|
+
const flight = { promise: null as Promise<void> | null, dirty: false }
|
|
89
|
+
const push = (): Promise<void> => {
|
|
90
|
+
if (!started.value) return Promise.resolve()
|
|
91
|
+
if (flight.promise !== null) {
|
|
92
|
+
flight.dirty = true
|
|
93
|
+
return flight.promise
|
|
94
|
+
}
|
|
95
|
+
const run = (async (): Promise<void> => {
|
|
96
|
+
const patches = await server.renderDiff()
|
|
97
|
+
if (patches.length > 0) transport.send({ _tag: 'Patches', patches })
|
|
98
|
+
})().finally(() => {
|
|
99
|
+
flight.promise = null
|
|
100
|
+
if (flight.dirty) {
|
|
101
|
+
flight.dirty = false
|
|
102
|
+
void push()
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
flight.promise = run
|
|
106
|
+
return run
|
|
107
|
+
}
|
|
108
|
+
// Server-initiated changes are flushed on a short DEBOUNCE rather than synchronously: it
|
|
109
|
+
// coalesces a burst (a procedure that dispatches several facts) into one render, and the
|
|
110
|
+
// delay lets the drain fold the change into state before we read it — so the diff reflects
|
|
111
|
+
// the settled result, and the background flush never races the synchronous invoke push
|
|
112
|
+
// above. A client invoke still pushes immediately (its own settle path), so user actions
|
|
113
|
+
// stay snappy; this path only carries updates no interaction triggered.
|
|
114
|
+
const debounce = { handle: null as ReturnType<typeof setTimeout> | null }
|
|
115
|
+
const scheduleFlush = (): void => {
|
|
116
|
+
if (!started.value || debounce.handle !== null) return
|
|
117
|
+
debounce.handle = setTimeout(() => {
|
|
118
|
+
debounce.handle = null
|
|
119
|
+
void push()
|
|
120
|
+
}, BACKGROUND_FLUSH_MS)
|
|
121
|
+
}
|
|
122
|
+
const start = async (): Promise<void> => {
|
|
123
|
+
const tree = await server.render()
|
|
124
|
+
transport.send({ _tag: 'Snapshot', tree })
|
|
125
|
+
started.value = true
|
|
126
|
+
// Flush anything that changed during the (async) opening render window.
|
|
127
|
+
scheduleFlush()
|
|
128
|
+
}
|
|
129
|
+
const off = transport.onMessage((message) => {
|
|
130
|
+
void server.invoke(message.handle, message.payload).then(push)
|
|
131
|
+
})
|
|
132
|
+
const offChange = server.subscribe(scheduleFlush)
|
|
133
|
+
return {
|
|
134
|
+
start,
|
|
135
|
+
dispose: async (): Promise<void> => {
|
|
136
|
+
off()
|
|
137
|
+
offChange()
|
|
138
|
+
if (debounce.handle !== null) clearTimeout(debounce.handle)
|
|
139
|
+
await server.dispose()
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ClientBinding {
|
|
145
|
+
/** The current rendered node — re-read on each `subscribe` notification. */
|
|
146
|
+
readonly node: () => ReactNode
|
|
147
|
+
/** Notified whenever applied patches change the tree. */
|
|
148
|
+
readonly subscribe: (listener: () => void) => () => void
|
|
149
|
+
/**
|
|
150
|
+
* The current wire tree — a stable reference that changes only when a frame is
|
|
151
|
+
* applied, so it is a valid `useSyncExternalStore` snapshot (the React binding
|
|
152
|
+
* builds on this).
|
|
153
|
+
*/
|
|
154
|
+
readonly snapshot: () => WireTree
|
|
155
|
+
readonly dispose: () => void
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Options for {@link connect} — a single named object, mirroring {@link ServeOptions}. */
|
|
159
|
+
export interface ConnectOptions<C extends RemoteContract> {
|
|
160
|
+
readonly transport: RemoteTransport<InvokeMessage, ServerMessage>
|
|
161
|
+
readonly views: RemoteViewSet<C>
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const connect = <C extends RemoteContract>(options: ConnectOptions<C>): ClientBinding => {
|
|
165
|
+
const { transport, views } = options
|
|
166
|
+
const state: { tree: WireTree } = { tree: [] }
|
|
167
|
+
const listeners = new Set<() => void>()
|
|
168
|
+
const config = {
|
|
169
|
+
views,
|
|
170
|
+
invoke: (handle: string, payload: unknown): void =>
|
|
171
|
+
transport.send({ _tag: 'Invoke', handle, payload }),
|
|
172
|
+
}
|
|
173
|
+
const off = transport.onMessage((message) => {
|
|
174
|
+
// A snapshot replaces the tree wholesale (a fresh or reconnected session);
|
|
175
|
+
// patches fold into it. So a reconnect re-syncs without leaking stale nodes.
|
|
176
|
+
state.tree = Match.value(message).pipe(
|
|
177
|
+
Match.tag('Snapshot', ({ tree }) => tree),
|
|
178
|
+
Match.tag('Patches', ({ patches }) => Wire.apply(state.tree, patches)),
|
|
179
|
+
Match.exhaustive,
|
|
180
|
+
)
|
|
181
|
+
for (const listener of listeners) listener()
|
|
182
|
+
})
|
|
183
|
+
return {
|
|
184
|
+
node: () => renderWireTree(state.tree, config),
|
|
185
|
+
subscribe: (listener) => {
|
|
186
|
+
listeners.add(listener)
|
|
187
|
+
return () => void listeners.delete(listener)
|
|
188
|
+
},
|
|
189
|
+
snapshot: () => state.tree,
|
|
190
|
+
dispose: off,
|
|
191
|
+
}
|
|
192
|
+
}
|
package/dist/client.d.ts
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
|
-
import { Schema } from 'effect';
|
|
3
|
-
import { type MadeView, Ui, type UiClass, type WireTree } from '@playfast/reform';
|
|
4
|
-
/**
|
|
5
|
-
* The client side of the remote transport: fold the server's patches with
|
|
6
|
-
* `Wire.apply`, then render the resulting `WireTree` with locally registered
|
|
7
|
-
* presentations. The presentations are the SAME `Ui.make` views the local
|
|
8
|
-
* `@playfast/react` renderer uses — one presentation, many consumers (REMOTE_UI.md):
|
|
9
|
-
*
|
|
10
|
-
* - Each wire node's view runs as a stable React COMPONENT, so a view body's own
|
|
11
|
-
* hooks (`useState`, effects) work exactly as under `@playfast/react`.
|
|
12
|
-
* - Slots are COMPONENTS that render that slot's wire children, so a view's
|
|
13
|
-
* `<slots.Foo/>` / `createElement(slots.Foo, props)` works unchanged (the
|
|
14
|
-
* per-slot props were captured server-side; the client component ignores them).
|
|
15
|
-
* - Data props are DECODED through the contract's own props schema — the inverse of
|
|
16
|
-
* the server's `Schema.encodeUnknown` — so `Option`/`Date`/branded props arrive as
|
|
17
|
-
* real instances, not their `{_tag,value}` wire shape.
|
|
18
|
-
*
|
|
19
|
-
* A client view is authored with `Ui.make(SomeUi, …)` — exactly like a local view —
|
|
20
|
-
* and registered with `remoteViews<AppContract>({ Some: SomeView, … })`. There is no
|
|
21
|
-
* separate `remoteView` call: a `Ui.make` view carries its own contract, so `remoteViews`
|
|
22
|
-
* recovers each view's wire name + props schema straight from it.
|
|
23
|
-
*/
|
|
24
|
-
/**
|
|
25
|
-
* The dynamic presentation shape the renderer calls — slots and events erased to
|
|
26
|
-
* their runtime form. Views are authored with `Ui.make` (contract-typed); this is
|
|
27
|
-
* only the internal shape `renderWireTree` invokes them through.
|
|
28
|
-
*/
|
|
29
|
-
export type RemoteView = (props: Record<string, unknown>, slots: Record<string, (slotProps?: {
|
|
30
|
-
readonly slotKey?: string;
|
|
31
|
-
}) => ReactNode>, events: Record<string, (payload: unknown) => void>) => ReactNode;
|
|
32
|
-
/** A contract-bound view paired with the contract name the renderer looks it up by, and
|
|
33
|
-
* (for a WIRED contract) the props schema used to DECODE wire props back into their
|
|
34
|
-
* decoded domain types — the symmetric inverse of the server's `Schema.encodeUnknown`.
|
|
35
|
-
* Without it, an `Option`/`Date`/branded prop would reach the view as its raw encoded
|
|
36
|
-
* shape (e.g. `{_tag:'Some',value}`) instead of a real `Option`. */
|
|
37
|
-
export interface RegisteredRemoteView {
|
|
38
|
-
readonly name: string;
|
|
39
|
-
readonly view: RemoteView;
|
|
40
|
-
readonly propsSchema?: Schema.Schema<Record<string, unknown>, unknown>;
|
|
41
|
-
}
|
|
42
|
-
/** The by-name presentation record the renderer walks — the unbranded runtime shape behind
|
|
43
|
-
* a `RemoteViewSet`. Internal: it is NOT part of the public API, so a raw record of this
|
|
44
|
-
* shape can never be handed to `connect`/`renderWireTree`/`<RemoteUI>` (those demand the
|
|
45
|
-
* branded `RemoteViewSet<C>`). A `RemoteViewSet<C>` is structurally `ViewRegistry & brand`,
|
|
46
|
-
* so it always widens back to this when the renderer needs the plain lookup. */
|
|
47
|
-
type ViewRegistry = Readonly<Record<string, RegisteredRemoteView>>;
|
|
48
|
-
/** Phantom brand carrying the server `RemoteContract` a view set was checked against. Typed
|
|
49
|
-
* as a function OF `C` (never called) so the contract appears in the type WITHOUT needing a
|
|
50
|
-
* runtime value of `C` — `remoteViews<C>(views)` can brand the set cast-free even though the
|
|
51
|
-
* client imports the contract as a TYPE only (trpc-style), never as a value. */
|
|
52
|
-
declare const ViewSetContract: unique symbol;
|
|
53
|
-
/**
|
|
54
|
-
* The presentation set the client renders by contract name — the `ViewRegistry` the renderer
|
|
55
|
-
* walks, BRANDED with the server `RemoteContract` `C` it was checked against. The brand is
|
|
56
|
-
* what makes the whole client chain typesafe: `connect`, `renderWireTree`, and `<RemoteUI>`
|
|
57
|
-
* accept only a `RemoteViewSet` *produced by* `remoteViews<C>(…)` — never an arbitrary
|
|
58
|
-
* `Record`, which lacks the brand — so a view set cannot reach the renderer without having
|
|
59
|
-
* been type-checked to implement exactly the server's shape. `C` is recovered by inference at
|
|
60
|
-
* each consumer, so passing a `RemoteViewSet<AppContract>` types the whole chain to that app.
|
|
61
|
-
*
|
|
62
|
-
* No default for `C`: a `RemoteViewSet` is ALWAYS bound to a concrete contract (inferred from
|
|
63
|
-
* `remoteViews<AppContract>(…)`), never silently widened to the `RemoteContract` bound — that
|
|
64
|
-
* is what keeps the safety end to end.
|
|
65
|
-
*/
|
|
66
|
-
export type RemoteViewSet<C extends RemoteContract> = ViewRegistry & {
|
|
67
|
-
readonly [ViewSetContract]: (contract: C) => void;
|
|
68
|
-
};
|
|
69
|
-
/**
|
|
70
|
-
* The shared "shape" the server publishes and the client implements — the trpc `AppRouter`
|
|
71
|
-
* analog. It is a registry of the server's UI contracts (the `ui(...)` classes the scene's
|
|
72
|
-
* wire tree can emit) keyed by a label. The SERVER declares it ONCE (`remoteContract({…})`)
|
|
73
|
-
* and exports `typeof` it; the CLIENT imports THAT TYPE and `remoteViews<AppContract>(…)`
|
|
74
|
-
* checks its view set against it, so the client is forced to implement exactly the server's
|
|
75
|
-
* views. The wire name + props schema are read from each view's own contract at runtime, so
|
|
76
|
-
* the label is only the type-level join key — the contract is never needed as a value here.
|
|
77
|
-
*
|
|
78
|
-
* `UiClass<any>` is the registry's *upper bound*; the precise per-contract types are
|
|
79
|
-
* preserved by inferring `C` narrowly at the `remoteContract` declaration site (so
|
|
80
|
-
* `Ui.Contract<C[K]>` recovers the real contract, not `any`).
|
|
81
|
-
*/
|
|
82
|
-
export type RemoteContract = Readonly<Record<string, UiClass<any>>>;
|
|
83
|
-
/**
|
|
84
|
-
* Declare a server's `RemoteContract` with its precise key/contract types inferred (the
|
|
85
|
-
* `const` type parameter keeps each value's specific `UiClass<…>` instead of widening to
|
|
86
|
-
* the `UiClass<any>` bound). Define it ONCE next to the scene and export `typeof` it as the
|
|
87
|
-
* UI requirements the client implements against:
|
|
88
|
-
*
|
|
89
|
-
* export const AppContract = remoteContract({ Shell: ShellUi, Sidebar: SidebarUi })
|
|
90
|
-
* export type AppContract = typeof AppContract
|
|
91
|
-
*
|
|
92
|
-
* The client then imports only the TYPE and implements it:
|
|
93
|
-
*
|
|
94
|
-
* import type { AppContract } from '…/app-contract'
|
|
95
|
-
* export const views = remoteViews<AppContract>({ Shell: ShellView, Sidebar: SidebarView })
|
|
96
|
-
*/
|
|
97
|
-
export declare const remoteContract: <const C extends RemoteContract>(contract: C) => C;
|
|
98
|
-
/**
|
|
99
|
-
* The client view set a `RemoteContract` demands: exactly one `Ui.make` view per contract,
|
|
100
|
-
* each typed to THAT contract (`MadeView<Ui.Contract<C[K]>>`). A missing key, an extra key,
|
|
101
|
-
* or a view authored for the wrong contract is a COMPILE error — the client cannot connect
|
|
102
|
-
* to the server without implementing precisely its shape, the same guarantee trpc gives a
|
|
103
|
-
* client built from `AppRouter`.
|
|
104
|
-
*/
|
|
105
|
-
export type RemoteViews<C extends RemoteContract> = {
|
|
106
|
-
readonly [K in keyof C]: MadeView<Ui.Contract<C[K]>>;
|
|
107
|
-
};
|
|
108
|
-
/**
|
|
109
|
-
* Implement a server's UI shape: `remoteViews<AppContract>({ … })` takes the server's
|
|
110
|
-
* `RemoteContract` as a TYPE PARAMETER (the client imports only `typeof AppContract`, never a
|
|
111
|
-
* value — trpc-style) and a set of `Ui.make` views type-checked to implement EXACTLY that
|
|
112
|
-
* contract. Each view carries its own contract (`Ui.make(SomeUi, …)`), so the wire name and
|
|
113
|
-
* props schema are recovered from the view itself — a renamed/retyped contract can never
|
|
114
|
-
* drift from its presentation, and the SAME view authored for the local renderer is reused.
|
|
115
|
-
* The result is a `RemoteViewSet<C>` (the branded set `connect`/`renderWireTree`/`<RemoteUI>`
|
|
116
|
-
* accept); an unbranded `Record` can never be substituted.
|
|
117
|
-
*/
|
|
118
|
-
export declare const remoteViews: <C extends RemoteContract>(views: RemoteViews<C>) => RemoteViewSet<C>;
|
|
119
|
-
export interface ClientConfig<C extends RemoteContract> {
|
|
120
|
-
/** Presentation (+ optional props schema) per contract name — a contract-checked set. */
|
|
121
|
-
readonly views: RemoteViewSet<C>;
|
|
122
|
-
/** Deliver a trigger invocation to the server (the transport's send). */
|
|
123
|
-
readonly invoke: (handle: string, payload: unknown) => void;
|
|
124
|
-
}
|
|
125
|
-
export declare const renderWireTree: <C extends RemoteContract>(tree: WireTree, config: ClientConfig<C>) => ReactNode;
|
|
126
|
-
export {};
|
|
127
|
-
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,SAAS,EAAU,MAAM,OAAO,CAAA;AACvE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EACL,KAAK,QAAQ,EACb,EAAE,EACF,KAAK,OAAO,EAIZ,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AAQzB;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,CACvB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAG9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,KAAK,SAAS,CAAC,EAC/E,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC,KAC/C,SAAS,CAAA;AAEd;;;;qEAIqE;AACrE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,CAAA;CACvE;AAED;;;;iFAIiF;AACjF,KAAK,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAA;AAElE;;;iFAGiF;AACjF,QAAA,MAAM,eAAe,EAAE,OAAO,MAAsD,CAAA;AAEpF;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,cAAc,IAAI,YAAY,GAAG;IACnE,QAAQ,CAAC,CAAC,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAA;CAClD,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAEnE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,CAAC,CAAC,SAAS,cAAc,EAAE,UAAU,CAAC,KAAG,CAAa,CAAA;AAE1F;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,cAAc,IAAI;IAClD,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACrD,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,cAAc,EAAE,OAAO,WAAW,CAAC,CAAC,CAAC,KAAG,aAAa,CAAC,CAAC,CAqB5F,CAAA;AAED,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,cAAc;IACpD,yFAAyF;IACzF,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,CAAA;IAChC,yEAAyE;IACzE,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;CAC5D;AAoGD,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,cAAc,EACrD,MAAM,QAAQ,EACd,QAAQ,YAAY,CAAC,CAAC,CAAC,KACtB,SAOA,CAAA"}
|
package/dist/client.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { createElement, Fragment, useRef } from 'react';
|
|
2
|
-
import { Schema } from 'effect';
|
|
3
|
-
import { Ui, UiViewContract, Wire, } from '@playfast/reform';
|
|
4
|
-
/** Phantom brand carrying the server `RemoteContract` a view set was checked against. Typed
|
|
5
|
-
* as a function OF `C` (never called) so the contract appears in the type WITHOUT needing a
|
|
6
|
-
* runtime value of `C` — `remoteViews<C>(views)` can brand the set cast-free even though the
|
|
7
|
-
* client imports the contract as a TYPE only (trpc-style), never as a value. */
|
|
8
|
-
const ViewSetContract = Symbol.for('reform-remote/view-set-contract');
|
|
9
|
-
/**
|
|
10
|
-
* Declare a server's `RemoteContract` with its precise key/contract types inferred (the
|
|
11
|
-
* `const` type parameter keeps each value's specific `UiClass<…>` instead of widening to
|
|
12
|
-
* the `UiClass<any>` bound). Define it ONCE next to the scene and export `typeof` it as the
|
|
13
|
-
* UI requirements the client implements against:
|
|
14
|
-
*
|
|
15
|
-
* export const AppContract = remoteContract({ Shell: ShellUi, Sidebar: SidebarUi })
|
|
16
|
-
* export type AppContract = typeof AppContract
|
|
17
|
-
*
|
|
18
|
-
* The client then imports only the TYPE and implements it:
|
|
19
|
-
*
|
|
20
|
-
* import type { AppContract } from '…/app-contract'
|
|
21
|
-
* export const views = remoteViews<AppContract>({ Shell: ShellView, Sidebar: SidebarView })
|
|
22
|
-
*/
|
|
23
|
-
export const remoteContract = (contract) => contract;
|
|
24
|
-
/**
|
|
25
|
-
* Implement a server's UI shape: `remoteViews<AppContract>({ … })` takes the server's
|
|
26
|
-
* `RemoteContract` as a TYPE PARAMETER (the client imports only `typeof AppContract`, never a
|
|
27
|
-
* value — trpc-style) and a set of `Ui.make` views type-checked to implement EXACTLY that
|
|
28
|
-
* contract. Each view carries its own contract (`Ui.make(SomeUi, …)`), so the wire name and
|
|
29
|
-
* props schema are recovered from the view itself — a renamed/retyped contract can never
|
|
30
|
-
* drift from its presentation, and the SAME view authored for the local renderer is reused.
|
|
31
|
-
* The result is a `RemoteViewSet<C>` (the branded set `connect`/`renderWireTree`/`<RemoteUI>`
|
|
32
|
-
* accept); an unbranded `Record` can never be substituted.
|
|
33
|
-
*/
|
|
34
|
-
export const remoteViews = (views) => {
|
|
35
|
-
const list = Object.values(views);
|
|
36
|
-
const byName = Object.fromEntries(list.map((view) => {
|
|
37
|
-
const viewContract = view[UiViewContract];
|
|
38
|
-
const propsSchema = viewContract.manifest.props;
|
|
39
|
-
// `Node` is `ReactNode` and `ViewImpl`'s params widen to the dynamic shape the
|
|
40
|
-
// renderer calls, so the contract-typed view IS a `RemoteView` — no cast.
|
|
41
|
-
const entry = {
|
|
42
|
-
name: viewContract.manifest.name,
|
|
43
|
-
view,
|
|
44
|
-
...(propsSchema !== undefined ? { propsSchema } : {}),
|
|
45
|
-
};
|
|
46
|
-
return [entry.name, entry];
|
|
47
|
-
}));
|
|
48
|
-
// Brand the record with the contract `C` it was checked against. The brand is a phantom
|
|
49
|
-
// function OF `C` that is never called, so it needs NO runtime value of `C` — the set is
|
|
50
|
-
// built cast-free even though the client only has the contract as a type. The renderer
|
|
51
|
-
// reads the set by string key; the brand is never read at runtime.
|
|
52
|
-
return Object.assign(byName, { [ViewSetContract]: (_contract) => { } });
|
|
53
|
-
};
|
|
54
|
-
/**
|
|
55
|
-
* A stable top-level component for one wire node — stable identity so React keeps a
|
|
56
|
-
* view's local state across frames (the `node.id` keys it). It decodes props, builds
|
|
57
|
-
* slot components, and runs the registered view INSIDE this component so the view's
|
|
58
|
-
* own hooks get a fiber. A child slot renders its wire children as nested `WireNodeView`s.
|
|
59
|
-
*/
|
|
60
|
-
const WireNodeView = ({ node, tree, config, }) => {
|
|
61
|
-
// The latest render inputs, for the stable slot closures below to read. Mutated every
|
|
62
|
-
// render so a slot always renders against the current tree, even though its function
|
|
63
|
-
// identity never changes.
|
|
64
|
-
const latest = useRef({ node, tree, config });
|
|
65
|
-
latest.current = { node, tree, config };
|
|
66
|
-
// Slot components cached by name across renders. CRITICAL for state preservation: a parent
|
|
67
|
-
// view that renders a slot as `<slots.Foo/>` (the Ui.make convention) passes the slot value
|
|
68
|
-
// as the element TYPE — if that value were a fresh closure each render (as it was before),
|
|
69
|
-
// React would see a new component type every frame and UNMOUNT+REMOUNT the whole slot
|
|
70
|
-
// subtree, resetting every descendant view's local hook state (e.g. a modal's open/step
|
|
71
|
-
// `useState` would snap back, closing the modal on any unrelated background patch). Caching
|
|
72
|
-
// the closure by slot name gives `<slots.Foo/>` a stable type, so React reconciles the slot's
|
|
73
|
-
// children by id instead of remounting them. The closure reads `latest` so it still renders
|
|
74
|
-
// the current tree.
|
|
75
|
-
const slotCache = useRef({});
|
|
76
|
-
const registered = config.views[node.name];
|
|
77
|
-
if (registered === undefined)
|
|
78
|
-
return null;
|
|
79
|
-
const encoded = {};
|
|
80
|
-
const events = {};
|
|
81
|
-
for (const prop of node.props) {
|
|
82
|
-
if (prop._tag === 'Data')
|
|
83
|
-
encoded[prop.name] = prop.value;
|
|
84
|
-
else
|
|
85
|
-
events[prop.name] = (payload) => config.invoke(prop.handle, payload);
|
|
86
|
-
}
|
|
87
|
-
const props = registered.propsSchema === undefined
|
|
88
|
-
? encoded
|
|
89
|
-
: Schema.decodeUnknownSync(registered.propsSchema)(encoded);
|
|
90
|
-
const children = Wire.childrenOf(tree, node.id);
|
|
91
|
-
const slots = {};
|
|
92
|
-
for (const slotName of new Set(children.map((child) => child.slot))) {
|
|
93
|
-
if (slotName === null)
|
|
94
|
-
continue;
|
|
95
|
-
// A slot is a COMPONENT rendering that slot's wire children — usable as `<slots.Foo/>`
|
|
96
|
-
// (Ui.make convention) or `slots.Foo()` (thunk convention). The function is cached so its
|
|
97
|
-
// identity is STABLE across renders (see `slotCache` above); it reads `latest.current` so
|
|
98
|
-
// each invocation renders against the current tree/config.
|
|
99
|
-
//
|
|
100
|
-
// KEYED SLOTS: when the caller passes `slotKey` (`<slots.Row slotKey={id} />`), render only
|
|
101
|
-
// the ONE wire child whose `key` matches — the per-item identity the server captured from
|
|
102
|
-
// the parent's React `key` (see WireNode.key). This is what lets a LIST slot be invoked once
|
|
103
|
-
// per item without each call rendering the WHOLE list (the duplicate-rows bug). With NO
|
|
104
|
-
// `slotKey` the behaviour is unchanged: render every child of the slot (correct for a
|
|
105
|
-
// singleton slot rendered once, e.g. `<slots.Create/>`).
|
|
106
|
-
const cached = slotCache.current[slotName];
|
|
107
|
-
const stable = cached ??
|
|
108
|
-
((slotProps) => {
|
|
109
|
-
const { node: currentNode, tree: currentTree, config: currentConfig } = latest.current;
|
|
110
|
-
const requestedKey = slotProps?.slotKey;
|
|
111
|
-
return createElement(Fragment, null, ...Wire.childrenOf(currentTree, currentNode.id)
|
|
112
|
-
.filter((child) => child.slot === slotName)
|
|
113
|
-
.filter((child) => requestedKey === undefined || child.key === requestedKey)
|
|
114
|
-
.map((child) => createElement(WireNodeView, {
|
|
115
|
-
key: child.id,
|
|
116
|
-
node: child,
|
|
117
|
-
tree: currentTree,
|
|
118
|
-
config: currentConfig,
|
|
119
|
-
})));
|
|
120
|
-
});
|
|
121
|
-
slotCache.current[slotName] = stable;
|
|
122
|
-
slots[slotName] = stable;
|
|
123
|
-
}
|
|
124
|
-
return registered.view(props, slots, events);
|
|
125
|
-
};
|
|
126
|
-
export const renderWireTree = (tree, config) => createElement(Fragment, null, ...Wire.roots(tree).map((node) => createElement(WireNodeView, { key: node.id, node, tree, config })));
|
|
127
|
-
//# sourceMappingURL=client.js.map
|
package/dist/client.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|