@playfast/reform-remote-node 1.0.1 → 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/index.ts +1 -19
- package/src/server.test.ts +2 -9
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/reform-remote-node",
|
|
3
3
|
"playbook": "./playbook",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Node WebSocket server adapter for reform-remote — serves a reform scene's UI over the `ws` package, one isolated runtime per connection.",
|
|
7
7
|
"keywords": [
|
package/src/index.ts
CHANGED
|
@@ -8,31 +8,15 @@ import {
|
|
|
8
8
|
serveShared,
|
|
9
9
|
} from '@playfast/reform-remote'
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* @playfast/reform-remote-node — serve a reform scene's UI over WebSocket from a
|
|
13
|
-
* Node process, using the `ws` package. The scene runs ONCE as a single shared
|
|
14
|
-
* runtime (`serveShared`); every connection attaches to it via `addClient`, so all
|
|
15
|
-
* clients see and drive the same state. Sockets are wrapped with JSON framing: the
|
|
16
|
-
* already-`_tag`-discriminated wire messages need no envelope. The Bun-native
|
|
17
|
-
* equivalent is `@playfast/reform-remote-bun`; the browser/web client is
|
|
18
|
-
* `@playfast/reform-remote-web`.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
11
|
export interface NodeWebSocketOptionsExternalApi {
|
|
22
|
-
/** The scene to run — ONE shared runtime that every connection attaches to. */
|
|
23
12
|
readonly scene: Scene
|
|
24
|
-
/** Listen on this port (use `0` for an ephemeral port; read it back from `wss.address()`). */
|
|
25
13
|
readonly port?: number
|
|
26
|
-
/** Attach to an existing HTTP server instead of opening a port. */
|
|
27
14
|
readonly server?: Server
|
|
28
|
-
/** Restrict upgrades to this path. */
|
|
29
15
|
readonly path?: string
|
|
30
16
|
}
|
|
31
17
|
|
|
32
18
|
export interface NodeWebSocketServer {
|
|
33
|
-
/** The underlying `ws` server — for reading the bound address, or further config. */
|
|
34
19
|
readonly wss: WebSocketServer
|
|
35
|
-
/** Dispose the shared runtime and close the server. */
|
|
36
20
|
readonly stop: () => Promise<void>
|
|
37
21
|
}
|
|
38
22
|
|
|
@@ -46,7 +30,6 @@ export const serveNodeWebSocket = (options: NodeWebSocketOptionsExternalApi): No
|
|
|
46
30
|
? new WebSocketServer({ server: options.server, path: options.path })
|
|
47
31
|
: new WebSocketServer({ port: options.port === undefined ? 0 : options.port, path: options.path })
|
|
48
32
|
|
|
49
|
-
// One shared runtime for the whole server; every socket attaches to it.
|
|
50
33
|
const shared = serveShared({ scene: options.scene })
|
|
51
34
|
|
|
52
35
|
wss.on('connection', (socket) => {
|
|
@@ -72,8 +55,7 @@ export const serveNodeWebSocket = (options: NodeWebSocketOptionsExternalApi): No
|
|
|
72
55
|
wss,
|
|
73
56
|
stop: async (): Promise<void> => {
|
|
74
57
|
await shared.dispose()
|
|
75
|
-
// Terminate live sockets so `wss.close` doesn't block waiting for
|
|
76
|
-
// drain on their own — `close` only fires its callback once all are gone.
|
|
58
|
+
// Terminate live sockets so `wss.close` doesn't block waiting for drain.
|
|
77
59
|
wss.clients.forEach((client) => client.terminate())
|
|
78
60
|
await new Promise<void>((resolve, reject) =>
|
|
79
61
|
wss.close((error) => (error === undefined ? resolve() : reject(error))),
|
package/src/server.test.ts
CHANGED
|
@@ -2,8 +2,7 @@ import type { AddressInfo } from 'node:net'
|
|
|
2
2
|
import { afterEach, expect, test, vi } from 'vitest'
|
|
3
3
|
import { Layer, Schema as S } from 'effect'
|
|
4
4
|
|
|
5
|
-
// Real loopback
|
|
6
|
-
// the 5s default (passes fast in isolation; 30s still fails a genuine hang).
|
|
5
|
+
// Real loopback I/O: headroom so parallel suite can't starve past 5s default.
|
|
7
6
|
vi.setConfig({ testTimeout: 30_000, hookTimeout: 30_000 })
|
|
8
7
|
import {
|
|
9
8
|
Composition,
|
|
@@ -23,8 +22,6 @@ import type { WireNode, WireProp } from '@playfast/reform'
|
|
|
23
22
|
import type { ServerMessage } from '@playfast/reform-remote'
|
|
24
23
|
import { type NodeWebSocketServer, serveNodeWebSocket } from './index'
|
|
25
24
|
|
|
26
|
-
// A self-contained counter scene (the adapter packages stay independent of the
|
|
27
|
-
// core package's test fixtures, which aren't part of its public surface).
|
|
28
25
|
class Count extends State.make('count', S.Number) {}
|
|
29
26
|
class Counters extends StateGroup.make(Count) {}
|
|
30
27
|
class Bumped extends Event.make('Bumped', S.Struct({ by: S.Number })) {}
|
|
@@ -111,9 +108,7 @@ test('every connection shares ONE runtime — a bump on one socket reaches the o
|
|
|
111
108
|
open.add(host)
|
|
112
109
|
const port = await listening(host)
|
|
113
110
|
|
|
114
|
-
// Attach
|
|
115
|
-
// off its pre-rendered baseline immediately, so a lazily-attached listener could miss
|
|
116
|
-
// the frame (a real `connect()` client registers its handler synchronously).
|
|
111
|
+
// Attach listeners before await: shared server snapshots immediately; lazy attach can miss the frame.
|
|
117
112
|
const a = new WebSocket(`ws://localhost:${port}`)
|
|
118
113
|
const snapAP = nextMessage(a, (m) => m._tag === 'Snapshot')
|
|
119
114
|
const b = new WebSocket(`ws://localhost:${port}`)
|
|
@@ -122,7 +117,6 @@ test('every connection shares ONE runtime — a bump on one socket reaches the o
|
|
|
122
117
|
const snapA = await snapAP
|
|
123
118
|
const snapB = await snapBP
|
|
124
119
|
|
|
125
|
-
// a bumps; BOTH a and b receive the broadcast diff off the shared runtime.
|
|
126
120
|
const frameAP = nextMessage(a, (m) => m._tag === 'Patches')
|
|
127
121
|
const frameBP = nextMessage(b, (m) => m._tag === 'Patches')
|
|
128
122
|
a.send(JSON.stringify({ _tag: 'Invoke', handle: '0:bump', payload: { by: 4 } }))
|
|
@@ -139,7 +133,6 @@ test('every connection shares ONE runtime — a bump on one socket reaches the o
|
|
|
139
133
|
)
|
|
140
134
|
expect(countOf(treeA[0]!)).toBe(4)
|
|
141
135
|
expect(countOf(treeB[0]!)).toBe(4)
|
|
142
|
-
// A late joiner snapshots the CURRENT shared count (4), not a fresh 0.
|
|
143
136
|
const c = new WebSocket(`ws://localhost:${port}`)
|
|
144
137
|
const snapC = await nextMessage(c, (m) => m._tag === 'Snapshot')
|
|
145
138
|
expect(countOf((snapC._tag === 'Snapshot' ? snapC.tree : [])[0]!)).toBe(4)
|