@peerbit/stream 1.0.20 → 2.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/src/logger.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  import { logger as logFn } from "@peerbit/logger";
2
- export const logger = logFn({ module: "direct-stream", level: "warn" });
2
+ export const logger = logFn({ module: "lazystream", level: "warn" });
package/src/metrics.ts CHANGED
@@ -1,70 +1,18 @@
1
1
  import { PublicSignKey } from "@peerbit/crypto";
2
2
 
3
- export class Frequency {
4
- private interval: ReturnType<typeof setInterval>;
5
- private lastFrequency: number;
6
- private events = 0;
7
- private intervalTime: number;
8
- constructor(properties: { intervalTime } = { intervalTime: 10 * 1000 }) {
9
- this.intervalTime = properties.intervalTime;
10
- this.interval = setInterval(() => {
11
- this.lastFrequency = this.events / this.intervalTime;
12
- this.events = 0;
13
- }, properties.intervalTime);
14
- }
15
-
16
- count(event: number) {
17
- this.events += event;
18
- }
19
-
20
- close() {
21
- clearInterval(this.interval);
22
- this.events = 0;
23
- }
3
+ export class MovingAverageTracker {
4
+ private lastTS = 0;
24
5
 
25
- get frequency() {
26
- return this.lastFrequency;
27
- }
28
- }
6
+ value = 0;
29
7
 
30
- export class RouteFrequency {
31
- private interval: ReturnType<typeof setInterval>;
32
- private lastFrequency: Map<string, number>;
33
- private events: Map<string, number>;
34
- private intervalTime: number;
35
- constructor(properties: { intervalTime } = { intervalTime: 10 * 1000 }) {
36
- this.intervalTime = properties.intervalTime;
37
- this.events = new Map();
8
+ constructor(readonly tau = 10) {
9
+ this.lastTS = +new Date();
38
10
  }
39
-
40
- start() {
41
- this.interval = setInterval(() => {
42
- this.lastFrequency = this.events;
43
- this.events = new Map();
44
- }, this.intervalTime);
45
- }
46
-
47
- increment(to: string, bytes: Uint8Array) {
48
- let value = this.events.get(to);
49
- if (value == null) {
50
- value = 1;
51
- this.events.set(to, bytes.length);
52
- } else {
53
- this.events.set(to, value + bytes.length);
54
- }
55
- }
56
-
57
- close() {
58
- clearInterval(this.interval);
59
- this.events.clear();
60
- }
61
-
62
- getFrequency(to: PublicSignKey) {
63
- const count = this.lastFrequency.get(to.hashcode());
64
- if (count) {
65
- return count / this.intervalTime;
66
- }
67
-
68
- return undefined;
11
+ add(number: number) {
12
+ const now = +new Date();
13
+ const dt = (now - this.lastTS) / 1000;
14
+ const alpha_t = 1 - Math.exp(-dt / this.tau);
15
+ this.value = (1 - alpha_t) * this.value + (alpha_t * number) / dt;
16
+ this.lastTS = now;
69
17
  }
70
18
  }
package/src/routes.ts CHANGED
@@ -1,283 +1,334 @@
1
- import Graphs from "graphology";
2
- import type { MultiUndirectedGraph } from "graphology";
3
- import { dijkstra, unweighted } from "graphology-shortest-path";
4
- import { logger } from "./logger.js";
5
- import { MinimalEdgeMapper } from "graphology-utils/getters";
6
-
7
- interface EdgeData {
8
- weight: number;
9
- time: number;
10
- }
1
+ import { PublicSignKey } from "@peerbit/crypto";
2
+
3
+ export const MAX_ROUTE_DISTANCE = Number.MAX_SAFE_INTEGER;
11
4
  export class Routes {
12
- graph: MultiUndirectedGraph<any, EdgeData>;
13
- private peerId: string;
14
- constructor(peerId: string) {
15
- this.peerId = peerId;
16
- this.graph = new (Graphs as any).UndirectedGraph();
17
- }
5
+ // END receiver -> Neighbour
18
6
 
19
- get linksCount() {
20
- return this.graph.edges().length;
7
+ routes: Map<
8
+ string,
9
+ Map<string, { session: number; list: { hash: string; distance: number }[] }>
10
+ > = new Map();
11
+
12
+ pendingRoutes: Map<
13
+ number,
14
+ Map<
15
+ string,
16
+ {
17
+ from: string;
18
+ neighbour: string;
19
+ distance: number;
20
+ }[]
21
+ >
22
+ > = new Map();
23
+ latestSession: number;
24
+
25
+ constructor(readonly me: string) {
26
+ this.latestSession = 0;
21
27
  }
22
28
 
23
- get nodeCount() {
24
- return this.graph.nodes().length;
29
+ clear() {
30
+ this.routes.clear();
31
+ this.pendingRoutes.clear();
25
32
  }
26
33
 
27
- /**
28
- *
29
- * @param from
30
- * @param to
31
- * @returns new nodes
32
- */
33
- addLink(
34
+ add(
34
35
  from: string,
35
- to: string,
36
- weight: number,
37
- origin: string = this.peerId
38
- ): string[] {
39
- const linkExisted = this.hasLink(from, to);
40
- const newReachableNodesFromOrigin: string[] = [];
41
- if (!linkExisted) {
42
- const currentTime = +new Date();
43
- const fromWasReachable =
44
- origin == from ||
45
- this.getPath(origin, from, { unweighted: true }).length;
46
- const toWasReachable =
47
- origin === to || this.getPath(origin, to, { unweighted: true }).length;
48
- const fromIsNowReachable = toWasReachable;
49
- const toIsNowReachable = fromWasReachable;
50
-
51
- const visited = new Set<string | number>();
52
- const newReachableNodes: string[] = [];
53
- if (fromIsNowReachable) {
54
- newReachableNodes.push(from);
55
- }
56
- if (toIsNowReachable) {
57
- newReachableNodes.push(to);
58
- }
59
- if (fromWasReachable) {
60
- visited.add(from);
61
- }
62
- if (toWasReachable) {
63
- visited.add(to);
36
+ neighbour: string,
37
+ target: string,
38
+ distance: number,
39
+ session: number
40
+ ) {
41
+ let fromMap = this.routes.get(from);
42
+ if (!fromMap) {
43
+ fromMap = new Map();
44
+ this.routes.set(from, fromMap);
45
+ }
46
+
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);
53
+
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
64
60
  }
61
+ }
65
62
 
66
- if (!this.graph.hasNode(from)) {
67
- this.graph.addNode(from);
63
+ if (from === this.me && neighbour === target) {
64
+ // force distance to neighbour as targets to always favor directly sending to them
65
+ // i.e. if target is our neighbour, always assume the shortest path to them is the direct path
66
+ distance = -1;
67
+ }
68
+
69
+ for (const route of prev.list) {
70
+ if (route.hash === neighbour) {
71
+ route.distance = Math.min(route.distance, distance);
72
+ prev.list.sort((a, b) => a.distance - b.distance);
73
+ return;
68
74
  }
69
- if (!this.graph.hasNode(to)) {
70
- this.graph.addNode(to);
75
+ }
76
+ prev.list.push({ distance, hash: neighbour });
77
+ prev.list.sort((a, b) => a.distance - b.distance);
78
+ fromMap.set(target, prev);
79
+ }
80
+
81
+ removeTarget(target: string) {
82
+ this.routes.delete(target);
83
+ for (const [fromMapKey, fromMap] of this.routes) {
84
+ // delete target
85
+ fromMap.delete(target);
86
+ if (fromMap.size === 0) {
87
+ this.routes.delete(fromMapKey);
71
88
  }
89
+ }
90
+ return [target];
91
+ }
72
92
 
73
- this.graph.addUndirectedEdge(from, to, { weight, time: currentTime });
93
+ removeNeighbour(target: string) {
94
+ this.routes.delete(target);
95
+ const maybeUnreachable: Set<string> = new Set([target]);
96
+ for (const [fromMapKey, fromMap] of this.routes) {
97
+ // delete target
98
+ fromMap.delete(target);
74
99
 
75
- for (const newReachableNode of newReachableNodes) {
76
- // get all nodes from this and add them to the new reachable set of nodes one can access from origin
100
+ // delete this as neighbour
101
+ for (const [remote, neighbours] of fromMap) {
102
+ neighbours.list = neighbours.list.filter((x) => x.hash !== target);
103
+ if (neighbours.list.length === 0) {
104
+ fromMap.delete(remote);
105
+ maybeUnreachable.add(remote);
106
+ }
107
+ }
77
108
 
78
- const stack = [newReachableNode]; // iterate from the not reachable node
79
- while (stack.length > 0) {
80
- const node = stack.shift();
81
- if (!node) {
82
- continue;
83
- }
84
- if (visited.has(node)) {
85
- continue;
86
- }
109
+ if (fromMap.size === 0) {
110
+ this.routes.delete(fromMapKey);
111
+ }
112
+ }
113
+ return [...maybeUnreachable].filter((x) => !this.isReachable(this.me, x));
114
+ }
87
115
 
88
- visited.add(node);
89
- const neighbors = this.graph.neighbors(node);
90
- for (const neighbor of neighbors) {
91
- const edge = this.graph.undirectedEdge(node, neighbor);
92
- if (!edge) {
93
- logger.warn(`Missing edge between: ${node} - ${neighbor}`);
94
- continue;
95
- }
116
+ findNeighbor(from: string, target: string) {
117
+ return this.routes.get(from)?.get(target);
118
+ }
96
119
 
97
- const attributes = this.graph.getEdgeAttributes(edge);
98
- if (attributes.time > currentTime) {
99
- continue; // a new link has been added while we are iterating, dont follow this path
100
- }
120
+ isReachable(from: string, target: string) {
121
+ return (
122
+ (this.routes.get(from)?.get(target)?.list[0]?.distance ??
123
+ Number.MAX_SAFE_INTEGER) < MAX_ROUTE_DISTANCE
124
+ );
125
+ }
101
126
 
102
- if (visited.has(neighbor)) {
103
- continue;
104
- }
127
+ hasShortestPath(target: string) {
128
+ const path = this.routes.get(this.me)?.get(target);
129
+ if (!path) {
130
+ return false;
131
+ }
132
+ return path.list[0].distance <= 0;
133
+ }
105
134
 
106
- stack.push(neighbor);
107
- }
108
- newReachableNodesFromOrigin.push(node);
109
- }
135
+ hasTarget(target: string) {
136
+ for (const [k, v] of this.routes) {
137
+ if (v.has(target)) {
138
+ return true;
110
139
  }
111
- } else {
112
- // update weight
113
- const edge = this.graph.undirectedEdge(from, to);
114
- this.graph.setEdgeAttribute(edge, "weight", weight);
115
- this.graph.setEdgeAttribute(edge, "time", +new Date());
116
140
  }
141
+ return false;
142
+ }
117
143
 
118
- return newReachableNodesFromOrigin;
144
+ getDependent(target: string) {
145
+ const dependent: string[] = [];
146
+ for (const [fromMapKey, fromMap] of this.routes) {
147
+ if (fromMapKey !== this.me && fromMap.has(target)) {
148
+ dependent.push(fromMapKey);
149
+ }
150
+ }
151
+ return dependent;
119
152
  }
120
153
 
121
- /**
122
- *
123
- * @param from
124
- * @param to
125
- * @param origin
126
- * @returns nodes that are no longer reachable from origin
127
- */
128
- deleteLink(from: string, to: string, origin: string = this.peerId): string[] {
129
- const link = this.getLink(from, to);
130
- if (link) {
131
- const date = +new Date();
132
- const fromWasReachable =
133
- origin == from ||
134
- this.getPath(origin, from, { unweighted: true }).length;
135
- const toWasReachable =
136
- origin === to || this.getPath(origin, to, { unweighted: true }).length;
137
- this.graph.dropEdge(link);
138
-
139
- const unreachableNodesFromOrigin: string[] = [];
140
- if (
141
- fromWasReachable &&
142
- origin !== from &&
143
- this.getPath(origin, from, { unweighted: true }).length === 0
144
- ) {
145
- unreachableNodesFromOrigin.push(from);
154
+ count(from = this.me) {
155
+ const set: Set<string> = new Set();
156
+ const map = this.routes.get(from);
157
+ if (map) {
158
+ for (const [k, v] of map) {
159
+ set.add(k);
160
+ for (const peer of v.list) {
161
+ set.add(peer.hash);
162
+ }
146
163
  }
147
- if (
148
- toWasReachable &&
149
- origin !== to &&
150
- this.getPath(origin, to, { unweighted: true }).length === 0
151
- ) {
152
- unreachableNodesFromOrigin.push(to);
164
+ }
165
+ return set.size;
166
+ }
167
+
168
+ countAll() {
169
+ let size = 0;
170
+ for (const [from, map] of this.routes) {
171
+ for (const [k, v] of map) {
172
+ size += v.list.length;
153
173
  }
174
+ }
175
+ return size;
176
+ }
154
177
 
155
- // remove subgraphs that are now disconnected from me
156
- for (const disconnected of [...unreachableNodesFromOrigin]) {
157
- const node = disconnected;
158
- if (!this.graph.hasNode(node)) {
159
- continue;
160
- }
178
+ // for all tos if
179
+ getFanout(
180
+ from: PublicSignKey,
181
+ tos: string[],
182
+ redundancy: number
183
+ ): Map<string, { to: string; timestamp: number }[]> | undefined {
184
+ if (tos.length === 0) {
185
+ return undefined;
186
+ }
161
187
 
162
- const stack = [disconnected];
163
- const visited = new Set<string | number>();
164
- while (stack.length > 0) {
165
- const node = stack.shift();
166
- const nodeId = node;
167
- if (!nodeId || !this.graph.hasNode(nodeId)) {
168
- continue;
169
- }
170
- if (visited.has(nodeId)) {
171
- continue;
172
- }
188
+ let fanoutMap:
189
+ | Map<string, { to: string; timestamp: number }[]>
190
+ | undefined = undefined;
173
191
 
174
- visited.add(nodeId);
192
+ const fromKey = from.hashcode();
175
193
 
176
- const neighbors = this.graph.neighbors(node);
194
+ // Message to > 0
195
+ if (tos.length > 0) {
196
+ for (const to of tos) {
197
+ if (to === this.me || fromKey === to) {
198
+ continue; // don't send to me or backwards
199
+ }
177
200
 
178
- for (const neighbor of neighbors) {
179
- const edge = this.graph.undirectedEdge(node, neighbor);
180
- if (!edge) {
181
- logger.warn(`Missing edge between: ${node} - ${neighbor}`);
182
- continue;
201
+ const neighbour = this.findNeighbor(fromKey, to);
202
+ if (neighbour) {
203
+ 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) {
211
+ break; // because neighbour listis sorted
183
212
  }
184
- const attributes = this.graph.getEdgeAttributes(edge);
185
- if (attributes.time > date) {
186
- continue; // don't follow path because this is a new link that might provide some new connectivity
213
+ if (distance <= 0) {
214
+ foundClosest = true;
187
215
  }
188
-
189
- if (visited.has(neighbor)) {
190
- continue;
216
+ const fanout: { to: string; timestamp: number }[] = (
217
+ fanoutMap || (fanoutMap = new Map())
218
+ ).get(neighbour.list[i].hash);
219
+ if (!fanout) {
220
+ fanoutMap.set(neighbour.list[i].hash, [
221
+ { to, timestamp: neighbour.session }
222
+ ]);
223
+ } else {
224
+ fanout.push({ to, timestamp: neighbour.session });
191
225
  }
192
-
193
- stack.push(neighbor);
194
226
  }
195
- this.graph.dropNode(nodeId);
196
- if (disconnected !== nodeId) {
197
- unreachableNodesFromOrigin.push(nodeId.toString());
227
+ if (!foundClosest && from.hashcode() === this.me) {
228
+ return undefined; // we dont have the shortest path to our target (yet). Send to all
198
229
  }
230
+
231
+ continue;
199
232
  }
233
+
234
+ // we can't find path, send message to all peers
235
+ return undefined;
200
236
  }
201
- return unreachableNodesFromOrigin;
202
237
  }
203
- return [];
238
+ return fanoutMap || (fanoutMap = new Map());
204
239
  }
205
240
 
206
- getLink(from: string, to: string): string | undefined {
207
- if (!this.graph.hasNode(from) || !this.graph.hasNode(to)) {
208
- return undefined;
241
+ /**
242
+ * Returns a list of a prunable nodes that are not needed to reach all remote nodes
243
+ */
244
+ getPrunable(neighbours: string[]): string[] {
245
+ const map = this.routes.get(this.me);
246
+ if (map) {
247
+ // check if all targets can be reached without it
248
+ return neighbours.filter((candidate) => {
249
+ for (const [target, neighbours] of map) {
250
+ if (
251
+ target !== candidate &&
252
+ neighbours.list.length === 1 &&
253
+ neighbours.list[0].hash === candidate
254
+ ) {
255
+ return false;
256
+ }
257
+ }
258
+ return true;
259
+ });
209
260
  }
261
+ return [];
262
+ }
210
263
 
211
- const edges = this.graph.edges(from, to);
212
- if (edges.length > 1) {
213
- throw new Error("Unexpected edge count: " + edges.length);
264
+ public addPendingRouteConnection(
265
+ session: number,
266
+ route: {
267
+ from: string;
268
+ neighbour: string;
269
+ target: string;
270
+ distance: number;
214
271
  }
215
- if (edges.length > 0) {
216
- return edges[0];
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);
217
282
  }
218
- return undefined;
219
- }
220
283
 
221
- getLinkData(from: string, to: string): EdgeData | undefined {
222
- const edgeId = this.getLink(from, to);
223
- if (edgeId) return this.graph.getEdgeAttributes(edgeId);
224
- return undefined;
225
- }
284
+ arr.push(route);
226
285
 
227
- hasLink(from: string, to: string): boolean {
228
- return this.graph.hasEdge(from, to);
229
- }
230
- hasNode(node: string): boolean {
231
- return this.graph.hasNode(node);
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
+ }
232
291
  }
233
292
 
234
- getPath(
235
- from: string,
236
- to: string,
237
- options?: { unweighted?: boolean } | { block?: string }
238
- ): unweighted.ShortestPath | dijkstra.BidirectionalDijstraResult {
239
- try {
240
- let getEdgeWeight:
241
- | keyof EdgeData
242
- | MinimalEdgeMapper<number, EdgeData> = (edge) =>
243
- this.graph.getEdgeAttribute(edge, "weight");
244
- const blockId = (options as { block?: string })?.block;
245
- if (blockId) {
246
- const neighBourEdges = new Set(
247
- this.graph
248
- .inboundNeighbors(blockId)
249
- .map((x) => this.graph.edges(x, blockId))
250
- .flat()
251
- );
252
- getEdgeWeight = (edge) => {
253
- if (neighBourEdges.has(edge)) {
254
- return Number.MAX_SAFE_INTEGER;
255
- }
256
- return this.graph.getEdgeAttribute(edge, "weight");
257
- };
258
- }
293
+ // always commit if we dont know the peer yet
294
+ // do pending commits per remote (?)
259
295
 
260
- // TODO catching for network changes and resuse last result
261
- const path =
262
- ((options as { unweighted?: boolean })?.unweighted
263
- ? unweighted.bidirectional(this.graph, from, to)
264
- : dijkstra.bidirectional(this.graph, from, to, getEdgeWeight)) || [];
265
- if (path?.length > 0 && path[0] !== from) {
266
- path.reverse();
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;
267
317
  }
268
-
269
- if (blockId) {
270
- if (path.includes(blockId)) {
271
- return []; // Path does not exist, as we go through a blocked node with inifite weight
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
+ );
272
328
  }
273
329
  }
274
-
275
- return path as any; // TODO fix types
276
- } catch (error) {
277
- return [];
278
330
  }
279
- }
280
- clear() {
281
- this.graph.clear();
331
+
332
+ this.pendingRoutes.delete(session);
282
333
  }
283
334
  }
@@ -1 +0,0 @@
1
- export type PeerMap<T> = Map<string, T>;
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=peer-map.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"peer-map.js","sourceRoot":"","sources":["../../src/peer-map.ts"],"names":[],"mappings":""}
package/src/peer-map.ts DELETED
@@ -1 +0,0 @@
1
- export type PeerMap<T> = Map<string, T>;