dropspace 0.0.0-stage → 0.1.1

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.
Files changed (67) hide show
  1. package/README.md +17 -2
  2. package/dist/automerge/index.d.ts +108 -0
  3. package/dist/automerge/index.d.ts.map +1 -0
  4. package/dist/automerge/index.js +437 -0
  5. package/dist/automerge/index.js.map +1 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +372 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/client/channel.d.ts +153 -0
  11. package/dist/client/channel.d.ts.map +1 -0
  12. package/dist/client/channel.js +704 -0
  13. package/dist/client/channel.js.map +1 -0
  14. package/dist/client/client.d.ts +376 -0
  15. package/dist/client/client.d.ts.map +1 -0
  16. package/dist/client/client.js +1421 -0
  17. package/dist/client/client.js.map +1 -0
  18. package/dist/client/events.d.ts +14 -0
  19. package/dist/client/events.d.ts.map +1 -0
  20. package/dist/client/events.js +71 -0
  21. package/dist/client/events.js.map +1 -0
  22. package/dist/client/identity.d.ts +89 -0
  23. package/dist/client/identity.d.ts.map +1 -0
  24. package/dist/client/identity.js +274 -0
  25. package/dist/client/identity.js.map +1 -0
  26. package/dist/client/index.d.ts +8 -0
  27. package/dist/client/index.d.ts.map +1 -0
  28. package/dist/client/index.js +8 -0
  29. package/dist/client/index.js.map +1 -0
  30. package/dist/client/peer-connection.d.ts +190 -0
  31. package/dist/client/peer-connection.d.ts.map +1 -0
  32. package/dist/client/peer-connection.js +647 -0
  33. package/dist/client/peer-connection.js.map +1 -0
  34. package/dist/client/peer-selection.d.ts +70 -0
  35. package/dist/client/peer-selection.d.ts.map +1 -0
  36. package/dist/client/peer-selection.js +193 -0
  37. package/dist/client/peer-selection.js.map +1 -0
  38. package/dist/client/storage.d.ts +118 -0
  39. package/dist/client/storage.d.ts.map +1 -0
  40. package/dist/client/storage.js +463 -0
  41. package/dist/client/storage.js.map +1 -0
  42. package/dist/files/index.d.ts +140 -0
  43. package/dist/files/index.d.ts.map +1 -0
  44. package/dist/files/index.js +751 -0
  45. package/dist/files/index.js.map +1 -0
  46. package/dist/files/sha256.d.ts +24 -0
  47. package/dist/files/sha256.d.ts.map +1 -0
  48. package/dist/files/sha256.js +147 -0
  49. package/dist/files/sha256.js.map +1 -0
  50. package/dist/index.d.ts +4 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +4 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/protocol/envelope.d.ts +61 -0
  55. package/dist/protocol/envelope.d.ts.map +1 -0
  56. package/dist/protocol/envelope.js +268 -0
  57. package/dist/protocol/envelope.js.map +1 -0
  58. package/dist/protocol/framing.d.ts +69 -0
  59. package/dist/protocol/framing.d.ts.map +1 -0
  60. package/dist/protocol/framing.js +145 -0
  61. package/dist/protocol/framing.js.map +1 -0
  62. package/dist/protocol/index.d.ts +3 -0
  63. package/dist/protocol/index.d.ts.map +1 -0
  64. package/dist/protocol/index.js +3 -0
  65. package/dist/protocol/index.js.map +1 -0
  66. package/fixtures/envelopes.json +132 -0
  67. package/package.json +51 -4
