@playfast/reform-remote-web 0.0.2 → 0.0.3
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.test.ts +155 -0
- package/{dist/errors.d.ts → src/errors.ts} +15 -11
- package/{dist/index.js → src/index.ts} +11 -5
- package/src/transport.ts +175 -0
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -6
- package/dist/errors.js.map +0 -1
- package/dist/index.d.ts +0 -13
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/transport.d.ts +0 -55
- package/dist/transport.d.ts.map +0 -1
- package/dist/transport.js +0 -108
- package/dist/transport.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/reform-remote-web",
|
|
3
3
|
"playbook": "./playbook",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Browser/web WebSocket client transport for reform-remote — streams a server-run reform scene to a thin renderer.",
|
|
7
7
|
"keywords": [
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"./*": "./src/*.ts"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
|
-
"
|
|
30
|
+
"src",
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { AddressInfo } from 'node:net'
|
|
2
|
+
import { createElement, Fragment, type ReactNode } from 'react'
|
|
3
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
4
|
+
import { afterEach, expect, test, vi } from 'vitest'
|
|
5
|
+
import { Layer, Schema as S } from 'effect'
|
|
6
|
+
import {
|
|
7
|
+
Composition,
|
|
8
|
+
Engine,
|
|
9
|
+
Event,
|
|
10
|
+
Reducer,
|
|
11
|
+
State,
|
|
12
|
+
StateGroup,
|
|
13
|
+
Ui,
|
|
14
|
+
provide,
|
|
15
|
+
scene,
|
|
16
|
+
ui,
|
|
17
|
+
} from '@playfast/reform'
|
|
18
|
+
import { connect, remoteViews } from '@playfast/reform-remote'
|
|
19
|
+
import { type NodeWebSocketServer, serveNodeWebSocket } from '@playfast/reform-remote-node'
|
|
20
|
+
import { createWebSocketClientTransport } from './index'
|
|
21
|
+
|
|
22
|
+
// Self-contained counter scene for the server end of the round-trip.
|
|
23
|
+
class Count extends State.make('count', S.Number) {}
|
|
24
|
+
class Counters extends StateGroup.make(Count) {}
|
|
25
|
+
class Bumped extends Event.make('Bumped', S.Struct({ by: S.Number })) {}
|
|
26
|
+
class Bump extends Reducer.make('Bump', { states: [Count], events: [Bumped] }) {}
|
|
27
|
+
class CounterUi extends ui('Counter', {
|
|
28
|
+
props: S.Struct({ count: S.Number }),
|
|
29
|
+
events: { bump: S.Struct({ by: S.Number }) },
|
|
30
|
+
}) {}
|
|
31
|
+
class Counter extends Composition.make('Counter', { title: 'Counter', ui: CounterUi, states: [Count] }) {}
|
|
32
|
+
|
|
33
|
+
const counterScene = () => {
|
|
34
|
+
const presentation = Layer.mergeAll(
|
|
35
|
+
provide(CounterUi, Ui.make(CounterUi, ({ count }) => `count:${count}`)),
|
|
36
|
+
StateGroup.live(Counters, { count: 0 }),
|
|
37
|
+
)
|
|
38
|
+
const app = Layer.mergeAll(
|
|
39
|
+
Composition.live(Counter, function* () {
|
|
40
|
+
const count = yield* StateGroup.select(Counters, 'count')
|
|
41
|
+
const bump = yield* Event.trigger(Bumped)
|
|
42
|
+
return (yield* CounterUi)({ count }, { bump })
|
|
43
|
+
}),
|
|
44
|
+
Reducer.live(Bump, (n, event) => n + event.by),
|
|
45
|
+
).pipe(Layer.provideMerge(presentation), Layer.provideMerge(Engine))
|
|
46
|
+
return scene(Counter, { provide: [app] })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface Probe {
|
|
50
|
+
props?: Record<string, unknown>
|
|
51
|
+
// The counter contract's only event, `bump`, takes `{ by: number }`.
|
|
52
|
+
events?: Record<string, (payload: { by: number }) => void>
|
|
53
|
+
}
|
|
54
|
+
const probeView = (probe: Probe) =>
|
|
55
|
+
Ui.make(CounterUi, (props, _slots, events) => {
|
|
56
|
+
probe.props = props
|
|
57
|
+
probe.events = events
|
|
58
|
+
return null
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Views render as React components now, so a test runs them by rendering node().
|
|
62
|
+
const draw = (node: ReactNode): void => void renderToStaticMarkup(createElement(Fragment, null, node))
|
|
63
|
+
|
|
64
|
+
const listening = (host: NodeWebSocketServer): Promise<number> =>
|
|
65
|
+
new Promise((resolve) =>
|
|
66
|
+
host.wss.once('listening', () => resolve((host.wss.address() as AddressInfo).port)),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const open = new Set<NodeWebSocketServer>()
|
|
70
|
+
afterEach(async () => {
|
|
71
|
+
await Promise.all([...open].map((host) => host.stop()))
|
|
72
|
+
open.clear()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('the web client renders server state over a real socket and drives it back', async () => {
|
|
76
|
+
const host = serveNodeWebSocket({ scene: counterScene() })
|
|
77
|
+
open.add(host)
|
|
78
|
+
const port = await listening(host)
|
|
79
|
+
|
|
80
|
+
const transport = createWebSocketClientTransport({ url: `ws://localhost:${port}` })
|
|
81
|
+
const probe: Probe = {}
|
|
82
|
+
const client = connect({
|
|
83
|
+
transport,
|
|
84
|
+
views: remoteViews<{ Counter: typeof CounterUi }>({ Counter: probeView(probe) }),
|
|
85
|
+
})
|
|
86
|
+
try {
|
|
87
|
+
await vi.waitFor(() => {
|
|
88
|
+
draw(client.node())
|
|
89
|
+
expect(probe.props).toEqual({ count: 0 })
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
probe.events?.['bump']?.({ by: 6 })
|
|
93
|
+
await vi.waitFor(() => {
|
|
94
|
+
draw(client.node())
|
|
95
|
+
expect(probe.props).toEqual({ count: 6 })
|
|
96
|
+
})
|
|
97
|
+
expect(transport.status()).toBe('open')
|
|
98
|
+
} finally {
|
|
99
|
+
transport.close()
|
|
100
|
+
client.dispose()
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('a dropped connection reconnects with backoff and re-syncs via the fresh snapshot', async () => {
|
|
105
|
+
const host = serveNodeWebSocket({ scene: counterScene() })
|
|
106
|
+
open.add(host)
|
|
107
|
+
const port = await listening(host)
|
|
108
|
+
|
|
109
|
+
const transport = createWebSocketClientTransport({
|
|
110
|
+
url: `ws://localhost:${port}`,
|
|
111
|
+
baseDelayMs: 20,
|
|
112
|
+
maxDelayMs: 80,
|
|
113
|
+
})
|
|
114
|
+
const probe: Probe = {}
|
|
115
|
+
const client = connect({
|
|
116
|
+
transport,
|
|
117
|
+
views: remoteViews<{ Counter: typeof CounterUi }>({ Counter: probeView(probe) }),
|
|
118
|
+
})
|
|
119
|
+
try {
|
|
120
|
+
// Live, then bump so the client holds a non-zero count.
|
|
121
|
+
await vi.waitFor(() => {
|
|
122
|
+
draw(client.node())
|
|
123
|
+
expect(probe.props).toEqual({ count: 0 })
|
|
124
|
+
})
|
|
125
|
+
probe.events?.['bump']?.({ by: 5 })
|
|
126
|
+
await vi.waitFor(() => {
|
|
127
|
+
draw(client.node())
|
|
128
|
+
expect(probe.props).toEqual({ count: 5 })
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Kill the server out from under the client.
|
|
132
|
+
await host.stop()
|
|
133
|
+
open.delete(host)
|
|
134
|
+
await vi.waitFor(() => expect(transport.status()).toBe('reconnecting'))
|
|
135
|
+
|
|
136
|
+
// Bring a fresh runtime back on the same port. The client reconnects and the
|
|
137
|
+
// new session's snapshot resets the tree — count is 0 again, proving re-sync
|
|
138
|
+
// (not the stale 5).
|
|
139
|
+
const restarted = serveNodeWebSocket({ scene: counterScene(), port })
|
|
140
|
+
open.add(restarted)
|
|
141
|
+
await listening(restarted)
|
|
142
|
+
|
|
143
|
+
await vi.waitFor(
|
|
144
|
+
() => {
|
|
145
|
+
draw(client.node())
|
|
146
|
+
expect(probe.props).toEqual({ count: 0 })
|
|
147
|
+
expect(transport.status()).toBe('open')
|
|
148
|
+
},
|
|
149
|
+
{ timeout: 4000 },
|
|
150
|
+
)
|
|
151
|
+
} finally {
|
|
152
|
+
transport.close()
|
|
153
|
+
client.dispose()
|
|
154
|
+
}
|
|
155
|
+
})
|
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
import { type Cause } from 'effect'
|
|
1
|
+
import { type Cause, Data } from 'effect'
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
4
|
* Tagged errors for the web client transport. Every failure carries a `_tag` and
|
|
4
5
|
* structured fields — never a bare `Error` — so callers can match with Effect
|
|
5
6
|
* `catchTag` / `Match`. Tags are namespaced by package to stay globally unique.
|
|
6
7
|
*/
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* The constructor shape `Data.TaggedError(tag)<A>` produces, named so the
|
|
9
11
|
* generated `.d.ts` can describe the `extends` base under `isolatedDeclarations`
|
|
10
12
|
* (which forbids an inferred expression in an extends clause).
|
|
11
13
|
*/
|
|
12
|
-
type TaggedErrorClass<Tag extends string, A extends Record<string, unknown>> = new (
|
|
13
|
-
|
|
14
|
-
} & Readonly<A
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
type TaggedErrorClass<Tag extends string, A extends Record<string, unknown>> = new (
|
|
15
|
+
args: A,
|
|
16
|
+
) => Cause.YieldableError & { readonly _tag: Tag } & Readonly<A>
|
|
17
|
+
|
|
18
|
+
const MissingWebSocketBase: TaggedErrorClass<
|
|
19
|
+
'@playfast/reform-remote-web/MissingWebSocket',
|
|
20
|
+
{ readonly message: string }
|
|
21
|
+
> = Data.TaggedError('@playfast/reform-remote-web/MissingWebSocket')<{
|
|
22
|
+
readonly message: string
|
|
23
|
+
}>
|
|
24
|
+
|
|
18
25
|
/** Thrown when no `WebSocket` is available and none was injected via options. */
|
|
19
|
-
export
|
|
20
|
-
}
|
|
21
|
-
export {};
|
|
22
|
-
//# sourceMappingURL=errors.d.ts.map
|
|
26
|
+
export class MissingWebSocket extends MissingWebSocketBase {}
|
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
* from "@playfast/reform-remote-web/transport"`). The select re-exports below keep
|
|
7
7
|
* the common entry points nameable from the root.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
export * as
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
|
|
10
|
+
export * as Transport from './transport.js'
|
|
11
|
+
export * as Errors from './errors.js'
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
type ClientStatus,
|
|
15
|
+
createWebSocketClientTransport,
|
|
16
|
+
type WebSocketClientTransport,
|
|
17
|
+
type WebSocketClientTransportOptions,
|
|
18
|
+
} from './transport.js'
|
|
19
|
+
export { MissingWebSocket } from './errors.js'
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Redacted } from 'effect'
|
|
2
|
+
import type { InvokeMessage, RemoteTransport, ServerMessage } from '@playfast/reform-remote'
|
|
3
|
+
import { MissingWebSocket } from './errors.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The browser/web client side of the remote transport. A factory that wraps a
|
|
7
|
+
* WebSocket into a `RemoteTransport` you hand to `connect({ transport, views })`:
|
|
8
|
+
* it sends `Invoke`s up and surfaces `Snapshot`/`Patches` down, with JSON framing.
|
|
9
|
+
*
|
|
10
|
+
* It owns reconnection so the caller doesn't have to: a dropped socket is retried
|
|
11
|
+
* with exponential backoff, and because the message handlers persist across
|
|
12
|
+
* sockets, the server's first frame on the new connection — always a `Snapshot` —
|
|
13
|
+
* re-syncs the tree automatically (no stale nodes leak). Works anywhere a global
|
|
14
|
+
* `WebSocket` exists (browsers, Bun, Node 22+); inject one via `options` otherwise.
|
|
15
|
+
* There is no React hook — this is the transport only.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type ClientStatus = 'connecting' | 'open' | 'reconnecting' | 'closed'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Options for {@link createWebSocketClientTransport}. A single named-options object
|
|
22
|
+
* (no positional args) so every reform-remote adapter constructor has the same
|
|
23
|
+
* shape and the client transports stay drop-in swappable.
|
|
24
|
+
*/
|
|
25
|
+
export interface WebSocketClientTransportOptions {
|
|
26
|
+
/** The WebSocket URL to connect to (e.g. `ws://127.0.0.1:8787/reform`). */
|
|
27
|
+
readonly url: string
|
|
28
|
+
/** First reconnect delay; doubles each attempt up to `maxDelayMs`. Default 250ms. */
|
|
29
|
+
readonly baseDelayMs?: number
|
|
30
|
+
/** Ceiling for the backoff delay. Default 5000ms. */
|
|
31
|
+
readonly maxDelayMs?: number
|
|
32
|
+
/** Subprotocols passed to the `WebSocket` constructor. */
|
|
33
|
+
readonly protocols?: string | ReadonlyArray<string>
|
|
34
|
+
/**
|
|
35
|
+
* A bearer token to authenticate the connection. A browser `WebSocket` can't set headers,
|
|
36
|
+
* so the only header-like channel is the subprotocol list: the token rides as an extra
|
|
37
|
+
* `bearer.<token>` subprotocol alongside any {@link protocols}. The server reads it off
|
|
38
|
+
* `Sec-WebSocket-Protocol` at the upgrade. Held `Redacted` so it never logs.
|
|
39
|
+
*/
|
|
40
|
+
readonly authToken?: Redacted.Redacted<string>
|
|
41
|
+
/** The `WebSocket` implementation to use. Defaults to the global. */
|
|
42
|
+
readonly WebSocket?: typeof WebSocket
|
|
43
|
+
/** Notified on every connection-status transition. */
|
|
44
|
+
readonly onStatus?: (status: ClientStatus) => void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A `RemoteTransport` that also exposes its connection status and a manual close. */
|
|
48
|
+
export type WebSocketClientTransport = RemoteTransport<InvokeMessage, ServerMessage> & {
|
|
49
|
+
readonly status: () => ClientStatus
|
|
50
|
+
/**
|
|
51
|
+
* Subscribe to status transitions (returns an unsubscribe). With `status()`
|
|
52
|
+
* this satisfies the React binding's `StatusReporter`, so `useConnectionStatus`
|
|
53
|
+
* can drive a reconnecting badge with no manual store.
|
|
54
|
+
*/
|
|
55
|
+
readonly onStatusChange: (listener: () => void) => () => void
|
|
56
|
+
/** Stop reconnecting and close the socket. The transport is inert afterwards. */
|
|
57
|
+
readonly close: () => void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const resolveWebSocket = (injected: typeof WebSocket | undefined): typeof WebSocket => {
|
|
61
|
+
const impl = injected ?? (globalThis as { WebSocket?: typeof WebSocket }).WebSocket
|
|
62
|
+
if (impl === undefined) {
|
|
63
|
+
throw new MissingWebSocket({
|
|
64
|
+
message: 'No global WebSocket; pass `WebSocket` in options (e.g. the `ws` package in Node <22).',
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
return impl
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const createWebSocketClientTransport = (
|
|
71
|
+
options: WebSocketClientTransportOptions,
|
|
72
|
+
): WebSocketClientTransport => {
|
|
73
|
+
const url = options.url
|
|
74
|
+
const WS = resolveWebSocket(options.WebSocket)
|
|
75
|
+
const baseDelayMs = options.baseDelayMs ?? 250
|
|
76
|
+
const maxDelayMs = options.maxDelayMs ?? 5000
|
|
77
|
+
|
|
78
|
+
// Normalize `protocols` to an array and fold in the auth token as a `bearer.<token>`
|
|
79
|
+
// subprotocol. Computed once (the token doesn't change across reconnects). `undefined`
|
|
80
|
+
// when there's nothing to send, so the no-auth path matches the original behavior.
|
|
81
|
+
const baseProtocols: ReadonlyArray<string> =
|
|
82
|
+
options.protocols === undefined
|
|
83
|
+
? []
|
|
84
|
+
: typeof options.protocols === 'string'
|
|
85
|
+
? [options.protocols]
|
|
86
|
+
: options.protocols
|
|
87
|
+
const protocols: ReadonlyArray<string> | undefined =
|
|
88
|
+
options.authToken !== undefined
|
|
89
|
+
? [`bearer.${Redacted.value(options.authToken)}`, ...baseProtocols]
|
|
90
|
+
: baseProtocols.length === 0
|
|
91
|
+
? undefined
|
|
92
|
+
: baseProtocols
|
|
93
|
+
|
|
94
|
+
const handlers = new Set<(message: ServerMessage) => void>()
|
|
95
|
+
const statusListeners = new Set<() => void>()
|
|
96
|
+
const queue: Array<InvokeMessage> = []
|
|
97
|
+
// A const holder whose fields swap — the codebase's no-`let` idiom.
|
|
98
|
+
const state: {
|
|
99
|
+
socket: WebSocket | null
|
|
100
|
+
status: ClientStatus
|
|
101
|
+
attempts: number
|
|
102
|
+
timer: ReturnType<typeof setTimeout> | null
|
|
103
|
+
} = { socket: null, status: 'connecting', attempts: 0, timer: null }
|
|
104
|
+
|
|
105
|
+
const setStatus = (status: ClientStatus): void => {
|
|
106
|
+
if (status === state.status) return
|
|
107
|
+
state.status = status
|
|
108
|
+
options.onStatus?.(status)
|
|
109
|
+
for (const listener of statusListeners) listener()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const flush = (): void => {
|
|
113
|
+
const socket = state.socket
|
|
114
|
+
if (socket === null || socket.readyState !== WS.OPEN) return
|
|
115
|
+
const pending = queue.splice(0, queue.length)
|
|
116
|
+
for (const message of pending) socket.send(JSON.stringify(message))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const scheduleReconnect = (): void => {
|
|
120
|
+
if (state.status === 'closed') return
|
|
121
|
+
setStatus('reconnecting')
|
|
122
|
+
const delay = Math.min(maxDelayMs, baseDelayMs * 2 ** state.attempts)
|
|
123
|
+
state.attempts += 1
|
|
124
|
+
state.timer = setTimeout(connect, delay)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// A drop while live (or a failed connect) backs off and retries. A socket can
|
|
128
|
+
// fire both `error` and `close`, so guard on identity — null out the current
|
|
129
|
+
// socket on the first drop, so the second event is a no-op and only one
|
|
130
|
+
// reconnect is scheduled. A user `close` flips the status, making this inert.
|
|
131
|
+
const handleDrop = (socket: WebSocket): void => {
|
|
132
|
+
if (state.socket !== socket) return
|
|
133
|
+
state.socket = null
|
|
134
|
+
scheduleReconnect()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function connect(): void {
|
|
138
|
+
const socket = protocols === undefined ? new WS(url) : new WS(url, [...protocols])
|
|
139
|
+
state.socket = socket
|
|
140
|
+
socket.addEventListener('open', () => {
|
|
141
|
+
state.attempts = 0
|
|
142
|
+
setStatus('open')
|
|
143
|
+
flush()
|
|
144
|
+
})
|
|
145
|
+
socket.addEventListener('message', (event: MessageEvent) => {
|
|
146
|
+
const message = JSON.parse(event.data as string) as ServerMessage
|
|
147
|
+
for (const handler of handlers) handler(message)
|
|
148
|
+
})
|
|
149
|
+
socket.addEventListener('close', () => handleDrop(socket))
|
|
150
|
+
socket.addEventListener('error', () => handleDrop(socket))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
connect()
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
send: (message) => {
|
|
157
|
+
queue.push(message)
|
|
158
|
+
flush()
|
|
159
|
+
},
|
|
160
|
+
onMessage: (handler) => {
|
|
161
|
+
handlers.add(handler)
|
|
162
|
+
return () => void handlers.delete(handler)
|
|
163
|
+
},
|
|
164
|
+
status: () => state.status,
|
|
165
|
+
onStatusChange: (listener) => {
|
|
166
|
+
statusListeners.add(listener)
|
|
167
|
+
return () => void statusListeners.delete(listener)
|
|
168
|
+
},
|
|
169
|
+
close: () => {
|
|
170
|
+
setStatus('closed')
|
|
171
|
+
if (state.timer !== null) clearTimeout(state.timer)
|
|
172
|
+
state.socket?.close()
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
}
|
package/dist/errors.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAQ,MAAM,QAAQ,CAAA;AAEzC;;;;GAIG;AAEH;;;;GAIG;AACH,KAAK,gBAAgB,CAAC,GAAG,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,KAC7E,IAAI,EAAE,CAAC,KACJ,KAAK,CAAC,cAAc,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAA;CAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;AAEhE,QAAA,MAAM,oBAAoB,EAAE,gBAAgB,CAC1C,8CAA8C,EAC9C;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAG5B,CAAA;AAEF,iFAAiF;AACjF,qBAAa,gBAAiB,SAAQ,oBAAoB;CAAG"}
|
package/dist/errors.js
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { Data } from 'effect';
|
|
2
|
-
const MissingWebSocketBase = (Data.TaggedError('@playfast/reform-remote-web/MissingWebSocket'));
|
|
3
|
-
/** Thrown when no `WebSocket` is available and none was injected via options. */
|
|
4
|
-
export class MissingWebSocket extends MissingWebSocketBase {
|
|
5
|
-
}
|
|
6
|
-
//# sourceMappingURL=errors.js.map
|
package/dist/errors.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,IAAI,EAAE,MAAM,QAAQ,CAAA;AAiBzC,MAAM,oBAAoB,GAGtB,CAAA,IAAI,CAAC,WAAW,CAAC,8CAA8C,CAEjE,CAAA,CAAA;AAEF,iFAAiF;AACjF,MAAM,OAAO,gBAAiB,SAAQ,oBAAoB;CAAG"}
|
package/dist/index.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@playfast/reform-remote-web` — the browser/web client side of the remote
|
|
3
|
-
* transport. Public API is exposed two ways (Effect-style): namespace barrels for
|
|
4
|
-
* discovery (`import { Transport } from "@playfast/reform-remote-web"`) and
|
|
5
|
-
* per-path subpaths for direct use (`import { createWebSocketClientTransport }
|
|
6
|
-
* from "@playfast/reform-remote-web/transport"`). The select re-exports below keep
|
|
7
|
-
* the common entry points nameable from the root.
|
|
8
|
-
*/
|
|
9
|
-
export * as Transport from './transport.js';
|
|
10
|
-
export * as Errors from './errors.js';
|
|
11
|
-
export { type ClientStatus, createWebSocketClientTransport, type WebSocketClientTransport, type WebSocketClientTransportOptions, } from './transport.js';
|
|
12
|
-
export { MissingWebSocket } from './errors.js';
|
|
13
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC,OAAO,EACL,KAAK,YAAY,EACjB,8BAA8B,EAC9B,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,GACrC,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC,OAAO,EAEL,8BAA8B,GAG/B,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/transport.d.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { Redacted } from 'effect';
|
|
2
|
-
import type { InvokeMessage, RemoteTransport, ServerMessage } from '@playfast/reform-remote';
|
|
3
|
-
/**
|
|
4
|
-
* The browser/web client side of the remote transport. A factory that wraps a
|
|
5
|
-
* WebSocket into a `RemoteTransport` you hand to `connect({ transport, views })`:
|
|
6
|
-
* it sends `Invoke`s up and surfaces `Snapshot`/`Patches` down, with JSON framing.
|
|
7
|
-
*
|
|
8
|
-
* It owns reconnection so the caller doesn't have to: a dropped socket is retried
|
|
9
|
-
* with exponential backoff, and because the message handlers persist across
|
|
10
|
-
* sockets, the server's first frame on the new connection — always a `Snapshot` —
|
|
11
|
-
* re-syncs the tree automatically (no stale nodes leak). Works anywhere a global
|
|
12
|
-
* `WebSocket` exists (browsers, Bun, Node 22+); inject one via `options` otherwise.
|
|
13
|
-
* There is no React hook — this is the transport only.
|
|
14
|
-
*/
|
|
15
|
-
export type ClientStatus = 'connecting' | 'open' | 'reconnecting' | 'closed';
|
|
16
|
-
/**
|
|
17
|
-
* Options for {@link createWebSocketClientTransport}. A single named-options object
|
|
18
|
-
* (no positional args) so every reform-remote adapter constructor has the same
|
|
19
|
-
* shape and the client transports stay drop-in swappable.
|
|
20
|
-
*/
|
|
21
|
-
export interface WebSocketClientTransportOptions {
|
|
22
|
-
/** The WebSocket URL to connect to (e.g. `ws://127.0.0.1:8787/reform`). */
|
|
23
|
-
readonly url: string;
|
|
24
|
-
/** First reconnect delay; doubles each attempt up to `maxDelayMs`. Default 250ms. */
|
|
25
|
-
readonly baseDelayMs?: number;
|
|
26
|
-
/** Ceiling for the backoff delay. Default 5000ms. */
|
|
27
|
-
readonly maxDelayMs?: number;
|
|
28
|
-
/** Subprotocols passed to the `WebSocket` constructor. */
|
|
29
|
-
readonly protocols?: string | ReadonlyArray<string>;
|
|
30
|
-
/**
|
|
31
|
-
* A bearer token to authenticate the connection. A browser `WebSocket` can't set headers,
|
|
32
|
-
* so the only header-like channel is the subprotocol list: the token rides as an extra
|
|
33
|
-
* `bearer.<token>` subprotocol alongside any {@link protocols}. The server reads it off
|
|
34
|
-
* `Sec-WebSocket-Protocol` at the upgrade. Held `Redacted` so it never logs.
|
|
35
|
-
*/
|
|
36
|
-
readonly authToken?: Redacted.Redacted<string>;
|
|
37
|
-
/** The `WebSocket` implementation to use. Defaults to the global. */
|
|
38
|
-
readonly WebSocket?: typeof WebSocket;
|
|
39
|
-
/** Notified on every connection-status transition. */
|
|
40
|
-
readonly onStatus?: (status: ClientStatus) => void;
|
|
41
|
-
}
|
|
42
|
-
/** A `RemoteTransport` that also exposes its connection status and a manual close. */
|
|
43
|
-
export type WebSocketClientTransport = RemoteTransport<InvokeMessage, ServerMessage> & {
|
|
44
|
-
readonly status: () => ClientStatus;
|
|
45
|
-
/**
|
|
46
|
-
* Subscribe to status transitions (returns an unsubscribe). With `status()`
|
|
47
|
-
* this satisfies the React binding's `StatusReporter`, so `useConnectionStatus`
|
|
48
|
-
* can drive a reconnecting badge with no manual store.
|
|
49
|
-
*/
|
|
50
|
-
readonly onStatusChange: (listener: () => void) => () => void;
|
|
51
|
-
/** Stop reconnecting and close the socket. The transport is inert afterwards. */
|
|
52
|
-
readonly close: () => void;
|
|
53
|
-
};
|
|
54
|
-
export declare const createWebSocketClientTransport: (options: WebSocketClientTransportOptions) => WebSocketClientTransport;
|
|
55
|
-
//# sourceMappingURL=transport.d.ts.map
|
package/dist/transport.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAA;AACjC,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAG5F;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,CAAA;AAE5E;;;;GAIG;AACH,MAAM,WAAW,+BAA+B;IAC9C,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,qFAAqF;IACrF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,qDAAqD;IACrD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAA;IACnD;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;IAC9C,qEAAqE;IACrE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,SAAS,CAAA;IACrC,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAA;CACnD;AAED,sFAAsF;AACtF,MAAM,MAAM,wBAAwB,GAAG,eAAe,CAAC,aAAa,EAAE,aAAa,CAAC,GAAG;IACrF,QAAQ,CAAC,MAAM,EAAE,MAAM,YAAY,CAAA;IACnC;;;;OAIG;IACH,QAAQ,CAAC,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAA;IAC7D,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAA;CAC3B,CAAA;AAYD,eAAO,MAAM,8BAA8B,GACzC,SAAS,+BAA+B,KACvC,wBAuGF,CAAA"}
|
package/dist/transport.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { Redacted } from 'effect';
|
|
2
|
-
import { MissingWebSocket } from './errors.js';
|
|
3
|
-
const resolveWebSocket = (injected) => {
|
|
4
|
-
const impl = injected ?? globalThis.WebSocket;
|
|
5
|
-
if (impl === undefined) {
|
|
6
|
-
throw new MissingWebSocket({
|
|
7
|
-
message: 'No global WebSocket; pass `WebSocket` in options (e.g. the `ws` package in Node <22).',
|
|
8
|
-
});
|
|
9
|
-
}
|
|
10
|
-
return impl;
|
|
11
|
-
};
|
|
12
|
-
export const createWebSocketClientTransport = (options) => {
|
|
13
|
-
const url = options.url;
|
|
14
|
-
const WS = resolveWebSocket(options.WebSocket);
|
|
15
|
-
const baseDelayMs = options.baseDelayMs ?? 250;
|
|
16
|
-
const maxDelayMs = options.maxDelayMs ?? 5000;
|
|
17
|
-
// Normalize `protocols` to an array and fold in the auth token as a `bearer.<token>`
|
|
18
|
-
// subprotocol. Computed once (the token doesn't change across reconnects). `undefined`
|
|
19
|
-
// when there's nothing to send, so the no-auth path matches the original behavior.
|
|
20
|
-
const baseProtocols = options.protocols === undefined
|
|
21
|
-
? []
|
|
22
|
-
: typeof options.protocols === 'string'
|
|
23
|
-
? [options.protocols]
|
|
24
|
-
: options.protocols;
|
|
25
|
-
const protocols = options.authToken !== undefined
|
|
26
|
-
? [`bearer.${Redacted.value(options.authToken)}`, ...baseProtocols]
|
|
27
|
-
: baseProtocols.length === 0
|
|
28
|
-
? undefined
|
|
29
|
-
: baseProtocols;
|
|
30
|
-
const handlers = new Set();
|
|
31
|
-
const statusListeners = new Set();
|
|
32
|
-
const queue = [];
|
|
33
|
-
// A const holder whose fields swap — the codebase's no-`let` idiom.
|
|
34
|
-
const state = { socket: null, status: 'connecting', attempts: 0, timer: null };
|
|
35
|
-
const setStatus = (status) => {
|
|
36
|
-
if (status === state.status)
|
|
37
|
-
return;
|
|
38
|
-
state.status = status;
|
|
39
|
-
options.onStatus?.(status);
|
|
40
|
-
for (const listener of statusListeners)
|
|
41
|
-
listener();
|
|
42
|
-
};
|
|
43
|
-
const flush = () => {
|
|
44
|
-
const socket = state.socket;
|
|
45
|
-
if (socket === null || socket.readyState !== WS.OPEN)
|
|
46
|
-
return;
|
|
47
|
-
const pending = queue.splice(0, queue.length);
|
|
48
|
-
for (const message of pending)
|
|
49
|
-
socket.send(JSON.stringify(message));
|
|
50
|
-
};
|
|
51
|
-
const scheduleReconnect = () => {
|
|
52
|
-
if (state.status === 'closed')
|
|
53
|
-
return;
|
|
54
|
-
setStatus('reconnecting');
|
|
55
|
-
const delay = Math.min(maxDelayMs, baseDelayMs * 2 ** state.attempts);
|
|
56
|
-
state.attempts += 1;
|
|
57
|
-
state.timer = setTimeout(connect, delay);
|
|
58
|
-
};
|
|
59
|
-
// A drop while live (or a failed connect) backs off and retries. A socket can
|
|
60
|
-
// fire both `error` and `close`, so guard on identity — null out the current
|
|
61
|
-
// socket on the first drop, so the second event is a no-op and only one
|
|
62
|
-
// reconnect is scheduled. A user `close` flips the status, making this inert.
|
|
63
|
-
const handleDrop = (socket) => {
|
|
64
|
-
if (state.socket !== socket)
|
|
65
|
-
return;
|
|
66
|
-
state.socket = null;
|
|
67
|
-
scheduleReconnect();
|
|
68
|
-
};
|
|
69
|
-
function connect() {
|
|
70
|
-
const socket = protocols === undefined ? new WS(url) : new WS(url, [...protocols]);
|
|
71
|
-
state.socket = socket;
|
|
72
|
-
socket.addEventListener('open', () => {
|
|
73
|
-
state.attempts = 0;
|
|
74
|
-
setStatus('open');
|
|
75
|
-
flush();
|
|
76
|
-
});
|
|
77
|
-
socket.addEventListener('message', (event) => {
|
|
78
|
-
const message = JSON.parse(event.data);
|
|
79
|
-
for (const handler of handlers)
|
|
80
|
-
handler(message);
|
|
81
|
-
});
|
|
82
|
-
socket.addEventListener('close', () => handleDrop(socket));
|
|
83
|
-
socket.addEventListener('error', () => handleDrop(socket));
|
|
84
|
-
}
|
|
85
|
-
connect();
|
|
86
|
-
return {
|
|
87
|
-
send: (message) => {
|
|
88
|
-
queue.push(message);
|
|
89
|
-
flush();
|
|
90
|
-
},
|
|
91
|
-
onMessage: (handler) => {
|
|
92
|
-
handlers.add(handler);
|
|
93
|
-
return () => void handlers.delete(handler);
|
|
94
|
-
},
|
|
95
|
-
status: () => state.status,
|
|
96
|
-
onStatusChange: (listener) => {
|
|
97
|
-
statusListeners.add(listener);
|
|
98
|
-
return () => void statusListeners.delete(listener);
|
|
99
|
-
},
|
|
100
|
-
close: () => {
|
|
101
|
-
setStatus('closed');
|
|
102
|
-
if (state.timer !== null)
|
|
103
|
-
clearTimeout(state.timer);
|
|
104
|
-
state.socket?.close();
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
};
|
|
108
|
-
//# sourceMappingURL=transport.js.map
|
package/dist/transport.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
|