@nmtjs/gateway 0.15.0-beta.1 → 0.15.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js ADDED
@@ -0,0 +1,3 @@
1
+ export function isAsyncIterable(value) {
2
+ return Boolean(value && typeof value === 'object' && Symbol.asyncIterator in value);
3
+ }
@@ -0,0 +1,29 @@
1
+ import { MAX_UINT32, throwError } from '@nmtjs/common';
2
+ export class ConnectionManager {
3
+ connections = new Map();
4
+ streamIds = new Map();
5
+ add(connection) {
6
+ this.connections.set(connection.id, connection);
7
+ this.streamIds.set(connection.id, 0);
8
+ }
9
+ get(id) {
10
+ return this.connections.get(id) ?? throwError('Connection not found');
11
+ }
12
+ has(id) {
13
+ return this.connections.has(id);
14
+ }
15
+ remove(id) {
16
+ this.connections.delete(id);
17
+ this.streamIds.delete(id);
18
+ }
19
+ getAll() {
20
+ return this.connections.values();
21
+ }
22
+ getStreamId(connectionId) {
23
+ let streamId = this.streamIds.get(connectionId);
24
+ if (streamId >= MAX_UINT32)
25
+ streamId = 0;
26
+ this.streamIds.set(connectionId, streamId + 1);
27
+ return streamId;
28
+ }
29
+ }
package/dist/enums.js ADDED
@@ -0,0 +1,16 @@
1
+ export var GatewayHook;
2
+ (function (GatewayHook) {
3
+ GatewayHook["Connect"] = "Connect";
4
+ GatewayHook["Disconnect"] = "Disconnect";
5
+ })(GatewayHook || (GatewayHook = {}));
6
+ export var ProxyableTransportType;
7
+ (function (ProxyableTransportType) {
8
+ ProxyableTransportType["WebSocket"] = "WebSocket";
9
+ ProxyableTransportType["HTTP"] = "HTTP";
10
+ })(ProxyableTransportType || (ProxyableTransportType = {}));
11
+ export var StreamTimeout;
12
+ (function (StreamTimeout) {
13
+ StreamTimeout["Pull"] = "Pull";
14
+ StreamTimeout["Consume"] = "Consume";
15
+ StreamTimeout["Finish"] = "Finish";
16
+ })(StreamTimeout || (StreamTimeout = {}));
@@ -0,0 +1,456 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { isTypedArray } from 'node:util/types';
3
+ import { anyAbortSignal, isAbortError } from '@nmtjs/common';
4
+ import { createFactoryInjectable, provide, Scope } from '@nmtjs/core';
5
+ import { ClientMessageType, isBlobInterface, kBlobKey, ProtocolBlob, ServerMessageType, } from '@nmtjs/protocol';
6
+ import { getFormat, ProtocolError, versions } from '@nmtjs/protocol/server';
7
+ import { ConnectionManager } from "./connections.js";
8
+ import { StreamTimeout } from "./enums.js";
9
+ import * as injectables from "./injectables.js";
10
+ import { RpcManager } from "./rpcs.js";
11
+ import { BlobStreamsManager } from "./streams.js";
12
+ export class Gateway {
13
+ logger;
14
+ connections;
15
+ rpcs;
16
+ blobStreams;
17
+ options;
18
+ constructor(options) {
19
+ this.options = {
20
+ rpcStreamConsumeTimeout: 5000,
21
+ streamTimeouts: {
22
+ //@ts-expect-error
23
+ [StreamTimeout.Pull]: options.streamTimeouts?.[StreamTimeout.Pull] ?? 5000,
24
+ //@ts-expect-error
25
+ [StreamTimeout.Consume]: options.streamTimeouts?.[StreamTimeout.Consume] ?? 5000,
26
+ //@ts-expect-error
27
+ [StreamTimeout.Finish]: options.streamTimeouts?.[StreamTimeout.Finish] ?? 10000,
28
+ },
29
+ ...options,
30
+ identity: options.identity ??
31
+ createFactoryInjectable({
32
+ dependencies: { connectionId: injectables.connectionId },
33
+ factory: ({ connectionId }) => connectionId,
34
+ }),
35
+ };
36
+ this.logger = options.logger.child({}, gatewayLoggerOptions);
37
+ this.connections = new ConnectionManager();
38
+ this.rpcs = new RpcManager();
39
+ this.blobStreams = new BlobStreamsManager({
40
+ timeouts: this.options.streamTimeouts,
41
+ });
42
+ }
43
+ async start() {
44
+ const hosts = [];
45
+ for (const key in this.options.transports) {
46
+ const { transport, proxyable } = this.options.transports[key];
47
+ const url = await transport.start({
48
+ formats: this.options.formats,
49
+ onConnect: this.onConnect(key),
50
+ onDisconnect: this.onDisconnect(key),
51
+ onMessage: this.onMessage(key),
52
+ onRpc: this.onRpc(key),
53
+ });
54
+ this.logger.info(`Transport [${key}] started on [${url}]`);
55
+ if (proxyable)
56
+ hosts.push({ url, type: proxyable });
57
+ }
58
+ return hosts;
59
+ }
60
+ async stop() {
61
+ // Close all connections
62
+ for (const connection of this.connections.getAll()) {
63
+ await this.closeConnection(connection.id);
64
+ }
65
+ for (const key in this.options.transports) {
66
+ const { transport } = this.options.transports[key];
67
+ await transport.stop({ formats: this.options.formats });
68
+ this.logger.debug(`Transport [${key}] stopped`);
69
+ }
70
+ }
71
+ send(transport, connectionId, data) {
72
+ if (transport in this.options.transports) {
73
+ const transportInstance = this.options.transports[transport].transport;
74
+ if (transportInstance.send) {
75
+ return transportInstance.send(connectionId, data);
76
+ }
77
+ }
78
+ }
79
+ async reload() {
80
+ for (const connections of this.connections.connections.values()) {
81
+ await connections.container.dispose();
82
+ }
83
+ }
84
+ createRpcContext(connection, messageContext, logger, gatewayRpc, signal) {
85
+ const { callId, payload, procedure, metadata } = gatewayRpc;
86
+ const controller = new AbortController();
87
+ this.rpcs.set(connection.id, callId, controller);
88
+ signal = signal
89
+ ? anyAbortSignal(signal, controller.signal)
90
+ : controller.signal;
91
+ const container = connection.container.fork(Scope.Call);
92
+ const dispose = async () => {
93
+ const streamAbortReason = 'Stream is not consumed by a user';
94
+ // Abort streams related to this call
95
+ this.blobStreams.abortClientCallStreams(connection.id, callId, streamAbortReason);
96
+ this.rpcs.delete(connection.id, callId);
97
+ this.rpcs.releasePull(connection.id, callId);
98
+ await container.dispose();
99
+ };
100
+ return {
101
+ ...messageContext,
102
+ callId,
103
+ payload,
104
+ procedure,
105
+ metadata,
106
+ container,
107
+ signal,
108
+ logger: logger.child({ callId, procedure }),
109
+ [Symbol.asyncDispose]: dispose,
110
+ };
111
+ }
112
+ createMessageContext(connection, transportKey) {
113
+ const transport = this.options.transports[transportKey].transport;
114
+ const { id: connectionId, protocol, decoder, encoder } = connection;
115
+ const streamId = this.connections.getStreamId.bind(this.connections, connectionId);
116
+ return {
117
+ connectionId,
118
+ protocol,
119
+ encoder,
120
+ decoder,
121
+ transport,
122
+ streamId,
123
+ addClientStream: ({ streamId, callId, metadata }) => {
124
+ const stream = this.blobStreams.createClientStream(connectionId, callId, streamId, metadata, {
125
+ read: (size) => {
126
+ transport.send(connectionId, protocol.encodeMessage(this.createMessageContext(connection, transportKey), ServerMessageType.ClientStreamPull, { streamId, size: size || 65535 }));
127
+ },
128
+ });
129
+ stream.once('error', () => {
130
+ this.send(transportKey, connectionId, protocol.encodeMessage(this.createMessageContext(connection, transportKey), ServerMessageType.ClientStreamAbort, { streamId }));
131
+ });
132
+ const consume = () => {
133
+ this.blobStreams.consumeClientStream(connectionId, callId, streamId);
134
+ return stream;
135
+ };
136
+ const consumer = Object.defineProperties(consume, {
137
+ [kBlobKey]: {
138
+ enumerable: false,
139
+ configurable: false,
140
+ writable: false,
141
+ value: true,
142
+ },
143
+ metadata: {
144
+ value: metadata,
145
+ enumerable: true,
146
+ configurable: false,
147
+ writable: false,
148
+ },
149
+ });
150
+ return consumer;
151
+ },
152
+ };
153
+ }
154
+ onConnect(transport) {
155
+ const logger = this.logger.child({ transport });
156
+ return async (options, ...injections) => {
157
+ logger.debug('Initiating new connection');
158
+ const protocol = versions[options.protocolVersion];
159
+ if (!protocol)
160
+ throw new Error('Unsupported protocol version');
161
+ const id = randomUUID();
162
+ const container = this.options.container.fork(Scope.Connection);
163
+ try {
164
+ await container.provide([
165
+ provide(injectables.connectionData, options.data),
166
+ provide(injectables.connectionId, id),
167
+ ]);
168
+ await container.provide(injections);
169
+ const identity = await container.resolve(this.options.identity);
170
+ const { accept, contentType, type } = options;
171
+ const { decoder, encoder } = getFormat(this.options.formats, {
172
+ accept,
173
+ contentType,
174
+ });
175
+ const connection = {
176
+ id,
177
+ type,
178
+ identity,
179
+ transport,
180
+ protocol,
181
+ container,
182
+ encoder,
183
+ decoder,
184
+ abortController: new AbortController(),
185
+ };
186
+ this.connections.add(connection);
187
+ await container.provide(injectables.connectionAbortSignal, connection.abortController.signal);
188
+ logger.debug({
189
+ id,
190
+ protocol: options.protocolVersion,
191
+ type,
192
+ accept,
193
+ contentType,
194
+ identity,
195
+ transportData: options.data,
196
+ }, 'Connection established');
197
+ return Object.assign(connection, {
198
+ [Symbol.asyncDispose]: async () => {
199
+ await this.onDisconnect(transport)(connection.id);
200
+ },
201
+ });
202
+ }
203
+ catch (error) {
204
+ logger.debug({ error }, 'Error establishing connection');
205
+ container.dispose();
206
+ throw error;
207
+ }
208
+ };
209
+ }
210
+ onDisconnect(transport) {
211
+ const logger = this.logger.child({ transport });
212
+ return async (connectionId) => {
213
+ logger.debug({ connectionId }, 'Disconnecting connection');
214
+ await this.closeConnection(connectionId);
215
+ };
216
+ }
217
+ onMessage(transport) {
218
+ const _logger = this.logger.child({ transport });
219
+ return async ({ connectionId, data }, ...injections) => {
220
+ const logger = _logger.child({ connectionId });
221
+ try {
222
+ const connection = this.connections.get(connectionId);
223
+ const messageContext = this.createMessageContext(connection, transport);
224
+ const message = connection.protocol.decodeMessage(messageContext, Buffer.from(data));
225
+ logger.trace(message, 'Received message');
226
+ switch (message.type) {
227
+ case ClientMessageType.Rpc: {
228
+ const rpcContext = this.createRpcContext(connection, messageContext, logger, message.rpc);
229
+ try {
230
+ await rpcContext.container.provide([
231
+ ...injections,
232
+ provide(injectables.createBlob, this.createBlobFunction(rpcContext)),
233
+ ]);
234
+ await this.handleRpcMessage(connection, rpcContext);
235
+ }
236
+ finally {
237
+ await rpcContext[Symbol.asyncDispose]();
238
+ }
239
+ break;
240
+ }
241
+ case ClientMessageType.RpcPull: {
242
+ this.rpcs.releasePull(connectionId, message.callId);
243
+ break;
244
+ }
245
+ case ClientMessageType.RpcAbort: {
246
+ this.rpcs.abort(connectionId, message.callId);
247
+ break;
248
+ }
249
+ case ClientMessageType.ClientStreamAbort: {
250
+ this.blobStreams.abortClientStream(connectionId, message.streamId, message.reason);
251
+ break;
252
+ }
253
+ case ClientMessageType.ClientStreamPush: {
254
+ this.blobStreams.pushToClientStream(connectionId, message.streamId, message.chunk);
255
+ break;
256
+ }
257
+ case ClientMessageType.ClientStreamEnd: {
258
+ this.blobStreams.endClientStream(connectionId, message.streamId);
259
+ break;
260
+ }
261
+ case ClientMessageType.ServerStreamAbort: {
262
+ this.blobStreams.abortServerStream(connectionId, message.streamId, message.reason);
263
+ break;
264
+ }
265
+ case ClientMessageType.ServerStreamPull: {
266
+ this.blobStreams.pullServerStream(connectionId, message.streamId);
267
+ break;
268
+ }
269
+ default:
270
+ throw new Error('Unknown message type');
271
+ }
272
+ }
273
+ catch (error) {
274
+ logger.trace({ error }, 'Error handling message');
275
+ throw error;
276
+ }
277
+ };
278
+ }
279
+ onRpc(transport) {
280
+ const _logger = this.logger.child({ transport });
281
+ return async (connection, rpc, signal, ...injections) => {
282
+ const logger = _logger.child({ connectionId: connection.id });
283
+ const messageContext = this.createMessageContext(connection, connection.transport);
284
+ const rpcContext = this.createRpcContext(connection, messageContext, logger, rpc, signal);
285
+ try {
286
+ await rpcContext.container.provide([
287
+ ...injections,
288
+ provide(injectables.rpcAbortSignal, signal),
289
+ provide(injectables.createBlob, this.createBlobFunction(rpcContext)),
290
+ ]);
291
+ const result = await this.options.api.call({
292
+ connection,
293
+ payload: rpc.payload,
294
+ procedure: rpc.procedure,
295
+ metadata: rpc.metadata,
296
+ container: rpcContext.container,
297
+ signal: rpcContext.signal,
298
+ });
299
+ if (typeof result === 'function') {
300
+ return result(async () => {
301
+ await rpcContext[Symbol.asyncDispose]();
302
+ });
303
+ }
304
+ else {
305
+ await rpcContext[Symbol.asyncDispose]();
306
+ return result;
307
+ }
308
+ }
309
+ catch (error) {
310
+ await rpcContext[Symbol.asyncDispose]();
311
+ throw error;
312
+ }
313
+ };
314
+ }
315
+ async handleRpcMessage(connection, context) {
316
+ const { container, connectionId, transport, protocol, signal, callId, procedure, payload, encoder, } = context;
317
+ try {
318
+ await container.provide(injectables.rpcAbortSignal, signal);
319
+ const response = await this.options.api.call({
320
+ connection: connection,
321
+ container,
322
+ payload,
323
+ procedure,
324
+ signal,
325
+ });
326
+ if (typeof response === 'function') {
327
+ transport.send(connectionId, protocol.encodeMessage(context, ServerMessageType.RpcStreamResponse, {
328
+ callId,
329
+ }));
330
+ try {
331
+ const consumeTimeoutSignal = this.options.rpcStreamConsumeTimeout
332
+ ? AbortSignal.timeout(this.options.rpcStreamConsumeTimeout)
333
+ : undefined;
334
+ const streamSignal = consumeTimeoutSignal
335
+ ? anyAbortSignal(signal, consumeTimeoutSignal)
336
+ : signal;
337
+ await this.rpcs.awaitPull(connectionId, callId, streamSignal);
338
+ for await (const chunk of response()) {
339
+ signal.throwIfAborted();
340
+ const chunkEncoded = encoder.encode(chunk);
341
+ transport.send(connectionId, protocol.encodeMessage(context, ServerMessageType.RpcStreamChunk, { callId, chunk: chunkEncoded }));
342
+ await this.rpcs.awaitPull(connectionId, callId);
343
+ }
344
+ transport.send(connectionId, protocol.encodeMessage(context, ServerMessageType.RpcStreamEnd, {
345
+ callId,
346
+ }));
347
+ }
348
+ catch (error) {
349
+ if (!isAbortError(error)) {
350
+ this.logger.error(error);
351
+ }
352
+ transport.send(connectionId, protocol.encodeMessage(context, ServerMessageType.RpcStreamAbort, {
353
+ callId,
354
+ }));
355
+ }
356
+ }
357
+ else {
358
+ const streams = this.blobStreams.getServerStreamsMetadata(connectionId, callId);
359
+ transport.send(connectionId, protocol.encodeMessage(context, ServerMessageType.RpcResponse, {
360
+ callId,
361
+ result: response,
362
+ streams,
363
+ error: null,
364
+ }));
365
+ }
366
+ }
367
+ catch (error) {
368
+ transport.send(connectionId, protocol.encodeMessage(context, ServerMessageType.RpcResponse, {
369
+ callId,
370
+ result: null,
371
+ streams: {},
372
+ error,
373
+ }));
374
+ const level = error instanceof ProtocolError ? 'trace' : 'error';
375
+ this.logger[level](error);
376
+ }
377
+ }
378
+ async closeConnection(connectionId) {
379
+ if (this.connections.has(connectionId)) {
380
+ const connection = this.connections.get(connectionId);
381
+ connection.abortController.abort();
382
+ connection.container.dispose();
383
+ }
384
+ this.rpcs.close(connectionId);
385
+ this.blobStreams.cleanupConnection(connectionId);
386
+ this.connections.remove(connectionId);
387
+ }
388
+ createBlobFunction(context) {
389
+ const { streamId: getStreamId, transport, protocol, connectionId, callId, encoder, } = context;
390
+ return (source, metadata) => {
391
+ const streamId = getStreamId();
392
+ const blob = ProtocolBlob.from(source, metadata, () => {
393
+ return encoder.encodeBlob(streamId);
394
+ });
395
+ const stream = this.blobStreams.createServerStream(connectionId, callId, streamId, blob);
396
+ stream.on('data', (chunk) => {
397
+ transport.send(connectionId, protocol.encodeMessage(context, ServerMessageType.ServerStreamPush, {
398
+ streamId: streamId,
399
+ chunk: Buffer.from(chunk),
400
+ }));
401
+ });
402
+ stream.on('error', (error) => {
403
+ transport.send(connectionId, protocol.encodeMessage(context, ServerMessageType.ServerStreamAbort, {
404
+ streamId: streamId,
405
+ reason: error.message,
406
+ }));
407
+ });
408
+ stream.once('finish', () => {
409
+ transport.send(connectionId, protocol.encodeMessage(context, ServerMessageType.ServerStreamEnd, {
410
+ streamId: streamId,
411
+ }));
412
+ });
413
+ stream.once('close', () => {
414
+ this.blobStreams.removeServerStream(connectionId, streamId);
415
+ });
416
+ return blob;
417
+ };
418
+ }
419
+ }
420
+ const gatewayLoggerOptions = {
421
+ serializers: {
422
+ chunk: (chunk) => isTypedArray(chunk) ? `<Buffer length=${chunk.byteLength}>` : chunk,
423
+ payload: (payload) => {
424
+ function traverseObject(obj) {
425
+ if (Array.isArray(obj)) {
426
+ return obj.map(traverseObject);
427
+ }
428
+ else if (isTypedArray(obj)) {
429
+ return `<${obj.constructor.name} length=${obj.byteLength}>`;
430
+ }
431
+ else if (typeof obj === 'object' && obj !== null) {
432
+ const result = {};
433
+ for (const [key, value] of Object.entries(obj)) {
434
+ result[key] = traverseObject(value);
435
+ }
436
+ return result;
437
+ }
438
+ else if (isBlobInterface(obj)) {
439
+ return `<ClientBlobStream metadata=${JSON.stringify(obj.metadata)}>`;
440
+ }
441
+ return obj;
442
+ }
443
+ return traverseObject(payload);
444
+ },
445
+ headers: (value) => {
446
+ if (value instanceof Headers) {
447
+ const obj = {};
448
+ value.forEach((v, k) => {
449
+ obj[k] = v;
450
+ });
451
+ return obj;
452
+ }
453
+ return value;
454
+ },
455
+ },
456
+ };
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export * from "./api.js";
2
+ export * from "./connections.js";
3
+ export * from "./enums.js";
4
+ export * from "./gateway.js";
5
+ export * from "./injectables.js";
6
+ export * from "./rpcs.js";
7
+ export * from "./streams.js";
8
+ export * from "./transport.js";
9
+ export * from "./types.js";
@@ -0,0 +1,27 @@
1
+ import { anyAbortSignal } from '@nmtjs/common';
2
+ import { createFactoryInjectable, createLazyInjectable, createOptionalInjectable, Scope, } from '@nmtjs/core';
3
+ export const connection = createLazyInjectable(Scope.Connection, 'Gateway connection');
4
+ export const connectionId = createLazyInjectable(Scope.Connection, 'Gateway connection id');
5
+ export const connectionData = createLazyInjectable(Scope.Connection, "Gateway connection's data");
6
+ export const connectionAbortSignal = createLazyInjectable(Scope.Connection, 'Connection abort signal');
7
+ export const rpcClientAbortSignal = createLazyInjectable(Scope.Call, 'RPC client abort signal');
8
+ export const rpcStreamAbortSignal = createLazyInjectable(Scope.Call, 'RPC stream abort signal');
9
+ export const rpcAbortSignal = createFactoryInjectable({
10
+ dependencies: {
11
+ rpcClientAbortSignal,
12
+ connectionAbortSignal,
13
+ rpcStreamAbortSignal: createOptionalInjectable(rpcStreamAbortSignal),
14
+ },
15
+ factory: (ctx) => anyAbortSignal(ctx.rpcClientAbortSignal, ctx.connectionAbortSignal, ctx.rpcStreamAbortSignal),
16
+ }, 'Any RPC abort signal');
17
+ export const createBlob = createLazyInjectable(Scope.Call, 'Create RPC blob');
18
+ export const GatewayInjectables = {
19
+ connection,
20
+ connectionId,
21
+ connectionData,
22
+ connectionAbortSignal,
23
+ rpcClientAbortSignal,
24
+ rpcStreamAbortSignal,
25
+ rpcAbortSignal,
26
+ createBlob,
27
+ };
package/dist/rpcs.js ADDED
@@ -0,0 +1,78 @@
1
+ import { createFuture } from '@nmtjs/common';
2
+ export class RpcManager {
3
+ // connectionId:callId -> AbortController
4
+ rpcs = new Map();
5
+ // connectionId:callId -> Future<void>
6
+ streams = new Map();
7
+ set(connectionId, callId, controller) {
8
+ const key = this.getKey(connectionId, callId);
9
+ this.rpcs.set(key, controller);
10
+ }
11
+ get(connectionId, callId) {
12
+ const key = this.getKey(connectionId, callId);
13
+ return this.rpcs.get(key);
14
+ }
15
+ delete(connectionId, callId) {
16
+ const key = this.getKey(connectionId, callId);
17
+ this.rpcs.delete(key);
18
+ }
19
+ abort(connectionId, callId) {
20
+ const key = this.getKey(connectionId, callId);
21
+ const controller = this.rpcs.get(key);
22
+ if (controller) {
23
+ controller.abort();
24
+ this.rpcs.delete(key);
25
+ this.releasePull(connectionId, callId);
26
+ }
27
+ }
28
+ awaitPull(connectionId, callId, signal) {
29
+ const key = this.getKey(connectionId, callId);
30
+ const rpc = this.rpcs.get(key);
31
+ if (!rpc)
32
+ throw new Error(`RPC not found`);
33
+ const future = this.streams.get(key);
34
+ if (future) {
35
+ return future.promise;
36
+ }
37
+ else {
38
+ const newFuture = createFuture();
39
+ if (signal)
40
+ signal.addEventListener('abort', () => newFuture.resolve(), {
41
+ once: true,
42
+ });
43
+ this.streams.set(key, newFuture);
44
+ return newFuture.promise;
45
+ }
46
+ }
47
+ releasePull(connectionId, callId) {
48
+ const key = this.getKey(connectionId, callId);
49
+ const future = this.streams.get(key);
50
+ if (future) {
51
+ future.resolve();
52
+ this.streams.delete(key);
53
+ }
54
+ }
55
+ close(connectionId) {
56
+ // Iterate all RPCs and abort those belonging to this connection
57
+ // Optimization: Maintain a Set<callId> per connectionId
58
+ for (const [key, controller] of this.rpcs) {
59
+ if (key.startsWith(`${connectionId}:`)) {
60
+ controller.abort();
61
+ this.rpcs.delete(key);
62
+ }
63
+ }
64
+ // Also release any pending pulls for this connection
65
+ for (const key of this.streams.keys()) {
66
+ if (key.startsWith(`${connectionId}:`)) {
67
+ const future = this.streams.get(key);
68
+ if (future) {
69
+ future.resolve();
70
+ this.streams.delete(key);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ getKey(connectionId, callId) {
76
+ return `${connectionId}:${callId}`;
77
+ }
78
+ }
@@ -0,0 +1,277 @@
1
+ import { noopFn } from '@nmtjs/common';
2
+ import { ProtocolClientStream, ProtocolServerStream, } from '@nmtjs/protocol/server';
3
+ import { StreamTimeout } from "./enums.js";
4
+ /**
5
+ * @todo Clarify Pull/Consume timeout semantics - currently ambiguous whether
6
+ * Pull timeout means "client not pulling" or "server not producing" for server streams
7
+ */
8
+ export class BlobStreamsManager {
9
+ clientStreams = new Map();
10
+ serverStreams = new Map();
11
+ // Index for quick lookup by callId (connectionId:callId -> Set<streamId>)
12
+ connectionClientStreams = new Map();
13
+ connectionServerStreams = new Map();
14
+ clientCallStreams = new Map();
15
+ serverCallStreams = new Map();
16
+ timeoutDurations;
17
+ constructor(config) {
18
+ this.timeoutDurations = {
19
+ [StreamTimeout.Pull]: config.timeouts[StreamTimeout.Pull],
20
+ [StreamTimeout.Consume]: config.timeouts[StreamTimeout.Consume],
21
+ [StreamTimeout.Finish]: config.timeouts[StreamTimeout.Finish],
22
+ };
23
+ }
24
+ // --- Client Streams (Upload) ---
25
+ createClientStream(connectionId, callId, streamId, metadata, options) {
26
+ const stream = new ProtocolClientStream(streamId, metadata, options);
27
+ stream.on('error', noopFn);
28
+ const key = this.getKey(connectionId, streamId);
29
+ const state = {
30
+ connectionId,
31
+ callId,
32
+ stream,
33
+ timeouts: {
34
+ [StreamTimeout.Pull]: undefined,
35
+ [StreamTimeout.Consume]: undefined,
36
+ [StreamTimeout.Finish]: undefined,
37
+ },
38
+ };
39
+ this.clientStreams.set(key, state);
40
+ this.trackClientCall(connectionId, callId, streamId);
41
+ this.trackConnectionClientStream(connectionId, streamId);
42
+ this.startTimeout(state, StreamTimeout.Consume);
43
+ return stream;
44
+ }
45
+ pushToClientStream(connectionId, streamId, chunk) {
46
+ const key = this.getKey(connectionId, streamId);
47
+ const state = this.clientStreams.get(key);
48
+ if (state) {
49
+ state.stream.write(chunk);
50
+ this.clearTimeout(state, StreamTimeout.Consume);
51
+ this.startTimeout(state, StreamTimeout.Pull);
52
+ }
53
+ }
54
+ endClientStream(connectionId, streamId) {
55
+ const key = this.getKey(connectionId, streamId);
56
+ const state = this.clientStreams.get(key);
57
+ if (state) {
58
+ state.stream.end(null);
59
+ this.removeClientStream(connectionId, streamId);
60
+ }
61
+ }
62
+ abortClientStream(connectionId, streamId, error = 'Aborted') {
63
+ const key = this.getKey(connectionId, streamId);
64
+ const state = this.clientStreams.get(key);
65
+ if (state) {
66
+ state.stream.destroy(new Error(error));
67
+ this.removeClientStream(connectionId, streamId);
68
+ }
69
+ }
70
+ consumeClientStream(connectionId, callId, streamId) {
71
+ this.untrackClientCall(connectionId, callId, streamId);
72
+ }
73
+ removeClientStream(connectionId, streamId) {
74
+ const key = this.getKey(connectionId, streamId);
75
+ const state = this.clientStreams.get(key);
76
+ if (state) {
77
+ this.clientStreams.delete(key);
78
+ this.clearTimeout(state, StreamTimeout.Finish);
79
+ this.clearTimeout(state, StreamTimeout.Pull);
80
+ this.clearTimeout(state, StreamTimeout.Consume);
81
+ this.untrackClientCall(connectionId, state.callId, streamId);
82
+ this.untrackConnectionClientStream(connectionId, streamId);
83
+ }
84
+ }
85
+ // --- Server Streams (Download) ---
86
+ getServerStreamsMetadata(connectionId, callId) {
87
+ const key = this.getCallKey(connectionId, callId);
88
+ const streamIds = this.serverCallStreams.get(key);
89
+ const streams = {};
90
+ if (streamIds) {
91
+ for (const streamId of streamIds) {
92
+ const streamKey = this.getKey(connectionId, streamId);
93
+ const state = this.serverStreams.get(streamKey);
94
+ if (state) {
95
+ streams[streamId] = state.stream.metadata;
96
+ }
97
+ }
98
+ }
99
+ return streams;
100
+ }
101
+ createServerStream(connectionId, callId, streamId, blob) {
102
+ const stream = new ProtocolServerStream(streamId, blob);
103
+ const key = this.getKey(connectionId, streamId);
104
+ const state = {
105
+ connectionId,
106
+ callId,
107
+ stream,
108
+ timeouts: {
109
+ [StreamTimeout.Pull]: undefined,
110
+ [StreamTimeout.Consume]: undefined,
111
+ [StreamTimeout.Finish]: undefined,
112
+ },
113
+ };
114
+ // Prevent unhandled 'error' events, in case the user did not subscribe to them
115
+ stream.on('error', noopFn);
116
+ this.serverStreams.set(key, state);
117
+ this.trackServerCall(connectionId, callId, streamId);
118
+ this.trackConnectionServerStream(connectionId, streamId);
119
+ this.startTimeout(state, StreamTimeout.Finish);
120
+ this.startTimeout(state, StreamTimeout.Consume);
121
+ return stream;
122
+ }
123
+ pullServerStream(connectionId, streamId) {
124
+ const key = this.getKey(connectionId, streamId);
125
+ const state = this.serverStreams.get(key);
126
+ if (state) {
127
+ state.stream.resume();
128
+ this.clearTimeout(state, StreamTimeout.Consume);
129
+ this.startTimeout(state, StreamTimeout.Pull);
130
+ }
131
+ }
132
+ abortServerStream(connectionId, streamId, error = 'Aborted') {
133
+ const key = this.getKey(connectionId, streamId);
134
+ const state = this.serverStreams.get(key);
135
+ if (state) {
136
+ state.stream.destroy(new Error(error));
137
+ this.removeServerStream(connectionId, streamId);
138
+ }
139
+ }
140
+ removeServerStream(connectionId, streamId) {
141
+ const key = this.getKey(connectionId, streamId);
142
+ const state = this.serverStreams.get(key);
143
+ if (state) {
144
+ this.serverStreams.delete(key);
145
+ this.clearTimeout(state, StreamTimeout.Pull);
146
+ this.clearTimeout(state, StreamTimeout.Consume);
147
+ this.clearTimeout(state, StreamTimeout.Finish);
148
+ this.untrackServerCall(connectionId, state.callId, streamId);
149
+ this.untrackConnectionServerStream(connectionId, streamId);
150
+ }
151
+ }
152
+ // --- Timeouts ---
153
+ startTimeout(state, type) {
154
+ this.clearTimeout(state, type);
155
+ const duration = this.timeoutDurations[type];
156
+ const timeout = setTimeout(() => {
157
+ if (state.stream instanceof ProtocolClientStream) {
158
+ this.abortClientStream(state.connectionId, state.stream.id, `${type} timeout`);
159
+ }
160
+ else {
161
+ this.abortServerStream(state.connectionId, state.stream.id, `${type} timeout`);
162
+ }
163
+ state.timeouts[type] = undefined;
164
+ }, duration);
165
+ state.timeouts[type] = timeout;
166
+ }
167
+ clearTimeout(state, type) {
168
+ const timeout = state.timeouts[type];
169
+ if (timeout) {
170
+ clearTimeout(timeout);
171
+ state.timeouts[type] = undefined;
172
+ }
173
+ }
174
+ // --- Helpers ---
175
+ getKey(connectionId, streamId) {
176
+ return `${connectionId}:${streamId}`;
177
+ }
178
+ getCallKey(connectionId, callId) {
179
+ return `${connectionId}:${callId}`;
180
+ }
181
+ trackClientCall(connectionId, callId, streamId) {
182
+ const key = this.getCallKey(connectionId, callId);
183
+ let set = this.clientCallStreams.get(key);
184
+ if (!set) {
185
+ set = new Set();
186
+ this.clientCallStreams.set(key, set);
187
+ }
188
+ set.add(streamId);
189
+ }
190
+ untrackClientCall(connectionId, callId, streamId) {
191
+ const key = this.getCallKey(connectionId, callId);
192
+ const set = this.clientCallStreams.get(key);
193
+ if (set) {
194
+ set.delete(streamId);
195
+ if (set.size === 0) {
196
+ this.clientCallStreams.delete(key);
197
+ }
198
+ }
199
+ }
200
+ trackServerCall(connectionId, callId, streamId) {
201
+ const key = this.getCallKey(connectionId, callId);
202
+ let set = this.serverCallStreams.get(key);
203
+ if (!set) {
204
+ set = new Set();
205
+ this.serverCallStreams.set(key, set);
206
+ }
207
+ set.add(streamId);
208
+ }
209
+ untrackServerCall(connectionId, callId, streamId) {
210
+ const key = this.getCallKey(connectionId, callId);
211
+ const set = this.serverCallStreams.get(key);
212
+ if (set) {
213
+ set.delete(streamId);
214
+ if (set.size === 0) {
215
+ this.serverCallStreams.delete(key);
216
+ }
217
+ }
218
+ }
219
+ trackConnectionClientStream(connectionId, streamId) {
220
+ let set = this.connectionClientStreams.get(connectionId);
221
+ if (!set) {
222
+ set = new Set();
223
+ this.connectionClientStreams.set(connectionId, set);
224
+ }
225
+ set.add(streamId);
226
+ }
227
+ untrackConnectionClientStream(connectionId, streamId) {
228
+ const set = this.connectionClientStreams.get(connectionId);
229
+ if (set) {
230
+ set.delete(streamId);
231
+ if (set.size === 0) {
232
+ this.connectionClientStreams.delete(connectionId);
233
+ }
234
+ }
235
+ }
236
+ trackConnectionServerStream(connectionId, streamId) {
237
+ let set = this.connectionServerStreams.get(connectionId);
238
+ if (!set) {
239
+ set = new Set();
240
+ this.connectionServerStreams.set(connectionId, set);
241
+ }
242
+ set.add(streamId);
243
+ }
244
+ untrackConnectionServerStream(connectionId, streamId) {
245
+ const set = this.connectionServerStreams.get(connectionId);
246
+ if (set) {
247
+ set.delete(streamId);
248
+ if (set.size === 0) {
249
+ this.connectionServerStreams.delete(connectionId);
250
+ }
251
+ }
252
+ }
253
+ // --- Cleanup ---
254
+ abortClientCallStreams(connectionId, callId, reason = 'Call aborted') {
255
+ const key = this.getCallKey(connectionId, callId);
256
+ const clientStreamIds = this.clientCallStreams.get(key);
257
+ if (clientStreamIds) {
258
+ for (const streamId of [...clientStreamIds]) {
259
+ this.abortClientStream(connectionId, streamId, reason);
260
+ }
261
+ }
262
+ }
263
+ cleanupConnection(connectionId) {
264
+ const clientStreamIds = this.connectionClientStreams.get(connectionId);
265
+ if (clientStreamIds) {
266
+ for (const streamId of [...clientStreamIds]) {
267
+ this.abortClientStream(connectionId, streamId, 'Connection closed');
268
+ }
269
+ }
270
+ const serverStreamIds = this.connectionServerStreams.get(connectionId);
271
+ if (serverStreamIds) {
272
+ for (const streamId of [...serverStreamIds]) {
273
+ this.abortServerStream(connectionId, streamId, 'Connection closed');
274
+ }
275
+ }
276
+ }
277
+ }
@@ -0,0 +1,3 @@
1
+ export function createTransport(config) {
2
+ return config;
3
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -6,26 +6,26 @@
6
6
  },
7
7
  "dependencies": {
8
8
  "hookable": "6.0.0-rc.1",
9
- "@nmtjs/common": "0.15.0-beta.1",
10
- "@nmtjs/core": "0.15.0-beta.1",
11
- "@nmtjs/type": "0.15.0-beta.1",
12
- "@nmtjs/protocol": "0.15.0-beta.1"
9
+ "@nmtjs/core": "0.15.0-beta.2",
10
+ "@nmtjs/type": "0.15.0-beta.2",
11
+ "@nmtjs/common": "0.15.0-beta.2",
12
+ "@nmtjs/protocol": "0.15.0-beta.2"
13
13
  },
14
14
  "peerDependencies": {
15
- "@nmtjs/common": "0.15.0-beta.1",
16
- "@nmtjs/core": "0.15.0-beta.1",
17
- "@nmtjs/type": "0.15.0-beta.1",
18
- "@nmtjs/protocol": "0.15.0-beta.1"
15
+ "@nmtjs/common": "0.15.0-beta.2",
16
+ "@nmtjs/type": "0.15.0-beta.2",
17
+ "@nmtjs/protocol": "0.15.0-beta.2",
18
+ "@nmtjs/core": "0.15.0-beta.2"
19
19
  },
20
20
  "devDependencies": {
21
- "@nmtjs/_tests": "0.15.0-beta.1"
21
+ "@nmtjs/_tests": "0.15.0-beta.2"
22
22
  },
23
23
  "files": [
24
24
  "dist",
25
25
  "LICENSE.md",
26
26
  "README.md"
27
27
  ],
28
- "version": "0.15.0-beta.1",
28
+ "version": "0.15.0-beta.2",
29
29
  "scripts": {
30
30
  "clean-build": "rm -rf ./dist",
31
31
  "build": "tsc",