@peerbit/stream 2.0.4 → 2.0.6

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/src/routes.ts CHANGED
@@ -1,36 +1,68 @@
1
- import { PublicSignKey } from "@peerbit/crypto";
1
+ import { AbortError, delay } from "@peerbit/time";
2
2
 
3
3
  export const MAX_ROUTE_DISTANCE = Number.MAX_SAFE_INTEGER - 1;
4
+ type RouteInfo = {
5
+ session: number;
6
+ hash: string;
7
+ expireAt?: number;
8
+ distance: number;
9
+ };
4
10
  export class Routes {
5
11
  // END receiver -> Neighbour
6
12
 
7
13
  routes: Map<
8
14
  string,
9
- Map<string, { session: number; list: { hash: string; distance: number }[] }>
10
- > = new Map();
11
-
12
- pendingRoutes: Map<
13
- number,
14
15
  Map<
15
16
  string,
16
17
  {
17
- from: string;
18
- neighbour: string;
19
- distance: number;
20
- }[]
18
+ latestSession: number;
19
+ list: RouteInfo[];
20
+ }
21
21
  >
22
22
  > = new Map();
23
- latestSession: number;
24
23
 
25
- constructor(readonly me: string) {
26
- this.latestSession = 0;
24
+ routeMaxRetentionPeriod: number;
25
+ signal: AbortSignal;
26
+
27
+ constructor(
28
+ readonly me: string,
29
+ options: { routeMaxRetentionPeriod: number; signal: AbortSignal }
30
+ ) {
31
+ this.routeMaxRetentionPeriod = options.routeMaxRetentionPeriod;
32
+ this.signal = options.signal;
27
33
  }
28
34
 
29
35
  clear() {
30
36
  this.routes.clear();
31
- this.pendingRoutes.clear();
32
37
  }
33
38
 
39
+ private cleanup(from: string, to: string) {
40
+ const fromMap = this.routes.get(from);
41
+ if (fromMap) {
42
+ const map = fromMap.get(to);
43
+ if (map) {
44
+ const now = +new Date();
45
+ const keepRoutes: RouteInfo[] = [];
46
+ for (const route of map.list) {
47
+ // delete all routes after a while
48
+ if (route.expireAt != null && route.expireAt < now) {
49
+ // expired
50
+ } else {
51
+ keepRoutes.push(route);
52
+ }
53
+ }
54
+
55
+ if (keepRoutes.length > 0) {
56
+ map.list = keepRoutes;
57
+ } else {
58
+ fromMap.delete(to);
59
+ if (fromMap.size === 1) {
60
+ this.routes.delete(from);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
34
66
  add(
35
67
  from: string,
36
68
  neighbour: string,
@@ -44,20 +76,11 @@ export class Routes {
44
76
  this.routes.set(from, fromMap);
45
77
  }
46
78
 
47
- let prev = fromMap.get(target) || {
48
- session: session ?? +new Date(),
49
- list: [] as { hash: string; distance: number }[]
50
- };
51
-
52
- this.latestSession = Math.max(this.latestSession, session);
79
+ let prev = fromMap.get(target);
53
80
 
54
- if (session != null) {
55
- // this condition means that when we add new routes in a session that is newer
56
- if (prev.session < session) {
57
- prev = { session, list: [] }; // reset route info how to reach this target
58
- } else if (prev.session > session) {
59
- return; // new routing information superseedes this
60
- }
81
+ if (!prev) {
82
+ prev = { latestSession: 0, list: [] as RouteInfo[] };
83
+ fromMap.set(target, prev);
61
84
  }
62
85
 
63
86
  if (from === this.me && neighbour === target) {
@@ -66,16 +89,60 @@ export class Routes {
66
89
  distance = -1;
67
90
  }
68
91
 
92
+ // Update routes and cleanup all old routes that are older than latest session - some threshold
93
+ const isNewSession = session > prev.latestSession;
94
+ prev.latestSession = Math.max(session, prev.latestSession);
95
+
96
+ if (isNewSession) {
97
+ // Mark previous routes as old
98
+
99
+ const expireAt = +new Date() + this.routeMaxRetentionPeriod;
100
+ for (const route of prev.list) {
101
+ // delete all routes after a while
102
+ if (!route.expireAt) {
103
+ route.expireAt = expireAt;
104
+ }
105
+ }
106
+
107
+ // Initiate cleanup
108
+ if (distance !== -1) {
109
+ delay(this.routeMaxRetentionPeriod + 100, { signal: this.signal })
110
+ .then(() => {
111
+ this.cleanup(from, target);
112
+ })
113
+ .catch((e) => {
114
+ if (e instanceof AbortError) {
115
+ // skip
116
+ return;
117
+ }
118
+ throw e;
119
+ });
120
+ }
121
+ }
122
+
123
+ // Modify list for new/update route
69
124
  for (const route of prev.list) {
70
125
  if (route.hash === neighbour) {
71
- route.distance = Math.min(route.distance, distance);
72
- prev.list.sort((a, b) => a.distance - b.distance);
73
- return;
126
+ // if route is faster or just as fast, update existing route
127
+ if (route.distance > distance) {
128
+ route.distance = distance;
129
+ route.session = session;
130
+ route.expireAt = undefined; // remove expiry since we updated
131
+ prev.list.sort((a, b) => a.distance - b.distance);
132
+ return;
133
+ } else if (route.distance === distance) {
134
+ route.session = session;
135
+ route.expireAt = undefined; // remove expiry since we updated
136
+ return;
137
+ }
138
+
139
+ // else break and push the route as a new route (that ought to be longer)
140
+ break;
74
141
  }
75
142
  }
76
- prev.list.push({ distance, hash: neighbour });
143
+
144
+ prev.list.push({ distance, session, hash: neighbour });
77
145
  prev.list.sort((a, b) => a.distance - b.distance);
78
- fromMap.set(target, prev);
79
146
  }
80
147
 
81
148
  removeTarget(target: string) {
@@ -177,54 +244,62 @@ export class Routes {
177
244
 
178
245
  // for all tos if
179
246
  getFanout(
180
- from: PublicSignKey,
247
+ from: string,
181
248
  tos: string[],
182
249
  redundancy: number
183
- ): Map<string, { to: string; timestamp: number }[]> | undefined {
250
+ ): Map<string, Map<string, { to: string; timestamp: number }>> | undefined {
184
251
  if (tos.length === 0) {
185
252
  return undefined;
186
253
  }
187
254
 
188
255
  let fanoutMap:
189
- | Map<string, { to: string; timestamp: number }[]>
256
+ | Map<string, Map<string, { to: string; timestamp: number }>>
190
257
  | undefined = undefined;
191
258
 
192
- const fromKey = from.hashcode();
193
-
194
259
  // Message to > 0
195
260
  if (tos.length > 0) {
196
261
  for (const to of tos) {
197
- if (to === this.me || fromKey === to) {
262
+ if (to === this.me || from === to) {
198
263
  continue; // don't send to me or backwards
199
264
  }
200
265
 
201
- const neighbour = this.findNeighbor(fromKey, to);
266
+ const neighbour = this.findNeighbor(from, to);
202
267
  if (neighbour) {
203
268
  let foundClosest = false;
204
- for (
205
- let i = 0;
206
- i < Math.min(neighbour.list.length, redundancy);
207
- i++
208
- ) {
209
- const distance = neighbour.list[i].distance;
210
- if (distance >= redundancy) {
269
+ let redundancyModified = redundancy;
270
+ for (let i = 0; i < neighbour.list.length; i++) {
271
+ const { distance, session } = neighbour.list[i];
272
+ if (distance >= redundancyModified) {
211
273
  break; // because neighbour listis sorted
212
274
  }
213
- if (distance <= 0) {
214
- foundClosest = true;
215
- }
216
- const fanout: { to: string; timestamp: number }[] = (
275
+
276
+ let fanout: Map<string, { to: string; timestamp: number }> = (
217
277
  fanoutMap || (fanoutMap = new Map())
218
278
  ).get(neighbour.list[i].hash);
219
279
  if (!fanout) {
220
- fanoutMap.set(neighbour.list[i].hash, [
221
- { to, timestamp: neighbour.session }
222
- ]);
223
- } else {
224
- fanout.push({ to, timestamp: neighbour.session });
280
+ fanout = new Map();
281
+ fanoutMap.set(neighbour.list[i].hash, fanout);
282
+ }
283
+
284
+ fanout.set(to, { to, timestamp: session });
285
+
286
+ if (
287
+ (distance == 0 && session === neighbour.latestSession) ||
288
+ distance == -1
289
+ ) {
290
+ foundClosest = true;
291
+
292
+ if (distance == -1) {
293
+ // remove 1 from the expected redunancy since we got a route with negative 1 distance
294
+ // if we do not do this, we would get 2 routes if redundancy = 1, {-1, 0}, while it should just be
295
+ // {-1} in this case
296
+ redundancyModified -= 1;
297
+ break;
298
+ }
225
299
  }
226
300
  }
227
- if (!foundClosest && from.hashcode() === this.me) {
301
+
302
+ if (!foundClosest && from === this.me) {
228
303
  return undefined; // we dont have the shortest path to our target (yet). Send to all
229
304
  }
230
305
 
@@ -260,75 +335,4 @@ export class Routes {
260
335
  }
261
336
  return [];
262
337
  }
263
-
264
- public addPendingRouteConnection(
265
- session: number,
266
- route: {
267
- from: string;
268
- neighbour: string;
269
- target: string;
270
- distance: number;
271
- }
272
- ) {
273
- let map = this.pendingRoutes.get(session);
274
- if (!map) {
275
- map = new Map();
276
- this.pendingRoutes.set(session, map);
277
- }
278
- let arr = map.get(route.target);
279
- if (!arr) {
280
- arr = [];
281
- map.set(route.target, arr);
282
- }
283
-
284
- arr.push(route);
285
-
286
- const neighbour = this.findNeighbor(route.from, route.target);
287
- if (!neighbour || neighbour.session === session) {
288
- // Commit directly since we dont have any data at all (better have something than nothing)
289
- this.commitPendingRouteConnection(session, route.target);
290
- }
291
- }
292
-
293
- // always commit if we dont know the peer yet
294
- // do pending commits per remote (?)
295
-
296
- public commitPendingRouteConnection(session: number, target?: string) {
297
- const map = this.pendingRoutes.get(session);
298
- if (!map) {
299
- return;
300
- }
301
- if (target) {
302
- const routes = map.get(target);
303
- if (routes) {
304
- for (const route of routes) {
305
- this.add(
306
- route.from,
307
- route.neighbour,
308
- target,
309
- route.distance,
310
- session
311
- );
312
- }
313
- map.delete(target);
314
- return;
315
- } else {
316
- return;
317
- }
318
- } else {
319
- for (const [target, routes] of map) {
320
- for (const route of routes) {
321
- this.add(
322
- route.from,
323
- route.neighbour,
324
- target,
325
- route.distance,
326
- session
327
- );
328
- }
329
- }
330
- }
331
-
332
- this.pendingRoutes.delete(session);
333
- }
334
338
  }