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