@peerbit/stream 4.6.0 → 5.0.0-b712c6b
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/dist/benchmark/directstream-sim.d.ts +12 -0
- package/dist/benchmark/directstream-sim.d.ts.map +1 -0
- package/dist/benchmark/directstream-sim.js +299 -0
- package/dist/benchmark/directstream-sim.js.map +1 -0
- package/dist/benchmark/index.d.ts +10 -0
- package/dist/benchmark/index.d.ts.map +1 -0
- package/dist/benchmark/index.js +48 -0
- package/dist/benchmark/index.js.map +1 -0
- package/dist/benchmark/topology-sim.d.ts +12 -0
- package/dist/benchmark/topology-sim.d.ts.map +1 -0
- package/dist/benchmark/topology-sim.js +410 -0
- package/dist/benchmark/topology-sim.js.map +1 -0
- package/dist/benchmark/transfer.js +2 -2
- package/dist/benchmark/transfer.js.map +1 -1
- package/dist/src/core/seek-routing.d.ts +39 -0
- package/dist/src/core/seek-routing.d.ts.map +1 -0
- package/dist/src/core/seek-routing.js +33 -0
- package/dist/src/core/seek-routing.js.map +1 -0
- package/dist/src/index.d.ts +39 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +522 -300
- package/dist/src/index.js.map +1 -1
- package/dist/src/routes.d.ts +15 -0
- package/dist/src/routes.d.ts.map +1 -1
- package/dist/src/routes.js +135 -16
- package/dist/src/routes.js.map +1 -1
- package/dist/src/wait-for-event.d.ts.map +1 -1
- package/dist/src/wait-for-event.js +14 -3
- package/dist/src/wait-for-event.js.map +1 -1
- package/package.json +9 -9
- package/src/core/seek-routing.ts +75 -0
- package/src/index.ts +805 -509
- package/src/routes.ts +158 -18
- package/src/wait-for-event.ts +23 -10
package/src/routes.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { DirectStreamAckRouteHint } from "@peerbit/stream-interface";
|
|
2
2
|
|
|
3
3
|
export const MAX_ROUTE_DISTANCE = Number.MAX_SAFE_INTEGER - 1;
|
|
4
4
|
|
|
5
|
+
const DEFAULT_MAX_FROM_ENTRIES = 2048;
|
|
6
|
+
const DEFAULT_MAX_TARGETS_PER_FROM = 10_000;
|
|
7
|
+
const DEFAULT_MAX_RELAYS_PER_TARGET = 32;
|
|
8
|
+
|
|
5
9
|
type RelayInfo = {
|
|
6
10
|
session: number;
|
|
7
11
|
hash: string;
|
|
12
|
+
updatedAt: number;
|
|
8
13
|
expireAt?: number;
|
|
9
14
|
distance: number;
|
|
10
15
|
};
|
|
@@ -44,17 +49,103 @@ export class Routes {
|
|
|
44
49
|
|
|
45
50
|
signal?: AbortSignal;
|
|
46
51
|
|
|
52
|
+
private pendingCleanupByFrom: Map<string, Set<string>> = new Map();
|
|
53
|
+
private cleanupTimer?: ReturnType<typeof setTimeout>;
|
|
54
|
+
private maxFromEntries: number;
|
|
55
|
+
private maxTargetsPerFrom: number;
|
|
56
|
+
private maxRelaysPerTarget: number;
|
|
57
|
+
|
|
47
58
|
constructor(
|
|
48
59
|
readonly me: string,
|
|
49
|
-
options?: {
|
|
60
|
+
options?: {
|
|
61
|
+
routeMaxRetentionPeriod?: number;
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
maxFromEntries?: number;
|
|
64
|
+
maxTargetsPerFrom?: number;
|
|
65
|
+
maxRelaysPerTarget?: number;
|
|
66
|
+
},
|
|
50
67
|
) {
|
|
51
68
|
this.routeMaxRetentionPeriod =
|
|
52
69
|
options?.routeMaxRetentionPeriod ?? 10 * 1000;
|
|
53
70
|
this.signal = options?.signal;
|
|
71
|
+
this.maxFromEntries = Math.max(
|
|
72
|
+
1,
|
|
73
|
+
Math.floor(options?.maxFromEntries ?? DEFAULT_MAX_FROM_ENTRIES),
|
|
74
|
+
);
|
|
75
|
+
this.maxTargetsPerFrom = Math.max(
|
|
76
|
+
1,
|
|
77
|
+
Math.floor(options?.maxTargetsPerFrom ?? DEFAULT_MAX_TARGETS_PER_FROM),
|
|
78
|
+
);
|
|
79
|
+
this.maxRelaysPerTarget = Math.max(
|
|
80
|
+
1,
|
|
81
|
+
Math.floor(options?.maxRelaysPerTarget ?? DEFAULT_MAX_RELAYS_PER_TARGET),
|
|
82
|
+
);
|
|
54
83
|
}
|
|
55
84
|
|
|
56
85
|
clear() {
|
|
57
86
|
this.routes.clear();
|
|
87
|
+
this.pendingCleanupByFrom.clear();
|
|
88
|
+
if (this.cleanupTimer) clearTimeout(this.cleanupTimer);
|
|
89
|
+
this.cleanupTimer = undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private requestCleanup(from: string, to: string) {
|
|
93
|
+
if (this.signal?.aborted) return;
|
|
94
|
+
let targets = this.pendingCleanupByFrom.get(from);
|
|
95
|
+
if (!targets) {
|
|
96
|
+
targets = new Set<string>();
|
|
97
|
+
this.pendingCleanupByFrom.set(from, targets);
|
|
98
|
+
}
|
|
99
|
+
targets.add(to);
|
|
100
|
+
|
|
101
|
+
// Coalesce cleanups into a single timer. The previous per-update timer approach
|
|
102
|
+
// scales poorly in large networks and can OOM in single-process simulations.
|
|
103
|
+
if (this.cleanupTimer) return;
|
|
104
|
+
this.cleanupTimer = setTimeout(() => {
|
|
105
|
+
this.cleanupTimer = undefined;
|
|
106
|
+
const pending = this.pendingCleanupByFrom;
|
|
107
|
+
this.pendingCleanupByFrom = new Map();
|
|
108
|
+
for (const [fromKey, tos] of pending) {
|
|
109
|
+
for (const toKey of tos) {
|
|
110
|
+
this.cleanup(fromKey, toKey);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}, this.routeMaxRetentionPeriod + 100);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private pruneFromMaps() {
|
|
117
|
+
if (this.routes.size <= this.maxFromEntries) return;
|
|
118
|
+
|
|
119
|
+
// Keep `me` pinned: local routes are used for pruning decisions and should be
|
|
120
|
+
// the last thing we evict under memory pressure.
|
|
121
|
+
while (this.routes.size > this.maxFromEntries) {
|
|
122
|
+
const oldest = this.routes.keys().next().value as string | undefined;
|
|
123
|
+
if (!oldest) return;
|
|
124
|
+
if (oldest === this.me) {
|
|
125
|
+
const selfMap = this.routes.get(oldest);
|
|
126
|
+
if (!selfMap) {
|
|
127
|
+
this.routes.delete(oldest);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
// Move to the end (most recently used) and continue eviction.
|
|
131
|
+
this.routes.delete(oldest);
|
|
132
|
+
this.routes.set(oldest, selfMap);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
this.routes.delete(oldest);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private pruneTargets(from: string, fromMap: Map<string, RouteInfo>) {
|
|
140
|
+
if (fromMap.size <= this.maxTargetsPerFrom) return;
|
|
141
|
+
while (fromMap.size > this.maxTargetsPerFrom) {
|
|
142
|
+
const oldestTarget = fromMap.keys().next().value as string | undefined;
|
|
143
|
+
if (!oldestTarget) break;
|
|
144
|
+
fromMap.delete(oldestTarget);
|
|
145
|
+
}
|
|
146
|
+
if (fromMap.size === 0) {
|
|
147
|
+
this.routes.delete(from);
|
|
148
|
+
}
|
|
58
149
|
}
|
|
59
150
|
|
|
60
151
|
private cleanup(from: string, to: string) {
|
|
@@ -73,6 +164,10 @@ export class Routes {
|
|
|
73
164
|
}
|
|
74
165
|
}
|
|
75
166
|
|
|
167
|
+
if (keepRoutes.length > this.maxRelaysPerTarget) {
|
|
168
|
+
keepRoutes.length = this.maxRelaysPerTarget;
|
|
169
|
+
}
|
|
170
|
+
|
|
76
171
|
if (keepRoutes.length > 0) {
|
|
77
172
|
map.list = keepRoutes;
|
|
78
173
|
} else {
|
|
@@ -96,6 +191,10 @@ export class Routes {
|
|
|
96
191
|
if (!fromMap) {
|
|
97
192
|
fromMap = new Map();
|
|
98
193
|
this.routes.set(from, fromMap);
|
|
194
|
+
} else {
|
|
195
|
+
// LRU-touch the `from` map.
|
|
196
|
+
this.routes.delete(from);
|
|
197
|
+
this.routes.set(from, fromMap);
|
|
99
198
|
}
|
|
100
199
|
|
|
101
200
|
let prev = fromMap.get(target);
|
|
@@ -106,6 +205,10 @@ export class Routes {
|
|
|
106
205
|
if (!prev) {
|
|
107
206
|
prev = { session, remoteSession, list: [] as RelayInfo[] };
|
|
108
207
|
fromMap.set(target, prev);
|
|
208
|
+
} else {
|
|
209
|
+
// LRU-touch the target entry.
|
|
210
|
+
fromMap.delete(target);
|
|
211
|
+
fromMap.set(target, prev);
|
|
109
212
|
}
|
|
110
213
|
|
|
111
214
|
const isRelayed = from !== this.me;
|
|
@@ -127,20 +230,6 @@ export class Routes {
|
|
|
127
230
|
|
|
128
231
|
prev.session = Math.max(session, prev.session);
|
|
129
232
|
|
|
130
|
-
const scheduleCleanup = () => {
|
|
131
|
-
return delay(this.routeMaxRetentionPeriod + 100, { signal: this.signal })
|
|
132
|
-
.then(() => {
|
|
133
|
-
this.cleanup(from, target);
|
|
134
|
-
})
|
|
135
|
-
.catch((e) => {
|
|
136
|
-
if (e instanceof AbortError) {
|
|
137
|
-
// skip
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
throw e;
|
|
141
|
-
});
|
|
142
|
-
};
|
|
143
|
-
|
|
144
233
|
// Update routes and cleanup all old routes that are older than latest session - some threshold
|
|
145
234
|
if (isNewSession) {
|
|
146
235
|
// Mark previous routes as old
|
|
@@ -156,10 +245,10 @@ export class Routes {
|
|
|
156
245
|
|
|
157
246
|
// Initiate cleanup
|
|
158
247
|
if (distance !== -1 && foundNodeToExpire) {
|
|
159
|
-
|
|
248
|
+
this.requestCleanup(from, target);
|
|
160
249
|
}
|
|
161
250
|
} else if (isOldSession) {
|
|
162
|
-
|
|
251
|
+
this.requestCleanup(from, target);
|
|
163
252
|
}
|
|
164
253
|
|
|
165
254
|
// Modify list for new/update route
|
|
@@ -171,12 +260,24 @@ export class Routes {
|
|
|
171
260
|
if (route.distance > distance) {
|
|
172
261
|
route.distance = distance;
|
|
173
262
|
route.session = session;
|
|
263
|
+
route.updatedAt = +new Date();
|
|
174
264
|
route.expireAt = undefined; // remove expiry since we updated
|
|
175
265
|
sortRoutes(prev.list);
|
|
266
|
+
if (prev.list.length > this.maxRelaysPerTarget) {
|
|
267
|
+
prev.list.length = this.maxRelaysPerTarget;
|
|
268
|
+
}
|
|
269
|
+
this.pruneTargets(from, fromMap);
|
|
270
|
+
this.pruneFromMaps();
|
|
176
271
|
return isNewRemoteSession ? "restart" : "updated";
|
|
177
272
|
} else if (route.distance === distance) {
|
|
178
273
|
route.session = session;
|
|
274
|
+
route.updatedAt = +new Date();
|
|
179
275
|
route.expireAt = undefined; // remove expiry since we updated
|
|
276
|
+
if (prev.list.length > this.maxRelaysPerTarget) {
|
|
277
|
+
prev.list.length = this.maxRelaysPerTarget;
|
|
278
|
+
}
|
|
279
|
+
this.pruneTargets(from, fromMap);
|
|
280
|
+
this.pruneFromMaps();
|
|
180
281
|
return isNewRemoteSession ? "restart" : "updated";
|
|
181
282
|
}
|
|
182
283
|
}
|
|
@@ -194,13 +295,20 @@ export class Routes {
|
|
|
194
295
|
distance,
|
|
195
296
|
session,
|
|
196
297
|
hash: neighbour,
|
|
298
|
+
updatedAt: +new Date(),
|
|
197
299
|
expireAt: isOldSession
|
|
198
300
|
? +new Date() + this.routeMaxRetentionPeriod
|
|
199
301
|
: undefined,
|
|
200
302
|
});
|
|
201
303
|
sortRoutes(prev.list);
|
|
304
|
+
if (prev.list.length > this.maxRelaysPerTarget) {
|
|
305
|
+
prev.list.length = this.maxRelaysPerTarget;
|
|
306
|
+
}
|
|
202
307
|
}
|
|
203
308
|
|
|
309
|
+
this.pruneTargets(from, fromMap);
|
|
310
|
+
this.pruneFromMaps();
|
|
311
|
+
|
|
204
312
|
return exist ? (isNewRemoteSession ? "restart" : "updated") : "new";
|
|
205
313
|
}
|
|
206
314
|
|
|
@@ -262,6 +370,38 @@ export class Routes {
|
|
|
262
370
|
return this.routes.get(from)?.get(target);
|
|
263
371
|
}
|
|
264
372
|
|
|
373
|
+
getRouteHints(from: string, target: string): DirectStreamAckRouteHint[] {
|
|
374
|
+
const route = this.routes.get(from)?.get(target);
|
|
375
|
+
if (!route) {
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
const now = Date.now();
|
|
379
|
+
const out: DirectStreamAckRouteHint[] = [];
|
|
380
|
+
for (const next of route.list) {
|
|
381
|
+
if (next.expireAt != null && next.expireAt < now) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
out.push({
|
|
385
|
+
kind: "directstream-ack",
|
|
386
|
+
from,
|
|
387
|
+
target,
|
|
388
|
+
nextHop: next.hash,
|
|
389
|
+
distance: next.distance,
|
|
390
|
+
session: next.session,
|
|
391
|
+
updatedAt: next.updatedAt,
|
|
392
|
+
expiresAt: next.expireAt,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
getBestRouteHint(
|
|
399
|
+
from: string,
|
|
400
|
+
target: string,
|
|
401
|
+
): DirectStreamAckRouteHint | undefined {
|
|
402
|
+
return this.getRouteHints(from, target)[0];
|
|
403
|
+
}
|
|
404
|
+
|
|
265
405
|
isReachable(from: string, target: string, maxDistance = MAX_ROUTE_DISTANCE) {
|
|
266
406
|
const remoteInfo = this.remoteInfo.get(target);
|
|
267
407
|
const routeInfo = this.routes.get(from)?.get(target);
|
package/src/wait-for-event.ts
CHANGED
|
@@ -19,6 +19,10 @@ export function waitForEvent<
|
|
|
19
19
|
timeout?: number;
|
|
20
20
|
},
|
|
21
21
|
): Promise<void> {
|
|
22
|
+
const traceEnabled =
|
|
23
|
+
(globalThis as any)?.process?.env?.PEERBIT_WAITFOREVENT_TRACE === "1";
|
|
24
|
+
const callsite = traceEnabled ? new Error("waitForEvent callsite").stack : undefined;
|
|
25
|
+
|
|
22
26
|
const deferred = pDefer<void>();
|
|
23
27
|
const abortFn = (e?: unknown) => {
|
|
24
28
|
if (e instanceof Error) {
|
|
@@ -39,15 +43,18 @@ export function waitForEvent<
|
|
|
39
43
|
const checkIsReady = (...args: any[]) => resolver(deferred);
|
|
40
44
|
let timeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
41
45
|
|
|
42
|
-
deferred.promise
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
signal
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
void deferred.promise
|
|
47
|
+
.finally(() => {
|
|
48
|
+
for (const event of events) {
|
|
49
|
+
emitter.removeEventListener(event as any, checkIsReady as any);
|
|
50
|
+
}
|
|
51
|
+
timeout && clearTimeout(timeout);
|
|
52
|
+
options?.signals?.forEach((signal) =>
|
|
53
|
+
signal.removeEventListener("abort", abortFn),
|
|
54
|
+
);
|
|
55
|
+
})
|
|
56
|
+
// Avoid triggering an unhandled rejection from the `.finally()` return promise.
|
|
57
|
+
.catch(() => {});
|
|
51
58
|
|
|
52
59
|
const abortedSignal = options?.signals?.find((signal) => signal.aborted);
|
|
53
60
|
if (abortedSignal) {
|
|
@@ -65,7 +72,13 @@ export function waitForEvent<
|
|
|
65
72
|
emitter.addEventListener(event as any, checkIsReady as any);
|
|
66
73
|
}
|
|
67
74
|
timeout = setTimeout(
|
|
68
|
-
() =>
|
|
75
|
+
() => {
|
|
76
|
+
const err = new TimeoutError("Timeout waiting for event");
|
|
77
|
+
if (callsite && err.stack) {
|
|
78
|
+
err.stack += `\n\n${callsite}`;
|
|
79
|
+
}
|
|
80
|
+
abortFn(err);
|
|
81
|
+
},
|
|
69
82
|
options?.timeout ?? 10 * 1000,
|
|
70
83
|
);
|
|
71
84
|
options?.signals?.forEach((signal) =>
|