@aegis-fluxion/core 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -2
- package/dist/index.cjs +733 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -1
- package/dist/index.d.ts +52 -1
- package/dist/index.js +733 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var crypto = require('crypto');
|
|
4
|
+
var stream = require('stream');
|
|
4
5
|
var WebSocket = require('ws');
|
|
5
6
|
|
|
6
7
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -16,6 +17,7 @@ var INTERNAL_HANDSHAKE_EVENT = "__handshake";
|
|
|
16
17
|
var INTERNAL_SESSION_TICKET_EVENT = "__session:ticket";
|
|
17
18
|
var INTERNAL_RPC_REQUEST_EVENT = "__rpc:req";
|
|
18
19
|
var INTERNAL_RPC_RESPONSE_EVENT = "__rpc:res";
|
|
20
|
+
var INTERNAL_STREAM_FRAME_EVENT = "__stream:frame";
|
|
19
21
|
var READY_EVENT = "ready";
|
|
20
22
|
var HANDSHAKE_CURVE = "prime256v1";
|
|
21
23
|
var HANDSHAKE_PROTOCOL_VERSION = 1;
|
|
@@ -33,6 +35,9 @@ var DEFAULT_HEARTBEAT_TIMEOUT_MS = 15e3;
|
|
|
33
35
|
var DEFAULT_SESSION_RESUMPTION_ENABLED = true;
|
|
34
36
|
var DEFAULT_SESSION_TICKET_TTL_MS = 10 * 6e4;
|
|
35
37
|
var DEFAULT_SESSION_TICKET_MAX_CACHE_SIZE = 1e4;
|
|
38
|
+
var STREAM_FRAME_VERSION = 1;
|
|
39
|
+
var DEFAULT_STREAM_CHUNK_SIZE_BYTES = 64 * 1024;
|
|
40
|
+
var MAX_STREAM_CHUNK_SIZE_BYTES = 1024 * 1024;
|
|
36
41
|
var RESUMPTION_NONCE_LENGTH = 16;
|
|
37
42
|
var DEFAULT_RECONNECT_INITIAL_DELAY_MS = 250;
|
|
38
43
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 1e4;
|
|
@@ -255,7 +260,7 @@ function decodeCloseReason(reason) {
|
|
|
255
260
|
return reason.toString("utf8");
|
|
256
261
|
}
|
|
257
262
|
function isReservedEmitEvent(event) {
|
|
258
|
-
return event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT || event === INTERNAL_RPC_REQUEST_EVENT || event === INTERNAL_RPC_RESPONSE_EVENT || event === READY_EVENT;
|
|
263
|
+
return event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT || event === INTERNAL_RPC_REQUEST_EVENT || event === INTERNAL_RPC_RESPONSE_EVENT || event === INTERNAL_STREAM_FRAME_EVENT || event === READY_EVENT;
|
|
259
264
|
}
|
|
260
265
|
function isPromiseLike(value) {
|
|
261
266
|
return typeof value === "object" && value !== null && "then" in value;
|
|
@@ -267,6 +272,240 @@ function normalizeRpcTimeout(timeoutMs) {
|
|
|
267
272
|
}
|
|
268
273
|
return resolvedTimeoutMs;
|
|
269
274
|
}
|
|
275
|
+
function normalizeStreamChunkSize(chunkSizeBytes) {
|
|
276
|
+
const resolvedChunkSize = chunkSizeBytes ?? DEFAULT_STREAM_CHUNK_SIZE_BYTES;
|
|
277
|
+
if (!Number.isInteger(resolvedChunkSize) || resolvedChunkSize <= 0) {
|
|
278
|
+
throw new Error("Stream chunkSizeBytes must be a positive integer.");
|
|
279
|
+
}
|
|
280
|
+
if (resolvedChunkSize > MAX_STREAM_CHUNK_SIZE_BYTES) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`Stream chunkSizeBytes cannot exceed ${MAX_STREAM_CHUNK_SIZE_BYTES} bytes.`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return resolvedChunkSize;
|
|
286
|
+
}
|
|
287
|
+
function resolveKnownStreamSourceSize(source, hint) {
|
|
288
|
+
if (hint !== void 0) {
|
|
289
|
+
if (!Number.isInteger(hint) || hint < 0) {
|
|
290
|
+
throw new Error("Stream totalBytes must be a non-negative integer.");
|
|
291
|
+
}
|
|
292
|
+
return hint;
|
|
293
|
+
}
|
|
294
|
+
if (Buffer.isBuffer(source)) {
|
|
295
|
+
return source.length;
|
|
296
|
+
}
|
|
297
|
+
if (source instanceof Uint8Array) {
|
|
298
|
+
return source.byteLength;
|
|
299
|
+
}
|
|
300
|
+
return void 0;
|
|
301
|
+
}
|
|
302
|
+
function normalizeChunkSourceValue(value) {
|
|
303
|
+
if (Buffer.isBuffer(value)) {
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
if (value instanceof Uint8Array) {
|
|
307
|
+
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
308
|
+
}
|
|
309
|
+
if (value instanceof ArrayBuffer) {
|
|
310
|
+
return Buffer.from(value);
|
|
311
|
+
}
|
|
312
|
+
if (typeof value === "string") {
|
|
313
|
+
return Buffer.from(value, "utf8");
|
|
314
|
+
}
|
|
315
|
+
throw new Error("Stream source yielded an unsupported chunk value.");
|
|
316
|
+
}
|
|
317
|
+
function isAsyncIterableValue(value) {
|
|
318
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
319
|
+
}
|
|
320
|
+
function isReadableSource(value) {
|
|
321
|
+
return value instanceof stream.Readable;
|
|
322
|
+
}
|
|
323
|
+
function splitChunkBuffer(chunk, chunkSizeBytes) {
|
|
324
|
+
if (chunk.length <= chunkSizeBytes) {
|
|
325
|
+
return [chunk];
|
|
326
|
+
}
|
|
327
|
+
const splitChunks = [];
|
|
328
|
+
for (let offset = 0; offset < chunk.length; offset += chunkSizeBytes) {
|
|
329
|
+
splitChunks.push(chunk.subarray(offset, offset + chunkSizeBytes));
|
|
330
|
+
}
|
|
331
|
+
return splitChunks;
|
|
332
|
+
}
|
|
333
|
+
async function* createChunkStreamIterator(source, chunkSizeBytes) {
|
|
334
|
+
if (Buffer.isBuffer(source)) {
|
|
335
|
+
yield* splitChunkBuffer(source, chunkSizeBytes);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (source instanceof Uint8Array) {
|
|
339
|
+
yield* splitChunkBuffer(
|
|
340
|
+
Buffer.from(source.buffer, source.byteOffset, source.byteLength),
|
|
341
|
+
chunkSizeBytes
|
|
342
|
+
);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (isReadableSource(source) || isAsyncIterableValue(source)) {
|
|
346
|
+
for await (const chunkValue of source) {
|
|
347
|
+
const normalizedChunk = normalizeChunkSourceValue(chunkValue);
|
|
348
|
+
if (normalizedChunk.length === 0) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
yield* splitChunkBuffer(normalizedChunk, chunkSizeBytes);
|
|
352
|
+
}
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
throw new Error("Unsupported stream source type.");
|
|
356
|
+
}
|
|
357
|
+
function parseStreamFramePayload(data) {
|
|
358
|
+
if (typeof data !== "object" || data === null) {
|
|
359
|
+
throw new Error("Invalid stream frame payload format.");
|
|
360
|
+
}
|
|
361
|
+
const payload = data;
|
|
362
|
+
if (payload.version !== STREAM_FRAME_VERSION) {
|
|
363
|
+
throw new Error(`Unsupported stream frame version: ${String(payload.version)}.`);
|
|
364
|
+
}
|
|
365
|
+
if (typeof payload.streamId !== "string" || payload.streamId.trim().length === 0) {
|
|
366
|
+
throw new Error("Stream frame streamId must be a non-empty string.");
|
|
367
|
+
}
|
|
368
|
+
if (payload.type === "start") {
|
|
369
|
+
if (typeof payload.event !== "string" || payload.event.trim().length === 0) {
|
|
370
|
+
throw new Error("Stream start frame event must be a non-empty string.");
|
|
371
|
+
}
|
|
372
|
+
if (payload.totalBytes !== void 0 && (!Number.isInteger(payload.totalBytes) || payload.totalBytes < 0)) {
|
|
373
|
+
throw new Error("Stream start frame totalBytes must be a non-negative integer.");
|
|
374
|
+
}
|
|
375
|
+
if (payload.metadata !== void 0 && !isPlainObject(payload.metadata)) {
|
|
376
|
+
throw new Error("Stream start frame metadata must be a plain object when provided.");
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
version: STREAM_FRAME_VERSION,
|
|
380
|
+
type: "start",
|
|
381
|
+
streamId: payload.streamId.trim(),
|
|
382
|
+
event: payload.event.trim(),
|
|
383
|
+
...payload.metadata ? { metadata: payload.metadata } : {},
|
|
384
|
+
...payload.totalBytes !== void 0 ? { totalBytes: payload.totalBytes } : {}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (payload.type === "chunk") {
|
|
388
|
+
const { index, byteLength } = payload;
|
|
389
|
+
if (typeof index !== "number" || !Number.isInteger(index) || index < 0) {
|
|
390
|
+
throw new Error("Stream chunk frame index must be a non-negative integer.");
|
|
391
|
+
}
|
|
392
|
+
if (typeof payload.payload !== "string" || payload.payload.length === 0) {
|
|
393
|
+
throw new Error("Stream chunk frame payload must be a non-empty base64 string.");
|
|
394
|
+
}
|
|
395
|
+
if (typeof byteLength !== "number" || !Number.isInteger(byteLength) || byteLength <= 0) {
|
|
396
|
+
throw new Error("Stream chunk frame byteLength must be a positive integer.");
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
version: STREAM_FRAME_VERSION,
|
|
400
|
+
type: "chunk",
|
|
401
|
+
streamId: payload.streamId.trim(),
|
|
402
|
+
index,
|
|
403
|
+
payload: payload.payload,
|
|
404
|
+
byteLength
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
if (payload.type === "end") {
|
|
408
|
+
const { chunkCount, totalBytes } = payload;
|
|
409
|
+
if (typeof chunkCount !== "number" || !Number.isInteger(chunkCount) || chunkCount < 0) {
|
|
410
|
+
throw new Error("Stream end frame chunkCount must be a non-negative integer.");
|
|
411
|
+
}
|
|
412
|
+
if (typeof totalBytes !== "number" || !Number.isInteger(totalBytes) || totalBytes < 0) {
|
|
413
|
+
throw new Error("Stream end frame totalBytes must be a non-negative integer.");
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
version: STREAM_FRAME_VERSION,
|
|
417
|
+
type: "end",
|
|
418
|
+
streamId: payload.streamId.trim(),
|
|
419
|
+
chunkCount,
|
|
420
|
+
totalBytes
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
if (payload.type === "abort") {
|
|
424
|
+
if (typeof payload.reason !== "string" || payload.reason.trim().length === 0) {
|
|
425
|
+
throw new Error("Stream abort frame reason must be a non-empty string.");
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
version: STREAM_FRAME_VERSION,
|
|
429
|
+
type: "abort",
|
|
430
|
+
streamId: payload.streamId.trim(),
|
|
431
|
+
reason: payload.reason.trim()
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
throw new Error("Unsupported stream frame type.");
|
|
435
|
+
}
|
|
436
|
+
async function transmitChunkedStreamFrames(event, source, options, sendFrame) {
|
|
437
|
+
const chunkSizeBytes = normalizeStreamChunkSize(options?.chunkSizeBytes);
|
|
438
|
+
const totalBytesHint = resolveKnownStreamSourceSize(source, options?.totalBytes);
|
|
439
|
+
if (options?.metadata !== void 0 && !isPlainObject(options.metadata)) {
|
|
440
|
+
throw new Error("Stream metadata must be a plain object when provided.");
|
|
441
|
+
}
|
|
442
|
+
if (options?.signal?.aborted) {
|
|
443
|
+
throw new Error("Stream transfer aborted before dispatch.");
|
|
444
|
+
}
|
|
445
|
+
const streamId = crypto.randomUUID();
|
|
446
|
+
let chunkCount = 0;
|
|
447
|
+
let totalBytes = 0;
|
|
448
|
+
await sendFrame({
|
|
449
|
+
version: STREAM_FRAME_VERSION,
|
|
450
|
+
type: "start",
|
|
451
|
+
streamId,
|
|
452
|
+
event,
|
|
453
|
+
...options?.metadata ? { metadata: options.metadata } : {},
|
|
454
|
+
...totalBytesHint !== void 0 ? { totalBytes: totalBytesHint } : {}
|
|
455
|
+
});
|
|
456
|
+
try {
|
|
457
|
+
for await (const chunkBuffer of createChunkStreamIterator(source, chunkSizeBytes)) {
|
|
458
|
+
if (options?.signal?.aborted) {
|
|
459
|
+
throw new Error("Stream transfer aborted by caller signal.");
|
|
460
|
+
}
|
|
461
|
+
if (chunkBuffer.length === 0) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
await sendFrame({
|
|
465
|
+
version: STREAM_FRAME_VERSION,
|
|
466
|
+
type: "chunk",
|
|
467
|
+
streamId,
|
|
468
|
+
index: chunkCount,
|
|
469
|
+
payload: chunkBuffer.toString("base64"),
|
|
470
|
+
byteLength: chunkBuffer.length
|
|
471
|
+
});
|
|
472
|
+
chunkCount += 1;
|
|
473
|
+
totalBytes += chunkBuffer.length;
|
|
474
|
+
}
|
|
475
|
+
if (totalBytesHint !== void 0 && totalBytes !== totalBytesHint) {
|
|
476
|
+
throw new Error(
|
|
477
|
+
`Stream totalBytes mismatch. Expected ${totalBytesHint}, received ${totalBytes}.`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
await sendFrame({
|
|
481
|
+
version: STREAM_FRAME_VERSION,
|
|
482
|
+
type: "end",
|
|
483
|
+
streamId,
|
|
484
|
+
chunkCount,
|
|
485
|
+
totalBytes
|
|
486
|
+
});
|
|
487
|
+
return {
|
|
488
|
+
streamId,
|
|
489
|
+
chunkCount,
|
|
490
|
+
totalBytes
|
|
491
|
+
};
|
|
492
|
+
} catch (error) {
|
|
493
|
+
const normalizedError = normalizeToError(
|
|
494
|
+
error,
|
|
495
|
+
`Chunked stream transfer failed for event "${event}".`
|
|
496
|
+
);
|
|
497
|
+
try {
|
|
498
|
+
await sendFrame({
|
|
499
|
+
version: STREAM_FRAME_VERSION,
|
|
500
|
+
type: "abort",
|
|
501
|
+
streamId,
|
|
502
|
+
reason: normalizedError.message
|
|
503
|
+
});
|
|
504
|
+
} catch {
|
|
505
|
+
}
|
|
506
|
+
throw normalizedError;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
270
509
|
function parseRpcRequestPayload(data) {
|
|
271
510
|
if (typeof data !== "object" || data === null) {
|
|
272
511
|
throw new Error("Invalid RPC request payload format.");
|
|
@@ -563,6 +802,7 @@ var SecureServer = class {
|
|
|
563
802
|
clientsById = /* @__PURE__ */ new Map();
|
|
564
803
|
clientIdBySocket = /* @__PURE__ */ new Map();
|
|
565
804
|
customEventHandlers = /* @__PURE__ */ new Map();
|
|
805
|
+
streamEventHandlers = /* @__PURE__ */ new Map();
|
|
566
806
|
connectionHandlers = /* @__PURE__ */ new Set();
|
|
567
807
|
disconnectHandlers = /* @__PURE__ */ new Set();
|
|
568
808
|
readyHandlers = /* @__PURE__ */ new Set();
|
|
@@ -573,6 +813,7 @@ var SecureServer = class {
|
|
|
573
813
|
sharedSecretBySocket = /* @__PURE__ */ new WeakMap();
|
|
574
814
|
encryptionKeyBySocket = /* @__PURE__ */ new WeakMap();
|
|
575
815
|
pendingPayloadsBySocket = /* @__PURE__ */ new WeakMap();
|
|
816
|
+
incomingStreamsBySocket = /* @__PURE__ */ new WeakMap();
|
|
576
817
|
pendingRpcRequestsBySocket = /* @__PURE__ */ new WeakMap();
|
|
577
818
|
heartbeatStateBySocket = /* @__PURE__ */ new WeakMap();
|
|
578
819
|
roomMembersByName = /* @__PURE__ */ new Map();
|
|
@@ -721,6 +962,38 @@ var SecureServer = class {
|
|
|
721
962
|
}
|
|
722
963
|
return this;
|
|
723
964
|
}
|
|
965
|
+
onStream(event, handler) {
|
|
966
|
+
try {
|
|
967
|
+
if (isReservedEmitEvent(event)) {
|
|
968
|
+
throw new Error(`The event "${event}" is reserved and cannot be used as a stream event.`);
|
|
969
|
+
}
|
|
970
|
+
const listeners = this.streamEventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
971
|
+
listeners.add(handler);
|
|
972
|
+
this.streamEventHandlers.set(event, listeners);
|
|
973
|
+
} catch (error) {
|
|
974
|
+
this.notifyError(
|
|
975
|
+
normalizeToError(error, "Failed to register server stream handler.")
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
return this;
|
|
979
|
+
}
|
|
980
|
+
offStream(event, handler) {
|
|
981
|
+
try {
|
|
982
|
+
const listeners = this.streamEventHandlers.get(event);
|
|
983
|
+
if (!listeners) {
|
|
984
|
+
return this;
|
|
985
|
+
}
|
|
986
|
+
listeners.delete(handler);
|
|
987
|
+
if (listeners.size === 0) {
|
|
988
|
+
this.streamEventHandlers.delete(event);
|
|
989
|
+
}
|
|
990
|
+
} catch (error) {
|
|
991
|
+
this.notifyError(
|
|
992
|
+
normalizeToError(error, "Failed to remove server stream handler.")
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
return this;
|
|
996
|
+
}
|
|
724
997
|
use(middleware) {
|
|
725
998
|
try {
|
|
726
999
|
if (typeof middleware !== "function") {
|
|
@@ -796,6 +1069,40 @@ var SecureServer = class {
|
|
|
796
1069
|
return false;
|
|
797
1070
|
}
|
|
798
1071
|
}
|
|
1072
|
+
async emitStreamTo(clientId, event, source, options) {
|
|
1073
|
+
try {
|
|
1074
|
+
if (isReservedEmitEvent(event)) {
|
|
1075
|
+
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
1076
|
+
}
|
|
1077
|
+
const client = this.clientsById.get(clientId);
|
|
1078
|
+
if (!client) {
|
|
1079
|
+
throw new Error(`Client with id ${clientId} was not found.`);
|
|
1080
|
+
}
|
|
1081
|
+
if (!this.isClientHandshakeReady(client.socket)) {
|
|
1082
|
+
throw new Error(
|
|
1083
|
+
`Cannot stream event "${event}" before secure handshake completion for client ${client.id}.`
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
return await transmitChunkedStreamFrames(
|
|
1087
|
+
event,
|
|
1088
|
+
source,
|
|
1089
|
+
options,
|
|
1090
|
+
async (framePayload) => {
|
|
1091
|
+
await this.sendEncryptedEnvelope(client.socket, {
|
|
1092
|
+
event: INTERNAL_STREAM_FRAME_EVENT,
|
|
1093
|
+
data: framePayload
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
);
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
const normalizedError = normalizeToError(
|
|
1099
|
+
error,
|
|
1100
|
+
`Failed to emit chunked stream event "${event}" to client ${clientId}.`
|
|
1101
|
+
);
|
|
1102
|
+
this.notifyError(normalizedError);
|
|
1103
|
+
throw normalizedError;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
799
1106
|
to(room) {
|
|
800
1107
|
const normalizedRoom = this.normalizeRoomName(room);
|
|
801
1108
|
return {
|
|
@@ -828,6 +1135,10 @@ var SecureServer = class {
|
|
|
828
1135
|
client.socket,
|
|
829
1136
|
new Error("Server closed before ACK response was received.")
|
|
830
1137
|
);
|
|
1138
|
+
this.cleanupIncomingStreamsForSocket(
|
|
1139
|
+
client.socket,
|
|
1140
|
+
"Server closed before stream transfer completed."
|
|
1141
|
+
);
|
|
831
1142
|
this.middlewareMetadataBySocket.delete(client.socket);
|
|
832
1143
|
if (client.socket.readyState === WebSocket__default.default.OPEN || client.socket.readyState === WebSocket__default.default.CONNECTING) {
|
|
833
1144
|
client.socket.close(code, reason);
|
|
@@ -1371,6 +1682,10 @@ var SecureServer = class {
|
|
|
1371
1682
|
await this.handleRpcRequest(client, decryptedEnvelope.data);
|
|
1372
1683
|
return;
|
|
1373
1684
|
}
|
|
1685
|
+
if (decryptedEnvelope.event === INTERNAL_STREAM_FRAME_EVENT) {
|
|
1686
|
+
this.handleIncomingStreamFrame(client, decryptedEnvelope.data);
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1374
1689
|
if (decryptedEnvelope.event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
1375
1690
|
this.notifyError(
|
|
1376
1691
|
new Error(
|
|
@@ -1412,6 +1727,10 @@ var SecureServer = class {
|
|
|
1412
1727
|
this.pendingRpcRequestsBySocket.delete(client.socket);
|
|
1413
1728
|
this.heartbeatStateBySocket.delete(client.socket);
|
|
1414
1729
|
this.middlewareMetadataBySocket.delete(client.socket);
|
|
1730
|
+
this.cleanupIncomingStreamsForSocket(
|
|
1731
|
+
client.socket,
|
|
1732
|
+
`Client ${client.id} disconnected before stream transfer completed.`
|
|
1733
|
+
);
|
|
1415
1734
|
const decodedReason = decodeCloseReason(reason);
|
|
1416
1735
|
for (const handler of this.disconnectHandlers) {
|
|
1417
1736
|
try {
|
|
@@ -1457,6 +1776,197 @@ var SecureServer = class {
|
|
|
1457
1776
|
}
|
|
1458
1777
|
}
|
|
1459
1778
|
}
|
|
1779
|
+
getOrCreateIncomingServerStreams(socket) {
|
|
1780
|
+
const existingStreams = this.incomingStreamsBySocket.get(socket);
|
|
1781
|
+
if (existingStreams) {
|
|
1782
|
+
return existingStreams;
|
|
1783
|
+
}
|
|
1784
|
+
const streamMap = /* @__PURE__ */ new Map();
|
|
1785
|
+
this.incomingStreamsBySocket.set(socket, streamMap);
|
|
1786
|
+
return streamMap;
|
|
1787
|
+
}
|
|
1788
|
+
cleanupIncomingStreamsForSocket(socket, reason) {
|
|
1789
|
+
const streamMap = this.incomingStreamsBySocket.get(socket);
|
|
1790
|
+
if (!streamMap) {
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
for (const streamState of streamMap.values()) {
|
|
1794
|
+
streamState.stream.destroy(new Error(reason));
|
|
1795
|
+
}
|
|
1796
|
+
streamMap.clear();
|
|
1797
|
+
this.incomingStreamsBySocket.delete(socket);
|
|
1798
|
+
}
|
|
1799
|
+
abortIncomingServerStream(socket, streamId, reason) {
|
|
1800
|
+
const streamMap = this.incomingStreamsBySocket.get(socket);
|
|
1801
|
+
if (!streamMap) {
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
const streamState = streamMap.get(streamId);
|
|
1805
|
+
if (!streamState) {
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
streamState.stream.destroy(new Error(reason));
|
|
1809
|
+
streamMap.delete(streamId);
|
|
1810
|
+
if (streamMap.size === 0) {
|
|
1811
|
+
this.incomingStreamsBySocket.delete(socket);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
dispatchServerStreamEvent(event, stream, info, client) {
|
|
1815
|
+
const handlers = this.streamEventHandlers.get(event);
|
|
1816
|
+
if (!handlers || handlers.size === 0) {
|
|
1817
|
+
stream.resume();
|
|
1818
|
+
this.notifyError(
|
|
1819
|
+
new Error(
|
|
1820
|
+
`No stream handler is registered for event "${event}" on server client ${client.id}.`
|
|
1821
|
+
)
|
|
1822
|
+
);
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
for (const handler of handlers) {
|
|
1826
|
+
try {
|
|
1827
|
+
const handlerResult = handler(stream, info, client);
|
|
1828
|
+
if (isPromiseLike(handlerResult)) {
|
|
1829
|
+
void Promise.resolve(handlerResult).catch((error) => {
|
|
1830
|
+
this.notifyError(
|
|
1831
|
+
normalizeToError(
|
|
1832
|
+
error,
|
|
1833
|
+
`Server stream handler failed for event ${event}.`
|
|
1834
|
+
)
|
|
1835
|
+
);
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
} catch (error) {
|
|
1839
|
+
this.notifyError(
|
|
1840
|
+
normalizeToError(
|
|
1841
|
+
error,
|
|
1842
|
+
`Server stream handler failed for event ${event}.`
|
|
1843
|
+
)
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
handleIncomingStreamStartFrame(client, framePayload) {
|
|
1849
|
+
if (isReservedEmitEvent(framePayload.event)) {
|
|
1850
|
+
throw new Error(
|
|
1851
|
+
`Reserved event "${framePayload.event}" cannot be used for stream transport.`
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
const incomingStreams = this.getOrCreateIncomingServerStreams(client.socket);
|
|
1855
|
+
if (incomingStreams.has(framePayload.streamId)) {
|
|
1856
|
+
throw new Error(
|
|
1857
|
+
`Stream ${framePayload.streamId} already exists for client ${client.id}.`
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
const stream$1 = new stream.PassThrough();
|
|
1861
|
+
const streamInfo = {
|
|
1862
|
+
streamId: framePayload.streamId,
|
|
1863
|
+
event: framePayload.event,
|
|
1864
|
+
startedAt: Date.now(),
|
|
1865
|
+
...framePayload.metadata !== void 0 ? { metadata: framePayload.metadata } : {},
|
|
1866
|
+
...framePayload.totalBytes !== void 0 ? { totalBytes: framePayload.totalBytes } : {}
|
|
1867
|
+
};
|
|
1868
|
+
incomingStreams.set(framePayload.streamId, {
|
|
1869
|
+
info: streamInfo,
|
|
1870
|
+
stream: stream$1,
|
|
1871
|
+
expectedChunkIndex: 0,
|
|
1872
|
+
receivedBytes: 0
|
|
1873
|
+
});
|
|
1874
|
+
this.dispatchServerStreamEvent(framePayload.event, stream$1, streamInfo, client);
|
|
1875
|
+
}
|
|
1876
|
+
handleIncomingStreamChunkFrame(client, framePayload) {
|
|
1877
|
+
const incomingStreams = this.incomingStreamsBySocket.get(client.socket);
|
|
1878
|
+
const streamState = incomingStreams?.get(framePayload.streamId);
|
|
1879
|
+
if (!incomingStreams || !streamState) {
|
|
1880
|
+
throw new Error(
|
|
1881
|
+
`Stream ${framePayload.streamId} is unknown for client ${client.id}.`
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
if (framePayload.index !== streamState.expectedChunkIndex) {
|
|
1885
|
+
throw new Error(
|
|
1886
|
+
`Out-of-order chunk index for stream ${framePayload.streamId}. Expected ${streamState.expectedChunkIndex}, received ${framePayload.index}.`
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
const chunkBuffer = decodeBase64ToBuffer(
|
|
1890
|
+
framePayload.payload,
|
|
1891
|
+
`Stream chunk payload (${framePayload.streamId})`
|
|
1892
|
+
);
|
|
1893
|
+
if (chunkBuffer.length !== framePayload.byteLength) {
|
|
1894
|
+
throw new Error(
|
|
1895
|
+
`Stream ${framePayload.streamId} byteLength mismatch. Expected ${framePayload.byteLength}, received ${chunkBuffer.length}.`
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
streamState.expectedChunkIndex += 1;
|
|
1899
|
+
streamState.receivedBytes += chunkBuffer.length;
|
|
1900
|
+
streamState.stream.write(chunkBuffer);
|
|
1901
|
+
}
|
|
1902
|
+
handleIncomingStreamEndFrame(client, framePayload) {
|
|
1903
|
+
const incomingStreams = this.incomingStreamsBySocket.get(client.socket);
|
|
1904
|
+
const streamState = incomingStreams?.get(framePayload.streamId);
|
|
1905
|
+
if (!incomingStreams || !streamState) {
|
|
1906
|
+
throw new Error(
|
|
1907
|
+
`Stream ${framePayload.streamId} is unknown for client ${client.id}.`
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
if (framePayload.chunkCount !== streamState.expectedChunkIndex) {
|
|
1911
|
+
throw new Error(
|
|
1912
|
+
`Stream ${framePayload.streamId} chunkCount mismatch. Expected ${streamState.expectedChunkIndex}, received ${framePayload.chunkCount}.`
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
if (framePayload.totalBytes !== streamState.receivedBytes) {
|
|
1916
|
+
throw new Error(
|
|
1917
|
+
`Stream ${framePayload.streamId} totalBytes mismatch. Expected ${streamState.receivedBytes}, received ${framePayload.totalBytes}.`
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
if (streamState.info.totalBytes !== void 0 && streamState.info.totalBytes !== streamState.receivedBytes) {
|
|
1921
|
+
throw new Error(
|
|
1922
|
+
`Stream ${framePayload.streamId} violated announced totalBytes (${streamState.info.totalBytes}).`
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
streamState.stream.end();
|
|
1926
|
+
incomingStreams.delete(framePayload.streamId);
|
|
1927
|
+
if (incomingStreams.size === 0) {
|
|
1928
|
+
this.incomingStreamsBySocket.delete(client.socket);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
handleIncomingStreamAbortFrame(client, framePayload) {
|
|
1932
|
+
this.abortIncomingServerStream(
|
|
1933
|
+
client.socket,
|
|
1934
|
+
framePayload.streamId,
|
|
1935
|
+
framePayload.reason
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
handleIncomingStreamFrame(client, data) {
|
|
1939
|
+
let framePayload = null;
|
|
1940
|
+
try {
|
|
1941
|
+
framePayload = parseStreamFramePayload(data);
|
|
1942
|
+
if (framePayload.type === "start") {
|
|
1943
|
+
this.handleIncomingStreamStartFrame(client, framePayload);
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
if (framePayload.type === "chunk") {
|
|
1947
|
+
this.handleIncomingStreamChunkFrame(client, framePayload);
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
if (framePayload.type === "end") {
|
|
1951
|
+
this.handleIncomingStreamEndFrame(client, framePayload);
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
this.handleIncomingStreamAbortFrame(client, framePayload);
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
const normalizedError = normalizeToError(
|
|
1957
|
+
error,
|
|
1958
|
+
`Failed to process incoming stream frame for client ${client.id}.`
|
|
1959
|
+
);
|
|
1960
|
+
if (framePayload) {
|
|
1961
|
+
this.abortIncomingServerStream(
|
|
1962
|
+
client.socket,
|
|
1963
|
+
framePayload.streamId,
|
|
1964
|
+
normalizedError.message
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
this.notifyError(normalizedError);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1460
1970
|
async executeServerMiddleware(context) {
|
|
1461
1971
|
if (this.middlewareHandlers.length === 0) {
|
|
1462
1972
|
return;
|
|
@@ -1885,6 +2395,9 @@ var SecureServer = class {
|
|
|
1885
2395
|
}
|
|
1886
2396
|
return this.emitTo(clientId, event, data, callbackOrOptions ?? {});
|
|
1887
2397
|
},
|
|
2398
|
+
emitStream: (event, source, options) => {
|
|
2399
|
+
return this.emitStreamTo(clientId, event, source, options);
|
|
2400
|
+
},
|
|
1888
2401
|
join: (room) => this.joinClientToRoom(clientId, room),
|
|
1889
2402
|
leave: (room) => this.leaveClientFromRoom(clientId, room),
|
|
1890
2403
|
leaveAll: () => this.leaveClientFromAllRooms(clientId)
|
|
@@ -2045,6 +2558,7 @@ var SecureClient = class {
|
|
|
2045
2558
|
reconnectTimer = null;
|
|
2046
2559
|
isManualDisconnectRequested = false;
|
|
2047
2560
|
customEventHandlers = /* @__PURE__ */ new Map();
|
|
2561
|
+
streamEventHandlers = /* @__PURE__ */ new Map();
|
|
2048
2562
|
connectHandlers = /* @__PURE__ */ new Set();
|
|
2049
2563
|
disconnectHandlers = /* @__PURE__ */ new Set();
|
|
2050
2564
|
readyHandlers = /* @__PURE__ */ new Set();
|
|
@@ -2052,6 +2566,7 @@ var SecureClient = class {
|
|
|
2052
2566
|
handshakeState = null;
|
|
2053
2567
|
pendingPayloadQueue = [];
|
|
2054
2568
|
pendingRpcRequests = /* @__PURE__ */ new Map();
|
|
2569
|
+
incomingStreams = /* @__PURE__ */ new Map();
|
|
2055
2570
|
sessionTicket = null;
|
|
2056
2571
|
get readyState() {
|
|
2057
2572
|
return this.socket?.readyState ?? null;
|
|
@@ -2161,6 +2676,38 @@ var SecureClient = class {
|
|
|
2161
2676
|
}
|
|
2162
2677
|
return this;
|
|
2163
2678
|
}
|
|
2679
|
+
onStream(event, handler) {
|
|
2680
|
+
try {
|
|
2681
|
+
if (isReservedEmitEvent(event)) {
|
|
2682
|
+
throw new Error(`The event "${event}" is reserved and cannot be used as a stream event.`);
|
|
2683
|
+
}
|
|
2684
|
+
const listeners = this.streamEventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
2685
|
+
listeners.add(handler);
|
|
2686
|
+
this.streamEventHandlers.set(event, listeners);
|
|
2687
|
+
} catch (error) {
|
|
2688
|
+
this.notifyError(
|
|
2689
|
+
normalizeToError(error, "Failed to register client stream handler.")
|
|
2690
|
+
);
|
|
2691
|
+
}
|
|
2692
|
+
return this;
|
|
2693
|
+
}
|
|
2694
|
+
offStream(event, handler) {
|
|
2695
|
+
try {
|
|
2696
|
+
const listeners = this.streamEventHandlers.get(event);
|
|
2697
|
+
if (!listeners) {
|
|
2698
|
+
return this;
|
|
2699
|
+
}
|
|
2700
|
+
listeners.delete(handler);
|
|
2701
|
+
if (listeners.size === 0) {
|
|
2702
|
+
this.streamEventHandlers.delete(event);
|
|
2703
|
+
}
|
|
2704
|
+
} catch (error) {
|
|
2705
|
+
this.notifyError(
|
|
2706
|
+
normalizeToError(error, "Failed to remove client stream handler.")
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
return this;
|
|
2710
|
+
}
|
|
2164
2711
|
emit(event, data, callbackOrOptions, maybeCallback) {
|
|
2165
2712
|
const ackArgs = resolveAckArguments(callbackOrOptions, maybeCallback);
|
|
2166
2713
|
try {
|
|
@@ -2206,6 +2753,39 @@ var SecureClient = class {
|
|
|
2206
2753
|
return false;
|
|
2207
2754
|
}
|
|
2208
2755
|
}
|
|
2756
|
+
async emitStream(event, source, options) {
|
|
2757
|
+
try {
|
|
2758
|
+
if (isReservedEmitEvent(event)) {
|
|
2759
|
+
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
2760
|
+
}
|
|
2761
|
+
if (!this.socket || this.socket.readyState !== WebSocket__default.default.OPEN) {
|
|
2762
|
+
throw new Error("Client socket is not connected.");
|
|
2763
|
+
}
|
|
2764
|
+
if (!this.isHandshakeReady()) {
|
|
2765
|
+
throw new Error(
|
|
2766
|
+
`Cannot stream event "${event}" before secure handshake completion.`
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2769
|
+
return await transmitChunkedStreamFrames(
|
|
2770
|
+
event,
|
|
2771
|
+
source,
|
|
2772
|
+
options,
|
|
2773
|
+
async (framePayload) => {
|
|
2774
|
+
await this.sendEncryptedEnvelope({
|
|
2775
|
+
event: INTERNAL_STREAM_FRAME_EVENT,
|
|
2776
|
+
data: framePayload
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
);
|
|
2780
|
+
} catch (error) {
|
|
2781
|
+
const normalizedError = normalizeToError(
|
|
2782
|
+
error,
|
|
2783
|
+
`Failed to emit chunked stream event "${event}".`
|
|
2784
|
+
);
|
|
2785
|
+
this.notifyError(normalizedError);
|
|
2786
|
+
throw normalizedError;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2209
2789
|
resolveReconnectConfig(reconnectOptions) {
|
|
2210
2790
|
if (typeof reconnectOptions === "boolean") {
|
|
2211
2791
|
return {
|
|
@@ -2369,6 +2949,10 @@ var SecureClient = class {
|
|
|
2369
2949
|
void this.handleRpcRequest(decryptedEnvelope.data);
|
|
2370
2950
|
return;
|
|
2371
2951
|
}
|
|
2952
|
+
if (decryptedEnvelope.event === INTERNAL_STREAM_FRAME_EVENT) {
|
|
2953
|
+
this.handleIncomingStreamFrame(decryptedEnvelope.data);
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2372
2956
|
if (decryptedEnvelope.event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
2373
2957
|
this.handleSessionTicket(decryptedEnvelope.data);
|
|
2374
2958
|
return;
|
|
@@ -2383,6 +2967,9 @@ var SecureClient = class {
|
|
|
2383
2967
|
this.socket = null;
|
|
2384
2968
|
this.handshakeState = null;
|
|
2385
2969
|
this.pendingPayloadQueue = [];
|
|
2970
|
+
this.cleanupIncomingStreams(
|
|
2971
|
+
"Client disconnected before stream transfer completed."
|
|
2972
|
+
);
|
|
2386
2973
|
this.rejectPendingRpcRequests(
|
|
2387
2974
|
new Error("Client disconnected before ACK response was received.")
|
|
2388
2975
|
);
|
|
@@ -2429,6 +3016,151 @@ var SecureClient = class {
|
|
|
2429
3016
|
}
|
|
2430
3017
|
}
|
|
2431
3018
|
}
|
|
3019
|
+
cleanupIncomingStreams(reason) {
|
|
3020
|
+
for (const streamState of this.incomingStreams.values()) {
|
|
3021
|
+
streamState.stream.destroy(new Error(reason));
|
|
3022
|
+
}
|
|
3023
|
+
this.incomingStreams.clear();
|
|
3024
|
+
}
|
|
3025
|
+
abortIncomingClientStream(streamId, reason) {
|
|
3026
|
+
const streamState = this.incomingStreams.get(streamId);
|
|
3027
|
+
if (!streamState) {
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
streamState.stream.destroy(new Error(reason));
|
|
3031
|
+
this.incomingStreams.delete(streamId);
|
|
3032
|
+
}
|
|
3033
|
+
dispatchClientStreamEvent(event, stream, info) {
|
|
3034
|
+
const handlers = this.streamEventHandlers.get(event);
|
|
3035
|
+
if (!handlers || handlers.size === 0) {
|
|
3036
|
+
stream.resume();
|
|
3037
|
+
this.notifyError(
|
|
3038
|
+
new Error(`No stream handler is registered for event "${event}" on client.`)
|
|
3039
|
+
);
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
for (const handler of handlers) {
|
|
3043
|
+
try {
|
|
3044
|
+
const handlerResult = handler(stream, info);
|
|
3045
|
+
if (isPromiseLike(handlerResult)) {
|
|
3046
|
+
void Promise.resolve(handlerResult).catch((error) => {
|
|
3047
|
+
this.notifyError(
|
|
3048
|
+
normalizeToError(
|
|
3049
|
+
error,
|
|
3050
|
+
`Client stream handler failed for event ${event}.`
|
|
3051
|
+
)
|
|
3052
|
+
);
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
} catch (error) {
|
|
3056
|
+
this.notifyError(
|
|
3057
|
+
normalizeToError(error, `Client stream handler failed for event ${event}.`)
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
handleIncomingClientStreamStartFrame(framePayload) {
|
|
3063
|
+
if (isReservedEmitEvent(framePayload.event)) {
|
|
3064
|
+
throw new Error(
|
|
3065
|
+
`Reserved event "${framePayload.event}" cannot be used for stream transport.`
|
|
3066
|
+
);
|
|
3067
|
+
}
|
|
3068
|
+
if (this.incomingStreams.has(framePayload.streamId)) {
|
|
3069
|
+
throw new Error(`Stream ${framePayload.streamId} already exists on client.`);
|
|
3070
|
+
}
|
|
3071
|
+
const stream$1 = new stream.PassThrough();
|
|
3072
|
+
const streamInfo = {
|
|
3073
|
+
streamId: framePayload.streamId,
|
|
3074
|
+
event: framePayload.event,
|
|
3075
|
+
startedAt: Date.now(),
|
|
3076
|
+
...framePayload.metadata !== void 0 ? { metadata: framePayload.metadata } : {},
|
|
3077
|
+
...framePayload.totalBytes !== void 0 ? { totalBytes: framePayload.totalBytes } : {}
|
|
3078
|
+
};
|
|
3079
|
+
this.incomingStreams.set(framePayload.streamId, {
|
|
3080
|
+
info: streamInfo,
|
|
3081
|
+
stream: stream$1,
|
|
3082
|
+
expectedChunkIndex: 0,
|
|
3083
|
+
receivedBytes: 0
|
|
3084
|
+
});
|
|
3085
|
+
this.dispatchClientStreamEvent(framePayload.event, stream$1, streamInfo);
|
|
3086
|
+
}
|
|
3087
|
+
handleIncomingClientStreamChunkFrame(framePayload) {
|
|
3088
|
+
const streamState = this.incomingStreams.get(framePayload.streamId);
|
|
3089
|
+
if (!streamState) {
|
|
3090
|
+
throw new Error(`Stream ${framePayload.streamId} is unknown on client.`);
|
|
3091
|
+
}
|
|
3092
|
+
if (framePayload.index !== streamState.expectedChunkIndex) {
|
|
3093
|
+
throw new Error(
|
|
3094
|
+
`Out-of-order chunk index for stream ${framePayload.streamId}. Expected ${streamState.expectedChunkIndex}, received ${framePayload.index}.`
|
|
3095
|
+
);
|
|
3096
|
+
}
|
|
3097
|
+
const chunkBuffer = decodeBase64ToBuffer(
|
|
3098
|
+
framePayload.payload,
|
|
3099
|
+
`Stream chunk payload (${framePayload.streamId})`
|
|
3100
|
+
);
|
|
3101
|
+
if (chunkBuffer.length !== framePayload.byteLength) {
|
|
3102
|
+
throw new Error(
|
|
3103
|
+
`Stream ${framePayload.streamId} byteLength mismatch. Expected ${framePayload.byteLength}, received ${chunkBuffer.length}.`
|
|
3104
|
+
);
|
|
3105
|
+
}
|
|
3106
|
+
streamState.expectedChunkIndex += 1;
|
|
3107
|
+
streamState.receivedBytes += chunkBuffer.length;
|
|
3108
|
+
streamState.stream.write(chunkBuffer);
|
|
3109
|
+
}
|
|
3110
|
+
handleIncomingClientStreamEndFrame(framePayload) {
|
|
3111
|
+
const streamState = this.incomingStreams.get(framePayload.streamId);
|
|
3112
|
+
if (!streamState) {
|
|
3113
|
+
throw new Error(`Stream ${framePayload.streamId} is unknown on client.`);
|
|
3114
|
+
}
|
|
3115
|
+
if (framePayload.chunkCount !== streamState.expectedChunkIndex) {
|
|
3116
|
+
throw new Error(
|
|
3117
|
+
`Stream ${framePayload.streamId} chunkCount mismatch. Expected ${streamState.expectedChunkIndex}, received ${framePayload.chunkCount}.`
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
if (framePayload.totalBytes !== streamState.receivedBytes) {
|
|
3121
|
+
throw new Error(
|
|
3122
|
+
`Stream ${framePayload.streamId} totalBytes mismatch. Expected ${streamState.receivedBytes}, received ${framePayload.totalBytes}.`
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
if (streamState.info.totalBytes !== void 0 && streamState.info.totalBytes !== streamState.receivedBytes) {
|
|
3126
|
+
throw new Error(
|
|
3127
|
+
`Stream ${framePayload.streamId} violated announced totalBytes (${streamState.info.totalBytes}).`
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
streamState.stream.end();
|
|
3131
|
+
this.incomingStreams.delete(framePayload.streamId);
|
|
3132
|
+
}
|
|
3133
|
+
handleIncomingClientStreamAbortFrame(framePayload) {
|
|
3134
|
+
this.abortIncomingClientStream(framePayload.streamId, framePayload.reason);
|
|
3135
|
+
}
|
|
3136
|
+
handleIncomingStreamFrame(data) {
|
|
3137
|
+
let framePayload = null;
|
|
3138
|
+
try {
|
|
3139
|
+
framePayload = parseStreamFramePayload(data);
|
|
3140
|
+
if (framePayload.type === "start") {
|
|
3141
|
+
this.handleIncomingClientStreamStartFrame(framePayload);
|
|
3142
|
+
return;
|
|
3143
|
+
}
|
|
3144
|
+
if (framePayload.type === "chunk") {
|
|
3145
|
+
this.handleIncomingClientStreamChunkFrame(framePayload);
|
|
3146
|
+
return;
|
|
3147
|
+
}
|
|
3148
|
+
if (framePayload.type === "end") {
|
|
3149
|
+
this.handleIncomingClientStreamEndFrame(framePayload);
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
this.handleIncomingClientStreamAbortFrame(framePayload);
|
|
3153
|
+
} catch (error) {
|
|
3154
|
+
const normalizedError = normalizeToError(
|
|
3155
|
+
error,
|
|
3156
|
+
"Failed to process incoming stream frame on client."
|
|
3157
|
+
);
|
|
3158
|
+
if (framePayload) {
|
|
3159
|
+
this.abortIncomingClientStream(framePayload.streamId, normalizedError.message);
|
|
3160
|
+
}
|
|
3161
|
+
this.notifyError(normalizedError);
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
2432
3164
|
notifyConnect() {
|
|
2433
3165
|
for (const handler of this.connectHandlers) {
|
|
2434
3166
|
try {
|