@affectively/aeon 1.3.0 → 5.0.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 (57) hide show
  1. package/LICENSE +5 -11
  2. package/README.md +90 -10
  3. package/dist/compression/index.cjs +20 -3
  4. package/dist/compression/index.cjs.map +1 -1
  5. package/dist/compression/index.js +20 -3
  6. package/dist/compression/index.js.map +1 -1
  7. package/dist/crypto/index.cjs +30 -0
  8. package/dist/crypto/index.cjs.map +1 -1
  9. package/dist/crypto/index.js +29 -1
  10. package/dist/crypto/index.js.map +1 -1
  11. package/dist/distributed/index.cjs +15 -8
  12. package/dist/distributed/index.cjs.map +1 -1
  13. package/dist/distributed/index.js +15 -8
  14. package/dist/distributed/index.js.map +1 -1
  15. package/dist/index.cjs +2923 -46
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.js +2879 -47
  18. package/dist/index.js.map +1 -1
  19. package/dist/optimization/index.cjs +6 -3
  20. package/dist/optimization/index.cjs.map +1 -1
  21. package/dist/optimization/index.js +6 -3
  22. package/dist/optimization/index.js.map +1 -1
  23. package/dist/persistence/index.cjs +91 -29
  24. package/dist/persistence/index.cjs.map +1 -1
  25. package/dist/persistence/index.js +91 -29
  26. package/dist/persistence/index.js.map +1 -1
  27. package/dist/presence/index.cjs.map +1 -1
  28. package/dist/presence/index.js.map +1 -1
  29. package/dist/versioning/index.cjs +4 -3
  30. package/dist/versioning/index.cjs.map +1 -1
  31. package/dist/versioning/index.js +4 -3
  32. package/dist/versioning/index.js.map +1 -1
  33. package/package.json +7 -8
  34. package/dist/compression/index.d.cts +0 -189
  35. package/dist/compression/index.d.ts +0 -189
  36. package/dist/core/index.d.cts +0 -216
  37. package/dist/core/index.d.ts +0 -216
  38. package/dist/crypto/index.d.cts +0 -446
  39. package/dist/crypto/index.d.ts +0 -446
  40. package/dist/distributed/index.d.cts +0 -1016
  41. package/dist/distributed/index.d.ts +0 -1016
  42. package/dist/index.d.cts +0 -12
  43. package/dist/index.d.ts +0 -12
  44. package/dist/offline/index.d.cts +0 -154
  45. package/dist/offline/index.d.ts +0 -154
  46. package/dist/optimization/index.d.cts +0 -347
  47. package/dist/optimization/index.d.ts +0 -347
  48. package/dist/persistence/index.d.cts +0 -63
  49. package/dist/persistence/index.d.ts +0 -63
  50. package/dist/presence/index.d.cts +0 -283
  51. package/dist/presence/index.d.ts +0 -283
  52. package/dist/types-B7gCpNX9.d.cts +0 -33
  53. package/dist/types-B7gCpNX9.d.ts +0 -33
  54. package/dist/utils/index.d.cts +0 -38
  55. package/dist/utils/index.d.ts +0 -38
  56. package/dist/versioning/index.d.cts +0 -537
  57. package/dist/versioning/index.d.ts +0 -537
package/dist/index.cjs CHANGED
@@ -57,12 +57,17 @@ var logger = {
57
57
  };
58
58
 
59
59
  // src/persistence/DashStorageAdapter.ts
