@playfast/reform-remote-node 1.0.2 → 1.1.0

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,36 +1,38 @@
1
1
  {
2
2
  "name": "@playfast/reform-remote-node",
3
- "playbook": "./playbook",
4
- "version": "1.0.2",
5
- "type": "module",
3
+ "version": "1.1.0",
6
4
  "description": "Node WebSocket server adapter for reform-remote — serves a reform scene's UI over the `ws` package, one isolated runtime per connection.",
7
5
  "keywords": [
8
- "reform",
9
6
  "effect",
10
- "websocket",
11
- "remote",
12
7
  "node",
13
- "server"
8
+ "reform",
9
+ "remote",
10
+ "server",
11
+ "websocket"
14
12
  ],
13
+ "bugs": {
14
+ "url": "https://github.com/playfast/reform/issues"
15
+ },
15
16
  "license": "MIT",
16
17
  "repository": {
17
18
  "type": "git",
18
19
  "url": "https://github.com/playfast/reform.git",
19
20
  "directory": "packages/reform-remote-node"
20
21
  },
21
- "bugs": {
22
- "url": "https://github.com/playfast/reform/issues"
23
- },
22
+ "files": [
23
+ "src",
24
+ "README.md"
25
+ ],
26
+ "type": "module",
24
27
  "sideEffects": false,
25
28
  "exports": {
26
29
  "./package.json": "./package.json",
27
30
  ".": "./src/index.ts",
28
31
  "./*": "./src/*.ts"
29
32
  },
30
- "files": [
31
- "src",
32
- "README.md"
33
- ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
34
36
  "scripts": {
35
37
  "clean": "rm -rf dist .tsbuildinfo",
36
38
  "check": "tsc --noEmit",
@@ -44,16 +46,14 @@
44
46
  "dependencies": {
45
47
  "ws": "^8.18.0"
46
48
  },
47
- "peerDependencies": {
48
- "effect": "*",
49
- "react": "^19.0.0",
50
- "@playfast/reform": "*",
51
- "@playfast/reform-remote": "*"
52
- },
53
49
  "devDependencies": {
54
50
  "@types/ws": "^8.5.13"
55
51
  },
56
- "publishConfig": {
57
- "access": "public"
58
- }
52
+ "peerDependencies": {
53
+ "@playfast/reform": "*",
54
+ "@playfast/reform-remote": "*",
55
+ "effect": "*",
56
+ "react": "^19.0.0"
57
+ },
58
+ "playbook": "./playbook"
59
59
  }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Server } from 'node:http'
2
+ import { Schema } from 'effect'
2
3
  import { type RawData, WebSocketServer } from 'ws'
3
- import type { Scene } from '@playfast/reform'
4
+ import type { AnyScene } from '@playfast/reform/internal'
4
5
  import {
5
6
  type InvokeMessage,
6
7
  type RemoteTransport,
@@ -9,7 +10,7 @@ import {
9
10
  } from '@playfast/reform-remote'
10
11
 
11
12
  export interface NodeWebSocketOptionsExternalApi {
12
- readonly scene: Scene
13
+ readonly scene: AnyScene
13
14
  readonly port?: number
14
15
  readonly server?: Server
15
16
  readonly path?: string
@@ -20,24 +21,72 @@ export interface NodeWebSocketServer {
20
21
  readonly stop: () => Promise<void>
21
22
  }
22
23
 
23
- const parseInvoke = (frame: RawData): InvokeMessage =>
24
- // oxlint-disable-next-line reform-rules/no-json-parse-stringify, reform-rules/no-type-assertion -- wire frame decode; InvokeMessage is a structural type with no Schema here
25
- JSON.parse(frame.toString()) as InvokeMessage
24
+ const WirePropSchema = Schema.Union(
25
+ Schema.Struct({
26
+ _tag: Schema.Literal('Data'),
27
+ name: Schema.String,
28
+ value: Schema.Unknown,
29
+ }),
30
+ Schema.Struct({
31
+ _tag: Schema.Literal('Event'),
32
+ name: Schema.String,
33
+ handle: Schema.String,
34
+ }),
35
+ )
36
+ const WireNodeSchema = Schema.Struct({
37
+ id: Schema.String,
38
+ name: Schema.String,
39
+ parentId: Schema.NullOr(Schema.String),
40
+ childIndex: Schema.Number,
41
+ slot: Schema.NullOr(Schema.String),
42
+ key: Schema.NullOr(Schema.String),
43
+ props: Schema.Array(WirePropSchema),
44
+ })
45
+ const WirePatchSchema = Schema.Union(
46
+ Schema.Struct({ _tag: Schema.Literal('Upsert'), node: WireNodeSchema }),
47
+ Schema.Struct({ _tag: Schema.Literal('Delete'), id: Schema.String }),
48
+ )
49
+ const InvokeMessageJson: Schema.Schema<InvokeMessage, string, never> = Schema.parseJson(
50
+ Schema.Struct({
51
+ _tag: Schema.Literal('Invoke'),
52
+ handle: Schema.String,
53
+ payload: Schema.Unknown,
54
+ }),
55
+ )
56
+ const ServerMessageJson: Schema.Schema<ServerMessage, string, never> = Schema.parseJson(
57
+ Schema.Union(
58
+ Schema.Struct({
59
+ _tag: Schema.Literal('Snapshot'),
60
+ tree: Schema.Array(WireNodeSchema),
61
+ }),
62
+ Schema.Struct({
63
+ _tag: Schema.Literal('Patches'),
64
+ patches: Schema.Array(WirePatchSchema),
65
+ }),
66
+ ),
67
+ )
68
+ const decodeInvokeMessage: (input: unknown) => InvokeMessage =
69
+ Schema.decodeUnknownSync(InvokeMessageJson)
70
+ const encodeServerMessage: (message: ServerMessage) => string = Schema.encodeSync(ServerMessageJson)
26
71
 
27
- export const serveNodeWebSocket = (options: NodeWebSocketOptionsExternalApi): NodeWebSocketServer => {
28
- const wss =
72
+ const parseInvoke = (frame: RawData): InvokeMessage => decodeInvokeMessage(frame.toString())
73
+
74
+ export const serveNodeWebSocket = (
75
+ options: NodeWebSocketOptionsExternalApi,
76
+ ): NodeWebSocketServer => {
77
+ const serverConfig =
29
78
  options.server !== undefined
30
- ? new WebSocketServer({ server: options.server, path: options.path })
31
- : new WebSocketServer({ port: options.port === undefined ? 0 : options.port, path: options.path })
79
+ ? { server: options.server, path: options.path }
80
+ : { port: options.port === undefined ? 0 : options.port, path: options.path }
81
+ const wss = new WebSocketServer(serverConfig)
32
82
 
33
83
  const shared = serveShared({ scene: options.scene })
34
84
 
35
85
  wss.on('connection', (socket) => {
36
86
  const handlers = new Set<(message: InvokeMessage) => void>()
37
87
  const transport: RemoteTransport<ServerMessage, InvokeMessage> = {
38
- // oxlint-disable-next-line reform-rules/no-json-parse-stringify -- wire frame encode; ServerMessage is a structural type with no Schema here
39
- send: (message) => socket.send(JSON.stringify(message)),
40
- onMessage: (handler) => {
88
+ send: (message: ServerMessage): void => void socket.send(encodeServerMessage(message)),
89
+ onMessage: (handler: (message: InvokeMessage) => void): (() => void) => {
41
90
  handlers.add(handler)
42
91
  return () => void handlers.delete(handler)
43
92
  },
@@ -55,7 +104,7 @@ export const serveNodeWebSocket = (options: NodeWebSocketOptionsExternalApi): No
55
104
  wss,
56
105
  stop: async (): Promise<void> => {
57
106
  await shared.dispose()
58
- // Terminate live sockets so `wss.close` doesn't block waiting for drain.
107
+ // `wss.close` waits for live sockets to drain.
59
108
  wss.clients.forEach((client) => client.terminate())
60
109
  await new Promise<void>((resolve, reject) =>
61
110
  wss.close((error) => (error === undefined ? resolve() : reject(error))),
@@ -12,13 +12,13 @@ import {
12
12
  State,
13
13
  StateGroup,
14
14
  Ui,
15
- Wire,
16
15
  mount,
17
16
  provide,
18
17
  scene,
19
18
  ui,
20
19
  } from '@playfast/reform'
21
- import type { WireNode, WireProp } from '@playfast/reform'
20
+ import { Wire } from '@playfast/reform/internal'
21
+ import type { WireNode, WireProp } from '@playfast/reform/internal'
22
22
  import type { ServerMessage } from '@playfast/reform-remote'
23
23
  import { type NodeWebSocketServer, serveNodeWebSocket } from './index'
24
24
 
@@ -30,11 +30,19 @@ class CounterUi extends ui('Counter', {
30
30
  props: S.Struct({ count: S.Number }),
31
31
  events: { bump: S.Struct({ by: S.Number }) },
32
32
  }) {}
33
- class Counter extends Composition.make('Counter', { title: 'Counter', ui: CounterUi, states: [Count] }) {}
33
+
34
+ class Counter extends Composition.make('Counter', {
35
+ title: 'Counter',
36
+ ui: CounterUi,
37
+ states: [Count],
38
+ })<Counter>() {}
34
39
 
35
40
  const counterScene = () => {
36
41
  const presentation = Layer.mergeAll(
37
- provide(CounterUi, Ui.make(CounterUi, ({ count }) => `count:${count}`)),
42
+ provide(
43
+ CounterUi,
44
+ Ui.make(CounterUi, ({ count }) => `count:${count}`),
45
+ ),
38
46
  StateGroup.live(Counters, { count: 0 }),
39
47
  )
40
48
  const app = Layer.mergeAll(