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