60
+ var DEFAULT_RULE = {
61
+ urgency: "deferred",
62
+ debounce: 50,
63
+ maxBufferSize: 5e3,
64
+ readThrough: true
65
+ };
60
66
  var DashStorageAdapter = class {
61
67
  backend;
62
68
  syncClient;
63
- syncDebounceMs;
64
- maxPendingChanges;
65
- onSyncError;
69
+ rules;
70
+ hooks;
66
71
  pendingChanges = /* @__PURE__ */ new Map();
67
72
  syncTimer = null;
68
73
  syncInFlight = false;
@@ -70,11 +75,33 @@ var DashStorageAdapter = class {
70
75
  constructor(backend, options = {}) {
71
76
  this.backend = backend;
72
77
  this.syncClient = options.syncClient ?? null;
73
- this.syncDebounceMs = options.syncDebounceMs ?? 50;
74
- this.maxPendingChanges = options.maxPendingChanges ?? 5e3;
75
- this.onSyncError = options.onSyncError ?? null;
78
+ this.hooks = options.hooks ?? {};
79
+ const defaultRule = {
80
+ ...DEFAULT_RULE,
81
+ ...options.rules?.default ?? {}
82
+ };
83
+ if (options.syncDebounceMs !== void 0)
84
+ defaultRule.debounce = options.syncDebounceMs;
85
+ if (options.maxPendingChanges !== void 0)
86
+ defaultRule.maxBufferSize = options.maxPendingChanges;
87
+ if (options.onSyncError && !this.hooks.onSyncError)
88
+ this.hooks.onSyncError = options.onSyncError;
89
+ this.rules = {
90
+ default: defaultRule,
91
+ prefixes: options.rules?.prefixes ?? {}
92
+ };
76
93
  }
94
+ /**
95
+ * Get an item, checking the write pool (pending changes) first for consistency.
96
+ */
77
97
  async getItem(key) {
98
+ const rule = this.getRuleForKey(key);
99
+ if (rule.readThrough !== false) {
100
+ const pending = this.pendingChanges.get(key);
101
+ if (pending) {
102
+ return pending.operation === "delete" ? null : pending.value ?? null;
103
+ }
104
+ }
78
105
  return await this.backend.get(key);
79
106
  }
80
107
  async setItem(key, value) {
@@ -109,34 +136,45 @@ var DashStorageAdapter = class {
109
136
  }
110
137
  trackChange(change) {
111
138
  this.pendingChanges.set(change.key, change);
112
- this.enforcePendingLimit();
113
- this.scheduleSync();
114
- }
115
- enforcePendingLimit() {
116
- if (this.pendingChanges.size <= this.maxPendingChanges) {
139
+ const rule = this.getRuleForKey(change.key);
140
+ if (rule.urgency === "realtime") {
141
+ void this.performSync();
117
142
  return;
118
143
  }
119
- const sorted = Array.from(this.pendingChanges.values()).sort(
120
- (a, b) => a.timestamp - b.timestamp
121
- );
122
- const overflow = this.pendingChanges.size - this.maxPendingChanges;
123
- for (let i = 0; i < overflow; i++) {
124
- const toDrop = sorted[i];
125
- if (toDrop) {
126
- this.pendingChanges.delete(toDrop.key);
127
- }
144
+ const maxSize = rule.maxBufferSize ?? 5e3;
145
+ if (this.pendingChanges.size >= maxSize) {
146
+ this.hooks.onBufferOverflow?.(
147
+ this.getPrefixMatch(change.key) || "default",
148
+ this.pendingChanges.size,
149
+ maxSize
150
+ );
151
+ void this.performSync();
152
+ return;
128
153
  }
154
+ this.scheduleSync(rule);
129
155
  }
130
- scheduleSync() {
131
- if (!this.syncClient) {
132
- return;
156
+ getRuleForKey(key) {
157
+ const prefix = this.getPrefixMatch(key);
158
+ return (prefix ? this.rules.prefixes?.[prefix] : this.rules.default) ?? this.rules.default;
159
+ }
160
+ getPrefixMatch(key) {
161
+ if (!this.rules.prefixes) {
162
+ return null;
133
163
  }
134
- if (this.syncTimer) {
135
- clearTimeout(this.syncTimer);
164
+ const prefixes = Object.keys(this.rules.prefixes).sort(
165
+ (a, b) => b.length - a.length
166
+ );
167
+ return prefixes.find((p) => key.startsWith(p)) ?? null;
168
+ }
169
+ scheduleSync(rule) {
170
+ if (!this.syncClient || this.syncTimer) {
171
+ return;
136
172
  }
173
+ const debounceMs = typeof rule.debounce === "string" ? this.parseInterval(rule.debounce) : rule.debounce ?? 50;
137
174
  this.syncTimer = setTimeout(() => {
175
+ this.syncTimer = null;
138
176
  void this.performSync();
139
- }, this.syncDebounceMs);
177
+ }, debounceMs);
140
178
  }
141
179
  async performSync() {
142
180
  if (!this.syncClient) {
@@ -156,6 +194,8 @@ var DashStorageAdapter = class {
156
194
  this.syncInFlight = true;
157
195
  try {
158
196
  await this.syncClient.syncChanges(changes);
197
+ this.hooks.onSync?.(changes);
198
+ this.hooks.onFlush?.(changes.length);
159
199
  } catch (error) {
160
200
  for (const change of changes) {
161
201
  const current = this.pendingChanges.get(change.key);
@@ -163,19 +203,39 @@ var DashStorageAdapter = class {
163
203
  this.pendingChanges.set(change.key, change);
164
204
  }
165
205
  }
166
- if (this.onSyncError) {
206
+ if (this.hooks.onSyncError) {
167
207
  const normalizedError = error instanceof Error ? error : new Error(String(error));
168
- this.onSyncError(normalizedError, changes);
208
+ this.hooks.onSyncError(normalizedError, changes);
169
209
  }
170
210
  } finally {
171
211
  this.syncInFlight = false;
172
212
  const rerun = this.syncPending || this.pendingChanges.size > 0;
173
213
  this.syncPending = false;
174
214
  if (rerun) {
175
- this.scheduleSync();
215
+ this.scheduleSync(this.rules.default);
176
216
  }
177
217
  }
178
218
  }
219
+ parseInterval(input) {
220
+ const match = input.match(/^(\d+)(ms|s|m|h|d)$/);
221
+ if (!match) return 50;
222
+ const value = parseInt(match[1], 10);
223
+ const unit = match[2];
224
+ switch (unit) {
225
+ case "ms":
226
+ return value;
227
+ case "s":
228
+ return value * 1e3;
229
+ case "m":
230
+ return value * 60 * 1e3;
231
+ case "h":
232
+ return value * 60 * 60 * 1e3;
233
+ case "d":
234
+ return value * 24 * 60 * 60 * 1e3;
235
+ default:
236
+ return 50;
237
+ }
238
+ }
179
239
  };
180
240
 
181
241
  // src/persistence/InMemoryStorageAdapter.ts
@@ -190,6 +250,8 @@ var InMemoryStorageAdapter = class {
190
250
  removeItem(key) {
191
251
  this.store.delete(key);
192
252
  }
253
+ async flushSync() {
254
+ }
193
255
  clear() {
194
256
  this.store.clear();
195
257
  }
@@ -353,10 +415,11 @@ var SchemaVersionManager = class {
353
415
  */
354
416
  parseVersion(versionString) {
355
417
  const parts = versionString.split(".").map(Number);
418
+ const safeInt = (v) => v !== void 0 && Number.isFinite(v) ? v : 0;
356
419
  return {
357
- major: parts[0] || 0,
358
- minor: parts[1] || 0,
359
- patch: parts[2] || 0,
420
+ major: safeInt(parts[0]),
421
+ minor: safeInt(parts[1]),
422
+ patch: safeInt(parts[2]),
360
423
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
361
424
  description: "",
362
425
  breaking: false
@@ -1323,7 +1386,8 @@ var MigrationTracker = class _MigrationTracker {
1323
1386
  return (hash >>> 0).toString(16).padStart(8, "0");
1324
1387
  }
1325
1388
  };
1326
- var SyncCoordinator = class extends eventemitter3.EventEmitter {
1389
+ var SyncCoordinator = class _SyncCoordinator extends eventemitter3.EventEmitter {
1390
+ static MAX_SYNC_EVENTS = 1e4;
1327
1391
  nodes = /* @__PURE__ */ new Map();
1328
1392
  sessions = /* @__PURE__ */ new Map();
1329
1393
  syncEvents = [];
@@ -1336,6 +1400,12 @@ var SyncCoordinator = class extends eventemitter3.EventEmitter {
1336
1400
  constructor() {
1337
1401
  super();
1338
1402
  }
1403
+ addSyncEvent(event) {
1404
+ this.syncEvents.push(event);
1405
+ if (this.syncEvents.length > _SyncCoordinator.MAX_SYNC_EVENTS) {
1406
+ this.syncEvents = this.syncEvents.slice(-_SyncCoordinator.MAX_SYNC_EVENTS);
1407
+ }
1408
+ }
1339
1409
  /**
1340
1410
  * Configure cryptographic provider for authenticated sync
1341
1411
  */
@@ -1375,7 +1445,7 @@ var SyncCoordinator = class extends eventemitter3.EventEmitter {
1375
1445
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1376
1446
  data: { did: nodeInfo.did, authenticated: true }
1377
1447
  };
1378
- this.syncEvents.push(event);
1448
+ this.addSyncEvent(event);
1379
1449
  this.emit("node-joined", node);
1380
1450
  logger.debug("[SyncCoordinator] Authenticated node registered", {
1381
1451
  nodeId: node.id,
@@ -1453,7 +1523,7 @@ var SyncCoordinator = class extends eventemitter3.EventEmitter {
1453
1523
  encryptionMode: session.encryptionMode
1454
1524
  }
1455
1525
  };
1456
- this.syncEvents.push(event);
1526
+ this.addSyncEvent(event);
1457
1527
  this.emit("sync-started", session);
1458
1528
  logger.debug("[SyncCoordinator] Authenticated sync session created", {
1459
1529
  sessionId: session.id,
@@ -1500,7 +1570,7 @@ var SyncCoordinator = class extends eventemitter3.EventEmitter {
1500
1570
  nodeId: node.id,
1501
1571
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1502
1572
  };
1503
- this.syncEvents.push(event);
1573
+ this.addSyncEvent(event);
1504
1574
  this.emit("node-joined", node);
1505
1575
  logger.debug("[SyncCoordinator] Node registered", {
1506
1576
  nodeId: node.id,
@@ -1523,7 +1593,7 @@ var SyncCoordinator = class extends eventemitter3.EventEmitter {
1523
1593
  nodeId,
1524
1594
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1525
1595
  };
1526
- this.syncEvents.push(event);
1596
+ this.addSyncEvent(event);
1527
1597
  this.emit("node-left", node);
1528
1598
  logger.debug("[SyncCoordinator] Node deregistered", { nodeId });
1529
1599
  }
@@ -1552,7 +1622,7 @@ var SyncCoordinator = class extends eventemitter3.EventEmitter {
1552
1622
  nodeId: initiatorId,
1553
1623
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1554
1624
  };
1555
- this.syncEvents.push(event);
1625
+ this.addSyncEvent(event);
1556
1626
  this.emit("sync-started", session);
1557
1627
  logger.debug("[SyncCoordinator] Sync session created", {
1558
1628
  sessionId: session.id,
@@ -1579,7 +1649,7 @@ var SyncCoordinator = class extends eventemitter3.EventEmitter {
1579
1649
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1580
1650
  data: { status: updates.status, itemsSynced: session.itemsSynced }
1581
1651
  };
1582
- this.syncEvents.push(event);
1652
+ this.addSyncEvent(event);
1583
1653
  this.emit("sync-completed", session);
1584
1654
  }
1585
1655
  logger.debug("[SyncCoordinator] Sync session updated", {
@@ -1602,7 +1672,7 @@ var SyncCoordinator = class extends eventemitter3.EventEmitter {
1602
1672
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1603
1673
  data: conflictData
1604
1674
  };
1605
- this.syncEvents.push(event);
1675
+ this.addSyncEvent(event);
1606
1676
  this.emit("conflict-detected", { session, nodeId, conflictData });
1607
1677
  logger.debug("[SyncCoordinator] Conflict recorded", {
1608
1678
  sessionId,
@@ -4150,7 +4220,10 @@ var CompressionEngine = class {
4150
4220
  */
4151
4221
  reassembleChunks(chunks) {
4152
4222
  const sorted = [...chunks].sort((a, b) => a.index - b.index);
4153
- const total = sorted[0]?.total ?? 0;
4223
+ if (sorted.length === 0) {
4224
+ throw new Error("Cannot reassemble: no chunks provided");
4225
+ }
4226
+ const total = sorted[0].total;
4154
4227
  if (sorted.length !== total) {
4155
4228
  throw new Error(
4156
4229
  `Missing chunks: got ${sorted.length}, expected ${total}`
@@ -4176,7 +4249,7 @@ var CompressionEngine = class {
4176
4249
  for (let i = 0; i < data.length; i++) {
4177
4250
  hash = (hash << 5) - hash + data[i] | 0;
4178
4251
  }
4179
- return hash.toString(16);
4252
+ return (hash >>> 0).toString(16);
4180
4253
  }
4181
4254
  /**
4182
4255
  * Update average compression ratio
@@ -4220,7 +4293,8 @@ function resetCompressionEngine() {
4220
4293
 
4221
4294
  // src/compression/DeltaSyncOptimizer.ts
4222
4295
  var logger4 = getLogger();
4223
- var DeltaSyncOptimizer = class {
4296
+ var DeltaSyncOptimizer = class _DeltaSyncOptimizer {
4297
+ static MAX_HISTORY_SIZE = 1e4;
4224
4298
  operationHistory = /* @__PURE__ */ new Map();
4225
4299
  stats = {
4226
4300
  totalOperations: 0,
@@ -4265,6 +4339,10 @@ var DeltaSyncOptimizer = class {
4265
4339
  ).byteLength;
4266
4340
  this.stats.totalDeltaSize += deltaSize2;
4267
4341
  this.operationHistory.set(operation.id, operation);
4342
+ if (this.operationHistory.size > _DeltaSyncOptimizer.MAX_HISTORY_SIZE) {
4343
+ const firstKey = this.operationHistory.keys().next().value;
4344
+ if (firstKey !== void 0) this.operationHistory.delete(firstKey);
4345
+ }
4268
4346
  return delta;
4269
4347
  }
4270
4348
  const changes = {};
@@ -4315,6 +4393,10 @@ var DeltaSyncOptimizer = class {
4315
4393
  this.stats.totalOriginalSize += originalSize;
4316
4394
  this.stats.totalDeltaSize += deltaSize;
4317
4395
  this.operationHistory.set(operation.id, operation);
4396
+ if (this.operationHistory.size > _DeltaSyncOptimizer.MAX_HISTORY_SIZE) {
4397
+ const firstKey = this.operationHistory.keys().next().value;
4398
+ if (firstKey !== void 0) this.operationHistory.delete(firstKey);
4399
+ }
4318
4400
  return finalDelta;
4319
4401
  }
4320
4402
  /**
@@ -4399,6 +4481,11 @@ var DeltaSyncOptimizer = class {
4399
4481
  for (const op of operations) {
4400
4482
  this.operationHistory.set(op.id, op);
4401
4483
  }
4484
+ while (this.operationHistory.size > _DeltaSyncOptimizer.MAX_HISTORY_SIZE) {
4485
+ const firstKey = this.operationHistory.keys().next().value;
4486
+ if (firstKey !== void 0) this.operationHistory.delete(firstKey);
4487
+ else break;
4488
+ }
4402
4489
  logger4.debug("[DeltaSyncOptimizer] History updated", {
4403
4490
  count: operations.length,
4404
4491
  totalHistorySize: this.operationHistory.size
@@ -4599,7 +4686,7 @@ var PrefetchingEngine = class {
4599
4686
  const predictions = [];
4600
4687
  const recentTypeSequence = recentOperations.slice(-3).map((op) => op.type).join(" \u2192 ");
4601
4688
  for (const [key, pattern] of this.patterns.entries()) {
4602
- if (key.includes(recentTypeSequence)) {
4689
+ if (key.includes(recentTypeSequence) && pattern.sequence.length > 0) {
4603
4690
  const nextType = pattern.sequence[pattern.sequence.length - 1];
4604
4691
  const prediction = {
4605
4692
  operationType: nextType,
@@ -5116,8 +5203,11 @@ var AdaptiveCompressionOptimizer = class {
5116
5203
  this.compressionHistory.shift();
5117
5204
  }
5118
5205
  this.stats.totalBatches++;
5119
- this.stats.averageCompressionMs = this.compressionHistory.reduce((sum, h) => sum + h.timeMs, 0) / this.compressionHistory.length;
5120
- this.stats.averageRatio = this.compressionHistory.reduce((sum, h) => sum + h.ratio, 0) / this.compressionHistory.length;
5206
+ const historyLength = this.compressionHistory.length;
5207
+ if (historyLength > 0) {
5208
+ this.stats.averageCompressionMs = this.compressionHistory.reduce((sum, h) => sum + h.timeMs, 0) / historyLength;
5209
+ this.stats.averageRatio = this.compressionHistory.reduce((sum, h) => sum + h.ratio, 0) / historyLength;
5210
+ }
5121
5211
  }
5122
5212
  /**
5123
5213
  * Get compression recommendation based on conditions
@@ -5810,29 +5900,2814 @@ var NullCryptoProvider = class {
5810
5900
  }
5811
5901
  };
5812
5902
 
5903
+ // src/crypto/transactionSigner.ts
5904
+ var NullTransactionSigner = class {
5905
+ async execute(request) {
5906
+ return {
5907
+ success: false,
5908
+ action: request.action,
5909
+ chainId: request.chainId || 0,
5910
+ errorCode: "signer_unavailable",
5911
+ errorMessage: "Transaction signer not configured"
5912
+ };
5913
+ }
5914
+ async getSigner(action) {
5915
+ throw new Error(
5916
+ `Transaction signer metadata unavailable for action: ${action}`
5917
+ );
5918
+ }
5919
+ async health() {
5920
+ return {
5921
+ ok: false,
5922
+ service: "transaction-signer",
5923
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5924
+ };
5925
+ }
5926
+ };
5927
+ function createTransactionSignerAdapter(contract) {
5928
+ return contract;
5929
+ }
5930
+
5931
+ // src/flow/FlowCodec.ts
5932
+ var HEADER_SIZE = 10;
5933
+ var MAX_PAYLOAD_LENGTH = 16777215;
5934
+ var FlowCodec = class _FlowCodec {
5935
+ wasmInstance = null;
5936
+ constructor(wasmInstance) {
5937
+ this.wasmInstance = wasmInstance;
5938
+ }
5939
+ /**
5940
+ * Create a FlowCodec. Tries WASM acceleration, falls back to JS.
5941
+ * The JS path is always correct — WASM is a performance optimization only.
5942
+ */
5943
+ static async create() {
5944
+ return new _FlowCodec(null);
5945
+ }
5946
+ /**
5947
+ * Create a FlowCodec synchronously (JS-only, no WASM attempt).
5948
+ */
5949
+ static createSync() {
5950
+ return new _FlowCodec(null);
5951
+ }
5952
+ /**
5953
+ * Whether WASM acceleration is active.
5954
+ */
5955
+ get isWasmAccelerated() {
5956
+ return this.wasmInstance !== null;
5957
+ }
5958
+ // ═══════════════════════════════════════════════════════════════════════
5959
+ // Encode
5960
+ // ═══════════════════════════════════════════════════════════════════════
5961
+ /**
5962
+ * Encode a single FlowFrame into a binary buffer.
5963
+ *
5964
+ * Returns a new Uint8Array containing the 10-byte header followed by
5965
+ * the payload bytes.
5966
+ */
5967
+ encode(frame) {
5968
+ const payloadLen = frame.payload.length;
5969
+ if (payloadLen > MAX_PAYLOAD_LENGTH) {
5970
+ throw new RangeError(
5971
+ `Payload length ${payloadLen} exceeds maximum ${MAX_PAYLOAD_LENGTH}`
5972
+ );
5973
+ }
5974
+ const buf = new Uint8Array(HEADER_SIZE + payloadLen);
5975
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
5976
+ view.setUint16(0, frame.streamId);
5977
+ view.setUint32(2, frame.sequence);
5978
+ buf[6] = frame.flags;
5979
+ buf[7] = payloadLen >>> 16 & 255;
5980
+ buf[8] = payloadLen >>> 8 & 255;
5981
+ buf[9] = payloadLen & 255;
5982
+ buf.set(frame.payload, HEADER_SIZE);
5983
+ return buf;
5984
+ }
5985
+ /**
5986
+ * Decode a single FlowFrame from a buffer at the given offset.
5987
+ *
5988
+ * The returned frame's `payload` is a zerocopy view into the original
5989
+ * buffer — no data is copied. Callers who need the payload to outlive
5990
+ * the original buffer should `.slice()` it.
5991
+ *
5992
+ * @returns The decoded frame and the number of bytes consumed.
5993
+ */
5994
+ decode(buffer, offset = 0) {
5995
+ if (buffer.length - offset < HEADER_SIZE) {
5996
+ throw new RangeError(
5997
+ `Buffer too small: need at least ${HEADER_SIZE} bytes, have ${buffer.length - offset}`
5998
+ );
5999
+ }
6000
+ const view = new DataView(
6001
+ buffer.buffer,
6002
+ buffer.byteOffset + offset,
6003
+ buffer.byteLength - offset
6004
+ );
6005
+ const streamId = view.getUint16(0);
6006
+ const sequence = view.getUint32(2);
6007
+ const flags = buffer[offset + 6];
6008
+ const length = buffer[offset + 7] << 16 | buffer[offset + 8] << 8 | buffer[offset + 9];
6009
+ if (buffer.length - offset - HEADER_SIZE < length) {
6010
+ throw new RangeError(
6011
+ `Buffer too small for payload: need ${length} bytes, have ${buffer.length - offset - HEADER_SIZE}`
6012
+ );
6013
+ }
6014
+ const payload = buffer.subarray(
6015
+ offset + HEADER_SIZE,
6016
+ offset + HEADER_SIZE + length
6017
+ );
6018
+ return {
6019
+ frame: { streamId, sequence, flags, payload },
6020
+ bytesRead: HEADER_SIZE + length
6021
+ };
6022
+ }
6023
+ // ═══════════════════════════════════════════════════════════════════════
6024
+ // Batch encode/decode
6025
+ // ═══════════════════════════════════════════════════════════════════════
6026
+ /**
6027
+ * Encode multiple frames into a single contiguous buffer.
6028
+ *
6029
+ * Frames are laid out sequentially: [header+payload][header+payload]...
6030
+ */
6031
+ encodeBatch(frames) {
6032
+ let totalSize = 0;
6033
+ for (const frame of frames) {
6034
+ if (frame.payload.length > MAX_PAYLOAD_LENGTH) {
6035
+ throw new RangeError(
6036
+ `Payload length ${frame.payload.length} exceeds maximum ${MAX_PAYLOAD_LENGTH}`
6037
+ );
6038
+ }
6039
+ totalSize += HEADER_SIZE + frame.payload.length;
6040
+ }
6041
+ const buf = new Uint8Array(totalSize);
6042
+ let writeOffset = 0;
6043
+ for (const frame of frames) {
6044
+ const encoded = this.encode(frame);
6045
+ buf.set(encoded, writeOffset);
6046
+ writeOffset += encoded.length;
6047
+ }
6048
+ return buf;
6049
+ }
6050
+ /**
6051
+ * Decode all frames from a contiguous buffer.
6052
+ *
6053
+ * Payloads are zerocopy views into the original buffer.
6054
+ */
6055
+ decodeBatch(buffer) {
6056
+ const frames = [];
6057
+ let offset = 0;
6058
+ while (offset < buffer.length) {
6059
+ if (buffer.length - offset < HEADER_SIZE) {
6060
+ throw new RangeError(
6061
+ `Truncated frame at offset ${offset}: need ${HEADER_SIZE} bytes, have ${buffer.length - offset}`
6062
+ );
6063
+ }
6064
+ const { frame, bytesRead } = this.decode(buffer, offset);
6065
+ frames.push(frame);
6066
+ offset += bytesRead;
6067
+ }
6068
+ return frames;
6069
+ }
6070
+ };
6071
+
6072
+ // src/flow/types.ts
6073
+ var FORK = 1;
6074
+ var RACE = 2;
6075
+ var COLLAPSE = 4;
6076
+ var POISON = 8;
6077
+ var FIN = 16;
6078
+ var DEFAULT_FLOW_CONFIG = {
6079
+ highWaterMark: 64,
6080
+ role: "client",
6081
+ maxConcurrentStreams: 256
6082
+ };
6083
+
6084
+ // src/flow/AeonFlowProtocol.ts
6085
+ var AeonFlowProtocol = class {
6086
+ streams = /* @__PURE__ */ new Map();
6087
+ nextEvenId = 0;
6088
+ nextOddId = 1;
6089
+ codec;
6090
+ transport;
6091
+ config;
6092
+ // Event handlers
6093
+ frameHandlers = /* @__PURE__ */ new Map();
6094
+ endHandlers = /* @__PURE__ */ new Map();
6095
+ poisonHandlers = /* @__PURE__ */ new Map();
6096
+ // Race tracking
6097
+ raceGroups = /* @__PURE__ */ new Map();
6098
+ // Collapse tracking
6099
+ collapseGroups = /* @__PURE__ */ new Map();
6100
+ constructor(transport, config) {
6101
+ this.transport = transport;
6102
+ this.config = { ...DEFAULT_FLOW_CONFIG, ...config };
6103
+ this.codec = FlowCodec.createSync();
6104
+ this.transport.onReceive((data) => {
6105
+ this.handleIncoming(data);
6106
+ });
6107
+ }
6108
+ // ═══════════════════════════════════════════════════════════════════════
6109
+ // Stream management
6110
+ // ═══════════════════════════════════════════════════════════════════════
6111
+ /**
6112
+ * Open a new root stream (no parent).
6113
+ *
6114
+ * Client-initiated streams get even IDs; server-initiated get odd IDs.
6115
+ */
6116
+ openStream() {
6117
+ const id = this.allocateStreamId();
6118
+ this.createStream(id);
6119
+ return id;
6120
+ }
6121
+ /**
6122
+ * Get the current state of a stream.
6123
+ */
6124
+ getStream(streamId) {
6125
+ return this.streams.get(streamId);
6126
+ }
6127
+ /**
6128
+ * Get all active streams.
6129
+ */
6130
+ getActiveStreams() {
6131
+ return Array.from(this.streams.values()).filter(
6132
+ (s) => s.state !== "closed" && s.state !== "poisoned"
6133
+ );
6134
+ }
6135
+ // ═══════════════════════════════════════════════════════════════════════
6136
+ // Fork: create N child streams from a parent
6137
+ // ═══════════════════════════════════════════════════════════════════════
6138
+ /**
6139
+ * Fork a parent stream into N child streams.
6140
+ *
6141
+ * Each child stream is independent and can send/receive frames.
6142
+ * The parent tracks all children. Poisoning the parent poisons all children.
6143
+ *
6144
+ * @param parentStreamId The stream to fork from
6145
+ * @param count Number of child streams to create
6146
+ * @returns Array of child stream IDs
6147
+ */
6148
+ fork(parentStreamId, count) {
6149
+ const parent = this.requireStream(parentStreamId, "open");
6150
+ if (count < 1) {
6151
+ throw new RangeError("Fork count must be at least 1");
6152
+ }
6153
+ if (this.streams.size + count > this.config.maxConcurrentStreams) {
6154
+ throw new Error(
6155
+ `Cannot fork ${count} streams: would exceed maxConcurrentStreams (${this.config.maxConcurrentStreams})`
6156
+ );
6157
+ }
6158
+ const childIds = [];
6159
+ for (let i = 0; i < count; i++) {
6160
+ const childId = this.allocateStreamId();
6161
+ this.createStream(childId, parentStreamId);
6162
+ parent.children.push(childId);
6163
+ childIds.push(childId);
6164
+ }
6165
+ this.sendFrame(parentStreamId, FORK, new Uint8Array(
6166
+ childIds.flatMap((id) => [id >>> 8 & 255, id & 255])
6167
+ ));
6168
+ return childIds;
6169
+ }
6170
+ // ═══════════════════════════════════════════════════════════════════════
6171
+ // Race: first stream to FIN wins, losers are poisoned
6172
+ // ═══════════════════════════════════════════════════════════════════════
6173
+ /**
6174
+ * Race multiple streams. The first to send a FIN frame wins.
6175
+ * All other streams in the race are automatically poisoned.
6176
+ *
6177
+ * @param streamIds Streams to race (must all be open)
6178
+ * @returns Promise resolving with the winner's stream ID and final payload
6179
+ */
6180
+ race(streamIds) {
6181
+ if (streamIds.length < 2) {
6182
+ throw new RangeError("Race requires at least 2 streams");
6183
+ }
6184
+ for (const id of streamIds) {
6185
+ const stream = this.requireStream(id);
6186
+ stream.state = "racing";
6187
+ }
6188
+ for (const id of streamIds) {
6189
+ const peerIds = streamIds.filter((sid) => sid !== id);
6190
+ this.sendFrame(id, RACE, new Uint8Array(
6191
+ peerIds.flatMap((pid) => [pid >>> 8 & 255, pid & 255])
6192
+ ));
6193
+ }
6194
+ const groupId = `race-${streamIds.join("-")}-${Date.now()}`;
6195
+ return new Promise((resolve) => {
6196
+ this.raceGroups.set(groupId, {
6197
+ streamIds,
6198
+ resolve,
6199
+ settled: false
6200
+ });
6201
+ for (const id of streamIds) {
6202
+ this.onStreamEnd(id, () => {
6203
+ this.settleRace(groupId, id);
6204
+ });
6205
+ }
6206
+ });
6207
+ }
6208
+ // ═══════════════════════════════════════════════════════════════════════
6209
+ // Collapse: wait for all streams, merge results
6210
+ // ═══════════════════════════════════════════════════════════════════════
6211
+ /**
6212
+ * Collapse multiple streams: wait for all to complete (or poison),
6213
+ * then merge their results.
6214
+ *
6215
+ * @param streamIds Streams to collapse
6216
+ * @param merger Function that merges results from all streams
6217
+ * @returns Promise resolving with the merged result
6218
+ */
6219
+ collapse(streamIds, merger) {
6220
+ if (streamIds.length < 1) {
6221
+ throw new RangeError("Collapse requires at least 1 stream");
6222
+ }
6223
+ for (const id of streamIds) {
6224
+ const stream = this.requireStream(id);
6225
+ stream.state = "collapsing";
6226
+ }
6227
+ for (const id of streamIds) {
6228
+ this.sendFrame(id, COLLAPSE, new Uint8Array(0));
6229
+ }
6230
+ const groupId = `collapse-${streamIds.join("-")}-${Date.now()}`;
6231
+ return new Promise((resolve) => {
6232
+ const group = {
6233
+ streamIds,
6234
+ merger,
6235
+ resolve,
6236
+ results: /* @__PURE__ */ new Map(),
6237
+ completed: /* @__PURE__ */ new Set(),
6238
+ settled: false
6239
+ };
6240
+ this.collapseGroups.set(groupId, group);
6241
+ for (const id of streamIds) {
6242
+ this.onStreamEnd(id, () => {
6243
+ this.settleCollapse(groupId, id, false);
6244
+ });
6245
+ this.onStreamPoisoned(id, () => {
6246
+ this.settleCollapse(groupId, id, true);
6247
+ });
6248
+ }
6249
+ });
6250
+ }
6251
+ // ═══════════════════════════════════════════════════════════════════════
6252
+ // Send / Poison / Close
6253
+ // ═══════════════════════════════════════════════════════════════════════
6254
+ /**
6255
+ * Send a payload on a stream.
6256
+ */
6257
+ send(streamId, payload, flags = 0) {
6258
+ const stream = this.requireStream(streamId);
6259
+ if (stream.bufferedFrames >= this.config.highWaterMark) {
6260
+ throw new Error(
6261
+ `Stream ${streamId} backpressure: ${stream.bufferedFrames} frames buffered (high-water mark: ${this.config.highWaterMark})`
6262
+ );
6263
+ }
6264
+ if (payload.length > 0) {
6265
+ stream.results.push(payload);
6266
+ }
6267
+ stream.bufferedFrames++;
6268
+ this.sendFrame(streamId, flags, payload);
6269
+ }
6270
+ /**
6271
+ * Finish a stream. Sends a FIN frame and transitions to 'closed'.
6272
+ *
6273
+ * @param streamId Stream to finish
6274
+ * @param finalPayload Optional final payload to include with the FIN
6275
+ */
6276
+ finish(streamId, finalPayload) {
6277
+ const stream = this.requireStream(streamId);
6278
+ if (finalPayload && finalPayload.length > 0) {
6279
+ stream.results.push(finalPayload);
6280
+ }
6281
+ this.sendFrame(streamId, FIN, finalPayload ?? new Uint8Array(0));
6282
+ stream.state = "closed";
6283
+ const handlers = this.endHandlers.get(streamId);
6284
+ if (handlers) {
6285
+ for (const handler of handlers) {
6286
+ handler();
6287
+ }
6288
+ }
6289
+ }
6290
+ /**
6291
+ * Poison a stream. Sends a POISON frame and propagates to all descendants.
6292
+ *
6293
+ * Poisoning is the protocol-level equivalent of NaN propagation,
6294
+ * AbortSignal cancellation, or error cascading.
6295
+ */
6296
+ poison(streamId) {
6297
+ const stream = this.streams.get(streamId);
6298
+ if (!stream || stream.state === "poisoned" || stream.state === "closed") {
6299
+ return;
6300
+ }
6301
+ stream.state = "poisoned";
6302
+ this.sendFrame(streamId, POISON, new Uint8Array(0));
6303
+ const handlers = this.poisonHandlers.get(streamId);
6304
+ if (handlers) {
6305
+ for (const handler of handlers) {
6306
+ handler();
6307
+ }
6308
+ }
6309
+ for (const childId of stream.children) {
6310
+ this.poison(childId);
6311
+ }
6312
+ }
6313
+ // ═══════════════════════════════════════════════════════════════════════
6314
+ // Event handlers
6315
+ // ═══════════════════════════════════════════════════════════════════════
6316
+ /**
6317
+ * Register a handler for frames arriving on a specific stream.
6318
+ */
6319
+ onFrame(streamId, handler) {
6320
+ let handlers = this.frameHandlers.get(streamId);
6321
+ if (!handlers) {
6322
+ handlers = /* @__PURE__ */ new Set();
6323
+ this.frameHandlers.set(streamId, handlers);
6324
+ }
6325
+ handlers.add(handler);
6326
+ return () => {
6327
+ handlers.delete(handler);
6328
+ };
6329
+ }
6330
+ /**
6331
+ * Register a handler for when a stream ends (FIN received).
6332
+ */
6333
+ onStreamEnd(streamId, handler) {
6334
+ let handlers = this.endHandlers.get(streamId);
6335
+ if (!handlers) {
6336
+ handlers = /* @__PURE__ */ new Set();
6337
+ this.endHandlers.set(streamId, handlers);
6338
+ }
6339
+ handlers.add(handler);
6340
+ return () => {
6341
+ handlers.delete(handler);
6342
+ };
6343
+ }
6344
+ /**
6345
+ * Register a handler for when a stream is poisoned.
6346
+ */
6347
+ onStreamPoisoned(streamId, handler) {
6348
+ let handlers = this.poisonHandlers.get(streamId);
6349
+ if (!handlers) {
6350
+ handlers = /* @__PURE__ */ new Set();
6351
+ this.poisonHandlers.set(streamId, handlers);
6352
+ }
6353
+ handlers.add(handler);
6354
+ return () => {
6355
+ handlers.delete(handler);
6356
+ };
6357
+ }
6358
+ // ═══════════════════════════════════════════════════════════════════════
6359
+ // Destroy
6360
+ // ═══════════════════════════════════════════════════════════════════════
6361
+ /**
6362
+ * Close the protocol and underlying transport.
6363
+ * Poisons all open streams first.
6364
+ */
6365
+ destroy() {
6366
+ for (const [id, stream] of this.streams) {
6367
+ if (stream.state !== "closed" && stream.state !== "poisoned") {
6368
+ stream.state = "closed";
6369
+ }
6370
+ }
6371
+ this.streams.clear();
6372
+ this.frameHandlers.clear();
6373
+ this.endHandlers.clear();
6374
+ this.poisonHandlers.clear();
6375
+ this.raceGroups.clear();
6376
+ this.collapseGroups.clear();
6377
+ this.transport.close();
6378
+ }
6379
+ // ═══════════════════════════════════════════════════════════════════════
6380
+ // Private: incoming frame handling
6381
+ // ═══════════════════════════════════════════════════════════════════════
6382
+ handleIncoming(data) {
6383
+ let frames;
6384
+ try {
6385
+ frames = this.codec.decodeBatch(data);
6386
+ } catch {
6387
+ return;
6388
+ }
6389
+ for (const frame of frames) {
6390
+ this.handleFrame(frame);
6391
+ }
6392
+ }
6393
+ handleFrame(frame) {
6394
+ const { streamId, flags } = frame;
6395
+ if (!this.streams.has(streamId)) {
6396
+ this.createStream(streamId);
6397
+ }
6398
+ const stream = this.streams.get(streamId);
6399
+ if (flags & POISON) {
6400
+ this.poison(streamId);
6401
+ return;
6402
+ }
6403
+ if (flags & FIN) {
6404
+ if (frame.payload.length > 0) {
6405
+ stream.results.push(frame.payload);
6406
+ }
6407
+ stream.state = "closed";
6408
+ const endHandlers = this.endHandlers.get(streamId);
6409
+ if (endHandlers) {
6410
+ for (const handler of endHandlers) {
6411
+ handler();
6412
+ }
6413
+ }
6414
+ return;
6415
+ }
6416
+ if (flags & FORK) {
6417
+ for (let i = 0; i + 1 < frame.payload.length; i += 2) {
6418
+ const childId = frame.payload[i] << 8 | frame.payload[i + 1];
6419
+ if (!this.streams.has(childId)) {
6420
+ this.createStream(childId, streamId);
6421
+ }
6422
+ if (!stream.children.includes(childId)) {
6423
+ stream.children.push(childId);
6424
+ }
6425
+ }
6426
+ return;
6427
+ }
6428
+ if (frame.payload.length > 0) {
6429
+ stream.results.push(frame.payload);
6430
+ }
6431
+ stream.bufferedFrames++;
6432
+ const handlers = this.frameHandlers.get(streamId);
6433
+ if (handlers) {
6434
+ for (const handler of handlers) {
6435
+ handler(frame);
6436
+ }
6437
+ }
6438
+ }
6439
+ // ═══════════════════════════════════════════════════════════════════════
6440
+ // Private: race/collapse settlement
6441
+ // ═══════════════════════════════════════════════════════════════════════
6442
+ settleRace(groupId, winnerId) {
6443
+ const group = this.raceGroups.get(groupId);
6444
+ if (!group || group.settled) return;
6445
+ group.settled = true;
6446
+ const winnerStream = this.streams.get(winnerId);
6447
+ const result = winnerStream ? this.concatenateResults(winnerStream.results) : new Uint8Array(0);
6448
+ for (const id of group.streamIds) {
6449
+ if (id !== winnerId) {
6450
+ this.poison(id);
6451
+ }
6452
+ }
6453
+ group.resolve({ winner: winnerId, result });
6454
+ this.raceGroups.delete(groupId);
6455
+ }
6456
+ settleCollapse(groupId, streamId, wasPoisoned) {
6457
+ const group = this.collapseGroups.get(groupId);
6458
+ if (!group || group.settled) return;
6459
+ group.completed.add(streamId);
6460
+ if (!wasPoisoned) {
6461
+ const stream = this.streams.get(streamId);
6462
+ if (stream) {
6463
+ group.results.set(streamId, this.concatenateResults(stream.results));
6464
+ }
6465
+ }
6466
+ if (group.completed.size >= group.streamIds.length) {
6467
+ group.settled = true;
6468
+ const merged = group.merger(group.results);
6469
+ group.resolve(merged);
6470
+ this.collapseGroups.delete(groupId);
6471
+ }
6472
+ }
6473
+ // ═══════════════════════════════════════════════════════════════════════
6474
+ // Private: stream helpers
6475
+ // ═══════════════════════════════════════════════════════════════════════
6476
+ allocateStreamId() {
6477
+ if (this.config.role === "client") {
6478
+ const id = this.nextEvenId;
6479
+ this.nextEvenId += 2;
6480
+ return id;
6481
+ } else {
6482
+ const id = this.nextOddId;
6483
+ this.nextOddId += 2;
6484
+ return id;
6485
+ }
6486
+ }
6487
+ createStream(id, parent) {
6488
+ const stream = {
6489
+ id,
6490
+ state: "open",
6491
+ parent,
6492
+ children: [],
6493
+ nextSequence: 0,
6494
+ bufferedFrames: 0,
6495
+ results: []
6496
+ };
6497
+ this.streams.set(id, stream);
6498
+ return stream;
6499
+ }
6500
+ requireStream(streamId, ...allowedStates) {
6501
+ const stream = this.streams.get(streamId);
6502
+ if (!stream) {
6503
+ throw new Error(`Stream ${streamId} does not exist`);
6504
+ }
6505
+ if (allowedStates.length > 0 && !allowedStates.includes(stream.state)) {
6506
+ throw new Error(
6507
+ `Stream ${streamId} is in state '${stream.state}', expected one of: ${allowedStates.join(", ")}`
6508
+ );
6509
+ }
6510
+ return stream;
6511
+ }
6512
+ sendFrame(streamId, flags, payload) {
6513
+ const stream = this.streams.get(streamId);
6514
+ if (!stream) return;
6515
+ const frame = {
6516
+ streamId,
6517
+ sequence: stream.nextSequence++,
6518
+ flags,
6519
+ payload
6520
+ };
6521
+ const encoded = this.codec.encode(frame);
6522
+ this.transport.send(encoded);
6523
+ }
6524
+ concatenateResults(chunks) {
6525
+ if (chunks.length === 0) return new Uint8Array(0);
6526
+ if (chunks.length === 1) return chunks[0];
6527
+ let totalLen = 0;
6528
+ for (const chunk of chunks) totalLen += chunk.length;
6529
+ const result = new Uint8Array(totalLen);
6530
+ let offset = 0;
6531
+ for (const chunk of chunks) {
6532
+ result.set(chunk, offset);
6533
+ offset += chunk.length;
6534
+ }
6535
+ return result;
6536
+ }
6537
+ };
6538
+
6539
+ // src/flow/frame-reassembler.ts
6540
+ var DEFAULT_REASSEMBLER_CONFIG = {
6541
+ maxBufferPerStream: 256,
6542
+ maxStreams: 1024,
6543
+ maxGap: 64
6544
+ };
6545
+ var FrameReassembler = class {
6546
+ config;
6547
+ streams = /* @__PURE__ */ new Map();
6548
+ stats = {
6549
+ framesReceived: 0,
6550
+ framesDelivered: 0,
6551
+ framesBuffered: 0,
6552
+ framesDropped: 0,
6553
+ framesReordered: 0,
6554
+ activeStreams: 0
6555
+ };
6556
+ constructor(config) {
6557
+ this.config = { ...DEFAULT_REASSEMBLER_CONFIG, ...config };
6558
+ }
6559
+ /**
6560
+ * Process an incoming frame. Returns an array of frames that are
6561
+ * now deliverable in order. May return 0 frames (buffered), 1 frame
6562
+ * (in order), or multiple frames (gap was filled, releasing buffered).
6563
+ *
6564
+ * This is the core reassembly operation:
6565
+ * - In-order frame → deliver immediately + flush any buffered followers
6566
+ * - Out-of-order frame → buffer until gap fills
6567
+ * - Duplicate → drop
6568
+ * - Beyond window → drop
6569
+ */
6570
+ push(frame) {
6571
+ this.stats.framesReceived++;
6572
+ const streamId = frame.streamId;
6573
+ let state = this.streams.get(streamId);
6574
+ if (!state) {
6575
+ if (this.streams.size >= this.config.maxStreams) {
6576
+ this.evictOldestStream();
6577
+ }
6578
+ state = {
6579
+ nextExpected: 0,
6580
+ buffer: /* @__PURE__ */ new Map(),
6581
+ lastActivity: Date.now(),
6582
+ emittedSequences: /* @__PURE__ */ new Set()
6583
+ };
6584
+ this.streams.set(streamId, state);
6585
+ this.stats.activeStreams = this.streams.size;
6586
+ }
6587
+ state.lastActivity = Date.now();
6588
+ if (state.emittedSequences.has(frame.sequence)) {
6589
+ this.stats.framesDropped++;
6590
+ return [];
6591
+ }
6592
+ if (state.buffer.has(frame.sequence)) {
6593
+ this.stats.framesDropped++;
6594
+ return [];
6595
+ }
6596
+ const seq = frame.sequence;
6597
+ if (seq === state.nextExpected) {
6598
+ return this.deliverAndFlush(state, frame);
6599
+ }
6600
+ if (seq < state.nextExpected) {
6601
+ this.stats.framesDropped++;
6602
+ return [];
6603
+ }
6604
+ const gap = seq - state.nextExpected;
6605
+ if (gap > this.config.maxGap) {
6606
+ state.nextExpected = seq;
6607
+ return this.deliverAndFlush(state, frame);
6608
+ }
6609
+ if (state.buffer.size >= this.config.maxBufferPerStream) {
6610
+ this.stats.framesDropped++;
6611
+ return [];
6612
+ }
6613
+ state.buffer.set(seq, frame);
6614
+ this.stats.framesBuffered++;
6615
+ return [];
6616
+ }
6617
+ /**
6618
+ * Get the sequence numbers that are missing (gaps) for a stream.
6619
+ * Used by the reliability layer to request retransmission.
6620
+ */
6621
+ getMissingSequences(streamId) {
6622
+ const state = this.streams.get(streamId);
6623
+ if (!state) return [];
6624
+ const missing = [];
6625
+ const maxBuffered = state.buffer.size > 0 ? Math.max(...state.buffer.keys()) : state.nextExpected;
6626
+ for (let seq = state.nextExpected; seq < maxBuffered; seq++) {
6627
+ if (!state.buffer.has(seq)) {
6628
+ missing.push(seq);
6629
+ }
6630
+ }
6631
+ return missing;
6632
+ }
6633
+ /**
6634
+ * Clean up a stream's reassembly state.
6635
+ * Call when a stream is closed or poisoned.
6636
+ */
6637
+ closeStream(streamId) {
6638
+ const state = this.streams.get(streamId);
6639
+ if (state) {
6640
+ this.stats.framesBuffered -= state.buffer.size;
6641
+ this.streams.delete(streamId);
6642
+ this.stats.activeStreams = this.streams.size;
6643
+ }
6644
+ }
6645
+ /**
6646
+ * Get current reassembly statistics.
6647
+ */
6648
+ getStats() {
6649
+ return { ...this.stats };
6650
+ }
6651
+ /**
6652
+ * Reset all state.
6653
+ */
6654
+ reset() {
6655
+ this.streams.clear();
6656
+ this.stats = {
6657
+ framesReceived: 0,
6658
+ framesDelivered: 0,
6659
+ framesBuffered: 0,
6660
+ framesDropped: 0,
6661
+ framesReordered: 0,
6662
+ activeStreams: 0
6663
+ };
6664
+ }
6665
+ // ─── Internal ──────────────────────────────────────────────────────────
6666
+ /**
6667
+ * Deliver a frame and flush any consecutively-buffered followers.
6668
+ */
6669
+ deliverAndFlush(state, frame) {
6670
+ const deliverable = [frame];
6671
+ state.emittedSequences.add(frame.sequence);
6672
+ state.nextExpected = frame.sequence + 1;
6673
+ this.stats.framesDelivered++;
6674
+ while (state.buffer.has(state.nextExpected)) {
6675
+ const buffered = state.buffer.get(state.nextExpected);
6676
+ state.buffer.delete(state.nextExpected);
6677
+ state.emittedSequences.add(state.nextExpected);
6678
+ state.nextExpected++;
6679
+ deliverable.push(buffered);
6680
+ this.stats.framesBuffered--;
6681
+ this.stats.framesDelivered++;
6682
+ this.stats.framesReordered++;
6683
+ }
6684
+ if (state.emittedSequences.size > 1024) {
6685
+ const cutoff = state.nextExpected - 512;
6686
+ for (const seq of state.emittedSequences) {
6687
+ if (seq < cutoff) state.emittedSequences.delete(seq);
6688
+ }
6689
+ }
6690
+ return deliverable;
6691
+ }
6692
+ /**
6693
+ * Evict the least-recently-active stream to make room.
6694
+ */
6695
+ evictOldestStream() {
6696
+ let oldestKey = -1;
6697
+ let oldestTime = Infinity;
6698
+ for (const [key, state] of this.streams) {
6699
+ if (state.lastActivity < oldestTime) {
6700
+ oldestTime = state.lastActivity;
6701
+ oldestKey = key;
6702
+ }
6703
+ }
6704
+ if (oldestKey >= 0) {
6705
+ this.closeStream(oldestKey);
6706
+ }
6707
+ }
6708
+ };
6709
+
6710
+ // src/flow/UDPFlowTransport.ts
6711
+ var UDP_MTU = 1472;
6712
+ var FRAGMENT_HEADER_SIZE = 4;
6713
+ var MAX_FRAGMENT_PAYLOAD = UDP_MTU - FRAGMENT_HEADER_SIZE;
6714
+ var ACK_FLAG = 128;
6715
+ var ACK_INTERVAL_MS = 50;
6716
+ var RETRANSMIT_TIMEOUT_MS = 200;
6717
+ var INITIAL_CWND = 16;
6718
+ var MAX_CWND = 256;
6719
+ var UDPFlowTransport = class {
6720
+ config;
6721
+ receiveHandler = null;
6722
+ closed = false;
6723
+ // UDP socket (set by bind/connect)
6724
+ socket = null;
6725
+ // Frame reassembly (out-of-order)
6726
+ reassembler;
6727
+ // Fragment reassembly (MTU splitting)
6728
+ fragmentGroups = /* @__PURE__ */ new Map();
6729
+ nextFrameId = 0;
6730
+ // Reliability: inflight tracking
6731
+ inflight = /* @__PURE__ */ new Map();
6732
+ // Reliability: ACK state
6733
+ receivedPerStream = /* @__PURE__ */ new Map();
6734
+ ackTimer = null;
6735
+ retransmitTimer = null;
6736
+ // Congestion control
6737
+ cwnd = INITIAL_CWND;
6738
+ inflightCount = 0;
6739
+ // Flow codec for ACK frame encoding
6740
+ codec = FlowCodec.createSync();
6741
+ constructor(config) {
6742
+ this.config = {
6743
+ host: config.host,
6744
+ port: config.port,
6745
+ remoteHost: config.remoteHost ?? "",
6746
+ remotePort: config.remotePort ?? 0,
6747
+ mtu: config.mtu ?? UDP_MTU,
6748
+ reliable: config.reliable ?? true,
6749
+ reassembler: config.reassembler ?? {}
6750
+ };
6751
+ this.reassembler = new FrameReassembler(this.config.reassembler);
6752
+ }
6753
+ /**
6754
+ * Bind and start the UDP transport.
6755
+ *
6756
+ * In server mode (no remoteHost), listens for incoming datagrams.
6757
+ * In client mode, binds and sets the remote endpoint.
6758
+ */
6759
+ async bind() {
6760
+ this.socket = await createUDPSocket(this.config.host, this.config.port);
6761
+ this.socket.onMessage((data, rinfo) => {
6762
+ if (this.closed) return;
6763
+ if (!this.config.remoteHost && rinfo.address) {
6764
+ this.config.remoteHost = rinfo.address;
6765
+ this.config.remotePort = rinfo.port;
6766
+ }
6767
+ this.handleDatagram(data);
6768
+ });
6769
+ if (this.config.reliable) {
6770
+ this.ackTimer = setInterval(() => this.sendAcks(), ACK_INTERVAL_MS);
6771
+ this.retransmitTimer = setInterval(() => this.retransmitLost(), RETRANSMIT_TIMEOUT_MS);
6772
+ }
6773
+ }
6774
+ // ─── FlowTransport interface ───────────────────────────────────────────
6775
+ send(data) {
6776
+ if (this.closed || !this.socket) return;
6777
+ if (this.config.reliable && this.inflightCount >= this.cwnd) {
6778
+ return;
6779
+ }
6780
+ const maxPayload = this.config.mtu - FRAGMENT_HEADER_SIZE;
6781
+ if (data.length <= maxPayload) {
6782
+ const frameId = this.nextFrameId++ & 65535;
6783
+ const datagram = this.wrapFragment(frameId, 0, 1, data);
6784
+ this.sendDatagram(datagram);
6785
+ this.trackInflight(data, frameId);
6786
+ } else {
6787
+ const frameId = this.nextFrameId++ & 65535;
6788
+ const totalFragments = Math.ceil(data.length / maxPayload);
6789
+ if (totalFragments > 255) {
6790
+ return;
6791
+ }
6792
+ for (let i = 0; i < totalFragments; i++) {
6793
+ const start = i * maxPayload;
6794
+ const end = Math.min(start + maxPayload, data.length);
6795
+ const chunk = data.slice(start, end);
6796
+ const datagram = this.wrapFragment(frameId, i, totalFragments, chunk);
6797
+ this.sendDatagram(datagram);
6798
+ }
6799
+ this.trackInflight(data, frameId);
6800
+ }
6801
+ }
6802
+ onReceive(handler) {
6803
+ this.receiveHandler = handler;
6804
+ }
6805
+ close() {
6806
+ if (this.closed) return;
6807
+ this.closed = true;
6808
+ if (this.ackTimer) clearInterval(this.ackTimer);
6809
+ if (this.retransmitTimer) clearInterval(this.retransmitTimer);
6810
+ this.socket?.close();
6811
+ this.socket = null;
6812
+ this.receiveHandler = null;
6813
+ this.inflight.clear();
6814
+ this.fragmentGroups.clear();
6815
+ this.reassembler.reset();
6816
+ }
6817
+ // ─── Stats ─────────────────────────────────────────────────────────────
6818
+ /** Get reassembly statistics */
6819
+ getReassemblerStats() {
6820
+ return this.reassembler.getStats();
6821
+ }
6822
+ /** Current congestion window size */
6823
+ get congestionWindow() {
6824
+ return this.cwnd;
6825
+ }
6826
+ /** Number of frames in flight (unacked) */
6827
+ get framesInFlight() {
6828
+ return this.inflightCount;
6829
+ }
6830
+ // ─── Internal: datagram handling ───────────────────────────────────────
6831
+ /**
6832
+ * Handle an incoming UDP datagram.
6833
+ *
6834
+ * Datagram format:
6835
+ * [frame_id:u16][frag_index:u8][frag_total:u8][payload...]
6836
+ */
6837
+ handleDatagram(data) {
6838
+ if (data.length < FRAGMENT_HEADER_SIZE) return;
6839
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
6840
+ const frameId = view.getUint16(0);
6841
+ const fragIndex = data[2];
6842
+ const fragTotal = data[3];
6843
+ const payload = data.subarray(FRAGMENT_HEADER_SIZE);
6844
+ if (fragTotal === 1) {
6845
+ this.handleReassembledFrame(payload);
6846
+ } else {
6847
+ this.handleFragment(frameId, fragIndex, fragTotal, payload);
6848
+ }
6849
+ }
6850
+ /**
6851
+ * Handle a fragment of a multi-datagram flow frame.
6852
+ */
6853
+ handleFragment(frameId, fragIndex, fragTotal, payload) {
6854
+ let group = this.fragmentGroups.get(frameId);
6855
+ if (!group) {
6856
+ group = {
6857
+ frameId,
6858
+ total: fragTotal,
6859
+ received: /* @__PURE__ */ new Map(),
6860
+ createdAt: Date.now()
6861
+ };
6862
+ this.fragmentGroups.set(frameId, group);
6863
+ }
6864
+ group.received.set(fragIndex, payload.slice());
6865
+ if (group.received.size === group.total) {
6866
+ let totalLen = 0;
6867
+ for (let i = 0; i < group.total; i++) {
6868
+ totalLen += group.received.get(i).length;
6869
+ }
6870
+ const reassembled = new Uint8Array(totalLen);
6871
+ let offset = 0;
6872
+ for (let i = 0; i < group.total; i++) {
6873
+ const frag = group.received.get(i);
6874
+ reassembled.set(frag, offset);
6875
+ offset += frag.length;
6876
+ }
6877
+ this.fragmentGroups.delete(frameId);
6878
+ this.handleReassembledFrame(reassembled);
6879
+ }
6880
+ if (this.fragmentGroups.size > 100) {
6881
+ const cutoff = Date.now() - 5e3;
6882
+ for (const [id, g] of this.fragmentGroups) {
6883
+ if (g.createdAt < cutoff) {
6884
+ this.fragmentGroups.delete(id);
6885
+ }
6886
+ }
6887
+ }
6888
+ }
6889
+ /**
6890
+ * Handle a complete (reassembled) flow frame.
6891
+ * This is where we do out-of-order reassembly using the stream_id + sequence.
6892
+ */
6893
+ handleReassembledFrame(data) {
6894
+ if (data.length >= HEADER_SIZE) {
6895
+ const flags = data[6];
6896
+ if (flags & ACK_FLAG) {
6897
+ this.handleAck(data);
6898
+ return;
6899
+ }
6900
+ }
6901
+ let frame;
6902
+ try {
6903
+ const result = this.codec.decode(data);
6904
+ frame = result.frame;
6905
+ } catch {
6906
+ return;
6907
+ }
6908
+ if (this.config.reliable) {
6909
+ let seqSet = this.receivedPerStream.get(frame.streamId);
6910
+ if (!seqSet) {
6911
+ seqSet = /* @__PURE__ */ new Set();
6912
+ this.receivedPerStream.set(frame.streamId, seqSet);
6913
+ }
6914
+ seqSet.add(frame.sequence);
6915
+ }
6916
+ const deliverable = this.reassembler.push(frame);
6917
+ for (const orderedFrame of deliverable) {
6918
+ const encoded = this.codec.encode(orderedFrame);
6919
+ if (this.receiveHandler) {
6920
+ this.receiveHandler(encoded);
6921
+ }
6922
+ }
6923
+ }
6924
+ // ─── Internal: fragmentation ───────────────────────────────────────────
6925
+ /**
6926
+ * Wrap a flow frame chunk in a fragment header.
6927
+ */
6928
+ wrapFragment(frameId, fragIndex, fragTotal, payload) {
6929
+ const datagram = new Uint8Array(FRAGMENT_HEADER_SIZE + payload.length);
6930
+ const view = new DataView(datagram.buffer);
6931
+ view.setUint16(0, frameId);
6932
+ datagram[2] = fragIndex;
6933
+ datagram[3] = fragTotal;
6934
+ datagram.set(payload, FRAGMENT_HEADER_SIZE);
6935
+ return datagram;
6936
+ }
6937
+ /**
6938
+ * Send a UDP datagram to the remote endpoint.
6939
+ */
6940
+ sendDatagram(data) {
6941
+ if (!this.socket || !this.config.remoteHost) return;
6942
+ this.socket.send(data, this.config.remoteHost, this.config.remotePort);
6943
+ }
6944
+ // ─── Internal: reliability ─────────────────────────────────────────────
6945
+ /**
6946
+ * Track a sent frame for potential retransmission.
6947
+ */
6948
+ trackInflight(data, frameId) {
6949
+ if (!this.config.reliable) return;
6950
+ if (data.length < HEADER_SIZE) return;
6951
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
6952
+ const streamId = view.getUint16(0);
6953
+ const sequence = view.getUint32(2);
6954
+ const key = `${streamId}:${sequence}`;
6955
+ this.inflight.set(key, {
6956
+ data: data.slice(),
6957
+ streamId,
6958
+ sequence,
6959
+ sentAt: Date.now(),
6960
+ retransmits: 0
6961
+ });
6962
+ this.inflightCount++;
6963
+ }
6964
+ /**
6965
+ * Handle an incoming ACK frame.
6966
+ *
6967
+ * ACK payload format:
6968
+ * [stream_id:u16][base_seq:u32][bitmap_hi:u32][bitmap_lo:u32]
6969
+ * (can repeat for multiple streams)
6970
+ */
6971
+ handleAck(data) {
6972
+ const payload = data.subarray(HEADER_SIZE);
6973
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
6974
+ for (let offset = 0; offset + 14 <= payload.length; offset += 14) {
6975
+ const streamId = view.getUint16(offset);
6976
+ const baseSeq = view.getUint32(offset + 2);
6977
+ const bitmapHi = view.getUint32(offset + 6);
6978
+ const bitmapLo = view.getUint32(offset + 10);
6979
+ for (let bit = 0; bit < 64; bit++) {
6980
+ const seq = baseSeq + bit;
6981
+ const isAcked = bit < 32 ? (bitmapLo & 1 << bit) !== 0 : (bitmapHi & 1 << bit - 32) !== 0;
6982
+ if (isAcked) {
6983
+ const key = `${streamId}:${seq}`;
6984
+ if (this.inflight.has(key)) {
6985
+ this.inflight.delete(key);
6986
+ this.inflightCount--;
6987
+ if (this.cwnd < MAX_CWND) {
6988
+ this.cwnd += 1 / this.cwnd;
6989
+ }
6990
+ }
6991
+ }
6992
+ }
6993
+ }
6994
+ }
6995
+ /**
6996
+ * Send ACK bitmaps for all received sequences.
6997
+ */
6998
+ sendAcks() {
6999
+ if (this.receivedPerStream.size === 0) return;
7000
+ const ackEntries = [];
7001
+ for (const [streamId, seqSet] of this.receivedPerStream) {
7002
+ if (seqSet.size === 0) continue;
7003
+ let baseSeq = Infinity;
7004
+ for (const seq of seqSet) {
7005
+ if (seq < baseSeq) baseSeq = seq;
7006
+ }
7007
+ let bitmapHi = 0;
7008
+ let bitmapLo = 0;
7009
+ for (const seq of seqSet) {
7010
+ const bit = seq - baseSeq;
7011
+ if (bit >= 0 && bit < 64) {
7012
+ if (bit < 32) {
7013
+ bitmapLo |= 1 << bit;
7014
+ } else {
7015
+ bitmapHi |= 1 << bit - 32;
7016
+ }
7017
+ }
7018
+ }
7019
+ const entry = new Uint8Array(14);
7020
+ const view = new DataView(entry.buffer);
7021
+ view.setUint16(0, streamId);
7022
+ view.setUint32(2, baseSeq);
7023
+ view.setUint32(6, bitmapHi);
7024
+ view.setUint32(10, bitmapLo);
7025
+ ackEntries.push(entry);
7026
+ if (seqSet.size > 128) {
7027
+ const cutoff = baseSeq + 64;
7028
+ for (const seq of seqSet) {
7029
+ if (seq < cutoff) seqSet.delete(seq);
7030
+ }
7031
+ }
7032
+ }
7033
+ if (ackEntries.length === 0) return;
7034
+ let totalLen = 0;
7035
+ for (const e of ackEntries) totalLen += e.length;
7036
+ const payload = new Uint8Array(totalLen);
7037
+ let offset = 0;
7038
+ for (const e of ackEntries) {
7039
+ payload.set(e, offset);
7040
+ offset += e.length;
7041
+ }
7042
+ const ackFrame = this.codec.encode({
7043
+ streamId: 65535,
7044
+ // Reserved stream for transport-level control
7045
+ sequence: 0,
7046
+ flags: ACK_FLAG,
7047
+ payload
7048
+ });
7049
+ const datagram = this.wrapFragment(
7050
+ this.nextFrameId++ & 65535,
7051
+ 0,
7052
+ 1,
7053
+ ackFrame
7054
+ );
7055
+ this.sendDatagram(datagram);
7056
+ }
7057
+ /**
7058
+ * Retransmit frames that haven't been ACKed.
7059
+ */
7060
+ retransmitLost() {
7061
+ const now = Date.now();
7062
+ for (const [key, frame] of this.inflight) {
7063
+ if (now - frame.sentAt > RETRANSMIT_TIMEOUT_MS) {
7064
+ if (frame.retransmits >= 5) {
7065
+ this.inflight.delete(key);
7066
+ this.inflightCount--;
7067
+ this.cwnd = Math.max(INITIAL_CWND, Math.floor(this.cwnd / 2));
7068
+ continue;
7069
+ }
7070
+ frame.retransmits++;
7071
+ frame.sentAt = now;
7072
+ const frameId = this.nextFrameId++ & 65535;
7073
+ const maxPayload = this.config.mtu - FRAGMENT_HEADER_SIZE;
7074
+ if (frame.data.length <= maxPayload) {
7075
+ const datagram = this.wrapFragment(frameId, 0, 1, frame.data);
7076
+ this.sendDatagram(datagram);
7077
+ } else {
7078
+ const totalFragments = Math.ceil(frame.data.length / maxPayload);
7079
+ for (let i = 0; i < totalFragments; i++) {
7080
+ const start = i * maxPayload;
7081
+ const end = Math.min(start + maxPayload, frame.data.length);
7082
+ const chunk = frame.data.slice(start, end);
7083
+ const datagram = this.wrapFragment(frameId, i, totalFragments, chunk);
7084
+ this.sendDatagram(datagram);
7085
+ }
7086
+ }
7087
+ this.cwnd = Math.max(INITIAL_CWND, Math.floor(this.cwnd / 2));
7088
+ }
7089
+ }
7090
+ }
7091
+ };
7092
+ async function createUDPSocket(host, port) {
7093
+ const dgram = await import('dgram');
7094
+ const socket = dgram.createSocket("udp4");
7095
+ return new Promise((resolve, reject) => {
7096
+ socket.on("error", (err) => reject(err));
7097
+ socket.bind(port, host, () => {
7098
+ const udpSocket = {
7099
+ send(data, remoteHost, remotePort) {
7100
+ socket.send(data, 0, data.length, remotePort, remoteHost);
7101
+ },
7102
+ onMessage(handler) {
7103
+ socket.on("message", (msg, rinfo) => {
7104
+ handler(new Uint8Array(msg), { address: rinfo.address, port: rinfo.port });
7105
+ });
7106
+ },
7107
+ close() {
7108
+ socket.close();
7109
+ }
7110
+ };
7111
+ resolve(udpSocket);
7112
+ });
7113
+ });
7114
+ }
7115
+ var WebTransportFlowTransport = class _WebTransportFlowTransport {
7116
+ wt;
7117
+ // WebTransport instance (typed as any for portability)
7118
+ writer;
7119
+ receiveHandler = null;
7120
+ closed = false;
7121
+ reassembler;
7122
+ fragmentGroups = /* @__PURE__ */ new Map();
7123
+ nextFrameId = 0;
7124
+ codec = FlowCodec.createSync();
7125
+ constructor(wt) {
7126
+ this.wt = wt;
7127
+ this.reassembler = new FrameReassembler();
7128
+ }
7129
+ /**
7130
+ * Connect to a WebTransport endpoint and return a FlowTransport.
7131
+ */
7132
+ static async connect(url) {
7133
+ if (typeof globalThis.WebTransport === "undefined") {
7134
+ throw new Error("WebTransport not available in this environment");
7135
+ }
7136
+ const wt = new globalThis.WebTransport(url);
7137
+ await wt.ready;
7138
+ const transport = new _WebTransportFlowTransport(wt);
7139
+ transport.writer = wt.datagrams.writable.getWriter();
7140
+ transport.readLoop(wt.datagrams.readable.getReader());
7141
+ return transport;
7142
+ }
7143
+ send(data) {
7144
+ if (this.closed) return;
7145
+ const maxPayload = UDP_MTU - FRAGMENT_HEADER_SIZE;
7146
+ if (data.length <= maxPayload) {
7147
+ const frameId = this.nextFrameId++ & 65535;
7148
+ const datagram = new Uint8Array(FRAGMENT_HEADER_SIZE + data.length);
7149
+ const view = new DataView(datagram.buffer);
7150
+ view.setUint16(0, frameId);
7151
+ datagram[2] = 0;
7152
+ datagram[3] = 1;
7153
+ datagram.set(data, FRAGMENT_HEADER_SIZE);
7154
+ this.writer.write(datagram).catch(() => {
7155
+ });
7156
+ } else {
7157
+ const frameId = this.nextFrameId++ & 65535;
7158
+ const totalFragments = Math.ceil(data.length / maxPayload);
7159
+ if (totalFragments > 255) return;
7160
+ for (let i = 0; i < totalFragments; i++) {
7161
+ const start = i * maxPayload;
7162
+ const end = Math.min(start + maxPayload, data.length);
7163
+ const chunk = data.slice(start, end);
7164
+ const datagram = new Uint8Array(FRAGMENT_HEADER_SIZE + chunk.length);
7165
+ const view = new DataView(datagram.buffer);
7166
+ view.setUint16(0, frameId);
7167
+ datagram[2] = i;
7168
+ datagram[3] = totalFragments;
7169
+ datagram.set(chunk, FRAGMENT_HEADER_SIZE);
7170
+ this.writer.write(datagram).catch(() => {
7171
+ });
7172
+ }
7173
+ }
7174
+ }
7175
+ onReceive(handler) {
7176
+ this.receiveHandler = handler;
7177
+ }
7178
+ close() {
7179
+ if (this.closed) return;
7180
+ this.closed = true;
7181
+ this.wt.close();
7182
+ this.receiveHandler = null;
7183
+ }
7184
+ async readLoop(reader) {
7185
+ try {
7186
+ while (!this.closed) {
7187
+ const { value, done } = await reader.read();
7188
+ if (done) break;
7189
+ const data = new Uint8Array(value);
7190
+ if (data.length < FRAGMENT_HEADER_SIZE) continue;
7191
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
7192
+ const frameId = view.getUint16(0);
7193
+ const fragIndex = data[2];
7194
+ const fragTotal = data[3];
7195
+ const payload = data.subarray(FRAGMENT_HEADER_SIZE);
7196
+ let reassembled;
7197
+ if (fragTotal === 1) {
7198
+ reassembled = payload;
7199
+ } else {
7200
+ let group = this.fragmentGroups.get(frameId);
7201
+ if (!group) {
7202
+ group = { frameId, total: fragTotal, received: /* @__PURE__ */ new Map(), createdAt: Date.now() };
7203
+ this.fragmentGroups.set(frameId, group);
7204
+ }
7205
+ group.received.set(fragIndex, payload.slice());
7206
+ if (group.received.size < group.total) continue;
7207
+ let totalLen = 0;
7208
+ for (let i = 0; i < group.total; i++) totalLen += group.received.get(i).length;
7209
+ reassembled = new Uint8Array(totalLen);
7210
+ let offset = 0;
7211
+ for (let i = 0; i < group.total; i++) {
7212
+ const frag = group.received.get(i);
7213
+ reassembled.set(frag, offset);
7214
+ offset += frag.length;
7215
+ }
7216
+ this.fragmentGroups.delete(frameId);
7217
+ }
7218
+ try {
7219
+ const result = this.codec.decode(reassembled);
7220
+ const deliverable = this.reassembler.push(result.frame);
7221
+ for (const frame of deliverable) {
7222
+ const encoded = this.codec.encode(frame);
7223
+ if (this.receiveHandler) {
7224
+ this.receiveHandler(encoded);
7225
+ }
7226
+ }
7227
+ } catch {
7228
+ }
7229
+ }
7230
+ } catch {
7231
+ }
7232
+ }
7233
+ };
7234
+
7235
+ // src/transport/dashrelay.ts
7236
+ var ENVELOPE_VERSION = 1;
7237
+ var FLAG_DIRECTED = 1;
7238
+ var FLAG_HAS_CHANNEL = 2;
7239
+ function encodeEnvelope(payload, targetPeerId, channel) {
7240
+ const encoder2 = new TextEncoder();
7241
+ const targetBytes = targetPeerId ? encoder2.encode(targetPeerId) : new Uint8Array(0);
7242
+ const channelBytes = channel ? encoder2.encode(channel) : new Uint8Array(0);
7243
+ let flags = 0;
7244
+ if (targetPeerId) flags |= FLAG_DIRECTED;
7245
+ if (channel) flags |= FLAG_HAS_CHANNEL;
7246
+ const headerSize = 10;
7247
+ const totalSize = headerSize + targetBytes.byteLength + channelBytes.byteLength + payload.byteLength;
7248
+ const envelope = new Uint8Array(totalSize);
7249
+ const view = new DataView(envelope.buffer);
7250
+ envelope[0] = ENVELOPE_VERSION;
7251
+ envelope[1] = flags;
7252
+ view.setUint32(2, targetBytes.byteLength, false);
7253
+ view.setUint32(6, channelBytes.byteLength, false);
7254
+ let offset = headerSize;
7255
+ envelope.set(targetBytes, offset);
7256
+ offset += targetBytes.byteLength;
7257
+ envelope.set(channelBytes, offset);
7258
+ offset += channelBytes.byteLength;
7259
+ envelope.set(payload, offset);
7260
+ return envelope;
7261
+ }
7262
+ function decodeEnvelope(data) {
7263
+ if (data.byteLength < 10) return null;
7264
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
7265
+ if (data[0] !== ENVELOPE_VERSION) return null;
7266
+ const flags = data[1];
7267
+ const targetLen = view.getUint32(2, false);
7268
+ const channelLen = view.getUint32(6, false);
7269
+ const headerSize = 10;
7270
+ if (data.byteLength < headerSize + targetLen + channelLen) return null;
7271
+ const decoder2 = new TextDecoder();
7272
+ let offset = headerSize;
7273
+ const target = flags & FLAG_DIRECTED ? decoder2.decode(data.subarray(offset, offset + targetLen)) : null;
7274
+ offset += targetLen;
7275
+ const channel = flags & FLAG_HAS_CHANNEL ? decoder2.decode(data.subarray(offset, offset + channelLen)) : null;
7276
+ offset += channelLen;
7277
+ const payload = data.subarray(offset);
7278
+ return { target, channel, payload };
7279
+ }
7280
+ var DashRelayFlowTransport = class {
7281
+ relay;
7282
+ receiveHandler = null;
7283
+ closed = false;
7284
+ config;
7285
+ peerHandler = null;
7286
+ /** Connected peers observed via relay events */
7287
+ peers = /* @__PURE__ */ new Set();
7288
+ peerJoinHandler = null;
7289
+ peerLeaveHandler = null;
7290
+ /** Optional handlers for peer lifecycle events */
7291
+ onPeerJoin = null;
7292
+ onPeerLeave = null;
7293
+ constructor(relay, config) {
7294
+ this.relay = relay;
7295
+ this.config = config ?? {};
7296
+ this.peerHandler = (senderId, payload) => {
7297
+ if (!this.receiveHandler) return;
7298
+ if (this.config.localPeerId && senderId === this.config.localPeerId) return;
7299
+ const envelope = decodeEnvelope(payload);
7300
+ if (!envelope) return;
7301
+ if (envelope.target && this.config.localPeerId && envelope.target !== this.config.localPeerId) {
7302
+ return;
7303
+ }
7304
+ if (this.config.channel && envelope.channel !== this.config.channel) {
7305
+ return;
7306
+ }
7307
+ this.receiveHandler(envelope.payload);
7308
+ };
7309
+ relay.on("message", this.peerHandler);
7310
+ this.peerJoinHandler = (peerId) => {
7311
+ this.peers.add(peerId);
7312
+ this.onPeerJoin?.(peerId);
7313
+ };
7314
+ this.peerLeaveHandler = (peerId) => {
7315
+ this.peers.delete(peerId);
7316
+ this.onPeerLeave?.(peerId);
7317
+ };
7318
+ relay.on("peerJoined", this.peerJoinHandler);
7319
+ relay.on("peerLeft", this.peerLeaveHandler);
7320
+ }
7321
+ // ─── FlowTransport interface ───────────────────────────────────────
7322
+ send(data) {
7323
+ if (this.closed) return;
7324
+ const envelope = encodeEnvelope(
7325
+ data,
7326
+ this.config.targetPeerId,
7327
+ this.config.channel
7328
+ );
7329
+ this.relay.broadcast(envelope);
7330
+ }
7331
+ onReceive(handler) {
7332
+ this.receiveHandler = handler;
7333
+ }
7334
+ close() {
7335
+ if (this.closed) return;
7336
+ this.closed = true;
7337
+ if (this.peerHandler) {
7338
+ this.relay.off("message", this.peerHandler);
7339
+ }
7340
+ if (this.peerJoinHandler) {
7341
+ this.relay.off("peerJoined", this.peerJoinHandler);
7342
+ }
7343
+ if (this.peerLeaveHandler) {
7344
+ this.relay.off("peerLeft", this.peerLeaveHandler);
7345
+ }
7346
+ this.receiveHandler = null;
7347
+ this.onPeerJoin = null;
7348
+ this.onPeerLeave = null;
7349
+ }
7350
+ // ─── Peer awareness ───────────────────────────────────────────────
7351
+ /** Get all known peers in the room */
7352
+ getPeers() {
7353
+ return Array.from(this.peers);
7354
+ }
7355
+ /** Number of peers in the room */
7356
+ get peerCount() {
7357
+ return this.peers.size;
7358
+ }
7359
+ /** Whether the transport is still open */
7360
+ get isOpen() {
7361
+ return !this.closed;
7362
+ }
7363
+ /** Send to a specific peer (overrides config.targetPeerId for this call) */
7364
+ sendTo(peerId, data) {
7365
+ if (this.closed) return;
7366
+ const envelope = encodeEnvelope(data, peerId, this.config.channel);
7367
+ this.relay.broadcast(envelope);
7368
+ }
7369
+ };
7370
+ function createDashRelayFlow(relay, channel, config) {
7371
+ return new DashRelayFlowTransport(relay, { ...config, channel });
7372
+ }
7373
+
7374
+ // src/transport/bluetooth.ts
7375
+ var AEON_FLOW_SERVICE_UUID = "0000af10-0000-1000-8000-00805f9b34fb";
7376
+ var AEON_FLOW_TX_UUID = "0000af11-0000-1000-8000-00805f9b34fb";
7377
+ var AEON_FLOW_RX_UUID = "0000af12-0000-1000-8000-00805f9b34fb";
7378
+ var DEFAULT_MTU = 512 - 3;
7379
+ var FRAME_BOUNDARY = new Uint8Array([174, 15, 16, 255]);
7380
+ var BluetoothFlowTransport = class _BluetoothFlowTransport {
7381
+ constructor(config = {}) {
7382
+ this.config = config;
7383
+ this.mtu = config.mtu ?? DEFAULT_MTU;
7384
+ }
7385
+ device = null;
7386
+ txChar = null;
7387
+ rxChar = null;
7388
+ receiveHandler = null;
7389
+ mtu;
7390
+ closed = false;
7391
+ /** Reassembly buffer for fragmented incoming frames */
7392
+ rxBuffer = [];
7393
+ /**
7394
+ * Scan for and connect to a nearby Aeon device.
7395
+ * Returns the connected device name.
7396
+ */
7397
+ async connect() {
7398
+ const serviceUuid = this.config.serviceUuid ?? AEON_FLOW_SERVICE_UUID;
7399
+ const filters = [];
7400
+ if (this.config.namePrefix) {
7401
+ filters.push({ namePrefix: this.config.namePrefix });
7402
+ }
7403
+ filters.push({ services: [serviceUuid] });
7404
+ this.device = await navigator.bluetooth.requestDevice({
7405
+ filters,
7406
+ optionalServices: [serviceUuid]
7407
+ });
7408
+ if (!this.device.gatt) {
7409
+ throw new Error("GATT not available on device");
7410
+ }
7411
+ const server = await this.device.gatt.connect();
7412
+ const service = await server.getPrimaryService(serviceUuid);
7413
+ this.txChar = await service.getCharacteristic(
7414
+ this.config.serviceUuid ? AEON_FLOW_TX_UUID : AEON_FLOW_TX_UUID
7415
+ );
7416
+ this.rxChar = await service.getCharacteristic(AEON_FLOW_RX_UUID);
7417
+ await this.rxChar.startNotifications();
7418
+ this.rxChar.addEventListener(
7419
+ "characteristicvaluechanged",
7420
+ this.handleRxNotification
7421
+ );
7422
+ this.device.addEventListener("gattserverdisconnected", () => {
7423
+ this.closed = true;
7424
+ this.receiveHandler = null;
7425
+ });
7426
+ return this.device.name || this.device.id;
7427
+ }
7428
+ /**
7429
+ * Create from an already-connected GATT server (for peripheral/server mode).
7430
+ */
7431
+ static fromCharacteristics(tx, rx, config) {
7432
+ const transport = new _BluetoothFlowTransport(config);
7433
+ transport.txChar = tx;
7434
+ transport.rxChar = rx;
7435
+ rx.addEventListener(
7436
+ "characteristicvaluechanged",
7437
+ transport.handleRxNotification
7438
+ );
7439
+ return transport;
7440
+ }
7441
+ // ─── FlowTransport interface ───────────────────────────────────────
7442
+ send(data) {
7443
+ if (this.closed || !this.txChar) return;
7444
+ const fragmentSize = this.mtu - FRAME_BOUNDARY.length;
7445
+ if (data.byteLength <= fragmentSize) {
7446
+ const packet = new Uint8Array(data.byteLength + FRAME_BOUNDARY.length);
7447
+ packet.set(data);
7448
+ packet.set(FRAME_BOUNDARY, data.byteLength);
7449
+ this.writeCharacteristic(packet);
7450
+ } else {
7451
+ let offset = 0;
7452
+ while (offset < data.byteLength) {
7453
+ const chunkSize = Math.min(fragmentSize, data.byteLength - offset);
7454
+ const isLast = offset + chunkSize >= data.byteLength;
7455
+ const chunk = data.subarray(offset, offset + chunkSize);
7456
+ if (isLast) {
7457
+ const packet = new Uint8Array(chunk.byteLength + FRAME_BOUNDARY.length);
7458
+ packet.set(chunk);
7459
+ packet.set(FRAME_BOUNDARY, chunk.byteLength);
7460
+ this.writeCharacteristic(packet);
7461
+ } else {
7462
+ this.writeCharacteristic(chunk);
7463
+ }
7464
+ offset += chunkSize;
7465
+ }
7466
+ }
7467
+ }
7468
+ onReceive(handler) {
7469
+ this.receiveHandler = handler;
7470
+ }
7471
+ close() {
7472
+ if (this.closed) return;
7473
+ this.closed = true;
7474
+ if (this.rxChar) {
7475
+ this.rxChar.removeEventListener(
7476
+ "characteristicvaluechanged",
7477
+ this.handleRxNotification
7478
+ );
7479
+ this.rxChar.stopNotifications().catch(() => {
7480
+ });
7481
+ }
7482
+ if (this.device?.gatt?.connected) {
7483
+ this.device.gatt.disconnect();
7484
+ }
7485
+ this.receiveHandler = null;
7486
+ this.rxBuffer = [];
7487
+ }
7488
+ /** Whether Bluetooth is connected */
7489
+ get isConnected() {
7490
+ return !this.closed && !!this.device?.gatt?.connected;
7491
+ }
7492
+ // ─── Internal ──────────────────────────────────────────────────────
7493
+ async writeCharacteristic(data) {
7494
+ if (!this.txChar) return;
7495
+ try {
7496
+ await this.txChar.writeValueWithoutResponse(data);
7497
+ } catch {
7498
+ }
7499
+ }
7500
+ handleRxNotification = (event) => {
7501
+ const target = event.target;
7502
+ if (!target.value) return;
7503
+ const chunk = new Uint8Array(target.value.buffer);
7504
+ if (this.endsWithBoundary(chunk)) {
7505
+ const frameData = chunk.subarray(0, chunk.byteLength - FRAME_BOUNDARY.length);
7506
+ if (this.rxBuffer.length > 0) {
7507
+ this.rxBuffer.push(frameData);
7508
+ const totalLen = this.rxBuffer.reduce((s, b) => s + b.byteLength, 0);
7509
+ const assembled = new Uint8Array(totalLen);
7510
+ let offset = 0;
7511
+ for (const buf of this.rxBuffer) {
7512
+ assembled.set(buf, offset);
7513
+ offset += buf.byteLength;
7514
+ }
7515
+ this.rxBuffer = [];
7516
+ this.receiveHandler?.(assembled);
7517
+ } else {
7518
+ this.receiveHandler?.(frameData);
7519
+ }
7520
+ } else {
7521
+ this.rxBuffer.push(chunk);
7522
+ }
7523
+ };
7524
+ endsWithBoundary(data) {
7525
+ if (data.byteLength < FRAME_BOUNDARY.length) return false;
7526
+ const tail = data.subarray(data.byteLength - FRAME_BOUNDARY.length);
7527
+ return tail[0] === FRAME_BOUNDARY[0] && tail[1] === FRAME_BOUNDARY[1] && tail[2] === FRAME_BOUNDARY[2] && tail[3] === FRAME_BOUNDARY[3];
7528
+ }
7529
+ };
7530
+
7531
+ // src/transport/webrtc.ts
7532
+ var WebRTCFlowTransport = class {
7533
+ pc = null;
7534
+ dc = null;
7535
+ signalingWs = null;
7536
+ receiveHandler = null;
7537
+ closed = false;
7538
+ peerId;
7539
+ config;
7540
+ maxBufferedAmount;
7541
+ constructor(config) {
7542
+ this.config = config;
7543
+ this.peerId = config.peerId ?? crypto.randomUUID();
7544
+ this.maxBufferedAmount = config.maxBufferedAmount ?? 16 * 1024 * 1024;
7545
+ }
7546
+ /**
7547
+ * Initialize as the offering peer (initiator).
7548
+ * Connects to signaling, creates offer, waits for answer.
7549
+ */
7550
+ async offer() {
7551
+ await this.connectSignaling();
7552
+ this.pc = new RTCPeerConnection({
7553
+ iceServers: this.config.iceServers ?? [
7554
+ { urls: "stun:stun.l.google.com:19302" }
7555
+ ]
7556
+ });
7557
+ const label = this.config.channelLabel ?? "aeon-flow";
7558
+ this.dc = this.pc.createDataChannel(label, {
7559
+ ordered: true,
7560
+ protocol: "aeon-flow"
7561
+ });
7562
+ this.dc.binaryType = "arraybuffer";
7563
+ this.wireDataChannel(this.dc);
7564
+ this.pc.onicecandidate = (event) => {
7565
+ if (event.candidate) {
7566
+ this.sendSignaling({
7567
+ type: "ice-candidate",
7568
+ from: this.peerId,
7569
+ to: "*",
7570
+ payload: event.candidate.toJSON()
7571
+ });
7572
+ }
7573
+ };
7574
+ const offer = await this.pc.createOffer();
7575
+ await this.pc.setLocalDescription(offer);
7576
+ this.sendSignaling({
7577
+ type: "offer",
7578
+ from: this.peerId,
7579
+ to: "*",
7580
+ payload: offer
7581
+ });
7582
+ }
7583
+ /**
7584
+ * Initialize as the answering peer (responder).
7585
+ * Waits for an offer, creates answer.
7586
+ */
7587
+ async answer() {
7588
+ await this.connectSignaling();
7589
+ this.pc = new RTCPeerConnection({
7590
+ iceServers: this.config.iceServers ?? [
7591
+ { urls: "stun:stun.l.google.com:19302" }
7592
+ ]
7593
+ });
7594
+ this.pc.onicecandidate = (event) => {
7595
+ if (event.candidate) {
7596
+ this.sendSignaling({
7597
+ type: "ice-candidate",
7598
+ from: this.peerId,
7599
+ to: "*",
7600
+ payload: event.candidate.toJSON()
7601
+ });
7602
+ }
7603
+ };
7604
+ this.pc.ondatachannel = (event) => {
7605
+ this.dc = event.channel;
7606
+ this.dc.binaryType = "arraybuffer";
7607
+ this.wireDataChannel(this.dc);
7608
+ };
7609
+ }
7610
+ /**
7611
+ * Wait until the DataChannel is open and ready.
7612
+ */
7613
+ async waitForOpen() {
7614
+ if (this.dc?.readyState === "open") return;
7615
+ return new Promise((resolve, reject) => {
7616
+ const timeout = setTimeout(() => {
7617
+ reject(new Error("WebRTC DataChannel open timeout"));
7618
+ }, 3e4);
7619
+ const check = () => {
7620
+ if (this.dc?.readyState === "open") {
7621
+ clearTimeout(timeout);
7622
+ resolve();
7623
+ }
7624
+ };
7625
+ const interval = setInterval(check, 100);
7626
+ check();
7627
+ if (this.dc) {
7628
+ const onOpen = () => {
7629
+ clearTimeout(timeout);
7630
+ clearInterval(interval);
7631
+ resolve();
7632
+ };
7633
+ this.dc.addEventListener("open", onOpen, { once: true });
7634
+ }
7635
+ });
7636
+ }
7637
+ // ─── FlowTransport interface ───────────────────────────────────────
7638
+ send(data) {
7639
+ if (this.closed || !this.dc) return;
7640
+ if (this.dc.readyState !== "open") return;
7641
+ if (this.dc.bufferedAmount > this.maxBufferedAmount) {
7642
+ console.warn("[aeon-webrtc] DataChannel buffer full, dropping frame");
7643
+ return;
7644
+ }
7645
+ this.dc.send(data);
7646
+ }
7647
+ onReceive(handler) {
7648
+ this.receiveHandler = handler;
7649
+ }
7650
+ close() {
7651
+ if (this.closed) return;
7652
+ this.closed = true;
7653
+ this.dc?.close();
7654
+ this.pc?.close();
7655
+ this.signalingWs?.close();
7656
+ this.receiveHandler = null;
7657
+ this.dc = null;
7658
+ this.pc = null;
7659
+ this.signalingWs = null;
7660
+ }
7661
+ /** Whether the DataChannel is open */
7662
+ get isOpen() {
7663
+ return !this.closed && this.dc?.readyState === "open";
7664
+ }
7665
+ /** Get this peer's ID */
7666
+ get id() {
7667
+ return this.peerId;
7668
+ }
7669
+ // ─── Internal ──────────────────────────────────────────────────────
7670
+ wireDataChannel(dc) {
7671
+ dc.onmessage = (event) => {
7672
+ if (!this.receiveHandler) return;
7673
+ if (event.data instanceof ArrayBuffer) {
7674
+ this.receiveHandler(new Uint8Array(event.data));
7675
+ }
7676
+ };
7677
+ dc.onclose = () => {
7678
+ this.closed = true;
7679
+ this.receiveHandler = null;
7680
+ };
7681
+ dc.onerror = () => {
7682
+ };
7683
+ }
7684
+ async connectSignaling() {
7685
+ const url = this.config.signalingUrl ?? "wss://relay.dashrelay.com/relay/sync";
7686
+ const roomUrl = `${url}?room=${encodeURIComponent(this.config.roomId)}&peer=${encodeURIComponent(this.peerId)}`;
7687
+ this.signalingWs = new WebSocket(roomUrl);
7688
+ await new Promise((resolve, reject) => {
7689
+ const ws = this.signalingWs;
7690
+ ws.onopen = () => resolve();
7691
+ ws.onerror = () => reject(new Error("Signaling connection failed"));
7692
+ });
7693
+ this.signalingWs.onmessage = async (event) => {
7694
+ try {
7695
+ const msg = JSON.parse(event.data);
7696
+ if (msg.from === this.peerId) return;
7697
+ switch (msg.type) {
7698
+ case "offer": {
7699
+ if (!this.pc) return;
7700
+ const desc = msg.payload;
7701
+ await this.pc.setRemoteDescription(desc);
7702
+ const answer = await this.pc.createAnswer();
7703
+ await this.pc.setLocalDescription(answer);
7704
+ this.sendSignaling({
7705
+ type: "answer",
7706
+ from: this.peerId,
7707
+ to: msg.from,
7708
+ payload: answer
7709
+ });
7710
+ break;
7711
+ }
7712
+ case "answer": {
7713
+ if (!this.pc) return;
7714
+ const desc = msg.payload;
7715
+ await this.pc.setRemoteDescription(desc);
7716
+ break;
7717
+ }
7718
+ case "ice-candidate": {
7719
+ if (!this.pc) return;
7720
+ const candidate = msg.payload;
7721
+ await this.pc.addIceCandidate(candidate);
7722
+ break;
7723
+ }
7724
+ }
7725
+ } catch {
7726
+ }
7727
+ };
7728
+ }
7729
+ sendSignaling(msg) {
7730
+ if (this.signalingWs?.readyState === WebSocket.OPEN) {
7731
+ this.signalingWs.send(JSON.stringify(msg));
7732
+ }
7733
+ }
7734
+ };
7735
+ async function createP2PFlow(roomId, role, config) {
7736
+ const transport = new WebRTCFlowTransport({ roomId, ...config });
7737
+ if (role === "initiator") {
7738
+ await transport.offer();
7739
+ } else {
7740
+ await transport.answer();
7741
+ }
7742
+ await transport.waitForOpen();
7743
+ return transport;
7744
+ }
7745
+
7746
+ // src/transport/tcp.ts
7747
+ var LENGTH_PREFIX_SIZE = 4;
7748
+ function encodeFrame(data) {
7749
+ const frame = new Uint8Array(LENGTH_PREFIX_SIZE + data.byteLength);
7750
+ const view = new DataView(frame.buffer);
7751
+ view.setUint32(0, data.byteLength, false);
7752
+ frame.set(data, LENGTH_PREFIX_SIZE);
7753
+ return frame;
7754
+ }
7755
+ var TCPFlowTransport = class {
7756
+ socket;
7757
+ receiveHandler = null;
7758
+ closed = false;
7759
+ /** Reassembly buffer for TCP stream → discrete messages */
7760
+ rxBuffer = new Uint8Array(0);
7761
+ constructor(socket, config) {
7762
+ this.socket = socket;
7763
+ socket.setNoDelay?.(true);
7764
+ socket.setKeepAlive?.(true, config?.keepAliveMs ?? 3e4);
7765
+ socket.on("data", (chunk) => {
7766
+ this.onData(new Uint8Array(chunk));
7767
+ });
7768
+ socket.on("close", () => {
7769
+ this.closed = true;
7770
+ this.receiveHandler = null;
7771
+ });
7772
+ socket.on("error", () => {
7773
+ });
7774
+ }
7775
+ // ─── FlowTransport interface ───────────────────────────────────────
7776
+ send(data) {
7777
+ if (this.closed) return;
7778
+ const frame = encodeFrame(data);
7779
+ this.socket.write(frame);
7780
+ }
7781
+ onReceive(handler) {
7782
+ this.receiveHandler = handler;
7783
+ }
7784
+ close() {
7785
+ if (this.closed) return;
7786
+ this.closed = true;
7787
+ this.receiveHandler = null;
7788
+ this.socket.end();
7789
+ }
7790
+ /** Whether the transport is still open */
7791
+ get isOpen() {
7792
+ return !this.closed;
7793
+ }
7794
+ // ─── Internal: Stream Reassembly ──────────────────────────────────
7795
+ onData(chunk) {
7796
+ const combined = new Uint8Array(this.rxBuffer.byteLength + chunk.byteLength);
7797
+ combined.set(this.rxBuffer);
7798
+ combined.set(chunk, this.rxBuffer.byteLength);
7799
+ this.rxBuffer = combined;
7800
+ while (this.rxBuffer.byteLength >= LENGTH_PREFIX_SIZE) {
7801
+ const view = new DataView(
7802
+ this.rxBuffer.buffer,
7803
+ this.rxBuffer.byteOffset,
7804
+ this.rxBuffer.byteLength
7805
+ );
7806
+ const msgLen = view.getUint32(0, false);
7807
+ if (this.rxBuffer.byteLength < LENGTH_PREFIX_SIZE + msgLen) {
7808
+ break;
7809
+ }
7810
+ const message = this.rxBuffer.slice(
7811
+ LENGTH_PREFIX_SIZE,
7812
+ LENGTH_PREFIX_SIZE + msgLen
7813
+ );
7814
+ this.rxBuffer = this.rxBuffer.slice(LENGTH_PREFIX_SIZE + msgLen);
7815
+ this.receiveHandler?.(message);
7816
+ }
7817
+ }
7818
+ };
7819
+ async function connectTCPFlow(host, port, config) {
7820
+ const net = await import('net');
7821
+ const timeout = config?.connectTimeout ?? 1e4;
7822
+ return new Promise((resolve, reject) => {
7823
+ const socket = net.createConnection({ host, port }, () => {
7824
+ resolve(new TCPFlowTransport(socket, config));
7825
+ });
7826
+ socket.setTimeout(timeout);
7827
+ socket.on("timeout", () => {
7828
+ socket.destroy();
7829
+ reject(new Error(`TCP connection timeout to ${host}:${port}`));
7830
+ });
7831
+ socket.on("error", (err) => {
7832
+ reject(new Error(`TCP connection failed to ${host}:${port}: ${err.message}`));
7833
+ });
7834
+ });
7835
+ }
7836
+ async function listenTCPFlow(port, host, onConnection) {
7837
+ const net = await import('net');
7838
+ const server = net.createServer((socket) => {
7839
+ const transport = new TCPFlowTransport(socket);
7840
+ onConnection(transport);
7841
+ });
7842
+ return new Promise((resolve, reject) => {
7843
+ server.listen(port, host, () => {
7844
+ resolve({
7845
+ close: () => server.close()
7846
+ });
7847
+ });
7848
+ server.on("error", (err) => {
7849
+ reject(new Error(`TCP flow server failed on ${host}:${port}: ${err.message}`));
7850
+ });
7851
+ });
7852
+ }
7853
+
7854
+ // src/transport/ipc.ts
7855
+ var MessagePortFlowTransport = class {
7856
+ port;
7857
+ receiveHandler = null;
7858
+ closed = false;
7859
+ transferBuffers;
7860
+ messageHandler = null;
7861
+ constructor(port, config) {
7862
+ this.port = port;
7863
+ this.transferBuffers = config?.transferBuffers ?? false;
7864
+ port.start?.();
7865
+ this.messageHandler = (event) => {
7866
+ if (!this.receiveHandler) return;
7867
+ const data = event.data;
7868
+ if (data instanceof ArrayBuffer) {
7869
+ this.receiveHandler(new Uint8Array(data));
7870
+ } else if (data instanceof Uint8Array) {
7871
+ this.receiveHandler(data);
7872
+ } else if (data?.type === "aeon-flow" && data.buffer instanceof ArrayBuffer) {
7873
+ this.receiveHandler(new Uint8Array(data.buffer));
7874
+ }
7875
+ };
7876
+ if (port.addEventListener) {
7877
+ port.addEventListener("message", this.messageHandler);
7878
+ } else {
7879
+ port.onmessage = this.messageHandler;
7880
+ }
7881
+ }
7882
+ // ─── FlowTransport interface ───────────────────────────────────────
7883
+ send(data) {
7884
+ if (this.closed) return;
7885
+ if (this.transferBuffers) {
7886
+ const copy = new Uint8Array(data);
7887
+ this.port.postMessage(copy.buffer, [copy.buffer]);
7888
+ } else {
7889
+ this.port.postMessage({ type: "aeon-flow", buffer: data.buffer.slice(0) });
7890
+ }
7891
+ }
7892
+ onReceive(handler) {
7893
+ this.receiveHandler = handler;
7894
+ }
7895
+ close() {
7896
+ if (this.closed) return;
7897
+ this.closed = true;
7898
+ if (this.messageHandler) {
7899
+ if (this.port.removeEventListener) {
7900
+ this.port.removeEventListener("message", this.messageHandler);
7901
+ } else {
7902
+ this.port.onmessage = null;
7903
+ }
7904
+ }
7905
+ this.receiveHandler = null;
7906
+ this.port.close?.();
7907
+ }
7908
+ /** Whether the transport is still open */
7909
+ get isOpen() {
7910
+ return !this.closed;
7911
+ }
7912
+ };
7913
+ var ChildProcessFlowTransport = class {
7914
+ process;
7915
+ receiveHandler = null;
7916
+ closed = false;
7917
+ ipcHandler = null;
7918
+ constructor(childProcess) {
7919
+ this.process = childProcess;
7920
+ this.ipcHandler = (data) => {
7921
+ if (!this.receiveHandler) return;
7922
+ if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
7923
+ this.receiveHandler(new Uint8Array(data));
7924
+ } else if (typeof data === "object" && data !== null && data.type === "aeon-flow") {
7925
+ const buffer = data.buffer;
7926
+ if (buffer instanceof ArrayBuffer) {
7927
+ this.receiveHandler(new Uint8Array(buffer));
7928
+ } else if (typeof buffer === "object" && buffer !== null && "data" in buffer) {
7929
+ const arr = buffer.data;
7930
+ this.receiveHandler(new Uint8Array(arr));
7931
+ }
7932
+ }
7933
+ };
7934
+ childProcess.on("message", this.ipcHandler);
7935
+ childProcess.on("exit", () => {
7936
+ this.closed = true;
7937
+ this.receiveHandler = null;
7938
+ });
7939
+ }
7940
+ // ─── FlowTransport interface ───────────────────────────────────────
7941
+ send(data) {
7942
+ if (this.closed) return;
7943
+ this.process.send({
7944
+ type: "aeon-flow",
7945
+ buffer: Array.from(data)
7946
+ // Serialize as plain array for V8 IPC
7947
+ });
7948
+ }
7949
+ onReceive(handler) {
7950
+ this.receiveHandler = handler;
7951
+ }
7952
+ close() {
7953
+ if (this.closed) return;
7954
+ this.closed = true;
7955
+ if (this.ipcHandler && this.process.removeListener) {
7956
+ this.process.removeListener("message", this.ipcHandler);
7957
+ }
7958
+ this.receiveHandler = null;
7959
+ }
7960
+ /** Whether the transport is still open */
7961
+ get isOpen() {
7962
+ return !this.closed;
7963
+ }
7964
+ };
7965
+ function createIPCPair(config) {
7966
+ const channel = new MessageChannel();
7967
+ return [
7968
+ new MessagePortFlowTransport(channel.port1, config),
7969
+ new MessagePortFlowTransport(channel.port2, config)
7970
+ ];
7971
+ }
7972
+
7973
+ // src/transport/usb.ts
7974
+ var AEON_USB_INTERFACE_CLASS = 255;
7975
+ var AEON_USB_SUBCLASS = 174;
7976
+ var AEON_USB_PROTOCOL = 1;
7977
+ var LENGTH_PREFIX_SIZE2 = 4;
7978
+ var DEFAULT_TRANSFER_SIZE = 64 * 1024;
7979
+ var USBFlowTransport = class _USBFlowTransport {
7980
+ device;
7981
+ receiveHandler = null;
7982
+ closed = false;
7983
+ interfaceNum;
7984
+ outEndpoint;
7985
+ inEndpoint;
7986
+ transferSize;
7987
+ readLoopRunning = false;
7988
+ /** Reassembly buffer for length-prefixed framing */
7989
+ rxBuffer = new Uint8Array(0);
7990
+ constructor(device, interfaceNum, outEndpoint, inEndpoint, transferSize) {
7991
+ this.device = device;
7992
+ this.interfaceNum = interfaceNum;
7993
+ this.outEndpoint = outEndpoint;
7994
+ this.inEndpoint = inEndpoint;
7995
+ this.transferSize = transferSize;
7996
+ }
7997
+ /**
7998
+ * Connect to a USB device and create a FlowTransport.
7999
+ *
8000
+ * Requests device access, opens it, claims the Aeon interface,
8001
+ * and auto-detects bulk endpoints if not specified.
8002
+ */
8003
+ static async connect(config) {
8004
+ const filters = [];
8005
+ if (config?.vendorId !== void 0) {
8006
+ const filter = { vendorId: config.vendorId };
8007
+ if (config.productId !== void 0) {
8008
+ filter.productId = config.productId;
8009
+ }
8010
+ filters.push(filter);
8011
+ }
8012
+ const device = await navigator.usb.requestDevice({
8013
+ filters: filters.length > 0 ? filters : [{ classCode: AEON_USB_INTERFACE_CLASS }]
8014
+ });
8015
+ await device.open();
8016
+ let interfaceNum = config?.interfaceNumber;
8017
+ let outEp = config?.outEndpoint;
8018
+ let inEp = config?.inEndpoint;
8019
+ if (interfaceNum === void 0 || outEp === void 0 || inEp === void 0) {
8020
+ const iface = device.configuration?.interfaces.find(
8021
+ (i) => i.alternates.some(
8022
+ (alt) => alt.interfaceClass === AEON_USB_INTERFACE_CLASS && alt.interfaceSubclass === AEON_USB_SUBCLASS
8023
+ )
8024
+ );
8025
+ if (!iface) {
8026
+ throw new Error("No Aeon USB interface found on device");
8027
+ }
8028
+ interfaceNum = iface.interfaceNumber;
8029
+ const alternate = iface.alternates.find(
8030
+ (alt) => alt.interfaceClass === AEON_USB_INTERFACE_CLASS && alt.interfaceSubclass === AEON_USB_SUBCLASS
8031
+ );
8032
+ for (const ep of alternate.endpoints) {
8033
+ if (ep.type === "bulk") {
8034
+ if (ep.direction === "out" && outEp === void 0) {
8035
+ outEp = ep.endpointNumber;
8036
+ } else if (ep.direction === "in" && inEp === void 0) {
8037
+ inEp = ep.endpointNumber;
8038
+ }
8039
+ }
8040
+ }
8041
+ }
8042
+ if (interfaceNum === void 0 || outEp === void 0 || inEp === void 0) {
8043
+ throw new Error("Could not find bulk endpoints on Aeon USB interface");
8044
+ }
8045
+ await device.claimInterface(interfaceNum);
8046
+ const transport = new _USBFlowTransport(
8047
+ device,
8048
+ interfaceNum,
8049
+ outEp,
8050
+ inEp,
8051
+ config?.transferSize ?? DEFAULT_TRANSFER_SIZE
8052
+ );
8053
+ transport.startReadLoop();
8054
+ return transport;
8055
+ }
8056
+ /**
8057
+ * Create from an already-opened USB device (for testing or manual setup).
8058
+ */
8059
+ static fromDevice(device, interfaceNum, outEndpoint, inEndpoint, config) {
8060
+ const transport = new _USBFlowTransport(
8061
+ device,
8062
+ interfaceNum,
8063
+ outEndpoint,
8064
+ inEndpoint,
8065
+ config?.transferSize ?? DEFAULT_TRANSFER_SIZE
8066
+ );
8067
+ transport.startReadLoop();
8068
+ return transport;
8069
+ }
8070
+ // ─── FlowTransport interface ───────────────────────────────────────
8071
+ send(data) {
8072
+ if (this.closed) return;
8073
+ const frame = new Uint8Array(LENGTH_PREFIX_SIZE2 + data.byteLength);
8074
+ const view = new DataView(frame.buffer);
8075
+ view.setUint32(0, data.byteLength, false);
8076
+ frame.set(data, LENGTH_PREFIX_SIZE2);
8077
+ this.device.transferOut(this.outEndpoint, frame).catch(() => {
8078
+ });
8079
+ }
8080
+ onReceive(handler) {
8081
+ this.receiveHandler = handler;
8082
+ }
8083
+ close() {
8084
+ if (this.closed) return;
8085
+ this.closed = true;
8086
+ this.readLoopRunning = false;
8087
+ this.receiveHandler = null;
8088
+ this.device.releaseInterface(this.interfaceNum).then(() => {
8089
+ this.device.close().catch(() => {
8090
+ });
8091
+ }).catch(() => {
8092
+ });
8093
+ }
8094
+ /** Whether the transport is still open */
8095
+ get isOpen() {
8096
+ return !this.closed;
8097
+ }
8098
+ /** Get the USB device info */
8099
+ get deviceInfo() {
8100
+ return {
8101
+ vendorId: this.device.vendorId,
8102
+ productId: this.device.productId,
8103
+ name: this.device.productName ?? this.device.serialNumber ?? "Unknown"
8104
+ };
8105
+ }
8106
+ // ─── Internal: Continuous Read Loop ───────────────────────────────
8107
+ startReadLoop() {
8108
+ if (this.readLoopRunning) return;
8109
+ this.readLoopRunning = true;
8110
+ const loop = async () => {
8111
+ while (this.readLoopRunning && !this.closed) {
8112
+ try {
8113
+ const result = await this.device.transferIn(
8114
+ this.inEndpoint,
8115
+ this.transferSize
8116
+ );
8117
+ if (result.status === "ok" && result.data && result.data.byteLength > 0) {
8118
+ const chunk = new Uint8Array(result.data.buffer);
8119
+ this.processChunk(chunk);
8120
+ }
8121
+ } catch {
8122
+ if (!this.closed) {
8123
+ this.closed = true;
8124
+ this.readLoopRunning = false;
8125
+ this.receiveHandler = null;
8126
+ }
8127
+ return;
8128
+ }
8129
+ }
8130
+ };
8131
+ void loop();
8132
+ }
8133
+ processChunk(chunk) {
8134
+ const combined = new Uint8Array(this.rxBuffer.byteLength + chunk.byteLength);
8135
+ combined.set(this.rxBuffer);
8136
+ combined.set(chunk, this.rxBuffer.byteLength);
8137
+ this.rxBuffer = combined;
8138
+ while (this.rxBuffer.byteLength >= LENGTH_PREFIX_SIZE2) {
8139
+ const view = new DataView(
8140
+ this.rxBuffer.buffer,
8141
+ this.rxBuffer.byteOffset,
8142
+ this.rxBuffer.byteLength
8143
+ );
8144
+ const msgLen = view.getUint32(0, false);
8145
+ if (this.rxBuffer.byteLength < LENGTH_PREFIX_SIZE2 + msgLen) {
8146
+ break;
8147
+ }
8148
+ const message = this.rxBuffer.slice(
8149
+ LENGTH_PREFIX_SIZE2,
8150
+ LENGTH_PREFIX_SIZE2 + msgLen
8151
+ );
8152
+ this.rxBuffer = this.rxBuffer.slice(LENGTH_PREFIX_SIZE2 + msgLen);
8153
+ this.receiveHandler?.(message);
8154
+ }
8155
+ }
8156
+ };
8157
+
8158
+ // src/transport/http.ts
8159
+ var encoder = new TextEncoder();
8160
+ var decoder = new TextDecoder();
8161
+ function encodeHTTPRequest(req) {
8162
+ const methodBytes = encoder.encode(req.method);
8163
+ const fullPath = req.query ? `${req.path}?${req.query}` : req.path;
8164
+ const pathBytes = encoder.encode(fullPath);
8165
+ const headersBytes = encoder.encode(JSON.stringify(req.headers));
8166
+ const bodyBytes = req.body ?? new Uint8Array(0);
8167
+ const headerSize = 16;
8168
+ const totalSize = headerSize + methodBytes.byteLength + pathBytes.byteLength + headersBytes.byteLength + bodyBytes.byteLength;
8169
+ const encoded = new Uint8Array(totalSize);
8170
+ const view = new DataView(encoded.buffer);
8171
+ view.setUint32(0, methodBytes.byteLength, false);
8172
+ view.setUint32(4, pathBytes.byteLength, false);
8173
+ view.setUint32(8, headersBytes.byteLength, false);
8174
+ view.setUint32(12, bodyBytes.byteLength, false);
8175
+ let offset = headerSize;
8176
+ encoded.set(methodBytes, offset);
8177
+ offset += methodBytes.byteLength;
8178
+ encoded.set(pathBytes, offset);
8179
+ offset += pathBytes.byteLength;
8180
+ encoded.set(headersBytes, offset);
8181
+ offset += headersBytes.byteLength;
8182
+ encoded.set(bodyBytes, offset);
8183
+ return encoded;
8184
+ }
8185
+ function decodeHTTPRequest(data) {
8186
+ if (data.byteLength < 16) throw new Error("Invalid HTTP request frame");
8187
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
8188
+ const methodLen = view.getUint32(0, false);
8189
+ const pathLen = view.getUint32(4, false);
8190
+ const headersLen = view.getUint32(8, false);
8191
+ const bodyLen = view.getUint32(12, false);
8192
+ let offset = 16;
8193
+ const method = decoder.decode(data.subarray(offset, offset + methodLen));
8194
+ offset += methodLen;
8195
+ const fullPath = decoder.decode(data.subarray(offset, offset + pathLen));
8196
+ offset += pathLen;
8197
+ const headersJson = decoder.decode(data.subarray(offset, offset + headersLen));
8198
+ offset += headersLen;
8199
+ const body = bodyLen > 0 ? data.slice(offset, offset + bodyLen) : void 0;
8200
+ const qIdx = fullPath.indexOf("?");
8201
+ const path = qIdx >= 0 ? fullPath.substring(0, qIdx) : fullPath;
8202
+ const query = qIdx >= 0 ? fullPath.substring(qIdx + 1) : void 0;
8203
+ return {
8204
+ method,
8205
+ path,
8206
+ query,
8207
+ headers: JSON.parse(headersJson),
8208
+ body,
8209
+ requestId: ""
8210
+ // Assigned by caller
8211
+ };
8212
+ }
8213
+ function encodeHTTPResponse(res) {
8214
+ const headersBytes = encoder.encode(JSON.stringify(res.headers));
8215
+ const bodyBytes = res.body;
8216
+ const headerSize = 10;
8217
+ const totalSize = headerSize + headersBytes.byteLength + bodyBytes.byteLength;
8218
+ const encoded = new Uint8Array(totalSize);
8219
+ const view = new DataView(encoded.buffer);
8220
+ view.setUint16(0, res.status, false);
8221
+ view.setUint32(2, headersBytes.byteLength, false);
8222
+ view.setUint32(6, bodyBytes.byteLength, false);
8223
+ let offset = headerSize;
8224
+ encoded.set(headersBytes, offset);
8225
+ offset += headersBytes.byteLength;
8226
+ encoded.set(bodyBytes, offset);
8227
+ return encoded;
8228
+ }
8229
+ function decodeHTTPResponse(data) {
8230
+ if (data.byteLength < 10) throw new Error("Invalid HTTP response frame");
8231
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
8232
+ const status = view.getUint16(0, false);
8233
+ const headersLen = view.getUint32(2, false);
8234
+ const bodyLen = view.getUint32(6, false);
8235
+ let offset = 10;
8236
+ const headersJson = decoder.decode(data.subarray(offset, offset + headersLen));
8237
+ offset += headersLen;
8238
+ const body = data.slice(offset, offset + bodyLen);
8239
+ return {
8240
+ status,
8241
+ headers: JSON.parse(headersJson),
8242
+ body
8243
+ };
8244
+ }
8245
+ var HTTPAeonBridge = class {
8246
+ transport;
8247
+ config;
8248
+ /** Pending HTTP requests waiting for flow responses */
8249
+ pending = /* @__PURE__ */ new Map();
8250
+ /** Handler for incoming HTTP requests (from nginx side) */
8251
+ requestHandler = null;
8252
+ constructor(transport, config) {
8253
+ this.transport = transport;
8254
+ this.config = config ?? {};
8255
+ transport.onReceive((data) => {
8256
+ this.handleIncoming(data);
8257
+ });
8258
+ }
8259
+ /**
8260
+ * Register a handler for incoming HTTP requests.
8261
+ * This is used on the Aeon side — the bridge receives HTTP requests
8262
+ * from nginx, translates them to flow, and calls this handler.
8263
+ */
8264
+ onRequest(handler) {
8265
+ this.requestHandler = handler;
8266
+ }
8267
+ /**
8268
+ * Send an HTTP request through the bridge (used by nginx side).
8269
+ * Translates the HTTP request into flow frames, waits for the
8270
+ * flow response, and returns it as an HTTP response.
8271
+ */
8272
+ async sendRequest(req) {
8273
+ const timeout = this.config.responseTimeout ?? 3e4;
8274
+ return new Promise((resolve, reject) => {
8275
+ const timer = setTimeout(() => {
8276
+ this.pending.delete(req.requestId);
8277
+ reject(new Error(`Flow response timeout for ${req.method} ${req.path}`));
8278
+ }, timeout);
8279
+ this.pending.set(req.requestId, { resolve, reject, timer, chunks: [] });
8280
+ const payload = encodeHTTPRequest(req);
8281
+ const reqIdBytes = encoder.encode(req.requestId);
8282
+ const frame = new Uint8Array(4 + reqIdBytes.byteLength + payload.byteLength);
8283
+ const view = new DataView(frame.buffer);
8284
+ view.setUint32(0, reqIdBytes.byteLength, false);
8285
+ frame.set(reqIdBytes, 4);
8286
+ frame.set(payload, 4 + reqIdBytes.byteLength);
8287
+ this.transport.send(frame);
8288
+ });
8289
+ }
8290
+ /**
8291
+ * Send an HTTP response back through the bridge (used by Aeon side).
8292
+ */
8293
+ sendResponse(res) {
8294
+ const payload = encodeHTTPResponse(res);
8295
+ const reqIdBytes = encoder.encode(res.requestId);
8296
+ const frame = new Uint8Array(5 + reqIdBytes.byteLength + payload.byteLength);
8297
+ const view = new DataView(frame.buffer);
8298
+ frame[0] = 2;
8299
+ view.setUint32(1, reqIdBytes.byteLength, false);
8300
+ frame.set(reqIdBytes, 5);
8301
+ frame.set(payload, 5 + reqIdBytes.byteLength);
8302
+ this.transport.send(frame);
8303
+ }
8304
+ close() {
8305
+ for (const [, pending] of this.pending) {
8306
+ clearTimeout(pending.timer);
8307
+ pending.reject(new Error("Bridge closed"));
8308
+ }
8309
+ this.pending.clear();
8310
+ this.transport.close();
8311
+ }
8312
+ // ─── Internal ──────────────────────────────────────────────────────
8313
+ handleIncoming(data) {
8314
+ if (data.byteLength < 5) return;
8315
+ if (data[0] === 2) {
8316
+ this.handleResponseFrame(data);
8317
+ } else {
8318
+ this.handleRequestFrame(data);
8319
+ }
8320
+ }
8321
+ handleResponseFrame(data) {
8322
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
8323
+ const reqIdLen = view.getUint32(1, false);
8324
+ const requestId = decoder.decode(data.subarray(5, 5 + reqIdLen));
8325
+ const payload = data.subarray(5 + reqIdLen);
8326
+ const pending = this.pending.get(requestId);
8327
+ if (!pending) return;
8328
+ clearTimeout(pending.timer);
8329
+ this.pending.delete(requestId);
8330
+ const response = decodeHTTPResponse(payload);
8331
+ pending.resolve({ ...response, requestId });
8332
+ }
8333
+ async handleRequestFrame(data) {
8334
+ if (!this.requestHandler) return;
8335
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
8336
+ const reqIdLen = view.getUint32(0, false);
8337
+ const requestId = decoder.decode(data.subarray(4, 4 + reqIdLen));
8338
+ const payload = data.subarray(4 + reqIdLen);
8339
+ const request = decodeHTTPRequest(payload);
8340
+ request.requestId = requestId;
8341
+ try {
8342
+ const response = await this.requestHandler(request);
8343
+ this.sendResponse(response);
8344
+ } catch (err) {
8345
+ this.sendResponse({
8346
+ requestId,
8347
+ status: 502,
8348
+ headers: { "content-type": "text/plain" },
8349
+ body: encoder.encode(err instanceof Error ? err.message : "Internal flow error")
8350
+ });
8351
+ }
8352
+ }
8353
+ };
8354
+
8355
+ // src/federation/federated-inference.ts
8356
+ var MSG_INFERENCE_REQUEST = 1;
8357
+ var MSG_INFERENCE_RESPONSE = 2;
8358
+ var MSG_CAPABILITY_ANNOUNCE = 3;
8359
+ var MSG_CAPABILITY_REQUEST = 4;
8360
+ var textEncoder = new TextEncoder();
8361
+ var textDecoder = new TextDecoder();
8362
+ function encodeInferenceRequest(req) {
8363
+ const json = JSON.stringify(req);
8364
+ const jsonBytes = textEncoder.encode(json);
8365
+ const frame = new Uint8Array(1 + jsonBytes.byteLength);
8366
+ frame[0] = MSG_INFERENCE_REQUEST;
8367
+ frame.set(jsonBytes, 1);
8368
+ return frame;
8369
+ }
8370
+ function encodeInferenceResponse(text, metrics) {
8371
+ const json = JSON.stringify({ text, ...metrics });
8372
+ const jsonBytes = textEncoder.encode(json);
8373
+ const frame = new Uint8Array(1 + jsonBytes.byteLength);
8374
+ frame[0] = MSG_INFERENCE_RESPONSE;
8375
+ frame.set(jsonBytes, 1);
8376
+ return frame;
8377
+ }
8378
+ function encodeCapabilities(caps) {
8379
+ const json = JSON.stringify(caps);
8380
+ const jsonBytes = textEncoder.encode(json);
8381
+ const frame = new Uint8Array(1 + jsonBytes.byteLength);
8382
+ frame[0] = MSG_CAPABILITY_ANNOUNCE;
8383
+ frame.set(jsonBytes, 1);
8384
+ return frame;
8385
+ }
8386
+ var FederatedInferenceCoordinator = class {
8387
+ peers = /* @__PURE__ */ new Map();
8388
+ config;
8389
+ listeners = /* @__PURE__ */ new Set();
8390
+ constructor(config) {
8391
+ this.config = config ?? {};
8392
+ }
8393
+ // ─── Peer Management ────────────────────────────────────────────
8394
+ /**
8395
+ * Register a peer with its flow transport.
8396
+ * The transport should already be connected.
8397
+ */
8398
+ addPeer(id, transport, capabilities) {
8399
+ const peer = {
8400
+ id,
8401
+ transport,
8402
+ capabilities: capabilities ?? { models: [], acceleration: "none" },
8403
+ lastSeen: Date.now(),
8404
+ available: true
8405
+ };
8406
+ this.peers.set(id, peer);
8407
+ this.emit({ type: "peer-added", peerId: id });
8408
+ if (!capabilities) {
8409
+ const reqFrame = new Uint8Array([MSG_CAPABILITY_REQUEST]);
8410
+ transport.send(reqFrame);
8411
+ }
8412
+ transport.onReceive((data) => {
8413
+ if (data[0] === MSG_CAPABILITY_ANNOUNCE) {
8414
+ const json = textDecoder.decode(data.subarray(1));
8415
+ peer.capabilities = JSON.parse(json);
8416
+ peer.lastSeen = Date.now();
8417
+ this.emit({ type: "peer-capabilities", peerId: id, capabilities: peer.capabilities });
8418
+ }
8419
+ });
8420
+ }
8421
+ /**
8422
+ * Remove a peer from the federation.
8423
+ */
8424
+ removePeer(id) {
8425
+ const peer = this.peers.get(id);
8426
+ if (peer) {
8427
+ peer.available = false;
8428
+ this.peers.delete(id);
8429
+ this.emit({ type: "peer-removed", peerId: id });
8430
+ }
8431
+ }
8432
+ /**
8433
+ * Get all available peers, optionally filtered by model support.
8434
+ */
8435
+ getAvailablePeers(model) {
8436
+ return Array.from(this.peers.values()).filter((peer) => {
8437
+ if (!peer.available) return false;
8438
+ if (model && !peer.capabilities.models.includes(model)) return false;
8439
+ return true;
8440
+ });
8441
+ }
8442
+ // ─── Federated Inference ──────────────────────────────────────────
8443
+ /**
8444
+ * Run federated inference across available peers.
8445
+ *
8446
+ * Forks the prompt to N peers, races them, returns the fastest result.
8447
+ * This is the core fork/race primitive applied at the network level.
8448
+ */
8449
+ async infer(request) {
8450
+ const availablePeers = this.getAvailablePeers(request.model);
8451
+ const minPeers = this.config.minPeers ?? 1;
8452
+ const maxPeers = this.config.maxPeers ?? 8;
8453
+ const timeout = this.config.timeout ?? 6e4;
8454
+ const includeLocal = this.config.includeLocal ?? true;
8455
+ const totalCandidates = availablePeers.length + (includeLocal ? 1 : 0);
8456
+ if (totalCandidates < minPeers) {
8457
+ throw new Error(
8458
+ `Not enough peers for federated inference: ${totalCandidates} available, ${minPeers} required`
8459
+ );
8460
+ }
8461
+ const selectedPeers = availablePeers.sort((a, b) => (b.capabilities.estimatedTps ?? 0) - (a.capabilities.estimatedTps ?? 0)).slice(0, maxPeers - (includeLocal ? 1 : 0));
8462
+ const startTime = Date.now();
8463
+ this.emit({ type: "inference-start", peerCount: selectedPeers.length + (includeLocal ? 1 : 0) });
8464
+ const raceEntries = [];
8465
+ for (const peer of selectedPeers) {
8466
+ const requestFrame = encodeInferenceRequest(request);
8467
+ const peerPromise = new Promise(
8468
+ (resolve, reject) => {
8469
+ const timer = setTimeout(() => {
8470
+ reject(new Error(`Peer ${peer.id} timeout`));
8471
+ }, timeout);
8472
+ const handler = (data) => {
8473
+ if (data[0] === MSG_INFERENCE_RESPONSE) {
8474
+ clearTimeout(timer);
8475
+ const json = textDecoder.decode(data.subarray(1));
8476
+ const result = JSON.parse(json);
8477
+ resolve(result);
8478
+ }
8479
+ };
8480
+ peer.transport.onReceive(handler);
8481
+ peer.transport.send(requestFrame);
8482
+ }
8483
+ );
8484
+ raceEntries.push({ id: peer.id, promise: peerPromise });
8485
+ }
8486
+ if (includeLocal && this.config.localInference) {
8487
+ const localStart = Date.now();
8488
+ const localPromise = this.config.localInference(request.prompt).then((text) => {
8489
+ const totalTime = Date.now() - localStart;
8490
+ const tokens = text.split(/\s+/).length;
8491
+ return {
8492
+ text,
8493
+ ttft: totalTime / 2,
8494
+ // Rough estimate
8495
+ totalTime,
8496
+ tps: tokens / (totalTime / 1e3)
8497
+ };
8498
+ });
8499
+ raceEntries.push({ id: "__local__", promise: localPromise });
8500
+ }
8501
+ if (request.collectAll) {
8502
+ const allResults = /* @__PURE__ */ new Map();
8503
+ let winner = null;
8504
+ await Promise.allSettled(
8505
+ raceEntries.map(async (entry) => {
8506
+ const result = await entry.promise;
8507
+ if (!winner) {
8508
+ winner = { id: entry.id, result };
8509
+ }
8510
+ allResults.set(entry.id, { text: result.text, time: result.totalTime });
8511
+ return { id: entry.id, result };
8512
+ })
8513
+ );
8514
+ if (!winner) {
8515
+ throw new Error("All peers failed inference");
8516
+ }
8517
+ const w = winner;
8518
+ return {
8519
+ winnerId: w.id,
8520
+ text: w.result.text,
8521
+ ttft: w.result.ttft,
8522
+ totalTime: w.result.totalTime,
8523
+ tokensPerSecond: w.result.tps,
8524
+ allResults
8525
+ };
8526
+ } else {
8527
+ const winner = await Promise.any(
8528
+ raceEntries.map(async (entry) => {
8529
+ const result = await entry.promise;
8530
+ return { id: entry.id, result };
8531
+ })
8532
+ );
8533
+ this.emit({
8534
+ type: "inference-complete",
8535
+ winnerId: winner.id,
8536
+ totalTime: Date.now() - startTime
8537
+ });
8538
+ return {
8539
+ winnerId: winner.id,
8540
+ text: winner.result.text,
8541
+ ttft: winner.result.ttft,
8542
+ totalTime: winner.result.totalTime,
8543
+ tokensPerSecond: winner.result.tps
8544
+ };
8545
+ }
8546
+ }
8547
+ // ─── Peer-Side: Handle Incoming Inference Requests ────────────────
8548
+ /**
8549
+ * Create a handler for incoming inference requests.
8550
+ * Call this on the peer side to process requests from the coordinator.
8551
+ *
8552
+ * @param inferFn - The actual inference function on this peer
8553
+ * @returns A FlowTransport receive handler
8554
+ */
8555
+ static createPeerHandler(inferFn) {
8556
+ return (data) => {
8557
+ if (data[0] !== MSG_INFERENCE_REQUEST) return;
8558
+ const json = textDecoder.decode(data.subarray(1));
8559
+ const request = JSON.parse(json);
8560
+ const startTime = Date.now();
8561
+ let ttft = 0;
8562
+ void inferFn(request.prompt, {
8563
+ maxTokens: request.maxTokens,
8564
+ temperature: request.temperature
8565
+ }).then((text) => {
8566
+ if (ttft === 0) ttft = Date.now() - startTime;
8567
+ const totalTime = Date.now() - startTime;
8568
+ const tokens = text.split(/\s+/).length;
8569
+ const tps = tokens / (totalTime / 1e3);
8570
+ return encodeInferenceResponse(text, { ttft, totalTime, tps });
8571
+ });
8572
+ };
8573
+ }
8574
+ /**
8575
+ * Set up a peer to respond to federated inference requests.
8576
+ *
8577
+ * @param transport - The FlowTransport connected to the coordinator
8578
+ * @param inferFn - The inference function to run
8579
+ * @param capabilities - This peer's capabilities to announce
8580
+ */
8581
+ static setupPeer(transport, inferFn, capabilities) {
8582
+ transport.send(encodeCapabilities(capabilities));
8583
+ transport.onReceive((data) => {
8584
+ if (data[0] === MSG_CAPABILITY_REQUEST) {
8585
+ transport.send(encodeCapabilities(capabilities));
8586
+ return;
8587
+ }
8588
+ if (data[0] !== MSG_INFERENCE_REQUEST) return;
8589
+ const json = textDecoder.decode(data.subarray(1));
8590
+ const request = JSON.parse(json);
8591
+ const startTime = Date.now();
8592
+ void inferFn(request.prompt, {
8593
+ maxTokens: request.maxTokens,
8594
+ temperature: request.temperature
8595
+ }).then((text) => {
8596
+ const totalTime = Date.now() - startTime;
8597
+ const tokens = text.split(/\s+/).length;
8598
+ const tps = tokens / Math.max(totalTime / 1e3, 1e-3);
8599
+ const response = encodeInferenceResponse(text, { ttft: totalTime / 2, totalTime, tps });
8600
+ transport.send(response);
8601
+ }).catch(() => {
8602
+ });
8603
+ });
8604
+ }
8605
+ // ─── Events ──────────────────────────────────────────────────────
8606
+ on(handler) {
8607
+ this.listeners.add(handler);
8608
+ }
8609
+ off(handler) {
8610
+ this.listeners.delete(handler);
8611
+ }
8612
+ emit(event) {
8613
+ for (const handler of this.listeners) {
8614
+ handler(event);
8615
+ }
8616
+ }
8617
+ // ─── Status ──────────────────────────────────────────────────────
8618
+ get peerCount() {
8619
+ return this.peers.size;
8620
+ }
8621
+ get availablePeerCount() {
8622
+ return Array.from(this.peers.values()).filter((p) => p.available).length;
8623
+ }
8624
+ destroy() {
8625
+ for (const peer of this.peers.values()) {
8626
+ peer.transport.close();
8627
+ }
8628
+ this.peers.clear();
8629
+ this.listeners.clear();
8630
+ }
8631
+ };
8632
+
8633
+ // src/index.ts
8634
+ var Link = (() => {
8635
+ throw new Error(
8636
+ "Link: Stub called from @affectively/aeon. Import from @affectively/aeon-flux-react or mock in tests."
8637
+ );
8638
+ });
8639
+ var useAeonPage = (() => {
8640
+ throw new Error(
8641
+ "useAeonPage: Stub called from @affectively/aeon. Import from @affectively/aeon-flux-react or mock in tests."
8642
+ );
8643
+ });
8644
+
8645
+ exports.ACK_FLAG = ACK_FLAG;
5813
8646
  exports.AEON_CAPABILITIES = AEON_CAPABILITIES;
8647
+ exports.AEON_FLOW_RX_UUID = AEON_FLOW_RX_UUID;
8648
+ exports.AEON_FLOW_SERVICE_UUID = AEON_FLOW_SERVICE_UUID;
8649
+ exports.AEON_FLOW_TX_UUID = AEON_FLOW_TX_UUID;
8650
+ exports.AEON_USB_INTERFACE_CLASS = AEON_USB_INTERFACE_CLASS;
8651
+ exports.AEON_USB_PROTOCOL = AEON_USB_PROTOCOL;
8652
+ exports.AEON_USB_SUBCLASS = AEON_USB_SUBCLASS;
5814
8653
  exports.AdaptiveCompressionOptimizer = AdaptiveCompressionOptimizer;
8654
+ exports.AeonFlowProtocol = AeonFlowProtocol;
5815
8655
  exports.AgentPresenceManager = AgentPresenceManager;
5816
8656
  exports.BatchTimingOptimizer = BatchTimingOptimizer;
8657
+ exports.BluetoothFlowTransport = BluetoothFlowTransport;
8658
+ exports.COLLAPSE = COLLAPSE;
8659
+ exports.ChildProcessFlowTransport = ChildProcessFlowTransport;
5817
8660
  exports.CompressionEngine = CompressionEngine;
5818
8661
  exports.DEFAULT_CRYPTO_CONFIG = DEFAULT_CRYPTO_CONFIG;
8662
+ exports.DEFAULT_FLOW_CONFIG = DEFAULT_FLOW_CONFIG;
8663
+ exports.DashRelayFlowTransport = DashRelayFlowTransport;
5819
8664
  exports.DashStorageAdapter = DashStorageAdapter;
5820
8665
  exports.DataTransformer = DataTransformer;
5821
8666
  exports.DeltaSyncOptimizer = DeltaSyncOptimizer;
8667
+ exports.FIN = FIN;
8668
+ exports.FORK = FORK;
8669
+ exports.FRAGMENT_HEADER_SIZE = FRAGMENT_HEADER_SIZE;
8670
+ exports.FederatedInferenceCoordinator = FederatedInferenceCoordinator;
8671
+ exports.FlowCodec = FlowCodec;
8672
+ exports.FrameReassembler = FrameReassembler;
8673
+ exports.HEADER_SIZE = HEADER_SIZE;
8674
+ exports.HTTPAeonBridge = HTTPAeonBridge;
5822
8675
  exports.InMemoryStorageAdapter = InMemoryStorageAdapter;
8676
+ exports.Link = Link;
8677
+ exports.MAX_FRAGMENT_PAYLOAD = MAX_FRAGMENT_PAYLOAD;
8678
+ exports.MAX_PAYLOAD_LENGTH = MAX_PAYLOAD_LENGTH;
8679
+ exports.MessagePortFlowTransport = MessagePortFlowTransport;
5823
8680
  exports.MigrationEngine = MigrationEngine;
5824
8681
  exports.MigrationTracker = MigrationTracker;
5825
8682
  exports.NullCryptoProvider = NullCryptoProvider;
8683
+ exports.NullTransactionSigner = NullTransactionSigner;
5826
8684
  exports.OfflineOperationQueue = OfflineOperationQueue;
8685
+ exports.POISON = POISON;
5827
8686
  exports.PrefetchingEngine = PrefetchingEngine;
8687
+ exports.RACE = RACE;
5828
8688
  exports.ReplicationManager = ReplicationManager;
5829
8689
  exports.SchemaVersionManager = SchemaVersionManager;
5830
8690
  exports.StateReconciler = StateReconciler;
5831
8691
  exports.SyncCoordinator = SyncCoordinator;
5832
8692
  exports.SyncProtocol = SyncProtocol;
8693
+ exports.TCPFlowTransport = TCPFlowTransport;
8694
+ exports.UDPFlowTransport = UDPFlowTransport;
8695
+ exports.UDP_MTU = UDP_MTU;
8696
+ exports.USBFlowTransport = USBFlowTransport;
8697
+ exports.WebRTCFlowTransport = WebRTCFlowTransport;
8698
+ exports.WebTransportFlowTransport = WebTransportFlowTransport;
5833
8699
  exports.clearAgentPresenceManager = clearAgentPresenceManager;
8700
+ exports.connectTCPFlow = connectTCPFlow;
8701
+ exports.createDashRelayFlow = createDashRelayFlow;
8702
+ exports.createIPCPair = createIPCPair;
5834
8703
  exports.createNamespacedLogger = createNamespacedLogger;
8704
+ exports.createP2PFlow = createP2PFlow;
8705
+ exports.createTransactionSignerAdapter = createTransactionSignerAdapter;
8706
+ exports.decodeHTTPRequest = decodeHTTPRequest;
8707
+ exports.decodeHTTPResponse = decodeHTTPResponse;
5835
8708
  exports.disableLogging = disableLogging;
8709
+ exports.encodeHTTPRequest = encodeHTTPRequest;
8710
+ exports.encodeHTTPResponse = encodeHTTPResponse;
5836
8711
  exports.getAdaptiveCompressionOptimizer = getAdaptiveCompressionOptimizer;
5837
8712
  exports.getAgentPresenceManager = getAgentPresenceManager;
5838
8713
  exports.getBatchTimingOptimizer = getBatchTimingOptimizer;
@@ -5841,6 +8716,7 @@ exports.getDeltaSyncOptimizer = getDeltaSyncOptimizer;
5841
8716
  exports.getLogger = getLogger;
5842
8717
  exports.getOfflineOperationQueue = getOfflineOperationQueue;
5843
8718
  exports.getPrefetchingEngine = getPrefetchingEngine;
8719
+ exports.listenTCPFlow = listenTCPFlow;
5844
8720
  exports.logger = logger;
5845
8721
  exports.resetAdaptiveCompressionOptimizer = resetAdaptiveCompressionOptimizer;
5846
8722
  exports.resetBatchTimingOptimizer = resetBatchTimingOptimizer;
@@ -5850,5 +8726,6 @@ exports.resetLogger = resetLogger;
5850
8726
  exports.resetOfflineOperationQueue = resetOfflineOperationQueue;
5851
8727
  exports.resetPrefetchingEngine = resetPrefetchingEngine;
5852
8728
  exports.setLogger = setLogger;
8729
+ exports.useAeonPage = useAeonPage;
5853
8730
  //# sourceMappingURL=index.cjs.map
5854
8731
  //# sourceMappingURL=index.cjs.map