@peerbit/stream 4.5.3 → 4.6.0-000e3f1
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 +36 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +528 -310
- package/dist/src/index.js.map +1 -1
- package/dist/src/routes.d.ts +11 -0
- package/dist/src/routes.d.ts.map +1 -1
- package/dist/src/routes.js +105 -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 +34 -10
- 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 +803 -519
- package/src/routes.ts +121 -19
- package/src/wait-for-event.ts +46 -15
package/src/routes.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { AbortError, delay } from "@peerbit/time";
|
|
2
|
-
|
|
3
1
|
export const MAX_ROUTE_DISTANCE = Number.MAX_SAFE_INTEGER - 1;
|
|
4
2
|
|
|
3
|
+
const DEFAULT_MAX_FROM_ENTRIES = 2048;
|
|
4
|
+
const DEFAULT_MAX_TARGETS_PER_FROM = 10_000;
|
|
5
|
+
const DEFAULT_MAX_RELAYS_PER_TARGET = 32;
|
|
6
|
+
|
|
5
7
|
type RelayInfo = {
|
|
6
8
|
session: number;
|
|
7
9
|
hash: string;
|
|
@@ -44,17 +46,103 @@ export class Routes {
|
|
|
44
46
|
|
|
45
47
|
signal?: AbortSignal;
|
|
46
48
|
|
|
49
|
+
private pendingCleanupByFrom: Map<string, Set<string>> = new Map();
|
|
50
|
+
private cleanupTimer?: ReturnType<typeof setTimeout>;
|
|
51
|
+
private maxFromEntries: number;
|
|
52
|
+
private maxTargetsPerFrom: number;
|
|
53
|
+
private maxRelaysPerTarget: number;
|
|
54
|
+
|
|
47
55
|
constructor(
|
|
48
56
|
readonly me: string,
|
|
49
|
-
options?: {
|
|
57
|
+
options?: {
|
|
58
|
+
routeMaxRetentionPeriod?: number;
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
maxFromEntries?: number;
|
|
61
|
+
maxTargetsPerFrom?: number;
|
|
62
|
+
maxRelaysPerTarget?: number;
|
|
63
|
+
},
|
|
50
64
|
) {
|
|
51
65
|
this.routeMaxRetentionPeriod =
|
|
52
66
|
options?.routeMaxRetentionPeriod ?? 10 * 1000;
|
|
53
67
|
this.signal = options?.signal;
|
|
68
|
+
this.maxFromEntries = Math.max(
|
|
69
|
+
1,
|
|
70
|
+
Math.floor(options?.maxFromEntries ?? DEFAULT_MAX_FROM_ENTRIES),
|
|
71
|
+
);
|
|
72
|
+
this.maxTargetsPerFrom = Math.max(
|
|
73
|
+
1,
|
|
74
|
+
Math.floor(options?.maxTargetsPerFrom ?? DEFAULT_MAX_TARGETS_PER_FROM),
|
|
75
|
+
);
|
|
76
|
+
this.maxRelaysPerTarget = Math.max(
|
|
77
|
+
1,
|
|
78
|
+
Math.floor(options?.maxRelaysPerTarget ?? DEFAULT_MAX_RELAYS_PER_TARGET),
|
|
79
|
+
);
|
|
54
80
|
}
|
|
55
81
|
|
|
56
82
|
clear() {
|
|
57
83
|
this.routes.clear();
|
|
84
|
+
this.pendingCleanupByFrom.clear();
|
|
85
|
+
if (this.cleanupTimer) clearTimeout(this.cleanupTimer);
|
|
86
|
+
this.cleanupTimer = undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private requestCleanup(from: string, to: string) {
|
|
90
|
+
if (this.signal?.aborted) return;
|
|
91
|
+
let targets = this.pendingCleanupByFrom.get(from);
|
|
92
|
+
if (!targets) {
|
|
93
|
+
targets = new Set<string>();
|
|
94
|
+
this.pendingCleanupByFrom.set(from, targets);
|
|
95
|
+
}
|
|
96
|
+
targets.add(to);
|
|
97
|
+
|
|
98
|
+
// Coalesce cleanups into a single timer. The previous per-update timer approach
|
|
99
|
+
// scales poorly in large networks and can OOM in single-process simulations.
|
|
100
|
+
if (this.cleanupTimer) return;
|
|
101
|
+
this.cleanupTimer = setTimeout(() => {
|
|
102
|
+
this.cleanupTimer = undefined;
|
|
103
|
+
const pending = this.pendingCleanupByFrom;
|
|
104
|
+
this.pendingCleanupByFrom = new Map();
|
|
105
|
+
for (const [fromKey, tos] of pending) {
|
|
106
|
+
for (const toKey of tos) {
|
|
107
|
+
this.cleanup(fromKey, toKey);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}, this.routeMaxRetentionPeriod + 100);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private pruneFromMaps() {
|
|
114
|
+
if (this.routes.size <= this.maxFromEntries) return;
|
|
115
|
+
|
|
116
|
+
// Keep `me` pinned: local routes are used for pruning decisions and should be
|
|
117
|
+
// the last thing we evict under memory pressure.
|
|
118
|
+
while (this.routes.size > this.maxFromEntries) {
|
|
119
|
+
const oldest = this.routes.keys().next().value as string | undefined;
|
|
120
|
+
if (!oldest) return;
|
|
121
|
+
if (oldest === this.me) {
|
|
122
|
+
const selfMap = this.routes.get(oldest);
|
|
123
|
+
if (!selfMap) {
|
|
124
|
+
this.routes.delete(oldest);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Move to the end (most recently used) and continue eviction.
|
|
128
|
+
this.routes.delete(oldest);
|
|
129
|
+
this.routes.set(oldest, selfMap);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
this.routes.delete(oldest);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private pruneTargets(from: string, fromMap: Map<string, RouteInfo>) {
|
|
137
|
+
if (fromMap.size <= this.maxTargetsPerFrom) return;
|
|
138
|
+
while (fromMap.size > this.maxTargetsPerFrom) {
|
|
139
|
+
const oldestTarget = fromMap.keys().next().value as string | undefined;
|
|
140
|
+
if (!oldestTarget) break;
|
|
141
|
+
fromMap.delete(oldestTarget);
|
|
142
|
+
}
|
|
143
|
+
if (fromMap.size === 0) {
|
|
144
|
+
this.routes.delete(from);
|
|
145
|
+
}
|
|
58
146
|
}
|
|
59
147
|
|
|
60
148
|
private cleanup(from: string, to: string) {
|
|
@@ -73,6 +161,10 @@ export class Routes {
|
|
|
73
161
|
}
|
|
74
162
|
}
|
|
75
163
|
|
|
164
|
+
if (keepRoutes.length > this.maxRelaysPerTarget) {
|
|
165
|
+
keepRoutes.length = this.maxRelaysPerTarget;
|
|
166
|
+
}
|
|
167
|
+
|
|
76
168
|
if (keepRoutes.length > 0) {
|
|
77
169
|
map.list = keepRoutes;
|
|
78
170
|
} else {
|
|
@@ -96,6 +188,10 @@ export class Routes {
|
|
|
96
188
|
if (!fromMap) {
|
|
97
189
|
fromMap = new Map();
|
|
98
190
|
this.routes.set(from, fromMap);
|
|
191
|
+
} else {
|
|
192
|
+
// LRU-touch the `from` map.
|
|
193
|
+
this.routes.delete(from);
|
|
194
|
+
this.routes.set(from, fromMap);
|
|
99
195
|
}
|
|
100
196
|
|
|
101
197
|
let prev = fromMap.get(target);
|
|
@@ -106,6 +202,10 @@ export class Routes {
|
|
|
106
202
|
if (!prev) {
|
|
107
203
|
prev = { session, remoteSession, list: [] as RelayInfo[] };
|
|
108
204
|
fromMap.set(target, prev);
|
|
205
|
+
} else {
|
|
206
|
+
// LRU-touch the target entry.
|
|
207
|
+
fromMap.delete(target);
|
|
208
|
+
fromMap.set(target, prev);
|
|
109
209
|
}
|
|
110
210
|
|
|
111
211
|
const isRelayed = from !== this.me;
|
|
@@ -127,20 +227,6 @@ export class Routes {
|
|
|
127
227
|
|
|
128
228
|
prev.session = Math.max(session, prev.session);
|
|
129
229
|
|
|
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
230
|
// Update routes and cleanup all old routes that are older than latest session - some threshold
|
|
145
231
|
if (isNewSession) {
|
|
146
232
|
// Mark previous routes as old
|
|
@@ -156,10 +242,10 @@ export class Routes {
|
|
|
156
242
|
|
|
157
243
|
// Initiate cleanup
|
|
158
244
|
if (distance !== -1 && foundNodeToExpire) {
|
|
159
|
-
|
|
245
|
+
this.requestCleanup(from, target);
|
|
160
246
|
}
|
|
161
247
|
} else if (isOldSession) {
|
|
162
|
-
|
|
248
|
+
this.requestCleanup(from, target);
|
|
163
249
|
}
|
|
164
250
|
|
|
165
251
|
// Modify list for new/update route
|
|
@@ -173,10 +259,20 @@ export class Routes {
|
|
|
173
259
|
route.session = session;
|
|
174
260
|
route.expireAt = undefined; // remove expiry since we updated
|
|
175
261
|
sortRoutes(prev.list);
|
|
262
|
+
if (prev.list.length > this.maxRelaysPerTarget) {
|
|
263
|
+
prev.list.length = this.maxRelaysPerTarget;
|
|
264
|
+
}
|
|
265
|
+
this.pruneTargets(from, fromMap);
|
|
266
|
+
this.pruneFromMaps();
|
|
176
267
|
return isNewRemoteSession ? "restart" : "updated";
|
|
177
268
|
} else if (route.distance === distance) {
|
|
178
269
|
route.session = session;
|
|
179
270
|
route.expireAt = undefined; // remove expiry since we updated
|
|
271
|
+
if (prev.list.length > this.maxRelaysPerTarget) {
|
|
272
|
+
prev.list.length = this.maxRelaysPerTarget;
|
|
273
|
+
}
|
|
274
|
+
this.pruneTargets(from, fromMap);
|
|
275
|
+
this.pruneFromMaps();
|
|
180
276
|
return isNewRemoteSession ? "restart" : "updated";
|
|
181
277
|
}
|
|
182
278
|
}
|
|
@@ -199,8 +295,14 @@ export class Routes {
|
|
|
199
295
|
: undefined,
|
|
200
296
|
});
|
|
201
297
|
sortRoutes(prev.list);
|
|
298
|
+
if (prev.list.length > this.maxRelaysPerTarget) {
|
|
299
|
+
prev.list.length = this.maxRelaysPerTarget;
|
|
300
|
+
}
|
|
202
301
|
}
|
|
203
302
|
|
|
303
|
+
this.pruneTargets(from, fromMap);
|
|
304
|
+
this.pruneFromMaps();
|
|
305
|
+
|
|
204
306
|
return exist ? (isNewRemoteSession ? "restart" : "updated") : "new";
|
|
205
307
|
}
|
|
206
308
|
|
package/src/wait-for-event.ts
CHANGED
|
@@ -19,35 +19,66 @@ 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
|
-
const abortFn = (e
|
|
27
|
+
const abortFn = (e?: unknown) => {
|
|
28
|
+
if (e instanceof Error) {
|
|
29
|
+
deferred.reject(e);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const reason = (e as any)?.target?.reason;
|
|
24
34
|
deferred.reject(
|
|
25
|
-
|
|
35
|
+
reason ??
|
|
26
36
|
new AbortError(
|
|
27
37
|
"Aborted waiting for event: " +
|
|
28
38
|
String(events.length > 1 ? events.join(", ") : events[0]),
|
|
29
39
|
),
|
|
30
40
|
);
|
|
41
|
+
};
|
|
31
42
|
|
|
32
43
|
const checkIsReady = (...args: any[]) => resolver(deferred);
|
|
44
|
+
let timeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
33
45
|
|
|
34
|
-
deferred.promise
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
signal
|
|
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(() => {});
|
|
58
|
+
|
|
59
|
+
const abortedSignal = options?.signals?.find((signal) => signal.aborted);
|
|
60
|
+
if (abortedSignal) {
|
|
61
|
+
deferred.reject(
|
|
62
|
+
abortedSignal.reason ??
|
|
63
|
+
new AbortError(
|
|
64
|
+
"Aborted waiting for event: " +
|
|
65
|
+
String(events.length > 1 ? events.join(", ") : events[0]),
|
|
66
|
+
),
|
|
41
67
|
);
|
|
42
|
-
|
|
68
|
+
return deferred.promise;
|
|
69
|
+
}
|
|
43
70
|
|
|
44
71
|
for (const event of events) {
|
|
45
|
-
emitter.addEventListener(event as any,
|
|
46
|
-
checkIsReady(event);
|
|
47
|
-
});
|
|
72
|
+
emitter.addEventListener(event as any, checkIsReady as any);
|
|
48
73
|
}
|
|
49
|
-
|
|
50
|
-
() =>
|
|
74
|
+
timeout = setTimeout(
|
|
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
|
+
},
|
|
51
82
|
options?.timeout ?? 10 * 1000,
|
|
52
83
|
);
|
|
53
84
|
options?.signals?.forEach((signal) =>
|