@polytric/openws-sdkgen 0.0.13 → 0.0.15
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/README.md +1 -1
- package/dist/main.cjs +1 -1
- package/dist/main.js +1 -1
- package/dist/plans/typescript.cjs +3 -3
- package/dist/plans/typescript.js +3 -3
- package/dist/templates/typescript/package.json.ejs +1 -1
- package/dist/templates/typescript/src/network.ts.ejs +109 -6
- package/dist/templates/typescript/src/roles/role.ts.ejs +1 -1
- package/dist/templates/typescript/src/sdk/role.ts.ejs +481 -72
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ The CLI exposes the following options:
|
|
|
41
41
|
- `--out` (string): Output directory for generated code.
|
|
42
42
|
- `--project` (string): Project/namespace prefix for generated code.
|
|
43
43
|
- `--network` (string): Network name to generate.
|
|
44
|
-
- `--hostRole` (string):
|
|
44
|
+
- `--hostRole` (string): Peer role name that represents the host side.
|
|
45
45
|
- `--language` (string): Target language (`csharp`, `javascript`, or `typescript`).
|
|
46
46
|
- `--environment` (string|array): Target environment (`unity`, `node`, `browser`).
|
|
47
47
|
|
package/dist/main.cjs
CHANGED
|
@@ -457,7 +457,7 @@ function parseInput(ctx) {
|
|
|
457
457
|
}).option("hostRole", {
|
|
458
458
|
type: "array",
|
|
459
459
|
string: true,
|
|
460
|
-
description: "The target
|
|
460
|
+
description: "The target peer roles that use the generated code",
|
|
461
461
|
demandOption: true
|
|
462
462
|
}).option("language", {
|
|
463
463
|
type: "string",
|
package/dist/main.js
CHANGED
|
@@ -434,7 +434,7 @@ function parseInput(ctx) {
|
|
|
434
434
|
}).option("hostRole", {
|
|
435
435
|
type: "array",
|
|
436
436
|
string: true,
|
|
437
|
-
description: "The target
|
|
437
|
+
description: "The target peer roles that use the generated code",
|
|
438
438
|
demandOption: true
|
|
439
439
|
}).option("language", {
|
|
440
440
|
type: "string",
|
|
@@ -216,7 +216,7 @@ function createPlan(ctx) {
|
|
|
216
216
|
const remoteRoles = getPeerRoles(networkSpec, rolesByName, hostRole.roleName).map(
|
|
217
217
|
(remoteRole) => ({
|
|
218
218
|
...remoteRole,
|
|
219
|
-
|
|
219
|
+
scopedPeerName: `${hostRole.className}${remoteRole.className}Peer`,
|
|
220
220
|
allowedMethodNames: getAllowedMessageMethodNames(
|
|
221
221
|
networkSpec,
|
|
222
222
|
remoteRole.roleName,
|
|
@@ -342,9 +342,9 @@ function toRoleInfo(role) {
|
|
|
342
342
|
className,
|
|
343
343
|
roleClassName: className,
|
|
344
344
|
hostRoleClassName: `${className}Host`,
|
|
345
|
-
|
|
345
|
+
peerName: `${className}Peer`,
|
|
346
346
|
varName: camelCase(role.name),
|
|
347
|
-
|
|
347
|
+
peerVarName: `${camelCase(role.name)}Peer`,
|
|
348
348
|
fileName,
|
|
349
349
|
roleFileName: `${fileName}-role`,
|
|
350
350
|
description: role.description || "",
|
package/dist/plans/typescript.js
CHANGED
|
@@ -176,7 +176,7 @@ function createPlan(ctx) {
|
|
|
176
176
|
const remoteRoles = getPeerRoles(networkSpec, rolesByName, hostRole.roleName).map(
|
|
177
177
|
(remoteRole) => ({
|
|
178
178
|
...remoteRole,
|
|
179
|
-
|
|
179
|
+
scopedPeerName: `${hostRole.className}${remoteRole.className}Peer`,
|
|
180
180
|
allowedMethodNames: getAllowedMessageMethodNames(
|
|
181
181
|
networkSpec,
|
|
182
182
|
remoteRole.roleName,
|
|
@@ -302,9 +302,9 @@ function toRoleInfo(role) {
|
|
|
302
302
|
className,
|
|
303
303
|
roleClassName: className,
|
|
304
304
|
hostRoleClassName: `${className}Host`,
|
|
305
|
-
|
|
305
|
+
peerName: `${className}Peer`,
|
|
306
306
|
varName: camelCase(role.name),
|
|
307
|
-
|
|
307
|
+
peerVarName: `${camelCase(role.name)}Peer`,
|
|
308
308
|
fileName,
|
|
309
309
|
roleFileName: `${fileName}-role`,
|
|
310
310
|
description: role.description || "",
|
|
@@ -9,12 +9,18 @@ export const endpoints = <%- JSON.stringify(
|
|
|
9
9
|
) %>
|
|
10
10
|
|
|
11
11
|
<% if (ctx.isTypeScript) { -%>
|
|
12
|
+
/**
|
|
13
|
+
* Wire-format message envelope used by this OpenWS SDK.
|
|
14
|
+
*/
|
|
12
15
|
export interface OpenWsEnvelope<Payload = unknown> {
|
|
13
16
|
fromRole: string
|
|
14
17
|
messageName: string
|
|
15
18
|
payload: Payload
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Connection endpoint metadata from the OpenWS spec.
|
|
23
|
+
*/
|
|
18
24
|
export interface OpenWsEndpoint {
|
|
19
25
|
scheme: string
|
|
20
26
|
host?: string
|
|
@@ -26,6 +32,12 @@ export type Unsubscribe = () => void
|
|
|
26
32
|
export type TransportEvent = 'message' | 'error' | 'close'
|
|
27
33
|
export type TransportHandler = (data: unknown) => void | Promise<void>
|
|
28
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Minimal transport contract used by generated SDK clients.
|
|
37
|
+
*
|
|
38
|
+
* Implement this interface to plug in a custom socket, browser WebSocket,
|
|
39
|
+
* server-side WebSocket client, or test transport.
|
|
40
|
+
*/
|
|
29
41
|
export interface Transport {
|
|
30
42
|
send(data: string): void | Promise<void>
|
|
31
43
|
on?(event: TransportEvent, handler: TransportHandler): unknown
|
|
@@ -38,24 +50,47 @@ export interface BindTransportOptions {
|
|
|
38
50
|
closeOnError?: boolean
|
|
39
51
|
}
|
|
40
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Callback surface used by `bindTransport`.
|
|
55
|
+
*
|
|
56
|
+
* `handle...` methods are framework entrypoints called by transport glue.
|
|
57
|
+
* Generated clients expose matching `on...` methods for application callbacks.
|
|
58
|
+
*/
|
|
41
59
|
export interface RawMessageHandler {
|
|
42
60
|
handleRawMessage(data: string): void | Promise<void>
|
|
43
|
-
|
|
44
|
-
|
|
61
|
+
handleMessageError?(error: unknown): void | Promise<void>
|
|
62
|
+
handleSocketError?(error: unknown): void | Promise<void>
|
|
63
|
+
handleSocketClose?(event: unknown): void | Promise<void>
|
|
45
64
|
}
|
|
46
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Encodes an OpenWS envelope for transport.
|
|
68
|
+
*/
|
|
47
69
|
export function encodeEnvelope(envelope: OpenWsEnvelope): string {
|
|
48
70
|
return JSON.stringify(envelope)
|
|
49
71
|
}
|
|
50
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Decodes raw transport data into an OpenWS envelope.
|
|
75
|
+
*/
|
|
51
76
|
export function decodeEnvelope(data: string): OpenWsEnvelope {
|
|
52
77
|
return JSON.parse(data) as OpenWsEnvelope
|
|
53
78
|
}
|
|
54
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Returns true when a transport can be bound to generated client callbacks.
|
|
82
|
+
*/
|
|
55
83
|
export function canBindTransport(transport: Transport): boolean {
|
|
56
84
|
return typeof transport.on === 'function'
|
|
57
85
|
}
|
|
58
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Binds transport events to a generated client or compatible raw message handler.
|
|
89
|
+
*
|
|
90
|
+
* Transport `message` events call `handleRawMessage`, message handling failures
|
|
91
|
+
* call `handleMessageError`, transport `error` events call `handleSocketError`,
|
|
92
|
+
* and transport `close` events call `handleSocketClose`.
|
|
93
|
+
*/
|
|
59
94
|
export function bindTransport(
|
|
60
95
|
transport: Transport,
|
|
61
96
|
handler: RawMessageHandler,
|
|
@@ -65,14 +100,17 @@ export function bindTransport(
|
|
|
65
100
|
try {
|
|
66
101
|
await handler.handleRawMessage(await normalizeMessageData(data))
|
|
67
102
|
} catch (error) {
|
|
68
|
-
await handler.
|
|
103
|
+
await handler.handleMessageError?.(error)
|
|
69
104
|
if (options.closeOnError) {
|
|
70
105
|
transport.close?.()
|
|
71
106
|
}
|
|
72
107
|
}
|
|
73
108
|
}
|
|
74
109
|
const handleSocketError = async (error: unknown) => {
|
|
75
|
-
await handler.
|
|
110
|
+
await handler.handleSocketError?.(error)
|
|
111
|
+
}
|
|
112
|
+
const handleSocketClose = async (event: unknown) => {
|
|
113
|
+
await handler.handleSocketClose?.(event)
|
|
76
114
|
}
|
|
77
115
|
|
|
78
116
|
const nodeHandler = (data: unknown, ..._args: unknown[]) => {
|
|
@@ -81,6 +119,9 @@ export function bindTransport(
|
|
|
81
119
|
const nodeErrorHandler = (error: unknown, ..._args: unknown[]) => {
|
|
82
120
|
void handleSocketError(error)
|
|
83
121
|
}
|
|
122
|
+
const nodeCloseHandler = (event: unknown, ..._args: unknown[]) => {
|
|
123
|
+
void handleSocketClose(event)
|
|
124
|
+
}
|
|
84
125
|
|
|
85
126
|
if (typeof transport.on !== 'function') {
|
|
86
127
|
throw new Error('Transport must support on("message")')
|
|
@@ -88,12 +129,17 @@ export function bindTransport(
|
|
|
88
129
|
|
|
89
130
|
const messageUnsubscribe = transport.on('message', nodeHandler)
|
|
90
131
|
const errorUnsubscribe = transport.on('error', nodeErrorHandler)
|
|
132
|
+
const closeUnsubscribe = transport.on('close', nodeCloseHandler)
|
|
91
133
|
return () => {
|
|
92
134
|
if (typeof messageUnsubscribe === 'function') messageUnsubscribe()
|
|
93
135
|
if (typeof errorUnsubscribe === 'function') errorUnsubscribe()
|
|
136
|
+
if (typeof closeUnsubscribe === 'function') closeUnsubscribe()
|
|
94
137
|
}
|
|
95
138
|
}
|
|
96
139
|
|
|
140
|
+
/**
|
|
141
|
+
* WebSocket-backed transport implementation for generated SDK clients.
|
|
142
|
+
*/
|
|
97
143
|
export class WsTransport implements Transport {
|
|
98
144
|
private socket?: unknown
|
|
99
145
|
private socketUnsubscribe?: Unsubscribe
|
|
@@ -110,6 +156,9 @@ export class WsTransport implements Transport {
|
|
|
110
156
|
}
|
|
111
157
|
}
|
|
112
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Opens the underlying WebSocket when needed and waits until it is ready.
|
|
161
|
+
*/
|
|
113
162
|
async connect(_roleName: string, endpoint?: OpenWsEndpoint): Promise<void> {
|
|
114
163
|
if (!this.socket) {
|
|
115
164
|
if (!endpoint) {
|
|
@@ -120,10 +169,16 @@ export class WsTransport implements Transport {
|
|
|
120
169
|
await this.waitForOpen()
|
|
121
170
|
}
|
|
122
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Closes the underlying WebSocket connection.
|
|
174
|
+
*/
|
|
123
175
|
async disconnect(): Promise<void> {
|
|
124
176
|
this.close()
|
|
125
177
|
}
|
|
126
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Sends already-encoded OpenWS envelope data.
|
|
181
|
+
*/
|
|
127
182
|
async send(data: string): Promise<void> {
|
|
128
183
|
await this.waitForOpen()
|
|
129
184
|
const socket = this.requireSocket() as { send?: (data: string) => void | Promise<void> }
|
|
@@ -133,6 +188,9 @@ export class WsTransport implements Transport {
|
|
|
133
188
|
await socket.send(data)
|
|
134
189
|
}
|
|
135
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Registers a transport event callback.
|
|
193
|
+
*/
|
|
136
194
|
on(event: TransportEvent, handler: TransportHandler): Unsubscribe {
|
|
137
195
|
this.listeners[event].add(handler)
|
|
138
196
|
return () => {
|
|
@@ -140,6 +198,9 @@ export class WsTransport implements Transport {
|
|
|
140
198
|
}
|
|
141
199
|
}
|
|
142
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Closes and clears the current socket.
|
|
203
|
+
*/
|
|
143
204
|
close(): void {
|
|
144
205
|
const socket = this.socket as { close?: () => void } | undefined
|
|
145
206
|
socket?.close?.()
|
|
@@ -311,31 +372,50 @@ function addSocketListener(
|
|
|
311
372
|
}
|
|
312
373
|
}
|
|
313
374
|
<% } else { -%>
|
|
375
|
+
/**
|
|
376
|
+
* Encodes an OpenWS envelope for transport.
|
|
377
|
+
*/
|
|
314
378
|
export function encodeEnvelope(envelope) {
|
|
315
379
|
return JSON.stringify(envelope)
|
|
316
380
|
}
|
|
317
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Decodes raw transport data into an OpenWS envelope.
|
|
384
|
+
*/
|
|
318
385
|
export function decodeEnvelope(data) {
|
|
319
386
|
return JSON.parse(data)
|
|
320
387
|
}
|
|
321
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Returns true when a transport can be bound to generated client callbacks.
|
|
391
|
+
*/
|
|
322
392
|
export function canBindTransport(transport) {
|
|
323
393
|
return typeof transport.on === 'function'
|
|
324
394
|
}
|
|
325
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Binds transport events to a generated client or compatible raw message handler.
|
|
398
|
+
*
|
|
399
|
+
* Transport `message` events call `handleRawMessage`, message handling failures
|
|
400
|
+
* call `handleMessageError`, transport `error` events call `handleSocketError`,
|
|
401
|
+
* and transport `close` events call `handleSocketClose`.
|
|
402
|
+
*/
|
|
326
403
|
export function bindTransport(transport, handler, options = {}) {
|
|
327
404
|
const handleData = async data => {
|
|
328
405
|
try {
|
|
329
406
|
await handler.handleRawMessage(await normalizeMessageData(data))
|
|
330
407
|
} catch (error) {
|
|
331
|
-
await handler.
|
|
408
|
+
await handler.handleMessageError?.(error)
|
|
332
409
|
if (options.closeOnError) {
|
|
333
410
|
transport.close?.()
|
|
334
411
|
}
|
|
335
412
|
}
|
|
336
413
|
}
|
|
337
414
|
const handleSocketError = async error => {
|
|
338
|
-
await handler.
|
|
415
|
+
await handler.handleSocketError?.(error)
|
|
416
|
+
}
|
|
417
|
+
const handleSocketClose = async event => {
|
|
418
|
+
await handler.handleSocketClose?.(event)
|
|
339
419
|
}
|
|
340
420
|
|
|
341
421
|
const nodeHandler = data => {
|
|
@@ -344,6 +424,9 @@ export function bindTransport(transport, handler, options = {}) {
|
|
|
344
424
|
const nodeErrorHandler = error => {
|
|
345
425
|
void handleSocketError(error)
|
|
346
426
|
}
|
|
427
|
+
const nodeCloseHandler = event => {
|
|
428
|
+
void handleSocketClose(event)
|
|
429
|
+
}
|
|
347
430
|
|
|
348
431
|
if (typeof transport.on !== 'function') {
|
|
349
432
|
throw new Error('Transport must support on("message")')
|
|
@@ -351,12 +434,17 @@ export function bindTransport(transport, handler, options = {}) {
|
|
|
351
434
|
|
|
352
435
|
const messageUnsubscribe = transport.on('message', nodeHandler)
|
|
353
436
|
const errorUnsubscribe = transport.on('error', nodeErrorHandler)
|
|
437
|
+
const closeUnsubscribe = transport.on('close', nodeCloseHandler)
|
|
354
438
|
return () => {
|
|
355
439
|
if (typeof messageUnsubscribe === 'function') messageUnsubscribe()
|
|
356
440
|
if (typeof errorUnsubscribe === 'function') errorUnsubscribe()
|
|
441
|
+
if (typeof closeUnsubscribe === 'function') closeUnsubscribe()
|
|
357
442
|
}
|
|
358
443
|
}
|
|
359
444
|
|
|
445
|
+
/**
|
|
446
|
+
* WebSocket-backed transport implementation for generated SDK clients.
|
|
447
|
+
*/
|
|
360
448
|
export class WsTransport {
|
|
361
449
|
socket
|
|
362
450
|
socketUnsubscribe
|
|
@@ -373,6 +461,9 @@ export class WsTransport {
|
|
|
373
461
|
}
|
|
374
462
|
}
|
|
375
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Opens the underlying WebSocket when needed and waits until it is ready.
|
|
466
|
+
*/
|
|
376
467
|
async connect(_roleName, endpoint) {
|
|
377
468
|
if (!this.socket) {
|
|
378
469
|
if (!endpoint) {
|
|
@@ -383,10 +474,16 @@ export class WsTransport {
|
|
|
383
474
|
await this.waitForOpen()
|
|
384
475
|
}
|
|
385
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Closes the underlying WebSocket connection.
|
|
479
|
+
*/
|
|
386
480
|
async disconnect() {
|
|
387
481
|
this.close()
|
|
388
482
|
}
|
|
389
483
|
|
|
484
|
+
/**
|
|
485
|
+
* Sends already-encoded OpenWS envelope data.
|
|
486
|
+
*/
|
|
390
487
|
async send(data) {
|
|
391
488
|
await this.waitForOpen()
|
|
392
489
|
const socket = this.requireSocket()
|
|
@@ -396,6 +493,9 @@ export class WsTransport {
|
|
|
396
493
|
await socket.send(data)
|
|
397
494
|
}
|
|
398
495
|
|
|
496
|
+
/**
|
|
497
|
+
* Registers a transport event callback.
|
|
498
|
+
*/
|
|
399
499
|
on(event, handler) {
|
|
400
500
|
this.listeners[event].add(handler)
|
|
401
501
|
return () => {
|
|
@@ -403,6 +503,9 @@ export class WsTransport {
|
|
|
403
503
|
}
|
|
404
504
|
}
|
|
405
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Closes and clears the current socket.
|
|
508
|
+
*/
|
|
406
509
|
close() {
|
|
407
510
|
this.socket?.close?.()
|
|
408
511
|
this.clearSocket()
|
|
@@ -10,7 +10,7 @@ import type { <%= payloadImports.join(', ') %> } from '<%= modelImportPath %>'
|
|
|
10
10
|
import { <%= peerRoleImports.join(', ') %> } from './index'
|
|
11
11
|
<% } -%>
|
|
12
12
|
|
|
13
|
-
export interface <%= ctx.
|
|
13
|
+
export interface <%= ctx.peerName %> {
|
|
14
14
|
<% for (const message of ctx.messages) { -%>
|
|
15
15
|
<%= message.methodName %>(payload: <%= message.payloadType %> | <%= message.payloadType %>Init): Promise<void>
|
|
16
16
|
<% } -%>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<% if (ctx.isTypeScript) { -%>
|
|
5
5
|
import * as WS from '@polytric/openws/class'
|
|
6
6
|
import * as Fluent from '@polytric/openws/fluent'
|
|
7
|
-
import type {
|
|
7
|
+
import type { PeerProto } from '@polytric/openws/fluent'
|
|
8
8
|
import type { BindTransportOptions, OpenWsEndpoint, OpenWsEnvelope, Transport, Unsubscribe } from '<%= networkImport %>'
|
|
9
9
|
import { WsTransport, bindTransport, canBindTransport, decodeEnvelope, encodeEnvelope } from '<%= networkImport %>'
|
|
10
10
|
<% if (ctx.handlers.length > 0) { -%>
|
|
@@ -19,34 +19,70 @@ import {
|
|
|
19
19
|
<%= ctx.hostRoleClassName %>,
|
|
20
20
|
<% for (const remoteRole of ctx.remoteRoles) { -%>
|
|
21
21
|
<%= remoteRole.roleClassName %>,
|
|
22
|
-
type <%= remoteRole.
|
|
22
|
+
type <%= remoteRole.peerName %>,
|
|
23
23
|
<% } -%>
|
|
24
24
|
} from '<%= rolesImport %>'
|
|
25
25
|
|
|
26
26
|
<% for (const remoteRole of ctx.remoteRoles) { -%>
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Connected <%- JSON.stringify(remoteRole.roleName) %> peer handle as seen by <%= ctx.className %>.
|
|
29
|
+
*/
|
|
30
|
+
export type <%= remoteRole.scopedPeerName %> = Pick<<%= remoteRole.peerName %>, <%- remoteRole.allowedMethodNames.length > 0 ? remoteRole.allowedMethodNames.map(methodName => JSON.stringify(methodName)).join(' | ') : 'never' %>>
|
|
28
31
|
<% } -%>
|
|
29
32
|
|
|
30
33
|
<% if (ctx.remoteRoles.length > 0) { -%>
|
|
31
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Union of peer handles this generated client can connect to.
|
|
36
|
+
*/
|
|
37
|
+
export type <%= ctx.className %>Peer = <%= ctx.remoteRoles.map(remoteRole => remoteRole.scopedPeerName).join(' | ') %>
|
|
32
38
|
|
|
33
39
|
<% } else { -%>
|
|
34
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Union of peer handles this generated client can connect to.
|
|
42
|
+
*/
|
|
43
|
+
export type <%= ctx.className %>Peer = unknown
|
|
35
44
|
|
|
36
45
|
<% } -%>
|
|
37
46
|
<% if (ctx.handlers.length > 0) { -%>
|
|
38
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Application callback for a received OpenWS message.
|
|
49
|
+
*/
|
|
50
|
+
export type <%= ctx.className %>MessageHandler<TPayload, TPeer = <%= ctx.className %>Peer> = (payload: TPayload, peer: TPeer) => void | Promise<void>
|
|
39
51
|
|
|
40
52
|
<% } -%>
|
|
53
|
+
/**
|
|
54
|
+
* Application callback for message or socket errors.
|
|
55
|
+
*/
|
|
41
56
|
export type <%= ctx.className %>ErrorHandler = (error: unknown) => void | Promise<void>
|
|
57
|
+
/**
|
|
58
|
+
* Application callback for connection lifecycle events.
|
|
59
|
+
*/
|
|
60
|
+
export type <%= ctx.className %>LifecycleHandler = (roleName: string) => void | Promise<void>
|
|
61
|
+
/**
|
|
62
|
+
* Application callback for connection lifecycle errors.
|
|
63
|
+
*/
|
|
64
|
+
export type <%= ctx.className %>LifecycleErrorHandler = (roleName: string, error: Error) => void | Promise<void>
|
|
65
|
+
|
|
66
|
+
type <%= ctx.className %>Connection = {
|
|
67
|
+
roleName: string
|
|
68
|
+
session: Fluent.Session
|
|
69
|
+
peer: PeerProto
|
|
70
|
+
}
|
|
42
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Generated OpenWS client for the <%- JSON.stringify(ctx.roleName) %> role in the <%- JSON.stringify(ctx.networkName) %> network.
|
|
74
|
+
*
|
|
75
|
+
* Application code calls command methods such as `connect` and registers
|
|
76
|
+
* callbacks with `on...` methods. Transport and framework glue call `handle...`
|
|
77
|
+
* methods to deliver inbound data, errors, and lifecycle events.
|
|
78
|
+
*/
|
|
43
79
|
export class <%= ctx.className %> {
|
|
44
80
|
static readonly CONFIG = <%= ctx.hostRoleClassName %>.CONFIG
|
|
45
81
|
|
|
46
82
|
readonly name = <%= ctx.className %>.CONFIG.name
|
|
47
83
|
readonly description = <%= ctx.className %>.CONFIG.description
|
|
48
84
|
<% for (const remoteRole of ctx.remoteRoles) { -%>
|
|
49
|
-
<%= remoteRole.
|
|
85
|
+
<%= remoteRole.peerVarName %>!: <%= remoteRole.scopedPeerName %>
|
|
50
86
|
<% } -%>
|
|
51
87
|
|
|
52
88
|
private readonly binder: Fluent.NetworkBinder
|
|
@@ -54,14 +90,18 @@ export class <%= ctx.className %> {
|
|
|
54
90
|
private readonly fromRole = <%- JSON.stringify(ctx.roleName) %>
|
|
55
91
|
private readonly sendEnvelope: (toRole: string, messageName: string, payload: unknown) => Promise<void>
|
|
56
92
|
private transportUnsubscribe?: Unsubscribe
|
|
57
|
-
private readonly
|
|
58
|
-
private readonly
|
|
59
|
-
private readonly
|
|
93
|
+
private readonly connections = new Set<<%= ctx.className %>Connection>()
|
|
94
|
+
private readonly peersByMessageName: Record<string, PeerProto> = {}
|
|
95
|
+
private readonly peerRoleByMessageName: Record<string, string> = {}
|
|
96
|
+
private readonly handlersByMessageName: Record<string, (payload: unknown, peer: <%= ctx.className %>Peer) => Promise<void>> = {}
|
|
60
97
|
private readonly messageErrorHandlers = new Set<<%= ctx.className %>ErrorHandler>()
|
|
61
98
|
private readonly socketErrorHandlers = new Set<<%= ctx.className %>ErrorHandler>()
|
|
99
|
+
private readonly openHandlers = new Set<<%= ctx.className %>LifecycleHandler>()
|
|
100
|
+
private readonly closeHandlers = new Set<<%= ctx.className %>LifecycleHandler>()
|
|
101
|
+
private readonly lifecycleErrorHandlers = new Set<<%= ctx.className %>LifecycleErrorHandler>()
|
|
62
102
|
<% for (const handler of ctx.handlers) { -%>
|
|
63
|
-
<% const
|
|
64
|
-
private readonly <%= handler.listenerFieldName %> = new Set<<%= ctx.className %>MessageHandler<<%= handler.payloadType %>, <%=
|
|
103
|
+
<% const handlerPeerType = handler.bindFromRoles.map(fromRole => ctx.remoteRoles.find(remoteRole => remoteRole.roleName === fromRole.roleName)?.scopedPeerName).filter(Boolean).join(' | ') || `${ctx.className}Peer` -%>
|
|
104
|
+
private readonly <%= handler.listenerFieldName %> = new Set<<%= ctx.className %>MessageHandler<<%= handler.payloadType %>, <%= handlerPeerType %>>>()
|
|
65
105
|
<% } -%>
|
|
66
106
|
|
|
67
107
|
constructor(
|
|
@@ -81,15 +121,26 @@ export class <%= ctx.className %> {
|
|
|
81
121
|
await this.transport.send(encodeEnvelope({ fromRole: this.fromRole, messageName, payload }))
|
|
82
122
|
}
|
|
83
123
|
<% for (const handler of ctx.handlers) { -%>
|
|
84
|
-
<% const
|
|
85
|
-
this.handlersByMessageName[<%- JSON.stringify(handler.messageName) %>] = async (payload,
|
|
86
|
-
await this.<%= handler.dispatchMethodName %>(payload as <%= handler.payloadType %>,
|
|
124
|
+
<% const handlerPeerType = handler.bindFromRoles.map(fromRole => ctx.remoteRoles.find(remoteRole => remoteRole.roleName === fromRole.roleName)?.scopedPeerName).filter(Boolean).join(' | ') || `${ctx.className}Peer` -%>
|
|
125
|
+
this.handlersByMessageName[<%- JSON.stringify(handler.messageName) %>] = async (payload, peer) => {
|
|
126
|
+
await this.<%= handler.dispatchMethodName %>(payload as <%= handler.payloadType %>, peer as <%= handlerPeerType %>)
|
|
87
127
|
}
|
|
88
128
|
<% for (const fromRole of handler.bindFromRoles) { -%>
|
|
89
|
-
this.binder.fromRoles[<%- JSON.stringify(fromRole.roleName) %>].on(<%- JSON.stringify(handler.messageName) %>, async (payload,
|
|
90
|
-
await this.<%= handler.dispatchMethodName %>(payload as <%= handler.payloadType %>,
|
|
129
|
+
this.binder.fromRoles[<%- JSON.stringify(fromRole.roleName) %>].on(<%- JSON.stringify(handler.messageName) %>, async (payload, peer) => {
|
|
130
|
+
await this.<%= handler.dispatchMethodName %>(payload as <%= handler.payloadType %>, peer as unknown as <%= handlerPeerType %>)
|
|
91
131
|
})
|
|
92
132
|
<% } -%>
|
|
133
|
+
<% } -%>
|
|
134
|
+
<% for (const remoteRole of ctx.remoteRoles) { -%>
|
|
135
|
+
this.binder.fromRoles[<%- JSON.stringify(remoteRole.roleName) %>].onOpen(async fromRole => {
|
|
136
|
+
await this.handleOpen(fromRole)
|
|
137
|
+
})
|
|
138
|
+
this.binder.fromRoles[<%- JSON.stringify(remoteRole.roleName) %>].onClose(async fromRole => {
|
|
139
|
+
await this.handleClose(fromRole)
|
|
140
|
+
})
|
|
141
|
+
this.binder.fromRoles[<%- JSON.stringify(remoteRole.roleName) %>].onError(async (fromRole, error) => {
|
|
142
|
+
await this.handleError(fromRole, error)
|
|
143
|
+
})
|
|
93
144
|
<% } -%>
|
|
94
145
|
if (canBindTransport(transport)) {
|
|
95
146
|
this.bindTransport(transport)
|
|
@@ -97,26 +148,33 @@ export class <%= ctx.className %> {
|
|
|
97
148
|
}
|
|
98
149
|
|
|
99
150
|
<% for (const remoteRole of ctx.remoteRoles) { -%>
|
|
100
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Connects this client to the <%- JSON.stringify(remoteRole.roleName) %> role and returns the connected peer handle.
|
|
153
|
+
*/
|
|
154
|
+
async connect(roleName: <%- JSON.stringify(remoteRole.roleName) %>, endpoint?: OpenWsEndpoint): Promise<<%= remoteRole.scopedPeerName %>>
|
|
101
155
|
<% } -%>
|
|
102
|
-
async connect(roleName: string, endpoint?: OpenWsEndpoint): Promise<<%= ctx.className %>
|
|
156
|
+
async connect(roleName: string, endpoint?: OpenWsEndpoint): Promise<<%= ctx.className %>Peer> {
|
|
103
157
|
switch (roleName) {
|
|
104
158
|
<% for (const remoteRole of ctx.remoteRoles) { -%>
|
|
105
159
|
case <%- JSON.stringify(remoteRole.roleName) %>: {
|
|
106
160
|
const remoteEndpoint = endpoint ?? (<%- remoteRole.endpoints.length > 0 ? JSON.stringify(remoteRole.endpoints[0]) : 'undefined' %> as OpenWsEndpoint | undefined)
|
|
107
161
|
await this.transport.connect?.(roleName, remoteEndpoint)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
162
|
+
const session = this.runtime.newSession(this.sendEnvelope)
|
|
163
|
+
const <%= remoteRole.varName %>Peer = await session.open(<%- JSON.stringify(remoteRole.roleName) %>)
|
|
164
|
+
this.connections.add({
|
|
165
|
+
roleName: <%- JSON.stringify(remoteRole.roleName) %>,
|
|
166
|
+
session,
|
|
167
|
+
peer: <%= remoteRole.varName %>Peer,
|
|
168
|
+
})
|
|
169
|
+
this.<%= remoteRole.peerVarName %> = <%= remoteRole.varName %>Peer as unknown as <%= remoteRole.scopedPeerName %>
|
|
112
170
|
<% for (const handler of ctx.handlers) { -%>
|
|
113
|
-
<% const
|
|
114
|
-
<% if (
|
|
115
|
-
|
|
171
|
+
<% const handlerDefaultPeer = handler.bindFromRoles.find(fromRole => ctx.remoteRoles.some(remoteRole => remoteRole.roleName === fromRole.roleName)) ?? ctx.remoteRoles[0] -%>
|
|
172
|
+
<% if (handlerDefaultPeer?.roleName === remoteRole.roleName) { -%>
|
|
173
|
+
this.peersByMessageName[<%- JSON.stringify(handler.messageName) %>] = <%= remoteRole.varName %>Peer
|
|
174
|
+
this.peerRoleByMessageName[<%- JSON.stringify(handler.messageName) %>] = <%- JSON.stringify(remoteRole.roleName) %>
|
|
116
175
|
<% } -%>
|
|
117
176
|
<% } -%>
|
|
118
|
-
|
|
119
|
-
return this.<%= remoteRole.apiVarName %>
|
|
177
|
+
return this.<%= remoteRole.peerVarName %>
|
|
120
178
|
}
|
|
121
179
|
<% } -%>
|
|
122
180
|
default:
|
|
@@ -124,28 +182,61 @@ export class <%= ctx.className %> {
|
|
|
124
182
|
}
|
|
125
183
|
}
|
|
126
184
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
185
|
+
<% for (const remoteRole of ctx.remoteRoles) { -%>
|
|
186
|
+
async disconnect(peer: <%= remoteRole.scopedPeerName %>): Promise<void>
|
|
187
|
+
<% } -%>
|
|
188
|
+
async disconnect(peer: string): Promise<void>
|
|
189
|
+
/**
|
|
190
|
+
* Disconnects from a peer and closes its session.
|
|
191
|
+
*
|
|
192
|
+
* Pass the peer returned by `connect` to close that exact peer connection.
|
|
193
|
+
* Passing a role name closes all active peer connections for that role.
|
|
194
|
+
*/
|
|
195
|
+
async disconnect(peer: string | <%= ctx.className %>Peer): Promise<void> {
|
|
196
|
+
if (typeof peer === 'string') {
|
|
197
|
+
await this.transport.disconnect?.(peer)
|
|
198
|
+
await this.closeSessions(peer)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
const connection = this.findConnectionByPeer(peer as PeerProto)
|
|
202
|
+
if (!connection) {
|
|
203
|
+
throw new Error('Peer is not connected')
|
|
204
|
+
}
|
|
205
|
+
await this.closeConnection(connection)
|
|
130
206
|
}
|
|
131
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Binds a transport to this client.
|
|
210
|
+
*
|
|
211
|
+
* Transport events call this client's `handle...` methods. Application code
|
|
212
|
+
* should use `on...` methods to observe those events.
|
|
213
|
+
*/
|
|
132
214
|
bindTransport(transport: Transport = this.transport, options: BindTransportOptions = {}): Unsubscribe {
|
|
133
215
|
this.transportUnsubscribe?.()
|
|
134
216
|
this.transportUnsubscribe = bindTransport(transport, this, options)
|
|
135
217
|
return this.transportUnsubscribe
|
|
136
218
|
}
|
|
137
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Removes the current transport event bindings.
|
|
222
|
+
*/
|
|
138
223
|
unbindTransport(): void {
|
|
139
224
|
this.transportUnsubscribe?.()
|
|
140
225
|
this.transportUnsubscribe = undefined
|
|
141
226
|
}
|
|
142
227
|
|
|
143
|
-
|
|
228
|
+
/**
|
|
229
|
+
* Framework entrypoint for message decoding or dispatch failures.
|
|
230
|
+
*/
|
|
231
|
+
async handleMessageError(error: unknown): Promise<void> {
|
|
144
232
|
for (const handler of this.messageErrorHandlers) {
|
|
145
233
|
await handler(error)
|
|
146
234
|
}
|
|
147
235
|
}
|
|
148
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Registers an application callback for message decoding or dispatch failures.
|
|
239
|
+
*/
|
|
149
240
|
onMessageError(handler: <%= ctx.className %>ErrorHandler): Unsubscribe {
|
|
150
241
|
this.messageErrorHandlers.add(handler)
|
|
151
242
|
return () => {
|
|
@@ -153,12 +244,22 @@ export class <%= ctx.className %> {
|
|
|
153
244
|
}
|
|
154
245
|
}
|
|
155
246
|
|
|
156
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Framework entrypoint for transport-level socket errors.
|
|
249
|
+
*/
|
|
250
|
+
async handleSocketError(error: unknown): Promise<void> {
|
|
251
|
+
const sessionError = toOpenWsError(error)
|
|
252
|
+
for (const connection of this.connections) {
|
|
253
|
+
await connection.session.error(sessionError)
|
|
254
|
+
}
|
|
157
255
|
for (const handler of this.socketErrorHandlers) {
|
|
158
256
|
await handler(error)
|
|
159
257
|
}
|
|
160
258
|
}
|
|
161
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Registers an application callback for transport-level socket errors.
|
|
262
|
+
*/
|
|
162
263
|
onSocketError(handler: <%= ctx.className %>ErrorHandler): Unsubscribe {
|
|
163
264
|
this.socketErrorHandlers.add(handler)
|
|
164
265
|
return () => {
|
|
@@ -166,15 +267,117 @@ export class <%= ctx.className %> {
|
|
|
166
267
|
}
|
|
167
268
|
}
|
|
168
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Framework entrypoint for transport-level socket close events.
|
|
272
|
+
*/
|
|
273
|
+
async handleSocketClose(_event: unknown): Promise<void> {
|
|
274
|
+
for (const connection of Array.from(this.connections)) {
|
|
275
|
+
await this.closeConnection(connection)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Framework entrypoint for a peer session opening.
|
|
281
|
+
*/
|
|
282
|
+
async handleOpen(roleName: string): Promise<void> {
|
|
283
|
+
for (const handler of this.openHandlers) {
|
|
284
|
+
await handler(roleName)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Registers an application callback for peer session open events.
|
|
290
|
+
*/
|
|
291
|
+
onOpen(handler: <%= ctx.className %>LifecycleHandler): Unsubscribe {
|
|
292
|
+
this.openHandlers.add(handler)
|
|
293
|
+
return () => {
|
|
294
|
+
this.openHandlers.delete(handler)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Framework entrypoint for a peer session closing.
|
|
300
|
+
*/
|
|
301
|
+
async handleClose(roleName: string): Promise<void> {
|
|
302
|
+
for (const handler of this.closeHandlers) {
|
|
303
|
+
await handler(roleName)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Registers an application callback for peer session close events.
|
|
309
|
+
*/
|
|
310
|
+
onClose(handler: <%= ctx.className %>LifecycleHandler): Unsubscribe {
|
|
311
|
+
this.closeHandlers.add(handler)
|
|
312
|
+
return () => {
|
|
313
|
+
this.closeHandlers.delete(handler)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Framework entrypoint for a peer session error.
|
|
319
|
+
*/
|
|
320
|
+
async handleError(roleName: string, error: Error): Promise<void> {
|
|
321
|
+
for (const handler of this.lifecycleErrorHandlers) {
|
|
322
|
+
await handler(roleName, error)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Registers an application callback for peer session errors.
|
|
328
|
+
*/
|
|
329
|
+
onError(handler: <%= ctx.className %>LifecycleErrorHandler): Unsubscribe {
|
|
330
|
+
this.lifecycleErrorHandlers.add(handler)
|
|
331
|
+
return () => {
|
|
332
|
+
this.lifecycleErrorHandlers.delete(handler)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private async closeSessions(roleName: string): Promise<void> {
|
|
337
|
+
for (const connection of this.findConnections(roleName)) {
|
|
338
|
+
await this.closeConnection(connection)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private async closeConnection(connection: <%= ctx.className %>Connection): Promise<void> {
|
|
343
|
+
if (!this.connections.delete(connection)) {
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
if (this.findConnections(connection.roleName).length === 0) {
|
|
347
|
+
for (const [messageName, peerRoleName] of Object.entries(this.peerRoleByMessageName)) {
|
|
348
|
+
if (peerRoleName !== connection.roleName) {
|
|
349
|
+
continue
|
|
350
|
+
}
|
|
351
|
+
delete this.peersByMessageName[messageName]
|
|
352
|
+
delete this.peerRoleByMessageName[messageName]
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
await connection.session.close()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private findConnections(roleName: string): <%= ctx.className %>Connection[] {
|
|
359
|
+
return Array.from(this.connections).filter(connection => connection.roleName === roleName)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private findConnectionByPeer(peer: PeerProto): <%= ctx.className %>Connection | undefined {
|
|
363
|
+
return Array.from(this.connections).find(connection => connection.peer === peer)
|
|
364
|
+
}
|
|
365
|
+
|
|
169
366
|
<% for (const handler of ctx.handlers) { -%>
|
|
170
|
-
<% const
|
|
171
|
-
|
|
367
|
+
<% const handlerPeerType = handler.bindFromRoles.map(fromRole => ctx.remoteRoles.find(remoteRole => remoteRole.roleName === fromRole.roleName)?.scopedPeerName).filter(Boolean).join(' | ') || `${ctx.className}Peer` -%>
|
|
368
|
+
/**
|
|
369
|
+
* Dispatches the <%- JSON.stringify(handler.messageName) %> message to registered application callbacks.
|
|
370
|
+
*/
|
|
371
|
+
async <%= handler.dispatchMethodName %>(payload: <%= handler.payloadType %>, peer: <%= handlerPeerType %>): Promise<void> {
|
|
172
372
|
for (const handler of this.<%= handler.listenerFieldName %>) {
|
|
173
|
-
await handler(payload,
|
|
373
|
+
await handler(payload, peer)
|
|
174
374
|
}
|
|
175
375
|
}
|
|
176
376
|
|
|
177
|
-
|
|
377
|
+
/**
|
|
378
|
+
* Registers an application callback for the <%- JSON.stringify(handler.messageName) %> message.
|
|
379
|
+
*/
|
|
380
|
+
<%= handler.onMethodName %>(handler: <%= ctx.className %>MessageHandler<<%= handler.payloadType %>, <%= handlerPeerType %>>): Unsubscribe {
|
|
178
381
|
this.<%= handler.listenerFieldName %>.add(handler)
|
|
179
382
|
return () => {
|
|
180
383
|
this.<%= handler.listenerFieldName %>.delete(handler)
|
|
@@ -182,28 +385,46 @@ export class <%= ctx.className %> {
|
|
|
182
385
|
}
|
|
183
386
|
|
|
184
387
|
<% } -%>
|
|
388
|
+
/**
|
|
389
|
+
* Framework entrypoint for a raw transport message.
|
|
390
|
+
*/
|
|
185
391
|
async handleRawMessage(data: string): Promise<void> {
|
|
186
392
|
await this.handleMessage(decodeEnvelope(data))
|
|
187
393
|
}
|
|
188
394
|
|
|
395
|
+
/**
|
|
396
|
+
* Framework entrypoint for a decoded OpenWS envelope.
|
|
397
|
+
*/
|
|
189
398
|
async handleMessage(envelope: OpenWsEnvelope): Promise<void> {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
399
|
+
const connections = this.findConnections(envelope.fromRole)
|
|
400
|
+
if (connections.length === 1) {
|
|
401
|
+
await connections[0].session.handleMessage(
|
|
402
|
+
envelope.fromRole,
|
|
403
|
+
envelope.messageName,
|
|
404
|
+
envelope.payload
|
|
405
|
+
)
|
|
194
406
|
return
|
|
195
407
|
}
|
|
408
|
+
if (connections.length > 1) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`Multiple sessions for remote role ${envelope.fromRole}; dispatch requires connection context`
|
|
411
|
+
)
|
|
412
|
+
}
|
|
196
413
|
|
|
197
414
|
const localHandler = this.handlersByMessageName[envelope.messageName]
|
|
198
|
-
const
|
|
199
|
-
if (envelope.fromRole === this.fromRole && localHandler &&
|
|
200
|
-
await localHandler(envelope.payload,
|
|
415
|
+
const localPeer = this.peersByMessageName[envelope.messageName]
|
|
416
|
+
if (envelope.fromRole === this.fromRole && localHandler && localPeer) {
|
|
417
|
+
await localHandler(envelope.payload, localPeer as unknown as <%= ctx.className %>Peer)
|
|
201
418
|
return
|
|
202
419
|
}
|
|
203
420
|
|
|
204
421
|
throw new Error(`Remote role ${envelope.fromRole} not found`)
|
|
205
422
|
}
|
|
206
423
|
}
|
|
424
|
+
|
|
425
|
+
function toOpenWsError(error: unknown): Error {
|
|
426
|
+
return error instanceof Error ? error : new Error(String(error))
|
|
427
|
+
}
|
|
207
428
|
<% } else { -%>
|
|
208
429
|
import * as WS from '@polytric/openws/class'
|
|
209
430
|
import * as Fluent from '@polytric/openws/fluent'
|
|
@@ -218,6 +439,13 @@ import {
|
|
|
218
439
|
<% } -%>
|
|
219
440
|
} from '<%= rolesImport %>'
|
|
220
441
|
|
|
442
|
+
/**
|
|
443
|
+
* Generated OpenWS client for the <%- JSON.stringify(ctx.roleName) %> role in the <%- JSON.stringify(ctx.networkName) %> network.
|
|
444
|
+
*
|
|
445
|
+
* Application code calls command methods such as `connect` and registers
|
|
446
|
+
* callbacks with `on...` methods. Transport and framework glue call `handle...`
|
|
447
|
+
* methods to deliver inbound data, errors, and lifecycle events.
|
|
448
|
+
*/
|
|
221
449
|
export class <%= ctx.className %> {
|
|
222
450
|
static CONFIG = <%= ctx.hostRoleClassName %>.CONFIG
|
|
223
451
|
|
|
@@ -226,11 +454,15 @@ export class <%= ctx.className %> {
|
|
|
226
454
|
runtime
|
|
227
455
|
sendEnvelope
|
|
228
456
|
#transportUnsubscribe
|
|
229
|
-
#
|
|
230
|
-
#
|
|
457
|
+
#connections = new Set()
|
|
458
|
+
#peersByMessageName = {}
|
|
459
|
+
#peerRoleByMessageName = {}
|
|
231
460
|
#handlersByMessageName = {}
|
|
232
461
|
#messageErrorHandlers = new Set()
|
|
233
462
|
#socketErrorHandlers = new Set()
|
|
463
|
+
#openHandlers = new Set()
|
|
464
|
+
#closeHandlers = new Set()
|
|
465
|
+
#lifecycleErrorHandlers = new Set()
|
|
234
466
|
#fromRole = <%- JSON.stringify(ctx.roleName) %>
|
|
235
467
|
<% for (const handler of ctx.handlers) { -%>
|
|
236
468
|
#<%= handler.listenerFieldName %> = new Set()
|
|
@@ -252,37 +484,55 @@ export class <%= ctx.className %> {
|
|
|
252
484
|
await this.transport.send(encodeEnvelope({ fromRole: this.#fromRole, messageName, payload }))
|
|
253
485
|
}
|
|
254
486
|
<% for (const handler of ctx.handlers) { -%>
|
|
255
|
-
this.#handlersByMessageName[<%- JSON.stringify(handler.messageName) %>] = async (payload,
|
|
256
|
-
await this.<%= handler.dispatchMethodName %>(payload,
|
|
487
|
+
this.#handlersByMessageName[<%- JSON.stringify(handler.messageName) %>] = async (payload, peer) => {
|
|
488
|
+
await this.<%= handler.dispatchMethodName %>(payload, peer)
|
|
257
489
|
}
|
|
258
490
|
<% for (const fromRole of handler.bindFromRoles) { -%>
|
|
259
|
-
this.binder.fromRoles[<%- JSON.stringify(fromRole.roleName) %>].on(<%- JSON.stringify(handler.messageName) %>, async (payload,
|
|
260
|
-
await this.<%= handler.dispatchMethodName %>(payload,
|
|
491
|
+
this.binder.fromRoles[<%- JSON.stringify(fromRole.roleName) %>].on(<%- JSON.stringify(handler.messageName) %>, async (payload, peer) => {
|
|
492
|
+
await this.<%= handler.dispatchMethodName %>(payload, peer)
|
|
261
493
|
})
|
|
262
494
|
<% } -%>
|
|
495
|
+
<% } -%>
|
|
496
|
+
<% for (const remoteRole of ctx.remoteRoles) { -%>
|
|
497
|
+
this.binder.fromRoles[<%- JSON.stringify(remoteRole.roleName) %>].onOpen(async fromRole => {
|
|
498
|
+
await this.handleOpen(fromRole)
|
|
499
|
+
})
|
|
500
|
+
this.binder.fromRoles[<%- JSON.stringify(remoteRole.roleName) %>].onClose(async fromRole => {
|
|
501
|
+
await this.handleClose(fromRole)
|
|
502
|
+
})
|
|
503
|
+
this.binder.fromRoles[<%- JSON.stringify(remoteRole.roleName) %>].onError(async (fromRole, error) => {
|
|
504
|
+
await this.handleError(fromRole, error)
|
|
505
|
+
})
|
|
263
506
|
<% } -%>
|
|
264
507
|
if (canBindTransport(transport)) {
|
|
265
508
|
this.bindTransport(transport)
|
|
266
509
|
}
|
|
267
510
|
}
|
|
268
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Connects this client to a role and returns the connected peer handle.
|
|
514
|
+
*/
|
|
269
515
|
async connect(roleName, endpoint) {
|
|
270
516
|
switch (roleName) {
|
|
271
517
|
<% for (const remoteRole of ctx.remoteRoles) { -%>
|
|
272
518
|
case <%- JSON.stringify(remoteRole.roleName) %>: {
|
|
273
519
|
const remoteEndpoint = endpoint ?? <%- remoteRole.endpoints.length > 0 ? JSON.stringify(remoteRole.endpoints[0]) : 'undefined' %>
|
|
274
520
|
await this.transport.connect?.(roleName, remoteEndpoint)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
521
|
+
const session = this.runtime.newSession(this.sendEnvelope)
|
|
522
|
+
this.<%= remoteRole.peerVarName %> = await session.open(<%- JSON.stringify(remoteRole.roleName) %>)
|
|
523
|
+
this.#connections.add({
|
|
524
|
+
roleName: <%- JSON.stringify(remoteRole.roleName) %>,
|
|
525
|
+
session,
|
|
526
|
+
peer: this.<%= remoteRole.peerVarName %>,
|
|
527
|
+
})
|
|
278
528
|
<% for (const handler of ctx.handlers) { -%>
|
|
279
|
-
<% const
|
|
280
|
-
<% if (
|
|
281
|
-
|
|
529
|
+
<% const handlerDefaultPeer = handler.bindFromRoles.find(fromRole => ctx.remoteRoles.some(remoteRole => remoteRole.roleName === fromRole.roleName)) ?? ctx.remoteRoles[0] -%>
|
|
530
|
+
<% if (handlerDefaultPeer?.roleName === remoteRole.roleName) { -%>
|
|
531
|
+
this.#peersByMessageName[<%- JSON.stringify(handler.messageName) %>] = this.<%= remoteRole.peerVarName %>
|
|
532
|
+
this.#peerRoleByMessageName[<%- JSON.stringify(handler.messageName) %>] = <%- JSON.stringify(remoteRole.roleName) %>
|
|
282
533
|
<% } -%>
|
|
283
534
|
<% } -%>
|
|
284
|
-
|
|
285
|
-
return this.<%= remoteRole.apiVarName %>
|
|
535
|
+
return this.<%= remoteRole.peerVarName %>
|
|
286
536
|
}
|
|
287
537
|
<% } -%>
|
|
288
538
|
default:
|
|
@@ -290,28 +540,57 @@ export class <%= ctx.className %> {
|
|
|
290
540
|
}
|
|
291
541
|
}
|
|
292
542
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
543
|
+
/**
|
|
544
|
+
* Disconnects from a peer and closes its session.
|
|
545
|
+
*
|
|
546
|
+
* Pass the peer returned by `connect` to close that exact peer connection.
|
|
547
|
+
* Passing a role name closes all active peer connections for that role.
|
|
548
|
+
*/
|
|
549
|
+
async disconnect(peer) {
|
|
550
|
+
if (typeof peer === 'string') {
|
|
551
|
+
await this.transport.disconnect?.(peer)
|
|
552
|
+
await this.#closeSessions(peer)
|
|
553
|
+
return
|
|
554
|
+
}
|
|
555
|
+
const connection = this.#findConnectionByPeer(peer)
|
|
556
|
+
if (!connection) {
|
|
557
|
+
throw new Error('Peer is not connected')
|
|
558
|
+
}
|
|
559
|
+
await this.#closeConnection(connection)
|
|
296
560
|
}
|
|
297
561
|
|
|
562
|
+
/**
|
|
563
|
+
* Binds a transport to this client.
|
|
564
|
+
*
|
|
565
|
+
* Transport events call this client's `handle...` methods. Application code
|
|
566
|
+
* should use `on...` methods to observe those events.
|
|
567
|
+
*/
|
|
298
568
|
bindTransport(transport = this.transport, options = {}) {
|
|
299
569
|
this.#transportUnsubscribe?.()
|
|
300
570
|
this.#transportUnsubscribe = bindTransport(transport, this, options)
|
|
301
571
|
return this.#transportUnsubscribe
|
|
302
572
|
}
|
|
303
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Removes the current transport event bindings.
|
|
576
|
+
*/
|
|
304
577
|
unbindTransport() {
|
|
305
578
|
this.#transportUnsubscribe?.()
|
|
306
579
|
this.#transportUnsubscribe = undefined
|
|
307
580
|
}
|
|
308
581
|
|
|
309
|
-
|
|
582
|
+
/**
|
|
583
|
+
* Framework entrypoint for message decoding or dispatch failures.
|
|
584
|
+
*/
|
|
585
|
+
async handleMessageError(error) {
|
|
310
586
|
for (const handler of this.#messageErrorHandlers) {
|
|
311
587
|
await handler(error)
|
|
312
588
|
}
|
|
313
589
|
}
|
|
314
590
|
|
|
591
|
+
/**
|
|
592
|
+
* Registers an application callback for message decoding or dispatch failures.
|
|
593
|
+
*/
|
|
315
594
|
onMessageError(handler) {
|
|
316
595
|
this.#messageErrorHandlers.add(handler)
|
|
317
596
|
return () => {
|
|
@@ -319,12 +598,22 @@ export class <%= ctx.className %> {
|
|
|
319
598
|
}
|
|
320
599
|
}
|
|
321
600
|
|
|
322
|
-
|
|
601
|
+
/**
|
|
602
|
+
* Framework entrypoint for transport-level socket errors.
|
|
603
|
+
*/
|
|
604
|
+
async handleSocketError(error) {
|
|
605
|
+
const sessionError = toOpenWsError(error)
|
|
606
|
+
for (const connection of this.#connections) {
|
|
607
|
+
await connection.session.error(sessionError)
|
|
608
|
+
}
|
|
323
609
|
for (const handler of this.#socketErrorHandlers) {
|
|
324
610
|
await handler(error)
|
|
325
611
|
}
|
|
326
612
|
}
|
|
327
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Registers an application callback for transport-level socket errors.
|
|
616
|
+
*/
|
|
328
617
|
onSocketError(handler) {
|
|
329
618
|
this.#socketErrorHandlers.add(handler)
|
|
330
619
|
return () => {
|
|
@@ -332,13 +621,115 @@ export class <%= ctx.className %> {
|
|
|
332
621
|
}
|
|
333
622
|
}
|
|
334
623
|
|
|
624
|
+
/**
|
|
625
|
+
* Framework entrypoint for transport-level socket close events.
|
|
626
|
+
*/
|
|
627
|
+
async handleSocketClose(_event) {
|
|
628
|
+
for (const connection of Array.from(this.#connections)) {
|
|
629
|
+
await this.#closeConnection(connection)
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Framework entrypoint for a peer session opening.
|
|
635
|
+
*/
|
|
636
|
+
async handleOpen(roleName) {
|
|
637
|
+
for (const handler of this.#openHandlers) {
|
|
638
|
+
await handler(roleName)
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Registers an application callback for peer session open events.
|
|
644
|
+
*/
|
|
645
|
+
onOpen(handler) {
|
|
646
|
+
this.#openHandlers.add(handler)
|
|
647
|
+
return () => {
|
|
648
|
+
this.#openHandlers.delete(handler)
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Framework entrypoint for a peer session closing.
|
|
654
|
+
*/
|
|
655
|
+
async handleClose(roleName) {
|
|
656
|
+
for (const handler of this.#closeHandlers) {
|
|
657
|
+
await handler(roleName)
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Registers an application callback for peer session close events.
|
|
663
|
+
*/
|
|
664
|
+
onClose(handler) {
|
|
665
|
+
this.#closeHandlers.add(handler)
|
|
666
|
+
return () => {
|
|
667
|
+
this.#closeHandlers.delete(handler)
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Framework entrypoint for a peer session error.
|
|
673
|
+
*/
|
|
674
|
+
async handleError(roleName, error) {
|
|
675
|
+
for (const handler of this.#lifecycleErrorHandlers) {
|
|
676
|
+
await handler(roleName, error)
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Registers an application callback for peer session errors.
|
|
682
|
+
*/
|
|
683
|
+
onError(handler) {
|
|
684
|
+
this.#lifecycleErrorHandlers.add(handler)
|
|
685
|
+
return () => {
|
|
686
|
+
this.#lifecycleErrorHandlers.delete(handler)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async #closeSessions(roleName) {
|
|
691
|
+
for (const connection of this.#findConnections(roleName)) {
|
|
692
|
+
await this.#closeConnection(connection)
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async #closeConnection(connection) {
|
|
697
|
+
if (!this.#connections.delete(connection)) {
|
|
698
|
+
return
|
|
699
|
+
}
|
|
700
|
+
if (this.#findConnections(connection.roleName).length === 0) {
|
|
701
|
+
for (const [messageName, peerRoleName] of Object.entries(this.#peerRoleByMessageName)) {
|
|
702
|
+
if (peerRoleName !== connection.roleName) {
|
|
703
|
+
continue
|
|
704
|
+
}
|
|
705
|
+
delete this.#peersByMessageName[messageName]
|
|
706
|
+
delete this.#peerRoleByMessageName[messageName]
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
await connection.session.close()
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
#findConnections(roleName) {
|
|
713
|
+
return Array.from(this.#connections).filter(connection => connection.roleName === roleName)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
#findConnectionByPeer(peer) {
|
|
717
|
+
return Array.from(this.#connections).find(connection => connection.peer === peer)
|
|
718
|
+
}
|
|
719
|
+
|
|
335
720
|
<% for (const handler of ctx.handlers) { -%>
|
|
336
|
-
|
|
721
|
+
/**
|
|
722
|
+
* Dispatches the <%- JSON.stringify(handler.messageName) %> message to registered application callbacks.
|
|
723
|
+
*/
|
|
724
|
+
async <%= handler.dispatchMethodName %>(payload, peer) {
|
|
337
725
|
for (const handler of this.#<%= handler.listenerFieldName %>) {
|
|
338
|
-
await handler(payload,
|
|
726
|
+
await handler(payload, peer)
|
|
339
727
|
}
|
|
340
728
|
}
|
|
341
729
|
|
|
730
|
+
/**
|
|
731
|
+
* Registers an application callback for the <%- JSON.stringify(handler.messageName) %> message.
|
|
732
|
+
*/
|
|
342
733
|
<%= handler.onMethodName %>(handler) {
|
|
343
734
|
this.#<%= handler.listenerFieldName %>.add(handler)
|
|
344
735
|
return () => {
|
|
@@ -347,26 +738,44 @@ export class <%= ctx.className %> {
|
|
|
347
738
|
}
|
|
348
739
|
|
|
349
740
|
<% } -%>
|
|
741
|
+
/**
|
|
742
|
+
* Framework entrypoint for a raw transport message.
|
|
743
|
+
*/
|
|
350
744
|
async handleRawMessage(data) {
|
|
351
745
|
await this.handleMessage(decodeEnvelope(data))
|
|
352
746
|
}
|
|
353
747
|
|
|
748
|
+
/**
|
|
749
|
+
* Framework entrypoint for a decoded OpenWS envelope.
|
|
750
|
+
*/
|
|
354
751
|
async handleMessage(envelope) {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
752
|
+
const connections = this.#findConnections(envelope.fromRole)
|
|
753
|
+
if (connections.length === 1) {
|
|
754
|
+
await connections[0].session.handleMessage(
|
|
755
|
+
envelope.fromRole,
|
|
756
|
+
envelope.messageName,
|
|
757
|
+
envelope.payload
|
|
758
|
+
)
|
|
359
759
|
return
|
|
360
760
|
}
|
|
761
|
+
if (connections.length > 1) {
|
|
762
|
+
throw new Error(
|
|
763
|
+
`Multiple sessions for remote role ${envelope.fromRole}; dispatch requires connection context`
|
|
764
|
+
)
|
|
765
|
+
}
|
|
361
766
|
|
|
362
767
|
const localHandler = this.#handlersByMessageName[envelope.messageName]
|
|
363
|
-
const
|
|
364
|
-
if (envelope.fromRole === this.#fromRole && localHandler &&
|
|
365
|
-
await localHandler(envelope.payload,
|
|
768
|
+
const localPeer = this.#peersByMessageName[envelope.messageName]
|
|
769
|
+
if (envelope.fromRole === this.#fromRole && localHandler && localPeer) {
|
|
770
|
+
await localHandler(envelope.payload, localPeer)
|
|
366
771
|
return
|
|
367
772
|
}
|
|
368
773
|
|
|
369
774
|
throw new Error(`Remote role ${envelope.fromRole} not found`)
|
|
370
775
|
}
|
|
371
776
|
}
|
|
777
|
+
|
|
778
|
+
function toOpenWsError(error) {
|
|
779
|
+
return error instanceof Error ? error : new Error(String(error))
|
|
780
|
+
}
|
|
372
781
|
<% } -%>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polytric/openws-sdkgen",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "OpenWS SDK generator CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"tsx": "^4.21.0",
|
|
51
51
|
"typescript": "^5.9.3",
|
|
52
52
|
"ws": "^8.18.3",
|
|
53
|
-
"@polytric/openws": "^0.0.
|
|
53
|
+
"@polytric/openws": "^0.0.8"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "tsup",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"test:typescript:node": "node dist/main.cjs --spec ./test/spec.json --out ./generated/typescript/node --project Example --network core --hostRole client --language typescript --environment node",
|
|
61
61
|
"test:typescript:server": "tsx ./test/typescript/server.ts",
|
|
62
62
|
"test:typescript:client": "tsx ./test/typescript/client.ts",
|
|
63
|
+
"test:client-lifecycle": "tsx ./test/typescript/client-lifecycle.ts",
|
|
63
64
|
"test:transport-close": "tsx ./test/typescript/transport-close.ts"
|
|
64
65
|
}
|
|
65
66
|
}
|