@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@playfast/reform-remote-node",
3
3
  "playbook": "./playbook",
4
- "version": "1.0.1",
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 them to
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))),
@@ -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 socket I/O: headroom so the full parallel suite can't starve it past
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 every listener BEFORE awaiting: the shared server snapshots a new client
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)