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