@parcel/graph 3.1.1-nightly.3123 → 3.1.1-nightly.3125
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/lib/AdjacencyList.js +532 -159
- package/package.json +2 -2
- package/src/AdjacencyList.js +594 -184
- package/test/AdjacencyList.test.js +37 -23
package/src/AdjacencyList.js
CHANGED
@@ -22,25 +22,111 @@ export type SerializedAdjacencyList<TEdgeType> = {|
|
|
22
22
|
|
23
23
|
// eslint-disable-next-line no-unused-vars
|
24
24
|
export type AdjacencyListOptions<TEdgeType> = {|
|
25
|
-
|
26
|
-
|
25
|
+
/** The initial number of edges to accommodate. */
|
26
|
+
initialCapacity?: number,
|
27
|
+
/** The max amount by which to grow the capacity. */
|
28
|
+
maxGrowFactor?: number,
|
29
|
+
/** The min amount by which to grow the capacity. */
|
30
|
+
minGrowFactor?: number,
|
31
|
+
/** The size after which to grow the capacity by the minimum factor. */
|
32
|
+
peakCapacity?: number,
|
33
|
+
/** The percentage of deleted edges above which the capcity should shink. */
|
34
|
+
unloadFactor?: number,
|
35
|
+
/** The amount by which to shrink the capacity. */
|
36
|
+
shrinkFactor?: number,
|
37
|
+
|};
|
38
|
+
|
39
|
+
type AdjacencyListParams = {|
|
40
|
+
initialCapacity: number,
|
41
|
+
unloadFactor: number,
|
42
|
+
maxGrowFactor: number,
|
43
|
+
minGrowFactor: number,
|
44
|
+
peakCapacity: number,
|
45
|
+
shrinkFactor: number,
|
27
46
|
|};
|
28
47
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
48
|
+
const DEFAULT_PARAMS: AdjacencyListParams = {
|
49
|
+
initialCapacity: 2,
|
50
|
+
unloadFactor: 0.3,
|
51
|
+
maxGrowFactor: 8,
|
52
|
+
minGrowFactor: 2,
|
53
|
+
peakCapacity: 2 ** 18,
|
54
|
+
shrinkFactor: 0.5,
|
55
|
+
};
|
56
|
+
|
57
|
+
/**
|
58
|
+
* An Enum representing the result of a call to `link`.
|
59
|
+
*
|
60
|
+
* `EdgeAdded` = `0`: the edge was successfully linked
|
61
|
+
* `EdgeExists` = `1`: the edge already exists
|
62
|
+
* `EdgesOverloaded` = `2`: the edge map is overloaded
|
63
|
+
* `TooManyDeletes` = `3`: the edge map has too many deleted edges
|
64
|
+
* `NodesOverloaded` = `4`: the node map is overloaded
|
65
|
+
*/
|
66
|
+
const LinkResult: {|
|
67
|
+
/** The edge was successfully linked */
|
68
|
+
EdgeAdded: 0,
|
69
|
+
/** The edge already exists */
|
70
|
+
EdgeExists: 1,
|
71
|
+
/** The edge map is overloaded */
|
72
|
+
EdgesOverloaded: 2,
|
73
|
+
/** The edge map has too many deleted edges */
|
74
|
+
TooManyDeletes: 3,
|
75
|
+
/** The node map is overloaded */
|
76
|
+
NodesOverloaded: 4,
|
77
|
+
|} = {
|
78
|
+
EdgeAdded: 0,
|
79
|
+
EdgeExists: 1,
|
80
|
+
EdgesOverloaded: 2,
|
81
|
+
TooManyDeletes: 3,
|
82
|
+
NodesOverloaded: 4,
|
83
|
+
};
|
84
|
+
|
85
|
+
/**
|
86
|
+
* Allow 3 attempts to link an edge before erroring.
|
87
|
+
*
|
88
|
+
* The three attempts correspond to the three possible inconclusive link results:
|
89
|
+
* - `LinkResult.EdgesOverloaded`
|
90
|
+
* - `LinkResult.TooManyDeletes`
|
91
|
+
* - `LinkResult.NodesOverloaded`
|
92
|
+
*
|
93
|
+
* If after 3 tries, the link result is still one of these,
|
94
|
+
* this is considered an error.
|
95
|
+
*/
|
96
|
+
const MAX_LINK_TRIES: 3 = 3;
|
39
97
|
|
98
|
+
/**
|
99
|
+
* `AdjacencyList` maps nodes to lists of their adjacent nodes.
|
100
|
+
*
|
101
|
+
* It is implemented as a hashmap of nodes, where each node has
|
102
|
+
* doubly linked lists of edges of each unique edge type.
|
103
|
+
* The edges are stored in a separate hashmap, where each edge has
|
104
|
+
* a pointer to the originating node, the terminating node, and
|
105
|
+
* the next and previous edges to and from adjacent nodes.
|
106
|
+
*
|
107
|
+
* The hash maps are each stored in a `Uint32Array` backed
|
108
|
+
* by a `SharedArrayBuffer`. See `SharedTypeMap` for more details.
|
109
|
+
*
|
110
|
+
* It's primary interface is through the `getNodeIdsConnectedFrom`
|
111
|
+
* and `getNodeIdsConnectedTo` methods, which return the list of
|
112
|
+
* nodes connected from or to a given node, respectively.
|
113
|
+
*
|
114
|
+
* It is also possible to get the lists of edges connected from or to
|
115
|
+
* a given node, using the `getOutboundEdgesByType` and
|
116
|
+
* `getInboundEdgesByType` methods.
|
117
|
+
*
|
118
|
+
*/
|
40
119
|
export default class AdjacencyList<TEdgeType: number = 1> {
|
41
120
|
#nodes /*: NodeTypeMap<TEdgeType | NullEdgeType> */;
|
42
121
|
#edges /*: EdgeTypeMap<TEdgeType | NullEdgeType> */;
|
43
122
|
|
123
|
+
#params /*: AdjacencyListParams */;
|
124
|
+
|
125
|
+
/**
|
126
|
+
* Create a new `AdjacencyList` in one of two ways:
|
127
|
+
* - with specified options, or
|
128
|
+
* - with data serialized from a previous `AdjacencyList`.
|
129
|
+
*/
|
44
130
|
constructor(
|
45
131
|
opts?:
|
46
132
|
| SerializedAdjacencyList<TEdgeType | NullEdgeType>
|
@@ -53,26 +139,28 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
53
139
|
({nodes, edges} = opts);
|
54
140
|
this.#nodes = new NodeTypeMap(nodes);
|
55
141
|
this.#edges = new EdgeTypeMap(edges);
|
142
|
+
this.#params = {...DEFAULT_PARAMS, initialCapacity: this.#edges.capacity};
|
56
143
|
} else {
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
this.#nodes = new NodeTypeMap(
|
70
|
-
this.#edges = new EdgeTypeMap(
|
144
|
+
this.#params = {...DEFAULT_PARAMS, ...opts};
|
145
|
+
|
146
|
+
let {initialCapacity} = this.#params;
|
147
|
+
|
148
|
+
// TODO: Find a heuristic for right-sizing nodes.
|
149
|
+
// e.g., given an average ratio of `e` edges for every `n` nodes,
|
150
|
+
// init nodes with `capacity * n / e`.
|
151
|
+
let initialNodeCapacity = 2;
|
152
|
+
|
153
|
+
NodeTypeMap.assertMaxCapacity(initialNodeCapacity);
|
154
|
+
EdgeTypeMap.assertMaxCapacity(initialCapacity);
|
155
|
+
|
156
|
+
this.#nodes = new NodeTypeMap(initialNodeCapacity);
|
157
|
+
this.#edges = new EdgeTypeMap(initialCapacity);
|
71
158
|
}
|
72
159
|
}
|
73
160
|
|
74
161
|
/**
|
75
|
-
* Create a new `AdjacencyList`
|
162
|
+
* Create a new `AdjacencyList` with data serialized
|
163
|
+
* from another `AdjacencyList`.
|
76
164
|
*/
|
77
165
|
static deserialize(
|
78
166
|
opts: SerializedAdjacencyList<TEdgeType>,
|
@@ -81,7 +169,7 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
81
169
|
}
|
82
170
|
|
83
171
|
/**
|
84
|
-
* Returns a serializable object of the nodes and edges in the
|
172
|
+
* Returns a serializable object of the nodes and edges in the AdjacencyList.
|
85
173
|
*/
|
86
174
|
serialize(): SerializedAdjacencyList<TEdgeType> {
|
87
175
|
return {
|
@@ -90,13 +178,14 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
90
178
|
};
|
91
179
|
}
|
92
180
|
|
181
|
+
/** Statistics about the current state of the `AdjacencyList`. */
|
93
182
|
get stats(): {|
|
183
|
+
/** The maximum number of edges the graph can contain. */
|
184
|
+
capacity: number,
|
94
185
|
/** The number of nodes in the graph. */
|
95
186
|
nodes: number,
|
96
187
|
/** The number of edge types associated with nodes in the graph. */
|
97
188
|
nodeEdgeTypes: number,
|
98
|
-
/** The maximum number of nodes the graph can contain. */
|
99
|
-
nodeCapacity: number,
|
100
189
|
/** The size of the raw nodes buffer, in mb. */
|
101
190
|
nodeBufferSize: string,
|
102
191
|
/** The current load on the nodes array. */
|
@@ -105,8 +194,8 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
105
194
|
edges: number,
|
106
195
|
/** The number of edges deleted from the graph. */
|
107
196
|
deleted: number,
|
108
|
-
/** The
|
109
|
-
|
197
|
+
/** The number of unique edge types in the graph. */
|
198
|
+
edgeTypes: number,
|
110
199
|
/** The size of the raw edges buffer, in mb. */
|
111
200
|
edgeBufferSize: string,
|
112
201
|
/** The current load on the edges array, including deletes. */
|
@@ -119,9 +208,17 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
119
208
|
maxCollisions: number,
|
120
209
|
/** The average number of collisions per hash. */
|
121
210
|
avgCollisions: number,
|
122
|
-
/**
|
211
|
+
/**
|
212
|
+
* The actual distribution of hashes vs. the expected (uniform) distribution.
|
213
|
+
*
|
214
|
+
* From: https://en.wikipedia.org/wiki/Hash_function#Testing_and_measurement
|
215
|
+
*
|
216
|
+
* > A ratio within one confidence interval (0.95 - 1.05) is indicative
|
217
|
+
* > that the hash function...has an expected uniform distribution.
|
218
|
+
*/
|
123
219
|
uniformity: number,
|
124
220
|
|} {
|
221
|
+
let edgeTypes = new Set();
|
125
222
|
let buckets = new Map();
|
126
223
|
for (let {from, to, type} of this.getAllEdges()) {
|
127
224
|
let hash = this.#edges.hash(from, to, type);
|
@@ -130,11 +227,20 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
130
227
|
assert(!bucket.has(key), `Duplicate node detected: ${key}`);
|
131
228
|
bucket.add(key);
|
132
229
|
buckets.set(hash, bucket);
|
230
|
+
edgeTypes.add(type);
|
133
231
|
}
|
134
232
|
|
135
233
|
let maxCollisions = 0;
|
136
234
|
let collisions = 0;
|
137
235
|
let distribution = 0;
|
236
|
+
/**
|
237
|
+
* The expected distribution of hashes across available hash buckets.
|
238
|
+
*
|
239
|
+
* See: https://en.wikipedia.org/wiki/Hash_function#Testing_and_measurement
|
240
|
+
*/
|
241
|
+
let uniformDistribution =
|
242
|
+
(this.#edges.count / (2 * this.#edges.capacity)) *
|
243
|
+
(this.#edges.count + 2 * this.#edges.capacity - 1);
|
138
244
|
|
139
245
|
for (let bucket of buckets.values()) {
|
140
246
|
maxCollisions = Math.max(maxCollisions, bucket.size - 1);
|
@@ -142,21 +248,17 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
142
248
|
distribution += (bucket.size * (bucket.size + 1)) / 2;
|
143
249
|
}
|
144
250
|
|
145
|
-
let uniformity =
|
146
|
-
distribution /
|
147
|
-
((this.#edges.count / (2 * this.#edges.capacity)) *
|
148
|
-
(this.#edges.count + 2 * this.#edges.capacity - 1));
|
149
|
-
|
150
251
|
return {
|
252
|
+
capacity: this.#edges.capacity,
|
253
|
+
|
151
254
|
nodes: fromNodeId(this.#nodes.nextId),
|
152
255
|
nodeEdgeTypes: this.#nodes.count,
|
153
|
-
nodeCapacity: this.#nodes.capacity,
|
154
256
|
nodeLoad: `${Math.round(this.#nodes.load * 100)}%`,
|
155
257
|
nodeBufferSize: this.#nodes.bufferSize,
|
156
258
|
|
157
259
|
edges: this.#edges.count,
|
158
260
|
deleted: this.#edges.deletes,
|
159
|
-
|
261
|
+
edgeTypes: edgeTypes.size,
|
160
262
|
edgeLoad: `${Math.round(this.#edges.load * 100)}%`,
|
161
263
|
edgeLoadWithDeletes: `${Math.round(
|
162
264
|
this.#edges.getLoad(this.#edges.count + this.#edges.deletes) * 100,
|
@@ -165,16 +267,18 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
165
267
|
|
166
268
|
collisions,
|
167
269
|
maxCollisions,
|
168
|
-
avgCollisions:
|
169
|
-
|
270
|
+
avgCollisions:
|
271
|
+
Math.round((collisions / this.#edges.count) * 100) / 100 || 0,
|
272
|
+
uniformity:
|
273
|
+
Math.round((distribution / uniformDistribution) * 100) / 100 || 0,
|
170
274
|
};
|
171
275
|
}
|
172
276
|
|
173
277
|
/**
|
174
278
|
* Resize the internal nodes array.
|
175
279
|
*
|
176
|
-
* This is used in `addNode`
|
177
|
-
* the
|
280
|
+
* This is used in `addNode` and in `addEdge` when
|
281
|
+
* the `nodes` array is at capacity,
|
178
282
|
*/
|
179
283
|
resizeNodes(size: number) {
|
180
284
|
let nodes = this.#nodes;
|
@@ -187,57 +291,72 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
187
291
|
/**
|
188
292
|
* Resize the internal edges array.
|
189
293
|
*
|
190
|
-
* This is used in `addEdge` when the `
|
191
|
-
* the allocated size of the `edges` array.
|
294
|
+
* This is used in `addEdge` when the `edges` array is at capacity.
|
192
295
|
*/
|
193
296
|
resizeEdges(size: number) {
|
194
297
|
// Allocate the required space for new `nodes` and `edges` maps.
|
195
|
-
let
|
196
|
-
|
197
|
-
edgeCapacity: size,
|
198
|
-
});
|
298
|
+
let edges = new EdgeTypeMap(size);
|
299
|
+
let nodes = new NodeTypeMap(this.#nodes.capacity);
|
199
300
|
|
200
301
|
// Copy the existing edges into the new array.
|
201
|
-
|
302
|
+
nodes.nextId = this.#nodes.nextId;
|
202
303
|
this.#edges.forEach(
|
203
304
|
edge =>
|
204
|
-
void
|
305
|
+
void link(
|
205
306
|
this.#edges.from(edge),
|
206
307
|
this.#edges.to(edge),
|
207
308
|
this.#edges.typeOf(edge),
|
309
|
+
edges,
|
310
|
+
nodes,
|
311
|
+
this.#params.unloadFactor,
|
208
312
|
),
|
209
313
|
);
|
210
314
|
|
211
315
|
// We expect to preserve the same number of edges.
|
212
316
|
assert(
|
213
|
-
this.#edges.count ===
|
214
|
-
`Edge mismatch! ${this.#edges.count} does not match ${
|
215
|
-
copy.#edges.count
|
216
|
-
}.`,
|
317
|
+
this.#edges.count === edges.count,
|
318
|
+
`Edge mismatch! ${this.#edges.count} does not match ${edges.count}.`,
|
217
319
|
);
|
218
320
|
|
219
321
|
// Finally, copy the new data arrays over to this graph.
|
220
|
-
this.#nodes =
|
221
|
-
this.#edges =
|
322
|
+
this.#nodes = nodes;
|
323
|
+
this.#edges = edges;
|
222
324
|
}
|
223
325
|
|
224
326
|
/**
|
225
327
|
* Adds a node to the graph.
|
226
328
|
*
|
329
|
+
* Note that this method does not increment the node count
|
330
|
+
* (that only happens in `addEdge`), it _may_ preemptively resize
|
331
|
+
* the nodes array if it is at capacity, under the asumption that
|
332
|
+
* at least 1 edge to or from this new node will be added.
|
333
|
+
*
|
227
334
|
* Returns the id of the added node.
|
228
335
|
*/
|
229
336
|
addNode(): NodeId {
|
230
337
|
let id = this.#nodes.getId();
|
231
|
-
|
232
|
-
|
233
|
-
|
338
|
+
if (this.#nodes.getLoad() >= 1) {
|
339
|
+
this.resizeNodes(
|
340
|
+
increaseNodeCapacity(this.#nodes.capacity, this.#params),
|
341
|
+
);
|
234
342
|
}
|
343
|
+
|
235
344
|
return id;
|
236
345
|
}
|
237
346
|
|
238
347
|
/**
|
239
348
|
* Adds an edge to the graph.
|
240
349
|
*
|
350
|
+
* This method will increment the edge count, and it _may_
|
351
|
+
* also increment the node count, if the originating or
|
352
|
+
* terminating node does not yet have any edges of the given type.
|
353
|
+
*
|
354
|
+
* If either the `nodes` or `edges` arrays are at capacity,
|
355
|
+
* this method will resize them before adding.
|
356
|
+
*
|
357
|
+
* Furthermore, if the `edges` array has a high number of
|
358
|
+
* deleted edges, it may reclaim the space before adding.
|
359
|
+
*
|
241
360
|
* Returns `true` if the edge was added,
|
242
361
|
* or `false` if the edge already exists.
|
243
362
|
*/
|
@@ -246,69 +365,54 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
246
365
|
to: NodeId,
|
247
366
|
type: TEdgeType | NullEdgeType = 1,
|
248
367
|
): boolean {
|
368
|
+
assert(from < this.#nodes.nextId, `Node ${from} does not exist.`);
|
369
|
+
assert(to < this.#nodes.nextId, `Node ${to} does not exist.`);
|
249
370
|
assert(type > 0, `Unsupported edge type ${type}`);
|
250
371
|
|
251
|
-
let
|
252
|
-
let
|
372
|
+
let result;
|
373
|
+
let tries = 0;
|
253
374
|
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
let capacity = this.#edges.capacity;
|
258
|
-
// We add 1 to account for the edge we are adding.
|
259
|
-
let count = this.#edges.count + 1;
|
260
|
-
// Since the space occupied by deleted edges isn't reclaimed,
|
261
|
-
// we include them in our count to avoid overflowing the `edges` array.
|
262
|
-
let deletes = this.#edges.deletes;
|
263
|
-
let total = count + deletes;
|
264
|
-
// If we have enough space to keep adding edges, we can
|
265
|
-
// put off reclaiming the deleted space until the next resize.
|
266
|
-
if (this.#edges.getLoad(total) > LOAD_FACTOR) {
|
267
|
-
if (this.#edges.getLoad(deletes) > UNLOAD_FACTOR) {
|
268
|
-
// If we have a significant number of deletes, we compute our new
|
269
|
-
// capacity based on the current count, even though we decided to
|
270
|
-
// resize based on the sum total of count and deletes.
|
271
|
-
// In this case, resizing is more like a compaction.
|
272
|
-
this.resizeEdges(
|
273
|
-
getNextEdgeCapacity(capacity, count, this.#edges.getLoad(count)),
|
274
|
-
);
|
275
|
-
} else {
|
276
|
-
this.resizeEdges(
|
277
|
-
getNextEdgeCapacity(capacity, total, this.#edges.getLoad(total)),
|
278
|
-
);
|
279
|
-
}
|
280
|
-
// We must rehash because the capacity has changed.
|
281
|
-
hash = this.#edges.hash(from, to, type);
|
282
|
-
}
|
283
|
-
|
284
|
-
let toNode = this.#nodes.addressOf(to, type);
|
285
|
-
let fromNode = this.#nodes.addressOf(from, type);
|
286
|
-
if (toNode === null || fromNode === null) {
|
287
|
-
// If we're in danger of overflowing the `nodes` array, resize it.
|
288
|
-
if (this.#nodes.load >= LOAD_FACTOR) {
|
289
|
-
this.resizeNodes(increaseNodeCapacity(this.#nodes.capacity));
|
290
|
-
// We need to update our indices since the `nodes` array has changed.
|
291
|
-
toNode = this.#nodes.addressOf(to, type);
|
292
|
-
fromNode = this.#nodes.addressOf(from, type);
|
293
|
-
}
|
294
|
-
}
|
295
|
-
if (toNode === null) toNode = this.#nodes.add(to, type);
|
296
|
-
if (fromNode === null) fromNode = this.#nodes.add(from, type);
|
297
|
-
|
298
|
-
// Add our new edge to its hash bucket.
|
299
|
-
edge = this.#edges.add(hash, from, to, type);
|
375
|
+
do {
|
376
|
+
assert(tries++ < MAX_LINK_TRIES, 'Failed to addEdge too many times!');
|
300
377
|
|
301
|
-
|
302
|
-
|
303
|
-
|
378
|
+
result = link(
|
379
|
+
from,
|
380
|
+
to,
|
381
|
+
type,
|
382
|
+
this.#edges,
|
383
|
+
this.#nodes,
|
384
|
+
this.#params.unloadFactor,
|
385
|
+
);
|
304
386
|
|
305
|
-
|
306
|
-
|
307
|
-
|
387
|
+
// Sometimes we need to resize before we can add.
|
388
|
+
switch (result) {
|
389
|
+
case LinkResult.NodesOverloaded: {
|
390
|
+
this.resizeNodes(
|
391
|
+
increaseNodeCapacity(this.#nodes.capacity, this.#params),
|
392
|
+
);
|
393
|
+
break;
|
394
|
+
}
|
395
|
+
case LinkResult.EdgesOverloaded: {
|
396
|
+
this.resizeEdges(
|
397
|
+
increaseEdgeCapacity(this.#edges.capacity, this.#params),
|
398
|
+
);
|
399
|
+
break;
|
400
|
+
}
|
401
|
+
case LinkResult.TooManyDeletes: {
|
402
|
+
this.resizeEdges(
|
403
|
+
decreaseEdgeCapacity(this.#edges.capacity, this.#params),
|
404
|
+
);
|
405
|
+
break;
|
406
|
+
}
|
407
|
+
}
|
408
|
+
} while (result > LinkResult.EdgeExists);
|
308
409
|
|
309
|
-
return
|
410
|
+
return result === LinkResult.EdgeAdded;
|
310
411
|
}
|
311
412
|
|
413
|
+
/**
|
414
|
+
* Iterate over all edges in insertion order.
|
415
|
+
*/
|
312
416
|
*getAllEdges(): Iterator<{|
|
313
417
|
type: TEdgeType | NullEdgeType,
|
314
418
|
from: NodeId,
|
@@ -344,7 +448,12 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
344
448
|
}
|
345
449
|
|
346
450
|
/**
|
451
|
+
* Remove an edge connecting the `from` and `to` nodes.
|
452
|
+
*
|
453
|
+
* Note that space for the deleted edge is not reclaimed
|
454
|
+
* until the `edges` array is resized.
|
347
455
|
*
|
456
|
+
* This method will increment the edge delete count.
|
348
457
|
*/
|
349
458
|
removeEdge(
|
350
459
|
from: NodeId,
|
@@ -386,6 +495,13 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
386
495
|
this.#edges.delete(edge);
|
387
496
|
}
|
388
497
|
|
498
|
+
/**
|
499
|
+
* Check if the given node has any edges incoming from other nodes.
|
500
|
+
*
|
501
|
+
* Essentially, this is an orphan check. If a node has no incoming edges,
|
502
|
+
* it (and its entire subgraph) is completely disconnected from the
|
503
|
+
* rest of the graph.
|
504
|
+
*/
|
389
505
|
hasInboundEdges(to: NodeId): boolean {
|
390
506
|
let node = this.#nodes.head(to);
|
391
507
|
while (node !== null) {
|
@@ -395,6 +511,10 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
395
511
|
return false;
|
396
512
|
}
|
397
513
|
|
514
|
+
/**
|
515
|
+
* Get a list of every node (labeled `from`) connecting _to_
|
516
|
+
* the given `to` node, along with the edge `type` connecting them.
|
517
|
+
*/
|
398
518
|
getInboundEdgesByType(
|
399
519
|
to: NodeId,
|
400
520
|
): {|type: TEdgeType | NullEdgeType, from: NodeId|}[] {
|
@@ -413,6 +533,10 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
413
533
|
return edges;
|
414
534
|
}
|
415
535
|
|
536
|
+
/**
|
537
|
+
* Get a list of every node (labeled `to`) connected _from_
|
538
|
+
* the given `from` node, along with the edge `type` connecting them.
|
539
|
+
*/
|
416
540
|
getOutboundEdgesByType(
|
417
541
|
from: NodeId,
|
418
542
|
): {|type: TEdgeType | NullEdgeType, to: NodeId|}[] {
|
@@ -432,7 +556,11 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
432
556
|
}
|
433
557
|
|
434
558
|
/**
|
435
|
-
* Get the list of
|
559
|
+
* Get the list of node ids connected from this node.
|
560
|
+
*
|
561
|
+
* If `type` is specified, only return nodes connected by edges of that type.
|
562
|
+
* If `type` is an array, return nodes connected by edges of any of those types.
|
563
|
+
* If `type` is `AllEdgeTypes` (`-1`), return nodes connected by edges of any type.
|
436
564
|
*/
|
437
565
|
getNodeIdsConnectedFrom(
|
438
566
|
from: NodeId,
|
@@ -486,7 +614,11 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
486
614
|
}
|
487
615
|
|
488
616
|
/**
|
489
|
-
* Get the list of
|
617
|
+
* Get the list of node ids connected to this node.
|
618
|
+
*
|
619
|
+
* If `type` is specified, only return nodes connected by edges of that type.
|
620
|
+
* If `type` is an array, return nodes connected by edges of any of those types.
|
621
|
+
* If `type` is `AllEdgeTypes` (`-1`), return nodes connected by edges of any type.
|
490
622
|
*/
|
491
623
|
getNodeIdsConnectedTo(
|
492
624
|
to: NodeId,
|
@@ -549,7 +681,7 @@ export default class AdjacencyList<TEdgeType: number = 1> {
|
|
549
681
|
* └──┴──┴──┴───────┴──┴──┴──┴───────┴──┴──┘
|
550
682
|
* └──┬──┘ └─────────┬─────────┘
|
551
683
|
* header items
|
552
|
-
* (HEADER_SIZE)
|
684
|
+
* (HEADER_SIZE) (capacity * ITEM_SIZE)
|
553
685
|
*
|
554
686
|
*
|
555
687
|
* An item is added with a hash key that fits within the range of the hash
|
@@ -608,31 +740,47 @@ export class SharedTypeMap<TItemType, THash, TAddress: number>
|
|
608
740
|
/** The offset at which an item's type is stored. */
|
609
741
|
static #TYPE: 1 = 1;
|
610
742
|
|
611
|
-
/** The
|
612
|
-
static
|
743
|
+
/** The largest possible capacity. */
|
744
|
+
static get MAX_CAPACITY(): number {
|
745
|
+
return Math.floor(
|
746
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length#what_went_wrong
|
747
|
+
(2 ** 31 - 1 - this.HEADER_SIZE) / this.ITEM_SIZE,
|
748
|
+
);
|
749
|
+
}
|
750
|
+
|
751
|
+
/** Assert that the given `capacity` does not exceed `MAX_CAPACITY`. */
|
752
|
+
static assertMaxCapacity(capacity: number): void {
|
753
|
+
assert(capacity <= this.MAX_CAPACITY, `${this.name} capacity overflow!`);
|
754
|
+
}
|
613
755
|
|
614
756
|
data: Uint32Array;
|
615
757
|
|
758
|
+
/** The total number of items that can fit in the map. */
|
616
759
|
get capacity(): number {
|
617
760
|
return this.data[SharedTypeMap.#CAPACITY];
|
618
761
|
}
|
619
762
|
|
763
|
+
/** The number of items in the map. */
|
620
764
|
get count(): number {
|
621
765
|
return this.data[SharedTypeMap.#COUNT];
|
622
766
|
}
|
623
767
|
|
768
|
+
/** The ratio of the count to the capacity. */
|
624
769
|
get load(): number {
|
625
770
|
return this.getLoad();
|
626
771
|
}
|
627
772
|
|
773
|
+
/** The total length of the map, in bytes. */
|
628
774
|
get length(): number {
|
629
775
|
return this.getLength();
|
630
776
|
}
|
631
777
|
|
778
|
+
/** The address of the first item in the map. */
|
632
779
|
get addressableLimit(): number {
|
633
780
|
return this.constructor.HEADER_SIZE + this.capacity;
|
634
781
|
}
|
635
782
|
|
783
|
+
/** The size of the map in mb, as a localized string. */
|
636
784
|
get bufferSize(): string {
|
637
785
|
return `${(this.data.byteLength / 1024 / 1024).toLocaleString(undefined, {
|
638
786
|
minimumFractionDigits: 2,
|
@@ -640,6 +788,11 @@ export class SharedTypeMap<TItemType, THash, TAddress: number>
|
|
640
788
|
})} mb`;
|
641
789
|
}
|
642
790
|
|
791
|
+
/**
|
792
|
+
* Create a new `SharedTypeMap` in one of two ways:
|
793
|
+
* - with a capacity of `capacityOrData` if it is a number,
|
794
|
+
* - or with `capacityOrData` as its data, if it is a `Uint32Array`.
|
795
|
+
*/
|
643
796
|
constructor(capacityOrData: number | Uint32Array) {
|
644
797
|
if (typeof capacityOrData === 'number') {
|
645
798
|
let {BYTES_PER_ELEMENT} = Uint32Array;
|
@@ -655,6 +808,13 @@ export class SharedTypeMap<TItemType, THash, TAddress: number>
|
|
655
808
|
}
|
656
809
|
}
|
657
810
|
|
811
|
+
/**
|
812
|
+
* Overwrite the data in this map with the given `data`.
|
813
|
+
*
|
814
|
+
* The `data` is expected to conform to the same
|
815
|
+
* partitioning and schema as the data in this map,
|
816
|
+
* and is expected to be of equal or smaller capacity to this map.
|
817
|
+
*/
|
658
818
|
set(data: Uint32Array): void {
|
659
819
|
let {HEADER_SIZE, ITEM_SIZE} = this.constructor;
|
660
820
|
let NEXT = SharedTypeMap.#NEXT;
|
@@ -686,14 +846,26 @@ export class SharedTypeMap<TItemType, THash, TAddress: number>
|
|
686
846
|
}
|
687
847
|
}
|
688
848
|
|
849
|
+
/**
|
850
|
+
* Given a `count` (defaulting to `this.count`),
|
851
|
+
* get the load on the map.
|
852
|
+
*
|
853
|
+
* The load is the ratio of the `count` the capacity of the map.
|
854
|
+
*
|
855
|
+
* If the load is `1`, it means the map is at capacity, and needs
|
856
|
+
* to be resized before adding more items.
|
857
|
+
*/
|
689
858
|
getLoad(count: number = this.count): number {
|
690
|
-
|
691
|
-
return count / (this.capacity * BUCKET_SIZE);
|
859
|
+
return count / this.capacity;
|
692
860
|
}
|
693
861
|
|
862
|
+
/**
|
863
|
+
* Given a `capacity` (defaulting to `this.capacity`),
|
864
|
+
* get the length of the map, in bytes.
|
865
|
+
*/
|
694
866
|
getLength(capacity: number = this.capacity): number {
|
695
|
-
let {HEADER_SIZE, ITEM_SIZE
|
696
|
-
return capacity + HEADER_SIZE + ITEM_SIZE *
|
867
|
+
let {HEADER_SIZE, ITEM_SIZE} = this.constructor;
|
868
|
+
return capacity + HEADER_SIZE + ITEM_SIZE * capacity;
|
697
869
|
}
|
698
870
|
|
699
871
|
/** Get the next available address in the map. */
|
@@ -714,10 +886,15 @@ export class SharedTypeMap<TItemType, THash, TAddress: number>
|
|
714
886
|
return (this.data[(item: any) + NEXT]: any) || null;
|
715
887
|
}
|
716
888
|
|
889
|
+
/** Get the type of the item at the given `item` address. */
|
717
890
|
typeOf(item: TAddress): TItemType {
|
718
891
|
return (this.data[item + SharedTypeMap.#TYPE]: any);
|
719
892
|
}
|
720
893
|
|
894
|
+
/**
|
895
|
+
* Store an item of `type` at the `item` address and
|
896
|
+
* link the address to the `hash` bucket.
|
897
|
+
*/
|
721
898
|
link(hash: THash, item: TAddress, type: TItemType): void {
|
722
899
|
let COUNT = SharedTypeMap.#COUNT;
|
723
900
|
let NEXT = SharedTypeMap.#NEXT;
|
@@ -741,6 +918,9 @@ export class SharedTypeMap<TItemType, THash, TAddress: number>
|
|
741
918
|
this.data[COUNT]++;
|
742
919
|
}
|
743
920
|
|
921
|
+
/**
|
922
|
+
* Remove the link to the `item` address from the `hash` bucket.
|
923
|
+
*/
|
744
924
|
unlink(hash: THash, item: TAddress): void {
|
745
925
|
let COUNT = SharedTypeMap.#COUNT;
|
746
926
|
let NEXT = SharedTypeMap.#NEXT;
|
@@ -815,13 +995,13 @@ export class SharedTypeMap<TItemType, THash, TAddress: number>
|
|
815
995
|
table: Uint32Array,
|
816
996
|
data: Uint32Array,
|
817
997
|
|} {
|
818
|
-
const {HEADER_SIZE
|
819
|
-
let min =
|
820
|
-
|
998
|
+
const {HEADER_SIZE} = this.constructor;
|
999
|
+
let min = this.addressableLimit;
|
1000
|
+
|
821
1001
|
return {
|
822
1002
|
header: this.data.subarray(0, HEADER_SIZE),
|
823
1003
|
table: this.data.subarray(HEADER_SIZE, min),
|
824
|
-
data: this.data.subarray(min
|
1004
|
+
data: this.data.subarray(min),
|
825
1005
|
};
|
826
1006
|
}
|
827
1007
|
}
|
@@ -829,7 +1009,17 @@ export class SharedTypeMap<TItemType, THash, TAddress: number>
|
|
829
1009
|
/**
|
830
1010
|
* Nodes are stored in a `SharedTypeMap`, keyed on node id plus an edge type.
|
831
1011
|
* This means that for any given unique node id, there may be `e` nodes in the
|
832
|
-
* map, where `e` is the number of
|
1012
|
+
* map, where `e` is the number of unique edge types in the graph.
|
1013
|
+
*
|
1014
|
+
* The _hash_ for a node is simply the node id (as issued by `getId`),
|
1015
|
+
* and forms the head of linked list of unique _edge types_ connected
|
1016
|
+
* to or from the same node id.
|
1017
|
+
*
|
1018
|
+
* In addition to a unique edge type, each Node contains the heads and tails
|
1019
|
+
* of doubly linked lists of incoming and outgoing edges of the same type.
|
1020
|
+
*
|
1021
|
+
* Note that the links in the doubly linked lists are Edges (not Nodes),
|
1022
|
+
* which are stored in a corresponding `EdgeTypeMap`.
|
833
1023
|
*/
|
834
1024
|
export class NodeTypeMap<TEdgeType> extends SharedTypeMap<
|
835
1025
|
TEdgeType,
|
@@ -849,6 +1039,15 @@ export class NodeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
849
1039
|
* ┌──────────┬───────┬─────────┐
|
850
1040
|
* │ CAPACITY │ COUNT │ NEXT_ID │
|
851
1041
|
* └──────────┴───────┴─────────┘
|
1042
|
+
*
|
1043
|
+
* The `nextId` is a count of the number of times `getId` has been called.
|
1044
|
+
* This is distinct concept from the `count`, which tracks the number of times
|
1045
|
+
* `add` has been called.
|
1046
|
+
*
|
1047
|
+
* The reason for this distinction is that `getId` is called once per node
|
1048
|
+
* (to issue a _unique_ id) and will _always increment_ the `nextId` counter,
|
1049
|
+
* whereas `add` is called once per edge, and will only increment the `count`
|
1050
|
+
* if the _type_ of edge is new for the given node.
|
852
1051
|
*/
|
853
1052
|
static HEADER_SIZE: number = 3;
|
854
1053
|
/** The offset from the header where the next available node id is stored. */
|
@@ -870,6 +1069,9 @@ export class NodeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
870
1069
|
* ┌──────┬──────┬──────────┬───────────┬─────────┬──────────┐
|
871
1070
|
* │ NEXT │ TYPE │ FIRST_IN │ FIRST_OUT │ LAST_IN │ LAST_OUT │
|
872
1071
|
* └──────┴──────┴──────────┴───────────┴─────────┴──────────┘
|
1072
|
+
*
|
1073
|
+
* The `Node` implicitly maps a node id (the hash the node was added with)
|
1074
|
+
* to the first and last incoming and outgoing edges of the same _edge type_.
|
873
1075
|
*/
|
874
1076
|
static ITEM_SIZE: number = 6;
|
875
1077
|
/** The offset at which a node's first incoming edge of this type is stored. */
|
@@ -881,16 +1083,6 @@ export class NodeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
881
1083
|
/** The offset at which a node's last outgoing edge of this type is stored. */
|
882
1084
|
static #LAST_OUT = 5;
|
883
1085
|
|
884
|
-
/** The smallest functional node map capacity. */
|
885
|
-
static MIN_CAPACITY: number = 2;
|
886
|
-
/** The largest possible node map capacity. */
|
887
|
-
static MAX_CAPACITY: number = Math.floor(
|
888
|
-
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length#what_went_wrong
|
889
|
-
(2 ** 31 - 1 - NodeTypeMap.HEADER_SIZE) /
|
890
|
-
NodeTypeMap.ITEM_SIZE /
|
891
|
-
NodeTypeMap.BUCKET_SIZE,
|
892
|
-
);
|
893
|
-
|
894
1086
|
get nextId(): NodeId {
|
895
1087
|
return toNodeId(this.data[NodeTypeMap.#NEXT_ID]);
|
896
1088
|
}
|
@@ -898,18 +1090,31 @@ export class NodeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
898
1090
|
this.data[NodeTypeMap.#NEXT_ID] = fromNodeId(nextId);
|
899
1091
|
}
|
900
1092
|
|
901
|
-
/**
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
1093
|
+
/**
|
1094
|
+
* Get the load on the node map.
|
1095
|
+
*
|
1096
|
+
* The load is the greater of either:
|
1097
|
+
* - the ratio of the number of node ids to the capacity of the map,
|
1098
|
+
* - or the ratio of the `count` to the capacity of the map.
|
1099
|
+
*
|
1100
|
+
* if `count` is not provided, the default is the number of items
|
1101
|
+
* currently added to the map.
|
1102
|
+
*/
|
1103
|
+
getLoad(count?: number): number {
|
907
1104
|
return Math.max(
|
908
1105
|
fromNodeId(this.nextId) / this.capacity,
|
909
1106
|
super.getLoad(count),
|
910
1107
|
);
|
911
1108
|
}
|
912
1109
|
|
1110
|
+
/** Increment the node counter to get a unique node id. */
|
1111
|
+
getId(): NodeId {
|
1112
|
+
return toNodeId(this.data[NodeTypeMap.#NEXT_ID]++);
|
1113
|
+
}
|
1114
|
+
|
1115
|
+
/**
|
1116
|
+
* Add new lists of edges of the given `type` to and from the given `node`.
|
1117
|
+
*/
|
913
1118
|
add(node: NodeId, type: TEdgeType): NodeAddress {
|
914
1119
|
let index = fromNodeId(node);
|
915
1120
|
assert(
|
@@ -921,6 +1126,10 @@ export class NodeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
921
1126
|
return address;
|
922
1127
|
}
|
923
1128
|
|
1129
|
+
/**
|
1130
|
+
* Get the address of the lists edges of the given `type`
|
1131
|
+
* to and from the given `node`.
|
1132
|
+
*/
|
924
1133
|
addressOf(node: NodeId, type: TEdgeType): NodeAddress | null {
|
925
1134
|
let address = this.head(node);
|
926
1135
|
while (address !== null) {
|
@@ -932,22 +1141,45 @@ export class NodeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
932
1141
|
return null;
|
933
1142
|
}
|
934
1143
|
|
1144
|
+
/**
|
1145
|
+
* Given a `node` address, get the _head_ of the linked list
|
1146
|
+
* of incoming edges of the same type to the same node.
|
1147
|
+
*/
|
935
1148
|
firstIn(node: NodeAddress): EdgeAddress | null {
|
936
1149
|
return this.data[node + NodeTypeMap.#FIRST_IN] || null;
|
937
1150
|
}
|
938
1151
|
|
1152
|
+
/**
|
1153
|
+
* Given a `node` address, get the _head_ of the linked list
|
1154
|
+
* of outgoing edges of the same type from the same node.
|
1155
|
+
*/
|
939
1156
|
firstOut(node: NodeAddress): EdgeAddress | null {
|
940
1157
|
return this.data[node + NodeTypeMap.#FIRST_OUT] || null;
|
941
1158
|
}
|
942
1159
|
|
1160
|
+
/**
|
1161
|
+
* Given a `node` address, get the _tail_ of the linked list
|
1162
|
+
* of incoming edges of the same type to the same node.
|
1163
|
+
*/
|
943
1164
|
lastIn(node: NodeAddress): EdgeAddress | null {
|
944
1165
|
return this.data[node + NodeTypeMap.#LAST_IN] || null;
|
945
1166
|
}
|
946
1167
|
|
1168
|
+
/**
|
1169
|
+
* Given a `node` address, get the _tail_ of the linked list
|
1170
|
+
* of outgoing edges of the same type from the same node.
|
1171
|
+
*/
|
947
1172
|
lastOut(node: NodeAddress): EdgeAddress | null {
|
948
1173
|
return this.data[node + NodeTypeMap.#LAST_OUT] || null;
|
949
1174
|
}
|
950
1175
|
|
1176
|
+
/**
|
1177
|
+
* Set `edge` as the last incoming edge to `node`.
|
1178
|
+
* If `node` has no incoming edges, set `edge`
|
1179
|
+
* as the first incoming edge, as well.
|
1180
|
+
*
|
1181
|
+
* Returns the address of the old last incoming edge, if any.
|
1182
|
+
*/
|
951
1183
|
linkIn(node: NodeAddress, edge: EdgeAddress): EdgeAddress | null {
|
952
1184
|
let first = this.firstIn(node);
|
953
1185
|
let last = this.lastIn(node);
|
@@ -956,6 +1188,13 @@ export class NodeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
956
1188
|
return last;
|
957
1189
|
}
|
958
1190
|
|
1191
|
+
/**
|
1192
|
+
* If `edge` is the last incoming edge to `node`,
|
1193
|
+
* update the node's last incoming edge to `prev`.
|
1194
|
+
*
|
1195
|
+
* If `edge` is the first incoming edge to `node`,
|
1196
|
+
* update the node's first incoming edge to `next`.
|
1197
|
+
*/
|
959
1198
|
unlinkIn(
|
960
1199
|
node: NodeAddress,
|
961
1200
|
edge: EdgeAddress,
|
@@ -972,6 +1211,13 @@ export class NodeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
972
1211
|
}
|
973
1212
|
}
|
974
1213
|
|
1214
|
+
/**
|
1215
|
+
* Set `edge` as the last outgoing edge from `node`.
|
1216
|
+
* If `node` has no outgoing edges, set `edge`
|
1217
|
+
* as the first outgoing edge, as well.
|
1218
|
+
*
|
1219
|
+
* Returns the address of the old last outgoing edge, if any.
|
1220
|
+
*/
|
975
1221
|
linkOut(node: NodeAddress, edge: EdgeAddress): EdgeAddress | null {
|
976
1222
|
let first = this.firstOut(node);
|
977
1223
|
let last = this.lastOut(node);
|
@@ -980,6 +1226,13 @@ export class NodeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
980
1226
|
return last;
|
981
1227
|
}
|
982
1228
|
|
1229
|
+
/**
|
1230
|
+
* If `edge` is the last outgoing edge from `node`,
|
1231
|
+
* update the node's last outgoing edge to `prev`.
|
1232
|
+
*
|
1233
|
+
* If `edge` is the first outgoing edge from `node`,
|
1234
|
+
* update the node's first outgoing edge to `next`.
|
1235
|
+
*/
|
983
1236
|
unlinkOut(
|
984
1237
|
node: NodeAddress,
|
985
1238
|
edge: EdgeAddress,
|
@@ -1000,6 +1253,14 @@ export class NodeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
1000
1253
|
/**
|
1001
1254
|
* Edges are stored in a `SharedTypeMap`,
|
1002
1255
|
* keyed on the 'from' and 'to' node ids, and the edge type.
|
1256
|
+
*
|
1257
|
+
* The _hash_ for an edge is a hash of the edge's `from`, `to`, and `type` values,
|
1258
|
+
* and forms the head of linked list of edges with the same hash.
|
1259
|
+
*
|
1260
|
+
* In addition to the `from`, `to` and `type` values, each Edge contains
|
1261
|
+
* the next and previous links of doubly linked lists of the _adjacent_ edges
|
1262
|
+
* of the same type, both incoming to the `to` node, and outgoing from
|
1263
|
+
* the `from` node.
|
1003
1264
|
*/
|
1004
1265
|
export class EdgeTypeMap<TEdgeType> extends SharedTypeMap<
|
1005
1266
|
TEdgeType,
|
@@ -1019,6 +1280,13 @@ export class EdgeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
1019
1280
|
* ┌──────────┬───────┬─────────┐
|
1020
1281
|
* │ CAPACITY │ COUNT │ DELETES │
|
1021
1282
|
* └──────────┴───────┴─────────┘
|
1283
|
+
*
|
1284
|
+
* Since new edges are always appended, the space for deleted edges
|
1285
|
+
* is not reused. Instead, the `deletes` count is incremented when an
|
1286
|
+
* edge is deleted. The next available address is calculated by
|
1287
|
+
* adding the `count` and `deletes` values to the header size.
|
1288
|
+
*
|
1289
|
+
* The only way to reclaim the space used by deleted edges is to resize the map.
|
1022
1290
|
*/
|
1023
1291
|
static HEADER_SIZE: number = 3;
|
1024
1292
|
/** The offset from the header where the delete count is stored. */
|
@@ -1042,6 +1310,10 @@ export class EdgeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
1042
1310
|
* ┌──────┬──────┬──────┬────┬─────────┬─────────┬──────────┬──────────┐
|
1043
1311
|
* │ NEXT │ TYPE │ FROM │ TO │ NEXT_IN │ PREV_IN │ NEXT_OUT │ PREV_OUT │
|
1044
1312
|
* └──────┴──────┴──────┴────┴─────────┴─────────┴──────────┴──────────┘
|
1313
|
+
*
|
1314
|
+
* The `Edge` implicitly maps an edge hash (the hash of the edge's `FROM`,
|
1315
|
+
* `TO`, and `TYPE` values) to the next and previous adjacent edges of the
|
1316
|
+
* same _edge type_.
|
1045
1317
|
*/
|
1046
1318
|
static ITEM_SIZE: number = 8;
|
1047
1319
|
/** The offset at which an edge's 'from' node id is stored. */
|
@@ -1057,27 +1329,21 @@ export class EdgeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
1057
1329
|
/** The offset at which the 'from' node's previous outgoing edge is stored. */
|
1058
1330
|
static #PREV_OUT = 7;
|
1059
1331
|
|
1060
|
-
/** The
|
1061
|
-
static MIN_CAPACITY: number = 2;
|
1062
|
-
/** The largest possible edge map capacity. */
|
1063
|
-
static MAX_CAPACITY: number = Math.floor(
|
1064
|
-
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length#what_went_wrong
|
1065
|
-
(2 ** 31 - 1 - EdgeTypeMap.HEADER_SIZE) /
|
1066
|
-
EdgeTypeMap.ITEM_SIZE /
|
1067
|
-
EdgeTypeMap.BUCKET_SIZE,
|
1068
|
-
);
|
1069
|
-
/** The size after which to grow the capacity by the minimum factor. */
|
1070
|
-
static PEAK_CAPACITY: number = 2 ** 18;
|
1071
|
-
|
1332
|
+
/** The number of deleted edges currently occupying space in the map. */
|
1072
1333
|
get deletes(): number {
|
1073
1334
|
return this.data[EdgeTypeMap.#DELETES];
|
1074
1335
|
}
|
1075
1336
|
|
1337
|
+
/** Get the next available address in the map. */
|
1076
1338
|
getNextAddress(): EdgeAddress {
|
1077
1339
|
let {ITEM_SIZE} = this.constructor;
|
1078
1340
|
return this.addressableLimit + (this.count + this.deletes) * ITEM_SIZE;
|
1079
1341
|
}
|
1080
1342
|
|
1343
|
+
/**
|
1344
|
+
* Add an edge of the given `type` between the `to` and `from` nodes
|
1345
|
+
* and link the address to the `hash` bucket.
|
1346
|
+
*/
|
1081
1347
|
add(hash: EdgeHash, from: NodeId, to: NodeId, type: TEdgeType): EdgeAddress {
|
1082
1348
|
assert(
|
1083
1349
|
hash >= 0 && hash < this.capacity,
|
@@ -1092,12 +1358,20 @@ export class EdgeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
1092
1358
|
return edge;
|
1093
1359
|
}
|
1094
1360
|
|
1361
|
+
/**
|
1362
|
+
* Remove the `to` and `from` nodes for the given `edge` address
|
1363
|
+
* and increment the `deletes` counter.
|
1364
|
+
*/
|
1095
1365
|
delete(edge: EdgeAddress): void {
|
1096
1366
|
this.data[edge + EdgeTypeMap.#FROM] = 0;
|
1097
1367
|
this.data[edge + EdgeTypeMap.#TO] = 0;
|
1098
1368
|
this.data[EdgeTypeMap.#DELETES]++;
|
1099
1369
|
}
|
1100
1370
|
|
1371
|
+
/**
|
1372
|
+
* Get the address of the edge with the given `hash`, `from` and `to` nodes,
|
1373
|
+
* and edge `type`.
|
1374
|
+
*/
|
1101
1375
|
addressOf(
|
1102
1376
|
hash: EdgeHash,
|
1103
1377
|
from: NodeId,
|
@@ -1118,27 +1392,44 @@ export class EdgeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
1118
1392
|
return null;
|
1119
1393
|
}
|
1120
1394
|
|
1395
|
+
/** Get the id of the 'from' node for the given `edge` address. */
|
1121
1396
|
from(edge: EdgeAddress): NodeId {
|
1122
1397
|
return toNodeId(this.data[edge + EdgeTypeMap.#FROM]);
|
1123
1398
|
}
|
1124
1399
|
|
1400
|
+
/** Get the id of the 'to' node for the given `edge` address. */
|
1125
1401
|
to(edge: EdgeAddress): NodeId {
|
1126
1402
|
return toNodeId(this.data[edge + EdgeTypeMap.#TO]);
|
1127
1403
|
}
|
1128
1404
|
|
1405
|
+
/**
|
1406
|
+
* Get the address of the next edge _of the same type_
|
1407
|
+
* incoming _to the same node_ as the edge at the given address.
|
1408
|
+
*/
|
1129
1409
|
nextIn(edge: EdgeAddress): EdgeAddress | null {
|
1130
1410
|
return this.data[edge + EdgeTypeMap.#NEXT_IN] || null;
|
1131
1411
|
}
|
1132
1412
|
|
1413
|
+
/**
|
1414
|
+
* Get the address of the previous edge _of the same type_
|
1415
|
+
* incoming _to the same node_ as the edge at the given address.
|
1416
|
+
*/
|
1133
1417
|
prevIn(edge: EdgeAddress): EdgeAddress | null {
|
1134
1418
|
return this.data[edge + EdgeTypeMap.#PREV_IN] || null;
|
1135
1419
|
}
|
1136
1420
|
|
1421
|
+
/** Link two adjacent edges of the same type incoming to the same node. */
|
1137
1422
|
linkIn(edge: EdgeAddress, next: EdgeAddress) {
|
1423
|
+
assert(this.typeOf(edge) === this.typeOf(next), 'Edge types must match.');
|
1424
|
+
assert(this.to(edge) === this.to(next), 'To nodes must match.');
|
1138
1425
|
this.data[edge + EdgeTypeMap.#NEXT_IN] = next;
|
1139
1426
|
this.data[next + EdgeTypeMap.#PREV_IN] = edge;
|
1140
1427
|
}
|
1141
1428
|
|
1429
|
+
/**
|
1430
|
+
* Unlink an edge from the doubly linked list of incoming edges
|
1431
|
+
* to the same node.
|
1432
|
+
*/
|
1142
1433
|
unlinkIn(edge: EdgeAddress) {
|
1143
1434
|
let next = this.nextIn(edge);
|
1144
1435
|
let prev = this.prevIn(edge);
|
@@ -1154,19 +1445,34 @@ export class EdgeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
1154
1445
|
}
|
1155
1446
|
}
|
1156
1447
|
|
1448
|
+
/**
|
1449
|
+
* Get the address of the next edge _of the same type_
|
1450
|
+
* outgoing _from the same node_ as the edge at the given address.
|
1451
|
+
*/
|
1157
1452
|
nextOut(edge: EdgeAddress): EdgeAddress | null {
|
1158
1453
|
return this.data[edge + EdgeTypeMap.#NEXT_OUT] || null;
|
1159
1454
|
}
|
1160
1455
|
|
1456
|
+
/**
|
1457
|
+
* Get the address of the previous edge _of the same type_
|
1458
|
+
* outgoing _from the same node_ as the edge at the given address.
|
1459
|
+
*/
|
1161
1460
|
prevOut(edge: EdgeAddress): EdgeAddress | null {
|
1162
1461
|
return this.data[edge + EdgeTypeMap.#PREV_OUT] || null;
|
1163
1462
|
}
|
1164
1463
|
|
1464
|
+
/** Link two adjacent edges of the same type outgoing from the same node. */
|
1165
1465
|
linkOut(edge: EdgeAddress, next: EdgeAddress) {
|
1466
|
+
assert(this.typeOf(edge) === this.typeOf(next), 'Edge types must match.');
|
1467
|
+
assert(this.from(edge) === this.from(next), 'From nodes must match.');
|
1166
1468
|
this.data[edge + EdgeTypeMap.#NEXT_OUT] = next;
|
1167
1469
|
this.data[next + EdgeTypeMap.#PREV_OUT] = edge;
|
1168
1470
|
}
|
1169
1471
|
|
1472
|
+
/**
|
1473
|
+
* Unlink an edge from the doubly linked list of outgoing edges
|
1474
|
+
* of the same type from the same node.
|
1475
|
+
*/
|
1170
1476
|
unlinkOut(edge: EdgeAddress) {
|
1171
1477
|
let next = this.nextOut(edge);
|
1172
1478
|
let prev = this.prevOut(edge);
|
@@ -1198,6 +1504,77 @@ export class EdgeTypeMap<TEdgeType> extends SharedTypeMap<
|
|
1198
1504
|
}
|
1199
1505
|
}
|
1200
1506
|
|
1507
|
+
/**
|
1508
|
+
* Links a node to another node with an edge of the given type.
|
1509
|
+
*
|
1510
|
+
* Returns one of the following numeric status codes:
|
1511
|
+
* - `0` EdgeAdded: the edge was added
|
1512
|
+
* - `1` EdgeExists: the edge already exists
|
1513
|
+
* - `2` EdgesOverloaded: the edge map is overloaded
|
1514
|
+
* - `3` TooManyDeletes: the edge map has too many deleted edges
|
1515
|
+
* - `4` NodesOverloaded: the node map is overloaded
|
1516
|
+
*/
|
1517
|
+
function link<TEdgeType: number>(
|
1518
|
+
from: NodeId,
|
1519
|
+
to: NodeId,
|
1520
|
+
type: TEdgeType | NullEdgeType,
|
1521
|
+
edges: EdgeTypeMap<TEdgeType | NullEdgeType>,
|
1522
|
+
nodes: NodeTypeMap<TEdgeType | NullEdgeType>,
|
1523
|
+
unloadFactor: number = DEFAULT_PARAMS.unloadFactor,
|
1524
|
+
): $Values<typeof LinkResult> {
|
1525
|
+
let hash = edges.hash(from, to, type);
|
1526
|
+
let edge = edges.addressOf(hash, from, to, type);
|
1527
|
+
|
1528
|
+
// The edge is already in the graph; do nothing.
|
1529
|
+
if (edge !== null) return LinkResult.EdgeExists;
|
1530
|
+
|
1531
|
+
let toNode = nodes.addressOf(to, type);
|
1532
|
+
let fromNode = nodes.addressOf(from, type);
|
1533
|
+
|
1534
|
+
let nodeCount = nodes.count;
|
1535
|
+
// add one for each node we must add.
|
1536
|
+
if (toNode === null) nodeCount++;
|
1537
|
+
if (fromNode === null) nodeCount++;
|
1538
|
+
// If we're in danger of overflowing the `nodes` array, resize it.
|
1539
|
+
if (nodes.getLoad(nodeCount) >= 1) {
|
1540
|
+
return LinkResult.NodesOverloaded;
|
1541
|
+
}
|
1542
|
+
|
1543
|
+
// We add 1 to account for the edge we are adding.
|
1544
|
+
let count = edges.count + 1;
|
1545
|
+
// Since the space occupied by deleted edges isn't reclaimed,
|
1546
|
+
// we include them in our count to avoid overflowing the `edges` array.
|
1547
|
+
let deletes = edges.deletes;
|
1548
|
+
let total = count + deletes;
|
1549
|
+
if (edges.getLoad(total) >= 1) {
|
1550
|
+
if (
|
1551
|
+
edges.getLoad(deletes) >= unloadFactor &&
|
1552
|
+
edges.getLoad(count) < unloadFactor
|
1553
|
+
) {
|
1554
|
+
// If we have a significant number of deletes, reclaim the space.
|
1555
|
+
return LinkResult.TooManyDeletes;
|
1556
|
+
} else {
|
1557
|
+
return LinkResult.EdgesOverloaded;
|
1558
|
+
}
|
1559
|
+
}
|
1560
|
+
|
1561
|
+
if (toNode === null) toNode = nodes.add(to, type);
|
1562
|
+
if (fromNode === null) fromNode = nodes.add(from, type);
|
1563
|
+
|
1564
|
+
// Add our new edge to its hash bucket.
|
1565
|
+
edge = edges.add(hash, from, to, type);
|
1566
|
+
|
1567
|
+
// Link this edge to the node's list of incoming edges.
|
1568
|
+
let prevIn = nodes.linkIn(toNode, edge);
|
1569
|
+
if (prevIn !== null) edges.linkIn(prevIn, edge);
|
1570
|
+
|
1571
|
+
// Link this edge to the node's list of outgoing edges.
|
1572
|
+
let prevOut = nodes.linkOut(fromNode, edge);
|
1573
|
+
if (prevOut !== null) edges.linkOut(prevOut, edge);
|
1574
|
+
|
1575
|
+
return LinkResult.EdgeAdded;
|
1576
|
+
}
|
1577
|
+
|
1201
1578
|
// From https://gist.github.com/badboy/6267743#32-bit-mix-functions
|
1202
1579
|
function hash32shift(key: number): number {
|
1203
1580
|
key = ~key + (key << 15); // key = (key << 15) - key - 1;
|
@@ -1213,33 +1590,66 @@ function interpolate(x: number, y: number, t: number): number {
|
|
1213
1590
|
return x + (y - x) * Math.min(1, Math.max(0, t));
|
1214
1591
|
}
|
1215
1592
|
|
1216
|
-
function increaseNodeCapacity(
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1593
|
+
function increaseNodeCapacity(
|
1594
|
+
currentCapacity: number,
|
1595
|
+
params: AdjacencyListParams,
|
1596
|
+
): number {
|
1597
|
+
let newCapacity = Math.max(
|
1598
|
+
// Make sure we have room for at least 2 more nodes.
|
1599
|
+
currentCapacity + 2,
|
1600
|
+
Math.ceil(currentCapacity * params.minGrowFactor),
|
1601
|
+
);
|
1602
|
+
|
1603
|
+
if (newCapacity >= NodeTypeMap.MAX_CAPACITY) {
|
1604
|
+
if (currentCapacity > NodeTypeMap.MAX_CAPACITY - 2) {
|
1605
|
+
throw new Error('Node capacity overflow!');
|
1606
|
+
}
|
1607
|
+
|
1608
|
+
return NodeTypeMap.MAX_CAPACITY;
|
1609
|
+
}
|
1610
|
+
|
1611
|
+
return newCapacity;
|
1221
1612
|
}
|
1222
1613
|
|
1223
|
-
function
|
1224
|
-
|
1225
|
-
|
1226
|
-
load: number,
|
1614
|
+
function increaseEdgeCapacity(
|
1615
|
+
currentCapacity: number,
|
1616
|
+
params: AdjacencyListParams,
|
1227
1617
|
): number {
|
1228
|
-
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
1232
|
-
|
1233
|
-
|
1234
|
-
|
1235
|
-
|
1236
|
-
|
1237
|
-
|
1238
|
-
|
1239
|
-
|
1240
|
-
|
1241
|
-
|
1242
|
-
|
1243
|
-
|
1244
|
-
|
1618
|
+
// This is intended to strike a balance between growing the edge capacity
|
1619
|
+
// in too small increments, which causes a lot of resizing, and growing
|
1620
|
+
// the edge capacity in too large increments, which results in a lot of
|
1621
|
+
// wasted memory.
|
1622
|
+
let pct = currentCapacity / params.peakCapacity;
|
1623
|
+
let growFactor = interpolate(params.maxGrowFactor, params.minGrowFactor, pct);
|
1624
|
+
|
1625
|
+
let newCapacity = Math.max(
|
1626
|
+
// Make sure we have room for at least one more edge.
|
1627
|
+
currentCapacity + 1,
|
1628
|
+
Math.ceil(currentCapacity * growFactor),
|
1629
|
+
);
|
1630
|
+
|
1631
|
+
if (newCapacity >= EdgeTypeMap.MAX_CAPACITY) {
|
1632
|
+
if (currentCapacity > EdgeTypeMap.MAX_CAPACITY - 1) {
|
1633
|
+
throw new Error('Edge capacity overflow!');
|
1634
|
+
}
|
1635
|
+
|
1636
|
+
return EdgeTypeMap.MAX_CAPACITY;
|
1637
|
+
}
|
1638
|
+
|
1639
|
+
return newCapacity;
|
1640
|
+
}
|
1641
|
+
|
1642
|
+
function decreaseEdgeCapacity(
|
1643
|
+
currentCapacity: number,
|
1644
|
+
params: AdjacencyListParams,
|
1645
|
+
): number {
|
1646
|
+
return Math.max(
|
1647
|
+
// Make sure we don't shrink the capacity _below_ 2.
|
1648
|
+
2,
|
1649
|
+
Math.min(
|
1650
|
+
// Make sure we shrink the capacity by at least 1.
|
1651
|
+
currentCapacity - 1,
|
1652
|
+
Math.ceil(currentCapacity * params.shrinkFactor),
|
1653
|
+
),
|
1654
|
+
);
|
1245
1655
|
}
|