@polytric/openws-sdkgen 0.0.12 → 0.0.14
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 +165 -18
- 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 +67 -63
|
@@ -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
|
<% } -%>
|