@@ -0,0 +1,704 @@
1
+ import { encodeWireFrame, decodeWireFrame, encodeJsonPayload, decodeJsonPayload, WireFrameType, WIRE_PREFIX_BYTES, DEFAULT_MAX_FRAME_PAYLOAD_BYTES, DEFAULT_MAX_TOTAL_FRAME_BYTES, } from "../protocol/index.js";
2
+ import { TypedEventEmitter } from "./events.js";
3
+ export class MessagingError extends Error {
4
+ code;
5
+ constructor(message, code) {
6
+ super(message);
7
+ this.code = code;
8
+ this.name = "MessagingError";
9
+ }
10
+ }
11
+ let msgCounter = 0;
12
+ function nextMessageId(prefix = "msg") {
13
+ msgCounter = (msgCounter + 1) % 1_000_000_000;
14
+ return `${prefix}_${Date.now().toString(36)}_${msgCounter.toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
15
+ }
16
+ /**
17
+ * Reliable binary-first framing, chunking, backpressure queue, and optional application ACK engine
18
+ * over an underlying `RTCDataChannel` (Section 13).
19
+ *
20
+ * Important Transport Note:
21
+ * Even an ordered, reliable `RTCDataChannel` only guarantees SCTP in-order delivery within a single
22
+ * active transport association. Across reconnects or new `RTCPeerConnection` instances, in-flight
23
+ * SCTP packets can be lost or re-sent. Enable `ack: "received" | "persisted" | "processed"` when
24
+ * application-level delivery confirmation and deduplication across retries are required.
25
+ */
26
+ export class PeerChannelMultiplexer {
27
+ options;
28
+ events = new TypedEventEmitter();
29
+ dataChannel = null;
30
+ maxFramePayloadBytes;
31
+ maxTotalFrameBytes;
32
+ highWaterMark;
33
+ lowWaterMark;
34
+ maxQueueFrames;
35
+ maxQueueBytes;
36
+ maxReassemblyChunks;
37
+ maxReassemblyBytes;
38
+ reassemblyTimeoutMs;
39
+ dedupWindowMs;
40
+ maxDedupEntries;
41
+ highQueue = [];
42
+ interactiveQueue = [];
43
+ bulkQueue = [];
44
+ queuedBytes = 0;
45
+ reassemblyBytes = 0;
46
+ isFlushing = false;
47
+ backpressurePaused = false;
48
+ closed = false;
49
+ pendingAcks = new Map();
50
+ seenMessageIds = new Map();
51
+ partialAssemblies = new Map();
52
+ boundOnMessage;
53
+ boundOnBufferedAmountLow;
54
+ boundOnClose;
55
+ constructor(options) {
56
+ this.options = options;
57
+ this.maxFramePayloadBytes =
58
+ options.maxFramePayloadBytes ?? DEFAULT_MAX_FRAME_PAYLOAD_BYTES;
59
+ this.maxTotalFrameBytes =
60
+ options.maxTotalFrameBytes ?? DEFAULT_MAX_TOTAL_FRAME_BYTES;
61
+ this.highWaterMark = options.bufferedAmountHighWaterMark ?? 256 * 1024; // 256 KiB
62
+ this.lowWaterMark = options.bufferedAmountLowWaterMark ?? 64 * 1024; // 64 KiB
63
+ this.maxQueueFrames = options.maxSendQueueFrames ?? 2048;
64
+ this.maxQueueBytes = options.maxSendQueueBytes ?? 16 * 1024 * 1024; // 16 MiB
65
+ this.maxReassemblyChunks = options.maxReassemblyChunks ?? 2048;
66
+ this.maxReassemblyBytes = options.maxReassemblyBytes ?? 16 * 1024 * 1024; // 16 MiB
67
+ this.reassemblyTimeoutMs = options.reassemblyTimeoutMs ?? 30_000;
68
+ this.dedupWindowMs = options.dedupWindowMs ?? 120_000;
69
+ this.maxDedupEntries = options.maxDedupEntries ?? 4096;
70
+ this.boundOnMessage = (ev) => this.handleIncomingRaw(ev.data);
71
+ this.boundOnBufferedAmountLow = () => {
72
+ this.backpressurePaused = false;
73
+ this.events.emit("backpressureChange", {
74
+ bufferedAmount: this.dataChannel?.bufferedAmount ?? 0,
75
+ queuedBytes: this.queuedBytes,
76
+ paused: false,
77
+ });
78
+ void this.flushQueue();
79
+ };
80
+ this.boundOnClose = () => {
81
+ this.rejectQueuedFrames(new MessagingError("DataChannel closed while frames were queued", "CHANNEL_CLOSED"));
82
+ this.events.emit("close", undefined);
83
+ };
84
+ }
85
+ attachDataChannel(dc) {
86
+ this.detachDataChannel();
87
+ this.closed = false;
88
+ this.dataChannel = dc;
89
+ if ("binaryType" in dc) {
90
+ dc.binaryType = "arraybuffer";
91
+ }
92
+ dc.bufferedAmountLowThreshold = this.lowWaterMark;
93
+ dc.addEventListener("message", this.boundOnMessage);
94
+ dc.addEventListener("bufferedamountlow", this.boundOnBufferedAmountLow);
95
+ dc.addEventListener("close", this.boundOnClose);
96
+ if (dc.readyState === "open") {
97
+ void this.flushQueue();
98
+ }
99
+ }
100
+ detachDataChannel() {
101
+ if (!this.dataChannel)
102
+ return;
103
+ this.dataChannel.removeEventListener("message", this.boundOnMessage);
104
+ this.dataChannel.removeEventListener("bufferedamountlow", this.boundOnBufferedAmountLow);
105
+ this.dataChannel.removeEventListener("close", this.boundOnClose);
106
+ this.dataChannel = null;
107
+ }
108
+ get totalQueuedFrames() {
109
+ return this.highQueue.length + this.interactiveQueue.length + this.bulkQueue.length;
110
+ }
111
+ setMaxTotalFrameBytes(limitBytes) {
112
+ if (Number.isFinite(limitBytes) && limitBytes >= 16 * 1024) {
113
+ this.maxTotalFrameBytes = Math.floor(limitBytes);
114
+ }
115
+ }
116
+ getMaxTotalFrameBytes() {
117
+ return this.maxTotalFrameBytes;
118
+ }
119
+ getEffectiveChunkPayloadBytes(headerOverheadEstimate = 512) {
120
+ const budget = Math.max(1024, this.maxTotalFrameBytes - WIRE_PREFIX_BYTES - headerOverheadEstimate);
121
+ return Math.min(this.maxFramePayloadBytes, budget);
122
+ }
123
+ getQueueMetrics() {
124
+ return {
125
+ queuedFrames: this.totalQueuedFrames,
126
+ queuedBytes: this.queuedBytes,
127
+ bufferedAmount: this.dataChannel?.bufferedAmount ?? 0,
128
+ };
129
+ }
130
+ async sendBinary(payload, options = {}) {
131
+ return this.sendInternal(WireFrameType.MESSAGE_BINARY, payload, options);
132
+ }
133
+ async sendJson(value, options = {}) {
134
+ const bytes = encodeJsonPayload(value);
135
+ return this.sendInternal(WireFrameType.MESSAGE_JSON, bytes, options);
136
+ }
137
+ priorityForFrameType(frameType) {
138
+ if (frameType === WireFrameType.CONTROL_JSON || frameType === WireFrameType.TRANSFER_CONTROL) {
139
+ return "high";
140
+ }
141
+ if (frameType === WireFrameType.TRANSFER_CHUNK) {
142
+ return "bulk";
143
+ }
144
+ return "interactive";
145
+ }
146
+ async sendRawWireFrame(frameType, header, payload = new Uint8Array(0), signal) {
147
+ const priority = this.priorityForFrameType(frameType);
148
+ const chunkPayloadMax = this.getEffectiveChunkPayloadBytes();
149
+ // Chunk CRDT_SYNC (or MESSAGE_*) if payload + header + prefix would exceed maxTotalFrameBytes
150
+ if (frameType === WireFrameType.CRDT_SYNC &&
151
+ payload.byteLength > chunkPayloadMax) {
152
+ const totalChunks = Math.ceil(payload.byteLength / chunkPayloadMax);
153
+ for (let i = 0; i < totalChunks; i++) {
154
+ if (signal?.aborted) {
155
+ throw new MessagingError("Frame send aborted during chunking", "ABORTED");
156
+ }
157
+ const start = i * chunkPayloadMax;
158
+ const end = Math.min(payload.byteLength, start + chunkPayloadMax);
159
+ const slice = payload.subarray(start, end);
160
+ const chunkHeader = {
161
+ ...header,
162
+ chunkIndex: i,
163
+ totalChunks,
164
+ totalByteLength: payload.byteLength,
165
+ };
166
+ const encoded = encodeWireFrame(frameType, chunkHeader, slice, {
167
+ maxTotalFrameBytes: this.maxTotalFrameBytes,
168
+ });
169
+ await this.enqueueEncodedFrame(encoded, signal, undefined, priority);
170
+ }
171
+ return;
172
+ }
173
+ const encoded = encodeWireFrame(frameType, header, payload, {
174
+ maxTotalFrameBytes: this.maxTotalFrameBytes,
175
+ });
176
+ await this.enqueueEncodedFrame(encoded, signal, undefined, priority);
177
+ }
178
+ async sendInternal(frameType, payload, options) {
179
+ if (this.closed) {
180
+ throw new MessagingError("Channel multiplexer is closed", "CHANNEL_CLOSED");
181
+ }
182
+ if (options.signal?.aborted) {
183
+ throw new MessagingError("Message send aborted before start", "ABORTED");
184
+ }
185
+ const messageId = options.messageId ?? nextMessageId("msg");
186
+ const channel = options.channel ?? "default";
187
+ const ackMode = options.ack ? options.ack : undefined;
188
+ const maxRetries = options.maxRetries ?? (ackMode ? 2 : 0);
189
+ const ackTimeoutMs = options.ackTimeoutMs ?? options.timeoutMs ?? 5000;
190
+ const chunkPayloadMax = this.getEffectiveChunkPayloadBytes(options.viaHubDeviceId ? 1024 : 512);
191
+ const totalChunks = Math.max(1, Math.ceil(payload.byteLength / chunkPayloadMax));
192
+ const sendAllChunksOnce = async () => {
193
+ for (let i = 0; i < totalChunks; i++) {
194
+ if (options.signal?.aborted) {
195
+ throw new MessagingError("Message send aborted during chunking", "ABORTED");
196
+ }
197
+ const start = i * chunkPayloadMax;
198
+ const end = Math.min(payload.byteLength, start + chunkPayloadMax);
199
+ const slice = payload.subarray(start, end);
200
+ const header = {
201
+ messageId,
202
+ channel,
203
+ timestamp: Date.now(),
204
+ chunkIndex: i,
205
+ totalChunks,
206
+ totalByteLength: payload.byteLength,
207
+ ackRequired: Boolean(ackMode) && i === totalChunks - 1,
208
+ ackMode,
209
+ meta: options.meta,
210
+ };
211
+ let frameBytes = encodeWireFrame(frameType, header, slice, {
212
+ maxTotalFrameBytes: this.maxTotalFrameBytes,
213
+ });
214
+ if (options.viaHubDeviceId) {
215
+ // Wrap inside HUB_RELAY envelope
216
+ const relayHeader = {
217
+ messageId: `${messageId}_chunk_${i}`,
218
+ channel: "hub_relay",
219
+ timestamp: Date.now(),
220
+ sourceDeviceId: this.options.localDeviceId,
221
+ targetDeviceId: this.options.peerDeviceId,
222
+ spaceId: options.spaceId,
223
+ hopCount: 0,
224
+ maxHops: options.maxHops ?? 2,
225
+ };
226
+ frameBytes = encodeWireFrame(WireFrameType.HUB_RELAY, relayHeader, frameBytes, {
227
+ maxTotalFrameBytes: this.maxTotalFrameBytes,
228
+ });
229
+ }
230
+ await this.enqueueEncodedFrame(frameBytes, options.signal, options.timeoutMs, "interactive");
231
+ }
232
+ };
233
+ if (!ackMode) {
234
+ await sendAllChunksOnce();
235
+ return { messageId };
236
+ }
237
+ let attempt = 0;
238
+ while (true) {
239
+ attempt++;
240
+ const ackPromise = this.waitForAck(messageId, ackMode, ackTimeoutMs, options.signal);
241
+ try {
242
+ await sendAllChunksOnce();
243
+ const confirmed = await ackPromise;
244
+ return { messageId, ackSemantics: confirmed };
245
+ }
246
+ catch (err) {
247
+ if (options.signal?.aborted) {
248
+ throw new MessagingError("Send aborted while awaiting ACK", "ABORTED");
249
+ }
250
+ if (attempt > maxRetries) {
251
+ throw err;
252
+ }
253
+ }
254
+ }
255
+ }
256
+ waitForAck(messageId, expectedSemantics, timeoutMs, signal) {
257
+ return new Promise((resolve, reject) => {
258
+ const existing = this.pendingAcks.get(messageId);
259
+ if (existing) {
260
+ clearTimeout(existing.timer);
261
+ }
262
+ let onAbort;
263
+ const cleanup = () => {
264
+ if (onAbort && signal) {
265
+ signal.removeEventListener("abort", onAbort);
266
+ }
267
+ };
268
+ const timer = setTimeout(() => {
269
+ this.pendingAcks.delete(messageId);
270
+ cleanup();
271
+ reject(new MessagingError(`Timed out waiting ${timeoutMs}ms for '${expectedSemantics}' ACK on message '${messageId}'`, "ACK_TIMEOUT"));
272
+ }, timeoutMs);
273
+ if (signal) {
274
+ onAbort = () => {
275
+ clearTimeout(timer);
276
+ this.pendingAcks.delete(messageId);
277
+ reject(new MessagingError("ACK wait aborted by AbortSignal", "ABORTED"));
278
+ };
279
+ signal.addEventListener("abort", onAbort, { once: true });
280
+ }
281
+ this.pendingAcks.set(messageId, {
282
+ messageId,
283
+ expectedSemantics,
284
+ resolve: (sem) => {
285
+ cleanup();
286
+ resolve(sem);
287
+ },
288
+ reject: (err) => {
289
+ cleanup();
290
+ reject(err);
291
+ },
292
+ timer,
293
+ });
294
+ });
295
+ }
296
+ queueForPriority(priority) {
297
+ if (priority === "high")
298
+ return this.highQueue;
299
+ if (priority === "bulk")
300
+ return this.bulkQueue;
301
+ return this.interactiveQueue;
302
+ }
303
+ dequeueNextFrame() {
304
+ return (this.highQueue.shift() ??
305
+ this.interactiveQueue.shift() ??
306
+ this.bulkQueue.shift());
307
+ }
308
+ removeQueuedItem(queue, item) {
309
+ const idx = queue.indexOf(item);
310
+ if (idx !== -1) {
311
+ queue.splice(idx, 1);
312
+ this.queuedBytes = Math.max(0, this.queuedBytes - item.bytes.byteLength);
313
+ return true;
314
+ }
315
+ return false;
316
+ }
317
+ enqueueEncodedFrame(bytes, signal, timeoutMs, priority = "interactive") {
318
+ if (this.closed) {
319
+ return Promise.reject(new MessagingError("Cannot send frame on closed multiplexer", "CHANNEL_CLOSED"));
320
+ }
321
+ if (signal?.aborted) {
322
+ return Promise.reject(new MessagingError("Frame send aborted", "ABORTED"));
323
+ }
324
+ if (this.totalQueuedFrames >= this.maxQueueFrames ||
325
+ this.queuedBytes + bytes.byteLength > this.maxQueueBytes) {
326
+ return Promise.reject(new MessagingError(`Send queue limit exceeded (${this.totalQueuedFrames} frames, ${this.queuedBytes} bytes)`, "QUEUE_OVERFLOW"));
327
+ }
328
+ const targetQueue = this.queueForPriority(priority);
329
+ return new Promise((resolve, reject) => {
330
+ let timeoutHandle;
331
+ const item = {
332
+ bytes,
333
+ resolve: () => {
334
+ if (timeoutHandle)
335
+ clearTimeout(timeoutHandle);
336
+ if (item.onAbort && signal) {
337
+ signal.removeEventListener("abort", item.onAbort);
338
+ }
339
+ resolve();
340
+ },
341
+ reject: (err) => {
342
+ if (timeoutHandle)
343
+ clearTimeout(timeoutHandle);
344
+ if (item.onAbort && signal) {
345
+ signal.removeEventListener("abort", item.onAbort);
346
+ }
347
+ reject(err);
348
+ },
349
+ signal,
350
+ };
351
+ if (signal) {
352
+ item.onAbort = () => {
353
+ this.removeQueuedItem(targetQueue, item);
354
+ item.reject(new MessagingError("Frame aborted while in send queue", "ABORTED"));
355
+ };
356
+ signal.addEventListener("abort", item.onAbort, { once: true });
357
+ }
358
+ if (timeoutMs && timeoutMs > 0) {
359
+ timeoutHandle = setTimeout(() => {
360
+ this.removeQueuedItem(targetQueue, item);
361
+ item.reject(new MessagingError(`Frame send timed out after ${timeoutMs}ms`, "TIMEOUT"));
362
+ }, timeoutMs);
363
+ }
364
+ targetQueue.push(item);
365
+ this.queuedBytes += bytes.byteLength;
366
+ void this.flushQueue();
367
+ });
368
+ }
369
+ async flushQueue() {
370
+ if (this.isFlushing || this.closed)
371
+ return;
372
+ if (!this.dataChannel || this.dataChannel.readyState !== "open")
373
+ return;
374
+ this.isFlushing = true;
375
+ try {
376
+ while (this.totalQueuedFrames > 0 && this.dataChannel && this.dataChannel.readyState === "open") {
377
+ if (this.dataChannel.bufferedAmount >= this.highWaterMark) {
378
+ this.backpressurePaused = true;
379
+ this.events.emit("backpressureChange", {
380
+ bufferedAmount: this.dataChannel.bufferedAmount,
381
+ queuedBytes: this.queuedBytes,
382
+ paused: true,
383
+ });
384
+ // Wait until bufferedAmount drops below highWaterMark (either via bufferedamountlow event or micro-poll fallback)
385
+ await this.waitForBufferDrain();
386
+ if (!this.dataChannel || this.dataChannel.readyState !== "open") {
387
+ break;
388
+ }
389
+ }
390
+ const next = this.dequeueNextFrame();
391
+ if (!next)
392
+ break;
393
+ this.queuedBytes = Math.max(0, this.queuedBytes - next.bytes.byteLength);
394
+ if (next.signal?.aborted) {
395
+ next.reject(new MessagingError("Frame aborted before send", "ABORTED"));
396
+ continue;
397
+ }
398
+ try {
399
+ this.dataChannel.send(next.bytes);
400
+ next.resolve();
401
+ }
402
+ catch (err) {
403
+ next.reject(err instanceof Error
404
+ ? err
405
+ : new MessagingError(String(err), "CHANNEL_CLOSED"));
406
+ break;
407
+ }
408
+ }
409
+ }
410
+ finally {
411
+ this.isFlushing = false;
412
+ }
413
+ }
414
+ waitForBufferDrain() {
415
+ return new Promise((resolve) => {
416
+ if (!this.dataChannel || this.dataChannel.bufferedAmount <= this.lowWaterMark) {
417
+ this.backpressurePaused = false;
418
+ resolve();
419
+ return;
420
+ }
421
+ let resolved = false;
422
+ const done = () => {
423
+ if (resolved)
424
+ return;
425
+ resolved = true;
426
+ clearInterval(pollTimer);
427
+ this.dataChannel?.removeEventListener("bufferedamountlow", onLow);
428
+ this.dataChannel?.removeEventListener("close", onClose);
429
+ this.backpressurePaused = false;
430
+ resolve();
431
+ };
432
+ const onLow = () => done();
433
+ const onClose = () => done();
434
+ this.dataChannel.addEventListener("bufferedamountlow", onLow);
435
+ this.dataChannel.addEventListener("close", onClose);
436
+ const pollTimer = setInterval(() => {
437
+ if (!this.dataChannel ||
438
+ this.dataChannel.readyState !== "open" ||
439
+ this.dataChannel.bufferedAmount <= this.lowWaterMark) {
440
+ done();
441
+ }
442
+ }, 15);
443
+ });
444
+ }
445
+ handleIncomingRaw(rawData) {
446
+ let decoded;
447
+ try {
448
+ decoded = decodeWireFrame(rawData);
449
+ }
450
+ catch (err) {
451
+ this.events.emit("error", err instanceof Error ? err : new Error("Failed to decode wire frame"));
452
+ return;
453
+ }
454
+ this.options.onRawFrame?.(decoded);
455
+ switch (decoded.frameType) {
456
+ case WireFrameType.CONTROL_JSON: {
457
+ this.handleControlFrame(decoded);
458
+ break;
459
+ }
460
+ case WireFrameType.TRANSFER_CONTROL:
461
+ case WireFrameType.TRANSFER_CHUNK: {
462
+ this.events.emit("transferFrame", decoded);
463
+ break;
464
+ }
465
+ case WireFrameType.CRDT_SYNC: {
466
+ if ((decoded.header.totalChunks ?? 1) > 1) {
467
+ this.handleApplicationFrame(decoded);
468
+ }
469
+ else {
470
+ this.events.emit("crdtFrame", decoded);
471
+ }
472
+ break;
473
+ }
474
+ case WireFrameType.HUB_RELAY: {
475
+ this.events.emit("hubRelayFrame", decoded);
476
+ break;
477
+ }
478
+ case WireFrameType.MESSAGE_BINARY:
479
+ case WireFrameType.MESSAGE_JSON: {
480
+ this.handleApplicationFrame(decoded);
481
+ break;
482
+ }
483
+ }
484
+ }
485
+ handleControlFrame(frame) {
486
+ try {
487
+ const ctrl = decodeJsonPayload(frame.payload);
488
+ if (ctrl.type === "ack" && ctrl.ackForMessageId) {
489
+ const pending = this.pendingAcks.get(ctrl.ackForMessageId);
490
+ if (pending) {
491
+ const sem = ctrl.semantics ?? "received";
492
+ // Resolve if semantics satisfy expected level
493
+ if (pending.expectedSemantics === "received" ||
494
+ pending.expectedSemantics === sem ||
495
+ sem === "processed") {
496
+ clearTimeout(pending.timer);
497
+ this.pendingAcks.delete(ctrl.ackForMessageId);
498
+ pending.resolve(sem);
499
+ }
500
+ }
501
+ }
502
+ }
503
+ catch {
504
+ // Ignore malformed control payload
505
+ }
506
+ }
507
+ prunePartialAssemblies(now) {
508
+ for (const [msgId, assembly] of this.partialAssemblies.entries()) {
509
+ if (now - assembly.createdAt > this.reassemblyTimeoutMs) {
510
+ this.reassemblyBytes = Math.max(0, this.reassemblyBytes - assembly.receivedBytes);
511
+ this.partialAssemblies.delete(msgId);
512
+ }
513
+ }
514
+ }
515
+ handleApplicationFrame(frame, routedViaHub) {
516
+ const { header, payload, frameType } = frame;
517
+ const totalChunks = header.totalChunks ?? 1;
518
+ const chunkIndex = header.chunkIndex ?? 0;
519
+ if (totalChunks <= 1) {
520
+ if (frameType === WireFrameType.CRDT_SYNC) {
521
+ this.events.emit("crdtFrame", frame);
522
+ }
523
+ else {
524
+ this.deliverReassembledMessage(frameType, header, payload, routedViaHub);
525
+ }
526
+ return;
527
+ }
528
+ if (!Number.isInteger(totalChunks) ||
529
+ totalChunks < 1 ||
530
+ totalChunks > this.maxReassemblyChunks ||
531
+ !Number.isInteger(chunkIndex) ||
532
+ chunkIndex < 0 ||
533
+ chunkIndex >= totalChunks) {
534
+ this.events.emit("error", new MessagingError(`Invalid fragment index/count (${chunkIndex}/${totalChunks}) for '${header.messageId}'`, "MESSAGE_TOO_LARGE"));
535
+ return;
536
+ }
537
+ const now = Date.now();
538
+ this.prunePartialAssemblies(now);
539
+ let assembly = this.partialAssemblies.get(header.messageId);
540
+ if (!assembly) {
541
+ assembly = {
542
+ frameType,
543
+ header,
544
+ totalChunks,
545
+ chunks: new Map(),
546
+ receivedBytes: 0,
547
+ createdAt: now,
548
+ };
549
+ this.partialAssemblies.set(header.messageId, assembly);
550
+ }
551
+ if (!assembly.chunks.has(chunkIndex)) {
552
+ if (this.reassemblyBytes + payload.byteLength > this.maxReassemblyBytes) {
553
+ this.reassemblyBytes = Math.max(0, this.reassemblyBytes - assembly.receivedBytes);
554
+ this.partialAssemblies.delete(header.messageId);
555
+ this.events.emit("error", new MessagingError(`Reassembly buffer limit (${this.maxReassemblyBytes} bytes) exceeded`, "MESSAGE_TOO_LARGE"));
556
+ return;
557
+ }
558
+ const copy = new Uint8Array(payload.byteLength);
559
+ copy.set(payload);
560
+ assembly.chunks.set(chunkIndex, copy);
561
+ assembly.receivedBytes += copy.byteLength;
562
+ this.reassemblyBytes += copy.byteLength;
563
+ }
564
+ if (header.ackRequired) {
565
+ assembly.header = header;
566
+ }
567
+ if (assembly.chunks.size === totalChunks) {
568
+ this.partialAssemblies.delete(header.messageId);
569
+ this.reassemblyBytes = Math.max(0, this.reassemblyBytes - assembly.receivedBytes);
570
+ const fullBuffer = new Uint8Array(assembly.receivedBytes);
571
+ let offset = 0;
572
+ for (let i = 0; i < totalChunks; i++) {
573
+ const part = assembly.chunks.get(i);
574
+ if (!part)
575
+ return;
576
+ fullBuffer.set(part, offset);
577
+ offset += part.byteLength;
578
+ }
579
+ if (assembly.frameType === WireFrameType.CRDT_SYNC) {
580
+ this.events.emit("crdtFrame", {
581
+ frameType: WireFrameType.CRDT_SYNC,
582
+ header: assembly.header,
583
+ payload: fullBuffer,
584
+ });
585
+ }
586
+ else {
587
+ this.deliverReassembledMessage(assembly.frameType, assembly.header, fullBuffer, routedViaHub);
588
+ }
589
+ }
590
+ }
591
+ deliverHubUnwrappedFrame(innerFrame, hubDeviceId) {
592
+ if (innerFrame.frameType === WireFrameType.MESSAGE_BINARY ||
593
+ innerFrame.frameType === WireFrameType.MESSAGE_JSON) {
594
+ this.handleApplicationFrame(innerFrame, hubDeviceId);
595
+ }
596
+ else if (innerFrame.frameType === WireFrameType.TRANSFER_CONTROL ||
597
+ innerFrame.frameType === WireFrameType.TRANSFER_CHUNK) {
598
+ this.events.emit("transferFrame", innerFrame);
599
+ }
600
+ else if (innerFrame.frameType === WireFrameType.CRDT_SYNC) {
601
+ this.events.emit("crdtFrame", innerFrame);
602
+ }
603
+ else if (innerFrame.frameType === WireFrameType.CONTROL_JSON) {
604
+ this.handleControlFrame(innerFrame);
605
+ }
606
+ }
607
+ deliverReassembledMessage(frameType, header, fullPayload, routedViaHub) {
608
+ const now = Date.now();
609
+ this.pruneDeduplicationMap(now);
610
+ const sendAck = async (semantics = "received") => {
611
+ if (!header.ackRequired)
612
+ return;
613
+ const ackPayload = encodeJsonPayload({
614
+ type: "ack",
615
+ ackForMessageId: header.messageId,
616
+ semantics,
617
+ });
618
+ const ackHeader = {
619
+ messageId: nextMessageId("ack"),
620
+ channel: header.channel,
621
+ timestamp: Date.now(),
622
+ correlationId: header.messageId,
623
+ };
624
+ try {
625
+ await this.sendRawWireFrame(WireFrameType.CONTROL_JSON, ackHeader, ackPayload);
626
+ }
627
+ catch {
628
+ // Best-effort ACK reply
629
+ }
630
+ };
631
+ // Deduplicate by messageId within window
632
+ const seenAt = this.seenMessageIds.get(header.messageId);
633
+ if (seenAt !== undefined && now - seenAt < this.dedupWindowMs) {
634
+ // Still send ACK if peer retried because previous ACK was lost
635
+ if (header.ackRequired) {
636
+ void sendAck(header.ackMode ?? "received");
637
+ }
638
+ return;
639
+ }
640
+ this.seenMessageIds.set(header.messageId, now);
641
+ // Automatically send "received" ACK immediately if requested mode is "received"
642
+ if (header.ackRequired && (!header.ackMode || header.ackMode === "received")) {
643
+ void sendAck("received");
644
+ }
645
+ let parsedJson;
646
+ if (frameType === WireFrameType.MESSAGE_JSON) {
647
+ try {
648
+ parsedJson = decodeJsonPayload(fullPayload);
649
+ }
650
+ catch {
651
+ parsedJson = undefined;
652
+ }
653
+ }
654
+ this.events.emit("message", {
655
+ peerDeviceId: header.sourceDeviceId ?? this.options.peerDeviceId,
656
+ channel: header.channel,
657
+ messageId: header.messageId,
658
+ frameType,
659
+ header,
660
+ binary: fullPayload,
661
+ json: parsedJson,
662
+ routedViaHub,
663
+ acknowledge: sendAck,
664
+ });
665
+ }
666
+ pruneDeduplicationMap(now) {
667
+ if (this.seenMessageIds.size <= this.maxDedupEntries)
668
+ return;
669
+ for (const [id, ts] of this.seenMessageIds.entries()) {
670
+ if (now - ts >= this.dedupWindowMs || this.seenMessageIds.size > this.maxDedupEntries) {
671
+ this.seenMessageIds.delete(id);
672
+ }
673
+ }
674
+ }
675
+ rejectQueuedFrames(err) {
676
+ for (const queue of [this.highQueue, this.interactiveQueue, this.bulkQueue]) {
677
+ while (queue.length > 0) {
678
+ const item = queue.shift();
679
+ item.reject(err);
680
+ }
681
+ }
682
+ this.queuedBytes = 0;
683
+ }
684
+ close() {
685
+ if (this.closed)
686
+ return;
687
+ this.closed = true;
688
+ this.detachDataChannel();
689
+ this.rejectQueuedFrames(new MessagingError("PeerChannelMultiplexer closed", "CHANNEL_CLOSED"));
690
+ for (const pending of this.pendingAcks.values()) {
691
+ clearTimeout(pending.timer);
692
+ pending.reject(new MessagingError("Channel closed while awaiting ACK", "CHANNEL_CLOSED"));
693
+ }
694
+ this.pendingAcks.clear();
695
+ this.partialAssemblies.clear();
696
+ this.reassemblyBytes = 0;
697
+ this.seenMessageIds.clear();
698
+ }
699
+ dispose() {
700
+ this.close();
701
+ this.events.removeAllListeners();
702
+ }
703
+ }
704
+ //# sourceMappingURL=channel.js.map