@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.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID, randomBytes, createHmac, createDecipheriv, createCipheriv, createECDH, timingSafeEqual, createHash } from 'crypto';
|
|
2
|
+
import { PassThrough, Readable } from 'stream';
|
|
2
3
|
import WebSocket, { WebSocketServer } from 'ws';
|
|
3
4
|
|
|
4
5
|
// src/index.ts
|
|
@@ -10,6 +11,7 @@ var INTERNAL_HANDSHAKE_EVENT = "__handshake";
|
|
|
10
11
|
var INTERNAL_SESSION_TICKET_EVENT = "__session:ticket";
|
|
11
12
|
var INTERNAL_RPC_REQUEST_EVENT = "__rpc:req";
|
|
12
13
|
var INTERNAL_RPC_RESPONSE_EVENT = "__rpc:res";
|
|
14
|
+
var INTERNAL_STREAM_FRAME_EVENT = "__stream:frame";
|
|
13
15
|
var READY_EVENT = "ready";
|
|
14
16
|
var HANDSHAKE_CURVE = "prime256v1";
|
|
15
17
|
var HANDSHAKE_PROTOCOL_VERSION = 1;
|
|
@@ -27,6 +29,9 @@ var DEFAULT_HEARTBEAT_TIMEOUT_MS = 15e3;
|
|
|
27
29
|
var DEFAULT_SESSION_RESUMPTION_ENABLED = true;
|
|
28
30
|
var DEFAULT_SESSION_TICKET_TTL_MS = 10 * 6e4;
|
|
29
31
|
var DEFAULT_SESSION_TICKET_MAX_CACHE_SIZE = 1e4;
|
|
32
|
+
var STREAM_FRAME_VERSION = 1;
|
|
33
|
+
var DEFAULT_STREAM_CHUNK_SIZE_BYTES = 64 * 1024;
|
|
34
|
+
var MAX_STREAM_CHUNK_SIZE_BYTES = 1024 * 1024;
|
|
30
35
|
var RESUMPTION_NONCE_LENGTH = 16;
|
|
31
36
|
var DEFAULT_RECONNECT_INITIAL_DELAY_MS = 250;
|
|
32
37
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 1e4;
|
|
@@ -249,7 +254,7 @@ function decodeCloseReason(reason) {
|
|
|
249
254
|
return reason.toString("utf8");
|
|
250
255
|
}
|
|
251
256
|
function isReservedEmitEvent(event) {
|
|
252
|
-
return event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT || event === INTERNAL_RPC_REQUEST_EVENT || event === INTERNAL_RPC_RESPONSE_EVENT || event === READY_EVENT;
|
|
257
|
+
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;
|
|
253
258
|
}
|
|
254
259
|
function isPromiseLike(value) {
|
|
255
260
|
return typeof value === "object" && value !== null && "then" in value;
|
|
@@ -261,6 +266,240 @@ function normalizeRpcTimeout(timeoutMs) {
|
|
|
261
266
|
}
|
|
262
267
|
return resolvedTimeoutMs;
|
|
263
268
|
}
|
|
269
|
+
function normalizeStreamChunkSize(chunkSizeBytes) {
|
|
270
|
+
const resolvedChunkSize = chunkSizeBytes ?? DEFAULT_STREAM_CHUNK_SIZE_BYTES;
|
|
271
|
+
if (!Number.isInteger(resolvedChunkSize) || resolvedChunkSize <= 0) {
|
|
272
|
+
throw new Error("Stream chunkSizeBytes must be a positive integer.");
|
|
273
|
+
}
|
|
274
|
+
if (resolvedChunkSize > MAX_STREAM_CHUNK_SIZE_BYTES) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Stream chunkSizeBytes cannot exceed ${MAX_STREAM_CHUNK_SIZE_BYTES} bytes.`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return resolvedChunkSize;
|
|
280
|
+
}
|
|
281
|
+
function resolveKnownStreamSourceSize(source, hint) {
|
|
282
|
+
if (hint !== void 0) {
|
|
283
|
+
if (!Number.isInteger(hint) || hint < 0) {
|
|
284
|
+
throw new Error("Stream totalBytes must be a non-negative integer.");
|
|
285
|
+
}
|
|
286
|
+
return hint;
|
|
287
|
+
}
|
|
288
|
+
if (Buffer.isBuffer(source)) {
|
|
289
|
+
return source.length;
|
|
290
|
+
}
|
|
291
|
+
if (source instanceof Uint8Array) {
|
|
292
|
+
return source.byteLength;
|
|
293
|
+
}
|
|
294
|
+
return void 0;
|
|
295
|
+
}
|
|
296
|
+
function normalizeChunkSourceValue(value) {
|
|
297
|
+
if (Buffer.isBuffer(value)) {
|
|
298
|
+
return value;
|
|
299
|
+
}
|
|
300
|
+
if (value instanceof Uint8Array) {
|
|
301
|
+
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
302
|
+
}
|
|
303
|
+
if (value instanceof ArrayBuffer) {
|
|
304
|
+
return Buffer.from(value);
|
|
305
|
+
}
|
|
306
|
+
if (typeof value === "string") {
|
|
307
|
+
return Buffer.from(value, "utf8");
|
|
308
|
+
}
|
|
309
|
+
throw new Error("Stream source yielded an unsupported chunk value.");
|
|
310
|
+
}
|
|
311
|
+
function isAsyncIterableValue(value) {
|
|
312
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
313
|
+
}
|
|
314
|
+
function isReadableSource(value) {
|
|
315
|
+
return value instanceof Readable;
|
|
316
|
+
}
|
|
317
|
+
function splitChunkBuffer(chunk, chunkSizeBytes) {
|
|
318
|
+
if (chunk.length <= chunkSizeBytes) {
|
|
319
|
+
return [chunk];
|
|
320
|
+
}
|
|
321
|
+
const splitChunks = [];
|
|
322
|
+
for (let offset = 0; offset < chunk.length; offset += chunkSizeBytes) {
|
|
323
|
+
splitChunks.push(chunk.subarray(offset, offset + chunkSizeBytes));
|
|
324
|
+
}
|
|
325
|
+
return splitChunks;
|
|
326
|
+
}
|
|
327
|
+
async function* createChunkStreamIterator(source, chunkSizeBytes) {
|
|
328
|
+
if (Buffer.isBuffer(source)) {
|
|
329
|
+
yield* splitChunkBuffer(source, chunkSizeBytes);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (source instanceof Uint8Array) {
|
|
333
|
+
yield* splitChunkBuffer(
|
|
334
|
+
Buffer.from(source.buffer, source.byteOffset, source.byteLength),
|
|
335
|
+
chunkSizeBytes
|
|
336
|
+
);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (isReadableSource(source) || isAsyncIterableValue(source)) {
|
|
340
|
+
for await (const chunkValue of source) {
|
|
341
|
+
const normalizedChunk = normalizeChunkSourceValue(chunkValue);
|
|
342
|
+
if (normalizedChunk.length === 0) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
yield* splitChunkBuffer(normalizedChunk, chunkSizeBytes);
|
|
346
|
+
}
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
throw new Error("Unsupported stream source type.");
|
|
350
|
+
}
|
|
351
|
+
function parseStreamFramePayload(data) {
|
|
352
|
+
if (typeof data !== "object" || data === null) {
|
|
353
|
+
throw new Error("Invalid stream frame payload format.");
|
|
354
|
+
}
|
|
355
|
+
const payload = data;
|
|
356
|
+
if (payload.version !== STREAM_FRAME_VERSION) {
|
|
357
|
+
throw new Error(`Unsupported stream frame version: ${String(payload.version)}.`);
|
|
358
|
+
}
|
|
359
|
+
if (typeof payload.streamId !== "string" || payload.streamId.trim().length === 0) {
|
|
360
|
+
throw new Error("Stream frame streamId must be a non-empty string.");
|
|
361
|
+
}
|
|
362
|
+
if (payload.type === "start") {
|
|
363
|
+
if (typeof payload.event !== "string" || payload.event.trim().length === 0) {
|
|
364
|
+
throw new Error("Stream start frame event must be a non-empty string.");
|
|
365
|
+
}
|
|
366
|
+
if (payload.totalBytes !== void 0 && (!Number.isInteger(payload.totalBytes) || payload.totalBytes < 0)) {
|
|
367
|
+
throw new Error("Stream start frame totalBytes must be a non-negative integer.");
|
|
368
|
+
}
|
|
369
|
+
if (payload.metadata !== void 0 && !isPlainObject(payload.metadata)) {
|
|
370
|
+
throw new Error("Stream start frame metadata must be a plain object when provided.");
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
version: STREAM_FRAME_VERSION,
|
|
374
|
+
type: "start",
|
|
375
|
+
streamId: payload.streamId.trim(),
|
|
376
|
+
event: payload.event.trim(),
|
|
377
|
+
...payload.metadata ? { metadata: payload.metadata } : {},
|
|
378
|
+
...payload.totalBytes !== void 0 ? { totalBytes: payload.totalBytes } : {}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (payload.type === "chunk") {
|
|
382
|
+
const { index, byteLength } = payload;
|
|
383
|
+
if (typeof index !== "number" || !Number.isInteger(index) || index < 0) {
|
|
384
|
+
throw new Error("Stream chunk frame index must be a non-negative integer.");
|
|
385
|
+
}
|
|
386
|
+
if (typeof payload.payload !== "string" || payload.payload.length === 0) {
|
|
387
|
+
throw new Error("Stream chunk frame payload must be a non-empty base64 string.");
|
|
388
|
+
}
|
|
389
|
+
if (typeof byteLength !== "number" || !Number.isInteger(byteLength) || byteLength <= 0) {
|
|
390
|
+
throw new Error("Stream chunk frame byteLength must be a positive integer.");
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
version: STREAM_FRAME_VERSION,
|
|
394
|
+
type: "chunk",
|
|
395
|
+
streamId: payload.streamId.trim(),
|
|
396
|
+
index,
|
|
397
|
+
payload: payload.payload,
|
|
398
|
+
byteLength
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
if (payload.type === "end") {
|
|
402
|
+
const { chunkCount, totalBytes } = payload;
|
|
403
|
+
if (typeof chunkCount !== "number" || !Number.isInteger(chunkCount) || chunkCount < 0) {
|
|
404
|
+
throw new Error("Stream end frame chunkCount must be a non-negative integer.");
|
|
405
|
+
}
|
|
406
|
+
if (typeof totalBytes !== "number" || !Number.isInteger(totalBytes) || totalBytes < 0) {
|
|
407
|
+
throw new Error("Stream end frame totalBytes must be a non-negative integer.");
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
version: STREAM_FRAME_VERSION,
|
|
411
|
+
type: "end",
|
|
412
|
+
streamId: payload.streamId.trim(),
|
|
413
|
+
chunkCount,
|
|
414
|
+
totalBytes
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
if (payload.type === "abort") {
|
|
418
|
+
if (typeof payload.reason !== "string" || payload.reason.trim().length === 0) {
|
|
419
|
+
throw new Error("Stream abort frame reason must be a non-empty string.");
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
version: STREAM_FRAME_VERSION,
|
|
423
|
+
type: "abort",
|
|
424
|
+
streamId: payload.streamId.trim(),
|
|
425
|
+
reason: payload.reason.trim()
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
throw new Error("Unsupported stream frame type.");
|
|
429
|
+
}
|
|
430
|
+
async function transmitChunkedStreamFrames(event, source, options, sendFrame) {
|
|
431
|
+
const chunkSizeBytes = normalizeStreamChunkSize(options?.chunkSizeBytes);
|
|
432
|
+
const totalBytesHint = resolveKnownStreamSourceSize(source, options?.totalBytes);
|
|
433
|
+
if (options?.metadata !== void 0 && !isPlainObject(options.metadata)) {
|
|
434
|
+
throw new Error("Stream metadata must be a plain object when provided.");
|
|
435
|
+
}
|
|
436
|
+
if (options?.signal?.aborted) {
|
|
437
|
+
throw new Error("Stream transfer aborted before dispatch.");
|
|
438
|
+
}
|
|
439
|
+
const streamId = randomUUID();
|
|
440
|
+
let chunkCount = 0;
|
|
441
|
+
let totalBytes = 0;
|
|
442
|
+
await sendFrame({
|
|
443
|
+
version: STREAM_FRAME_VERSION,
|
|
444
|
+
type: "start",
|
|
445
|
+
streamId,
|
|
446
|
+
event,
|
|
447
|
+
...options?.metadata ? { metadata: options.metadata } : {},
|
|
448
|
+
...totalBytesHint !== void 0 ? { totalBytes: totalBytesHint } : {}
|
|
449
|
+
});
|
|
450
|
+
try {
|
|
451
|
+
for await (const chunkBuffer of createChunkStreamIterator(source, chunkSizeBytes)) {
|
|
452
|
+
if (options?.signal?.aborted) {
|
|
453
|
+
throw new Error("Stream transfer aborted by caller signal.");
|
|
454
|
+
}
|
|
455
|
+
if (chunkBuffer.length === 0) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
await sendFrame({
|
|
459
|
+
version: STREAM_FRAME_VERSION,
|
|
460
|
+
type: "chunk",
|
|
461
|
+
streamId,
|
|
462
|
+
index: chunkCount,
|
|
463
|
+
payload: chunkBuffer.toString("base64"),
|
|
464
|
+
byteLength: chunkBuffer.length
|
|
465
|
+
});
|
|
466
|
+
chunkCount += 1;
|
|
467
|
+
totalBytes += chunkBuffer.length;
|
|
468
|
+
}
|
|
469
|
+
if (totalBytesHint !== void 0 && totalBytes !== totalBytesHint) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
`Stream totalBytes mismatch. Expected ${totalBytesHint}, received ${totalBytes}.`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
await sendFrame({
|
|
475
|
+
version: STREAM_FRAME_VERSION,
|
|
476
|
+
type: "end",
|
|
477
|
+
streamId,
|
|
478
|
+
chunkCount,
|
|
479
|
+
totalBytes
|
|
480
|
+
});
|
|
481
|
+
return {
|
|
482
|
+
streamId,
|
|
483
|
+
chunkCount,
|
|
484
|
+
totalBytes
|
|
485
|
+
};
|
|
486
|
+
} catch (error) {
|
|
487
|
+
const normalizedError = normalizeToError(
|
|
488
|
+
error,
|
|
489
|
+
`Chunked stream transfer failed for event "${event}".`
|
|
490
|
+
);
|
|
491
|
+
try {
|
|
492
|
+
await sendFrame({
|
|
493
|
+
version: STREAM_FRAME_VERSION,
|
|
494
|
+
type: "abort",
|
|
495
|
+
streamId,
|
|
496
|
+
reason: normalizedError.message
|
|
497
|
+
});
|
|
498
|
+
} catch {
|
|
499
|
+
}
|
|
500
|
+
throw normalizedError;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
264
503
|
function parseRpcRequestPayload(data) {
|
|
265
504
|
if (typeof data !== "object" || data === null) {
|
|
266
505
|
throw new Error("Invalid RPC request payload format.");
|
|
@@ -557,6 +796,7 @@ var SecureServer = class {
|
|
|
557
796
|
clientsById = /* @__PURE__ */ new Map();
|
|
558
797
|
clientIdBySocket = /* @__PURE__ */ new Map();
|
|
559
798
|
customEventHandlers = /* @__PURE__ */ new Map();
|
|
799
|
+
streamEventHandlers = /* @__PURE__ */ new Map();
|
|
560
800
|
connectionHandlers = /* @__PURE__ */ new Set();
|
|
561
801
|
disconnectHandlers = /* @__PURE__ */ new Set();
|
|
562
802
|
readyHandlers = /* @__PURE__ */ new Set();
|
|
@@ -567,6 +807,7 @@ var SecureServer = class {
|
|
|
567
807
|
sharedSecretBySocket = /* @__PURE__ */ new WeakMap();
|
|
568
808
|
encryptionKeyBySocket = /* @__PURE__ */ new WeakMap();
|
|
569
809
|
pendingPayloadsBySocket = /* @__PURE__ */ new WeakMap();
|
|
810
|
+
incomingStreamsBySocket = /* @__PURE__ */ new WeakMap();
|
|
570
811
|
pendingRpcRequestsBySocket = /* @__PURE__ */ new WeakMap();
|
|
571
812
|
heartbeatStateBySocket = /* @__PURE__ */ new WeakMap();
|
|
572
813
|
roomMembersByName = /* @__PURE__ */ new Map();
|
|
@@ -715,6 +956,38 @@ var SecureServer = class {
|
|
|
715
956
|
}
|
|
716
957
|
return this;
|
|
717
958
|
}
|
|
959
|
+
onStream(event, handler) {
|
|
960
|
+
try {
|
|
961
|
+
if (isReservedEmitEvent(event)) {
|
|
962
|
+
throw new Error(`The event "${event}" is reserved and cannot be used as a stream event.`);
|
|
963
|
+
}
|
|
964
|
+
const listeners = this.streamEventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
965
|
+
listeners.add(handler);
|
|
966
|
+
this.streamEventHandlers.set(event, listeners);
|
|
967
|
+
} catch (error) {
|
|
968
|
+
this.notifyError(
|
|
969
|
+
normalizeToError(error, "Failed to register server stream handler.")
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
return this;
|
|
973
|
+
}
|
|
974
|
+
offStream(event, handler) {
|
|
975
|
+
try {
|
|
976
|
+
const listeners = this.streamEventHandlers.get(event);
|
|
977
|
+
if (!listeners) {
|
|
978
|
+
return this;
|
|
979
|
+
}
|
|
980
|
+
listeners.delete(handler);
|
|
981
|
+
if (listeners.size === 0) {
|
|
982
|
+
this.streamEventHandlers.delete(event);
|
|
983
|
+
}
|
|
984
|
+
} catch (error) {
|
|
985
|
+
this.notifyError(
|
|
986
|
+
normalizeToError(error, "Failed to remove server stream handler.")
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
return this;
|
|
990
|
+
}
|
|
718
991
|
use(middleware) {
|
|
719
992
|
try {
|
|
720
993
|
if (typeof middleware !== "function") {
|
|
@@ -790,6 +1063,40 @@ var SecureServer = class {
|
|
|
790
1063
|
return false;
|
|
791
1064
|
}
|
|
792
1065
|
}
|
|
1066
|
+
async emitStreamTo(clientId, event, source, options) {
|
|
1067
|
+
try {
|
|
1068
|
+
if (isReservedEmitEvent(event)) {
|
|
1069
|
+
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
1070
|
+
}
|
|
1071
|
+
const client = this.clientsById.get(clientId);
|
|
1072
|
+
if (!client) {
|
|
1073
|
+
throw new Error(`Client with id ${clientId} was not found.`);
|
|
1074
|
+
}
|
|
1075
|
+
if (!this.isClientHandshakeReady(client.socket)) {
|
|
1076
|
+
throw new Error(
|
|
1077
|
+
`Cannot stream event "${event}" before secure handshake completion for client ${client.id}.`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
return await transmitChunkedStreamFrames(
|
|
1081
|
+
event,
|
|
1082
|
+
source,
|
|
1083
|
+
options,
|
|
1084
|
+
async (framePayload) => {
|
|
1085
|
+
await this.sendEncryptedEnvelope(client.socket, {
|
|
1086
|
+
event: INTERNAL_STREAM_FRAME_EVENT,
|
|
1087
|
+
data: framePayload
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
);
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
const normalizedError = normalizeToError(
|
|
1093
|
+
error,
|
|
1094
|
+
`Failed to emit chunked stream event "${event}" to client ${clientId}.`
|
|
1095
|
+
);
|
|
1096
|
+
this.notifyError(normalizedError);
|
|
1097
|
+
throw normalizedError;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
793
1100
|
to(room) {
|
|
794
1101
|
const normalizedRoom = this.normalizeRoomName(room);
|
|
795
1102
|
return {
|
|
@@ -822,6 +1129,10 @@ var SecureServer = class {
|
|
|
822
1129
|
client.socket,
|
|
823
1130
|
new Error("Server closed before ACK response was received.")
|
|
824
1131
|
);
|
|
1132
|
+
this.cleanupIncomingStreamsForSocket(
|
|
1133
|
+
client.socket,
|
|
1134
|
+
"Server closed before stream transfer completed."
|
|
1135
|
+
);
|
|
825
1136
|
this.middlewareMetadataBySocket.delete(client.socket);
|
|
826
1137
|
if (client.socket.readyState === WebSocket.OPEN || client.socket.readyState === WebSocket.CONNECTING) {
|
|
827
1138
|
client.socket.close(code, reason);
|
|
@@ -1365,6 +1676,10 @@ var SecureServer = class {
|
|
|
1365
1676
|
await this.handleRpcRequest(client, decryptedEnvelope.data);
|
|
1366
1677
|
return;
|
|
1367
1678
|
}
|
|
1679
|
+
if (decryptedEnvelope.event === INTERNAL_STREAM_FRAME_EVENT) {
|
|
1680
|
+
this.handleIncomingStreamFrame(client, decryptedEnvelope.data);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1368
1683
|
if (decryptedEnvelope.event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
1369
1684
|
this.notifyError(
|
|
1370
1685
|
new Error(
|
|
@@ -1406,6 +1721,10 @@ var SecureServer = class {
|
|
|
1406
1721
|
this.pendingRpcRequestsBySocket.delete(client.socket);
|
|
1407
1722
|
this.heartbeatStateBySocket.delete(client.socket);
|
|
1408
1723
|
this.middlewareMetadataBySocket.delete(client.socket);
|
|
1724
|
+
this.cleanupIncomingStreamsForSocket(
|
|
1725
|
+
client.socket,
|
|
1726
|
+
`Client ${client.id} disconnected before stream transfer completed.`
|
|
1727
|
+
);
|
|
1409
1728
|
const decodedReason = decodeCloseReason(reason);
|
|
1410
1729
|
for (const handler of this.disconnectHandlers) {
|
|
1411
1730
|
try {
|
|
@@ -1451,6 +1770,197 @@ var SecureServer = class {
|
|
|
1451
1770
|
}
|
|
1452
1771
|
}
|
|
1453
1772
|
}
|
|
1773
|
+
getOrCreateIncomingServerStreams(socket) {
|
|
1774
|
+
const existingStreams = this.incomingStreamsBySocket.get(socket);
|
|
1775
|
+
if (existingStreams) {
|
|
1776
|
+
return existingStreams;
|
|
1777
|
+
}
|
|
1778
|
+
const streamMap = /* @__PURE__ */ new Map();
|
|
1779
|
+
this.incomingStreamsBySocket.set(socket, streamMap);
|
|
1780
|
+
return streamMap;
|
|
1781
|
+
}
|
|
1782
|
+
cleanupIncomingStreamsForSocket(socket, reason) {
|
|
1783
|
+
const streamMap = this.incomingStreamsBySocket.get(socket);
|
|
1784
|
+
if (!streamMap) {
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
for (const streamState of streamMap.values()) {
|
|
1788
|
+
streamState.stream.destroy(new Error(reason));
|
|
1789
|
+
}
|
|
1790
|
+
streamMap.clear();
|
|
1791
|
+
this.incomingStreamsBySocket.delete(socket);
|
|
1792
|
+
}
|
|
1793
|
+
abortIncomingServerStream(socket, streamId, reason) {
|
|
1794
|
+
const streamMap = this.incomingStreamsBySocket.get(socket);
|
|
1795
|
+
if (!streamMap) {
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
const streamState = streamMap.get(streamId);
|
|
1799
|
+
if (!streamState) {
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
streamState.stream.destroy(new Error(reason));
|
|
1803
|
+
streamMap.delete(streamId);
|
|
1804
|
+
if (streamMap.size === 0) {
|
|
1805
|
+
this.incomingStreamsBySocket.delete(socket);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
dispatchServerStreamEvent(event, stream, info, client) {
|
|
1809
|
+
const handlers = this.streamEventHandlers.get(event);
|
|
1810
|
+
if (!handlers || handlers.size === 0) {
|
|
1811
|
+
stream.resume();
|
|
1812
|
+
this.notifyError(
|
|
1813
|
+
new Error(
|
|
1814
|
+
`No stream handler is registered for event "${event}" on server client ${client.id}.`
|
|
1815
|
+
)
|
|
1816
|
+
);
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
for (const handler of handlers) {
|
|
1820
|
+
try {
|
|
1821
|
+
const handlerResult = handler(stream, info, client);
|
|
1822
|
+
if (isPromiseLike(handlerResult)) {
|
|
1823
|
+
void Promise.resolve(handlerResult).catch((error) => {
|
|
1824
|
+
this.notifyError(
|
|
1825
|
+
normalizeToError(
|
|
1826
|
+
error,
|
|
1827
|
+
`Server stream handler failed for event ${event}.`
|
|
1828
|
+
)
|
|
1829
|
+
);
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
} catch (error) {
|
|
1833
|
+
this.notifyError(
|
|
1834
|
+
normalizeToError(
|
|
1835
|
+
error,
|
|
1836
|
+
`Server stream handler failed for event ${event}.`
|
|
1837
|
+
)
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
handleIncomingStreamStartFrame(client, framePayload) {
|
|
1843
|
+
if (isReservedEmitEvent(framePayload.event)) {
|
|
1844
|
+
throw new Error(
|
|
1845
|
+
`Reserved event "${framePayload.event}" cannot be used for stream transport.`
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
const incomingStreams = this.getOrCreateIncomingServerStreams(client.socket);
|
|
1849
|
+
if (incomingStreams.has(framePayload.streamId)) {
|
|
1850
|
+
throw new Error(
|
|
1851
|
+
`Stream ${framePayload.streamId} already exists for client ${client.id}.`
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
const stream = new PassThrough();
|
|
1855
|
+
const streamInfo = {
|
|
1856
|
+
streamId: framePayload.streamId,
|
|
1857
|
+
event: framePayload.event,
|
|
1858
|
+
startedAt: Date.now(),
|
|
1859
|
+
...framePayload.metadata !== void 0 ? { metadata: framePayload.metadata } : {},
|
|
1860
|
+
...framePayload.totalBytes !== void 0 ? { totalBytes: framePayload.totalBytes } : {}
|
|
1861
|
+
};
|
|
1862
|
+
incomingStreams.set(framePayload.streamId, {
|
|
1863
|
+
info: streamInfo,
|
|
1864
|
+
stream,
|
|
1865
|
+
expectedChunkIndex: 0,
|
|
1866
|
+
receivedBytes: 0
|
|
1867
|
+
});
|
|
1868
|
+
this.dispatchServerStreamEvent(framePayload.event, stream, streamInfo, client);
|
|
1869
|
+
}
|
|
1870
|
+
handleIncomingStreamChunkFrame(client, framePayload) {
|
|
1871
|
+
const incomingStreams = this.incomingStreamsBySocket.get(client.socket);
|
|
1872
|
+
const streamState = incomingStreams?.get(framePayload.streamId);
|
|
1873
|
+
if (!incomingStreams || !streamState) {
|
|
1874
|
+
throw new Error(
|
|
1875
|
+
`Stream ${framePayload.streamId} is unknown for client ${client.id}.`
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
if (framePayload.index !== streamState.expectedChunkIndex) {
|
|
1879
|
+
throw new Error(
|
|
1880
|
+
`Out-of-order chunk index for stream ${framePayload.streamId}. Expected ${streamState.expectedChunkIndex}, received ${framePayload.index}.`
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
const chunkBuffer = decodeBase64ToBuffer(
|
|
1884
|
+
framePayload.payload,
|
|
1885
|
+
`Stream chunk payload (${framePayload.streamId})`
|
|
1886
|
+
);
|
|
1887
|
+
if (chunkBuffer.length !== framePayload.byteLength) {
|
|
1888
|
+
throw new Error(
|
|
1889
|
+
`Stream ${framePayload.streamId} byteLength mismatch. Expected ${framePayload.byteLength}, received ${chunkBuffer.length}.`
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
streamState.expectedChunkIndex += 1;
|
|
1893
|
+
streamState.receivedBytes += chunkBuffer.length;
|
|
1894
|
+
streamState.stream.write(chunkBuffer);
|
|
1895
|
+
}
|
|
1896
|
+
handleIncomingStreamEndFrame(client, framePayload) {
|
|
1897
|
+
const incomingStreams = this.incomingStreamsBySocket.get(client.socket);
|
|
1898
|
+
const streamState = incomingStreams?.get(framePayload.streamId);
|
|
1899
|
+
if (!incomingStreams || !streamState) {
|
|
1900
|
+
throw new Error(
|
|
1901
|
+
`Stream ${framePayload.streamId} is unknown for client ${client.id}.`
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
if (framePayload.chunkCount !== streamState.expectedChunkIndex) {
|
|
1905
|
+
throw new Error(
|
|
1906
|
+
`Stream ${framePayload.streamId} chunkCount mismatch. Expected ${streamState.expectedChunkIndex}, received ${framePayload.chunkCount}.`
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1909
|
+
if (framePayload.totalBytes !== streamState.receivedBytes) {
|
|
1910
|
+
throw new Error(
|
|
1911
|
+
`Stream ${framePayload.streamId} totalBytes mismatch. Expected ${streamState.receivedBytes}, received ${framePayload.totalBytes}.`
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
if (streamState.info.totalBytes !== void 0 && streamState.info.totalBytes !== streamState.receivedBytes) {
|
|
1915
|
+
throw new Error(
|
|
1916
|
+
`Stream ${framePayload.streamId} violated announced totalBytes (${streamState.info.totalBytes}).`
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
streamState.stream.end();
|
|
1920
|
+
incomingStreams.delete(framePayload.streamId);
|
|
1921
|
+
if (incomingStreams.size === 0) {
|
|
1922
|
+
this.incomingStreamsBySocket.delete(client.socket);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
handleIncomingStreamAbortFrame(client, framePayload) {
|
|
1926
|
+
this.abortIncomingServerStream(
|
|
1927
|
+
client.socket,
|
|
1928
|
+
framePayload.streamId,
|
|
1929
|
+
framePayload.reason
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
handleIncomingStreamFrame(client, data) {
|
|
1933
|
+
let framePayload = null;
|
|
1934
|
+
try {
|
|
1935
|
+
framePayload = parseStreamFramePayload(data);
|
|
1936
|
+
if (framePayload.type === "start") {
|
|
1937
|
+
this.handleIncomingStreamStartFrame(client, framePayload);
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
if (framePayload.type === "chunk") {
|
|
1941
|
+
this.handleIncomingStreamChunkFrame(client, framePayload);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
if (framePayload.type === "end") {
|
|
1945
|
+
this.handleIncomingStreamEndFrame(client, framePayload);
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
this.handleIncomingStreamAbortFrame(client, framePayload);
|
|
1949
|
+
} catch (error) {
|
|
1950
|
+
const normalizedError = normalizeToError(
|
|
1951
|
+
error,
|
|
1952
|
+
`Failed to process incoming stream frame for client ${client.id}.`
|
|
1953
|
+
);
|
|
1954
|
+
if (framePayload) {
|
|
1955
|
+
this.abortIncomingServerStream(
|
|
1956
|
+
client.socket,
|
|
1957
|
+
framePayload.streamId,
|
|
1958
|
+
normalizedError.message
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
this.notifyError(normalizedError);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1454
1964
|
async executeServerMiddleware(context) {
|
|
1455
1965
|
if (this.middlewareHandlers.length === 0) {
|
|
1456
1966
|
return;
|
|
@@ -1879,6 +2389,9 @@ var SecureServer = class {
|
|
|
1879
2389
|
}
|
|
1880
2390
|
return this.emitTo(clientId, event, data, callbackOrOptions ?? {});
|
|
1881
2391
|
},
|
|
2392
|
+
emitStream: (event, source, options) => {
|
|
2393
|
+
return this.emitStreamTo(clientId, event, source, options);
|
|
2394
|
+
},
|
|
1882
2395
|
join: (room) => this.joinClientToRoom(clientId, room),
|
|
1883
2396
|
leave: (room) => this.leaveClientFromRoom(clientId, room),
|
|
1884
2397
|
leaveAll: () => this.leaveClientFromAllRooms(clientId)
|
|
@@ -2039,6 +2552,7 @@ var SecureClient = class {
|
|
|
2039
2552
|
reconnectTimer = null;
|
|
2040
2553
|
isManualDisconnectRequested = false;
|
|
2041
2554
|
customEventHandlers = /* @__PURE__ */ new Map();
|
|
2555
|
+
streamEventHandlers = /* @__PURE__ */ new Map();
|
|
2042
2556
|
connectHandlers = /* @__PURE__ */ new Set();
|
|
2043
2557
|
disconnectHandlers = /* @__PURE__ */ new Set();
|
|
2044
2558
|
readyHandlers = /* @__PURE__ */ new Set();
|
|
@@ -2046,6 +2560,7 @@ var SecureClient = class {
|
|
|
2046
2560
|
handshakeState = null;
|
|
2047
2561
|
pendingPayloadQueue = [];
|
|
2048
2562
|
pendingRpcRequests = /* @__PURE__ */ new Map();
|
|
2563
|
+
incomingStreams = /* @__PURE__ */ new Map();
|
|
2049
2564
|
sessionTicket = null;
|
|
2050
2565
|
get readyState() {
|
|
2051
2566
|
return this.socket?.readyState ?? null;
|
|
@@ -2155,6 +2670,38 @@ var SecureClient = class {
|
|
|
2155
2670
|
}
|
|
2156
2671
|
return this;
|
|
2157
2672
|
}
|
|
2673
|
+
onStream(event, handler) {
|
|
2674
|
+
try {
|
|
2675
|
+
if (isReservedEmitEvent(event)) {
|
|
2676
|
+
throw new Error(`The event "${event}" is reserved and cannot be used as a stream event.`);
|
|
2677
|
+
}
|
|
2678
|
+
const listeners = this.streamEventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
2679
|
+
listeners.add(handler);
|
|
2680
|
+
this.streamEventHandlers.set(event, listeners);
|
|
2681
|
+
} catch (error) {
|
|
2682
|
+
this.notifyError(
|
|
2683
|
+
normalizeToError(error, "Failed to register client stream handler.")
|
|
2684
|
+
);
|
|
2685
|
+
}
|
|
2686
|
+
return this;
|
|
2687
|
+
}
|
|
2688
|
+
offStream(event, handler) {
|
|
2689
|
+
try {
|
|
2690
|
+
const listeners = this.streamEventHandlers.get(event);
|
|
2691
|
+
if (!listeners) {
|
|
2692
|
+
return this;
|
|
2693
|
+
}
|
|
2694
|
+
listeners.delete(handler);
|
|
2695
|
+
if (listeners.size === 0) {
|
|
2696
|
+
this.streamEventHandlers.delete(event);
|
|
2697
|
+
}
|
|
2698
|
+
} catch (error) {
|
|
2699
|
+
this.notifyError(
|
|
2700
|
+
normalizeToError(error, "Failed to remove client stream handler.")
|
|
2701
|
+
);
|
|
2702
|
+
}
|
|
2703
|
+
return this;
|
|
2704
|
+
}
|
|
2158
2705
|
emit(event, data, callbackOrOptions, maybeCallback) {
|
|
2159
2706
|
const ackArgs = resolveAckArguments(callbackOrOptions, maybeCallback);
|
|
2160
2707
|
try {
|
|
@@ -2200,6 +2747,39 @@ var SecureClient = class {
|
|
|
2200
2747
|
return false;
|
|
2201
2748
|
}
|
|
2202
2749
|
}
|
|
2750
|
+
async emitStream(event, source, options) {
|
|
2751
|
+
try {
|
|
2752
|
+
if (isReservedEmitEvent(event)) {
|
|
2753
|
+
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
2754
|
+
}
|
|
2755
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
2756
|
+
throw new Error("Client socket is not connected.");
|
|
2757
|
+
}
|
|
2758
|
+
if (!this.isHandshakeReady()) {
|
|
2759
|
+
throw new Error(
|
|
2760
|
+
`Cannot stream event "${event}" before secure handshake completion.`
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
return await transmitChunkedStreamFrames(
|
|
2764
|
+
event,
|
|
2765
|
+
source,
|
|
2766
|
+
options,
|
|
2767
|
+
async (framePayload) => {
|
|
2768
|
+
await this.sendEncryptedEnvelope({
|
|
2769
|
+
event: INTERNAL_STREAM_FRAME_EVENT,
|
|
2770
|
+
data: framePayload
|
|
2771
|
+
});
|
|
2772
|
+
}
|
|
2773
|
+
);
|
|
2774
|
+
} catch (error) {
|
|
2775
|
+
const normalizedError = normalizeToError(
|
|
2776
|
+
error,
|
|
2777
|
+
`Failed to emit chunked stream event "${event}".`
|
|
2778
|
+
);
|
|
2779
|
+
this.notifyError(normalizedError);
|
|
2780
|
+
throw normalizedError;
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2203
2783
|
resolveReconnectConfig(reconnectOptions) {
|
|
2204
2784
|
if (typeof reconnectOptions === "boolean") {
|
|
2205
2785
|
return {
|
|
@@ -2363,6 +2943,10 @@ var SecureClient = class {
|
|
|
2363
2943
|
void this.handleRpcRequest(decryptedEnvelope.data);
|
|
2364
2944
|
return;
|
|
2365
2945
|
}
|
|
2946
|
+
if (decryptedEnvelope.event === INTERNAL_STREAM_FRAME_EVENT) {
|
|
2947
|
+
this.handleIncomingStreamFrame(decryptedEnvelope.data);
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2366
2950
|
if (decryptedEnvelope.event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
2367
2951
|
this.handleSessionTicket(decryptedEnvelope.data);
|
|
2368
2952
|
return;
|
|
@@ -2377,6 +2961,9 @@ var SecureClient = class {
|
|
|
2377
2961
|
this.socket = null;
|
|
2378
2962
|
this.handshakeState = null;
|
|
2379
2963
|
this.pendingPayloadQueue = [];
|
|
2964
|
+
this.cleanupIncomingStreams(
|
|
2965
|
+
"Client disconnected before stream transfer completed."
|
|
2966
|
+
);
|
|
2380
2967
|
this.rejectPendingRpcRequests(
|
|
2381
2968
|
new Error("Client disconnected before ACK response was received.")
|
|
2382
2969
|
);
|
|
@@ -2423,6 +3010,151 @@ var SecureClient = class {
|
|
|
2423
3010
|
}
|
|
2424
3011
|
}
|
|
2425
3012
|
}
|
|
3013
|
+
cleanupIncomingStreams(reason) {
|
|
3014
|
+
for (const streamState of this.incomingStreams.values()) {
|
|
3015
|
+
streamState.stream.destroy(new Error(reason));
|
|
3016
|
+
}
|
|
3017
|
+
this.incomingStreams.clear();
|
|
3018
|
+
}
|
|
3019
|
+
abortIncomingClientStream(streamId, reason) {
|
|
3020
|
+
const streamState = this.incomingStreams.get(streamId);
|
|
3021
|
+
if (!streamState) {
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
streamState.stream.destroy(new Error(reason));
|
|
3025
|
+
this.incomingStreams.delete(streamId);
|
|
3026
|
+
}
|
|
3027
|
+
dispatchClientStreamEvent(event, stream, info) {
|
|
3028
|
+
const handlers = this.streamEventHandlers.get(event);
|
|
3029
|
+
if (!handlers || handlers.size === 0) {
|
|
3030
|
+
stream.resume();
|
|
3031
|
+
this.notifyError(
|
|
3032
|
+
new Error(`No stream handler is registered for event "${event}" on client.`)
|
|
3033
|
+
);
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
for (const handler of handlers) {
|
|
3037
|
+
try {
|
|
3038
|
+
const handlerResult = handler(stream, info);
|
|
3039
|
+
if (isPromiseLike(handlerResult)) {
|
|
3040
|
+
void Promise.resolve(handlerResult).catch((error) => {
|
|
3041
|
+
this.notifyError(
|
|
3042
|
+
normalizeToError(
|
|
3043
|
+
error,
|
|
3044
|
+
`Client stream handler failed for event ${event}.`
|
|
3045
|
+
)
|
|
3046
|
+
);
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
} catch (error) {
|
|
3050
|
+
this.notifyError(
|
|
3051
|
+
normalizeToError(error, `Client stream handler failed for event ${event}.`)
|
|
3052
|
+
);
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
handleIncomingClientStreamStartFrame(framePayload) {
|
|
3057
|
+
if (isReservedEmitEvent(framePayload.event)) {
|
|
3058
|
+
throw new Error(
|
|
3059
|
+
`Reserved event "${framePayload.event}" cannot be used for stream transport.`
|
|
3060
|
+
);
|
|
3061
|
+
}
|
|
3062
|
+
if (this.incomingStreams.has(framePayload.streamId)) {
|
|
3063
|
+
throw new Error(`Stream ${framePayload.streamId} already exists on client.`);
|
|
3064
|
+
}
|
|
3065
|
+
const stream = new PassThrough();
|
|
3066
|
+
const streamInfo = {
|
|
3067
|
+
streamId: framePayload.streamId,
|
|
3068
|
+
event: framePayload.event,
|
|
3069
|
+
startedAt: Date.now(),
|
|
3070
|
+
...framePayload.metadata !== void 0 ? { metadata: framePayload.metadata } : {},
|
|
3071
|
+
...framePayload.totalBytes !== void 0 ? { totalBytes: framePayload.totalBytes } : {}
|
|
3072
|
+
};
|
|
3073
|
+
this.incomingStreams.set(framePayload.streamId, {
|
|
3074
|
+
info: streamInfo,
|
|
3075
|
+
stream,
|
|
3076
|
+
expectedChunkIndex: 0,
|
|
3077
|
+
receivedBytes: 0
|
|
3078
|
+
});
|
|
3079
|
+
this.dispatchClientStreamEvent(framePayload.event, stream, streamInfo);
|
|
3080
|
+
}
|
|
3081
|
+
handleIncomingClientStreamChunkFrame(framePayload) {
|
|
3082
|
+
const streamState = this.incomingStreams.get(framePayload.streamId);
|
|
3083
|
+
if (!streamState) {
|
|
3084
|
+
throw new Error(`Stream ${framePayload.streamId} is unknown on client.`);
|
|
3085
|
+
}
|
|
3086
|
+
if (framePayload.index !== streamState.expectedChunkIndex) {
|
|
3087
|
+
throw new Error(
|
|
3088
|
+
`Out-of-order chunk index for stream ${framePayload.streamId}. Expected ${streamState.expectedChunkIndex}, received ${framePayload.index}.`
|
|
3089
|
+
);
|
|
3090
|
+
}
|
|
3091
|
+
const chunkBuffer = decodeBase64ToBuffer(
|
|
3092
|
+
framePayload.payload,
|
|
3093
|
+
`Stream chunk payload (${framePayload.streamId})`
|
|
3094
|
+
);
|
|
3095
|
+
if (chunkBuffer.length !== framePayload.byteLength) {
|
|
3096
|
+
throw new Error(
|
|
3097
|
+
`Stream ${framePayload.streamId} byteLength mismatch. Expected ${framePayload.byteLength}, received ${chunkBuffer.length}.`
|
|
3098
|
+
);
|
|
3099
|
+
}
|
|
3100
|
+
streamState.expectedChunkIndex += 1;
|
|
3101
|
+
streamState.receivedBytes += chunkBuffer.length;
|
|
3102
|
+
streamState.stream.write(chunkBuffer);
|
|
3103
|
+
}
|
|
3104
|
+
handleIncomingClientStreamEndFrame(framePayload) {
|
|
3105
|
+
const streamState = this.incomingStreams.get(framePayload.streamId);
|
|
3106
|
+
if (!streamState) {
|
|
3107
|
+
throw new Error(`Stream ${framePayload.streamId} is unknown on client.`);
|
|
3108
|
+
}
|
|
3109
|
+
if (framePayload.chunkCount !== streamState.expectedChunkIndex) {
|
|
3110
|
+
throw new Error(
|
|
3111
|
+
`Stream ${framePayload.streamId} chunkCount mismatch. Expected ${streamState.expectedChunkIndex}, received ${framePayload.chunkCount}.`
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
if (framePayload.totalBytes !== streamState.receivedBytes) {
|
|
3115
|
+
throw new Error(
|
|
3116
|
+
`Stream ${framePayload.streamId} totalBytes mismatch. Expected ${streamState.receivedBytes}, received ${framePayload.totalBytes}.`
|
|
3117
|
+
);
|
|
3118
|
+
}
|
|
3119
|
+
if (streamState.info.totalBytes !== void 0 && streamState.info.totalBytes !== streamState.receivedBytes) {
|
|
3120
|
+
throw new Error(
|
|
3121
|
+
`Stream ${framePayload.streamId} violated announced totalBytes (${streamState.info.totalBytes}).`
|
|
3122
|
+
);
|
|
3123
|
+
}
|
|
3124
|
+
streamState.stream.end();
|
|
3125
|
+
this.incomingStreams.delete(framePayload.streamId);
|
|
3126
|
+
}
|
|
3127
|
+
handleIncomingClientStreamAbortFrame(framePayload) {
|
|
3128
|
+
this.abortIncomingClientStream(framePayload.streamId, framePayload.reason);
|
|
3129
|
+
}
|
|
3130
|
+
handleIncomingStreamFrame(data) {
|
|
3131
|
+
let framePayload = null;
|
|
3132
|
+
try {
|
|
3133
|
+
framePayload = parseStreamFramePayload(data);
|
|
3134
|
+
if (framePayload.type === "start") {
|
|
3135
|
+
this.handleIncomingClientStreamStartFrame(framePayload);
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
3138
|
+
if (framePayload.type === "chunk") {
|
|
3139
|
+
this.handleIncomingClientStreamChunkFrame(framePayload);
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
if (framePayload.type === "end") {
|
|
3143
|
+
this.handleIncomingClientStreamEndFrame(framePayload);
|
|
3144
|
+
return;
|
|
3145
|
+
}
|
|
3146
|
+
this.handleIncomingClientStreamAbortFrame(framePayload);
|
|
3147
|
+
} catch (error) {
|
|
3148
|
+
const normalizedError = normalizeToError(
|
|
3149
|
+
error,
|
|
3150
|
+
"Failed to process incoming stream frame on client."
|
|
3151
|
+
);
|
|
3152
|
+
if (framePayload) {
|
|
3153
|
+
this.abortIncomingClientStream(framePayload.streamId, normalizedError.message);
|
|
3154
|
+
}
|
|
3155
|
+
this.notifyError(normalizedError);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
2426
3158
|
notifyConnect() {
|
|
2427
3159
|
for (const handler of this.connectHandlers) {
|
|
2428
3160
|
try {
|