@playfast/reform-remote-node 1.0.2 → 1.1.1
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 +23 -23
- package/src/index.ts +71 -16
- package/src/server.test.ts +12 -4
package/package.json
CHANGED
|
@@ -1,36 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/reform-remote-node",
|
|
3
|
-
"
|
|
4
|
-
"version": "1.0.2",
|
|
5
|
-
"type": "module",
|
|
3
|
+
"version": "1.1.1",
|
|
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
|
-
"
|
|
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
|
-
"
|
|
22
|
-
"
|
|
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
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
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
|
-
"
|
|
57
|
-
"
|
|
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 { Option, Schema } from 'effect'
|
|
2
3
|
import { type RawData, WebSocketServer } from 'ws'
|
|
3
|
-
import type {
|
|
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:
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
?
|
|
31
|
-
:
|
|
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
|
-
|
|
39
|
-
|
|
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,11 +104,17 @@ export const serveNodeWebSocket = (options: NodeWebSocketOptionsExternalApi): No
|
|
|
55
104
|
wss,
|
|
56
105
|
stop: async (): Promise<void> => {
|
|
57
106
|
await shared.dispose()
|
|
58
|
-
//
|
|
107
|
+
// `wss.close` waits for live sockets to drain.
|
|
59
108
|
wss.clients.forEach((client) => client.terminate())
|
|
60
|
-
await new Promise<void>((resolve, reject) =>
|
|
61
|
-
wss.close((error) => (error === undefined ? resolve() : reject(error)))
|
|
62
|
-
|
|
109
|
+
await new Promise<void>((resolve, reject) => {
|
|
110
|
+
wss.close((error) => (error === undefined ? resolve() : reject(error)))
|
|
111
|
+
// Bun's `ws` frees the port but never invokes that callback nor emits 'close',
|
|
112
|
+
// which would hang teardown forever. Both runtimes drop `address()` the moment
|
|
113
|
+
// the listener is gone, so that fact — not the signal — is what we settle on.
|
|
114
|
+
if (Option.isNone(Option.fromNullable(wss.address()))) {
|
|
115
|
+
resolve()
|
|
116
|
+
}
|
|
117
|
+
})
|
|
63
118
|
},
|
|
64
119
|
}
|
|
65
120
|
}
|
package/src/server.test.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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(
|