@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/lib/AdjacencyList.js
CHANGED
@@ -26,20 +26,77 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
|
|
26
26
|
/** The address of the edge in the edges map. */
|
27
27
|
// eslint-disable-next-line no-unused-vars
|
28
28
|
// eslint-disable-next-line no-unused-vars
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
29
|
+
const DEFAULT_PARAMS = {
|
30
|
+
initialCapacity: 2,
|
31
|
+
unloadFactor: 0.3,
|
32
|
+
maxGrowFactor: 8,
|
33
|
+
minGrowFactor: 2,
|
34
|
+
peakCapacity: 2 ** 18,
|
35
|
+
shrinkFactor: 0.5
|
36
|
+
};
|
37
|
+
|
38
|
+
/**
|
39
|
+
* An Enum representing the result of a call to `link`.
|
40
|
+
*
|
41
|
+
* `EdgeAdded` = `0`: the edge was successfully linked
|
42
|
+
* `EdgeExists` = `1`: the edge already exists
|
43
|
+
* `EdgesOverloaded` = `2`: the edge map is overloaded
|
44
|
+
* `TooManyDeletes` = `3`: the edge map has too many deleted edges
|
45
|
+
* `NodesOverloaded` = `4`: the node map is overloaded
|
46
|
+
*/
|
47
|
+
const LinkResult = {
|
48
|
+
EdgeAdded: 0,
|
49
|
+
EdgeExists: 1,
|
50
|
+
EdgesOverloaded: 2,
|
51
|
+
TooManyDeletes: 3,
|
52
|
+
NodesOverloaded: 4
|
53
|
+
};
|
54
|
+
|
55
|
+
/**
|
56
|
+
* Allow 3 attempts to link an edge before erroring.
|
57
|
+
*
|
58
|
+
* The three attempts correspond to the three possible inconclusive link results:
|
59
|
+
* - `LinkResult.EdgesOverloaded`
|
60
|
+
* - `LinkResult.TooManyDeletes`
|
61
|
+
* - `LinkResult.NodesOverloaded`
|
62
|
+
*
|
63
|
+
* If after 3 tries, the link result is still one of these,
|
64
|
+
* this is considered an error.
|
65
|
+
*/
|
66
|
+
const MAX_LINK_TRIES = 3;
|
67
|
+
|
68
|
+
/**
|
69
|
+
* `AdjacencyList` maps nodes to lists of their adjacent nodes.
|
70
|
+
*
|
71
|
+
* It is implemented as a hashmap of nodes, where each node has
|
72
|
+
* doubly linked lists of edges of each unique edge type.
|
73
|
+
* The edges are stored in a separate hashmap, where each edge has
|
74
|
+
* a pointer to the originating node, the terminating node, and
|
75
|
+
* the next and previous edges to and from adjacent nodes.
|
76
|
+
*
|
77
|
+
* The hash maps are each stored in a `Uint32Array` backed
|
78
|
+
* by a `SharedArrayBuffer`. See `SharedTypeMap` for more details.
|
79
|
+
*
|
80
|
+
* It's primary interface is through the `getNodeIdsConnectedFrom`
|
81
|
+
* and `getNodeIdsConnectedTo` methods, which return the list of
|
82
|
+
* nodes connected from or to a given node, respectively.
|
83
|
+
*
|
84
|
+
* It is also possible to get the lists of edges connected from or to
|
85
|
+
* a given node, using the `getOutboundEdgesByType` and
|
86
|
+
* `getInboundEdgesByType` methods.
|
87
|
+
*
|
88
|
+
*/
|
39
89
|
class AdjacencyList {
|
40
90
|
#nodes /*: NodeTypeMap<TEdgeType | NullEdgeType> */;
|
41
91
|
#edges /*: EdgeTypeMap<TEdgeType | NullEdgeType> */;
|
42
92
|
|
93
|
+
#params /*: AdjacencyListParams */;
|
94
|
+
|
95
|
+
/**
|
96
|
+
* Create a new `AdjacencyList` in one of two ways:
|
97
|
+
* - with specified options, or
|
98
|
+
* - with data serialized from a previous `AdjacencyList`.
|
99
|
+
*/
|
43
100
|
constructor(opts) {
|
44
101
|
let nodes;
|
45
102
|
let edges;
|
@@ -50,27 +107,40 @@ class AdjacencyList {
|
|
50
107
|
} = opts);
|
51
108
|
this.#nodes = new NodeTypeMap(nodes);
|
52
109
|
this.#edges = new EdgeTypeMap(edges);
|
110
|
+
this.#params = {
|
111
|
+
...DEFAULT_PARAMS,
|
112
|
+
initialCapacity: this.#edges.capacity
|
113
|
+
};
|
53
114
|
} else {
|
115
|
+
this.#params = {
|
116
|
+
...DEFAULT_PARAMS,
|
117
|
+
...opts
|
118
|
+
};
|
54
119
|
let {
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
120
|
+
initialCapacity
|
121
|
+
} = this.#params;
|
122
|
+
|
123
|
+
// TODO: Find a heuristic for right-sizing nodes.
|
124
|
+
// e.g., given an average ratio of `e` edges for every `n` nodes,
|
125
|
+
// init nodes with `capacity * n / e`.
|
126
|
+
let initialNodeCapacity = 2;
|
127
|
+
NodeTypeMap.assertMaxCapacity(initialNodeCapacity);
|
128
|
+
EdgeTypeMap.assertMaxCapacity(initialCapacity);
|
129
|
+
this.#nodes = new NodeTypeMap(initialNodeCapacity);
|
130
|
+
this.#edges = new EdgeTypeMap(initialCapacity);
|
62
131
|
}
|
63
132
|
}
|
64
133
|
|
65
134
|
/**
|
66
|
-
* Create a new `AdjacencyList`
|
135
|
+
* Create a new `AdjacencyList` with data serialized
|
136
|
+
* from another `AdjacencyList`.
|
67
137
|
*/
|
68
138
|
static deserialize(opts) {
|
69
139
|
return new AdjacencyList(opts);
|
70
140
|
}
|
71
141
|
|
72
142
|
/**
|
73
|
-
* Returns a serializable object of the nodes and edges in the
|
143
|
+
* Returns a serializable object of the nodes and edges in the AdjacencyList.
|
74
144
|
*/
|
75
145
|
serialize() {
|
76
146
|
return {
|
@@ -78,7 +148,10 @@ class AdjacencyList {
|
|
78
148
|
edges: this.#edges.data
|
79
149
|
};
|
80
150
|
}
|
151
|
+
|
152
|
+
/** Statistics about the current state of the `AdjacencyList`. */
|
81
153
|
get stats() {
|
154
|
+
let edgeTypes = new Set();
|
82
155
|
let buckets = new Map();
|
83
156
|
for (let {
|
84
157
|
from,
|
@@ -91,40 +164,46 @@ class AdjacencyList {
|
|
91
164
|
(0, _assert().default)(!bucket.has(key), `Duplicate node detected: ${key}`);
|
92
165
|
bucket.add(key);
|
93
166
|
buckets.set(hash, bucket);
|
167
|
+
edgeTypes.add(type);
|
94
168
|
}
|
95
169
|
let maxCollisions = 0;
|
96
170
|
let collisions = 0;
|
97
171
|
let distribution = 0;
|
172
|
+
/**
|
173
|
+
* The expected distribution of hashes across available hash buckets.
|
174
|
+
*
|
175
|
+
* See: https://en.wikipedia.org/wiki/Hash_function#Testing_and_measurement
|
176
|
+
*/
|
177
|
+
let uniformDistribution = this.#edges.count / (2 * this.#edges.capacity) * (this.#edges.count + 2 * this.#edges.capacity - 1);
|
98
178
|
for (let bucket of buckets.values()) {
|
99
179
|
maxCollisions = Math.max(maxCollisions, bucket.size - 1);
|
100
180
|
collisions += bucket.size - 1;
|
101
181
|
distribution += bucket.size * (bucket.size + 1) / 2;
|
102
182
|
}
|
103
|
-
let uniformity = distribution / (this.#edges.count / (2 * this.#edges.capacity) * (this.#edges.count + 2 * this.#edges.capacity - 1));
|
104
183
|
return {
|
184
|
+
capacity: this.#edges.capacity,
|
105
185
|
nodes: (0, _types.fromNodeId)(this.#nodes.nextId),
|
106
186
|
nodeEdgeTypes: this.#nodes.count,
|
107
|
-
nodeCapacity: this.#nodes.capacity,
|
108
187
|
nodeLoad: `${Math.round(this.#nodes.load * 100)}%`,
|
109
188
|
nodeBufferSize: this.#nodes.bufferSize,
|
110
189
|
edges: this.#edges.count,
|
111
190
|
deleted: this.#edges.deletes,
|
112
|
-
|
191
|
+
edgeTypes: edgeTypes.size,
|
113
192
|
edgeLoad: `${Math.round(this.#edges.load * 100)}%`,
|
114
193
|
edgeLoadWithDeletes: `${Math.round(this.#edges.getLoad(this.#edges.count + this.#edges.deletes) * 100)}%`,
|
115
194
|
edgeBufferSize: this.#edges.bufferSize,
|
116
195
|
collisions,
|
117
196
|
maxCollisions,
|
118
|
-
avgCollisions: Math.round(collisions /
|
119
|
-
uniformity: Math.round(
|
197
|
+
avgCollisions: Math.round(collisions / this.#edges.count * 100) / 100 || 0,
|
198
|
+
uniformity: Math.round(distribution / uniformDistribution * 100) / 100 || 0
|
120
199
|
};
|
121
200
|
}
|
122
201
|
|
123
202
|
/**
|
124
203
|
* Resize the internal nodes array.
|
125
204
|
*
|
126
|
-
* This is used in `addNode`
|
127
|
-
* the
|
205
|
+
* This is used in `addNode` and in `addEdge` when
|
206
|
+
* the `nodes` array is at capacity,
|
128
207
|
*/
|
129
208
|
resizeNodes(size) {
|
130
209
|
let nodes = this.#nodes;
|
@@ -137,38 +216,39 @@ class AdjacencyList {
|
|
137
216
|
/**
|
138
217
|
* Resize the internal edges array.
|
139
218
|
*
|
140
|
-
* This is used in `addEdge` when the `
|
141
|
-
* the allocated size of the `edges` array.
|
219
|
+
* This is used in `addEdge` when the `edges` array is at capacity.
|
142
220
|
*/
|
143
221
|
resizeEdges(size) {
|
144
222
|
// Allocate the required space for new `nodes` and `edges` maps.
|
145
|
-
let
|
146
|
-
|
147
|
-
edgeCapacity: size
|
148
|
-
});
|
223
|
+
let edges = new EdgeTypeMap(size);
|
224
|
+
let nodes = new NodeTypeMap(this.#nodes.capacity);
|
149
225
|
|
150
226
|
// Copy the existing edges into the new array.
|
151
|
-
|
152
|
-
this.#edges.forEach(edge => void
|
227
|
+
nodes.nextId = this.#nodes.nextId;
|
228
|
+
this.#edges.forEach(edge => void link(this.#edges.from(edge), this.#edges.to(edge), this.#edges.typeOf(edge), edges, nodes, this.#params.unloadFactor));
|
153
229
|
|
154
230
|
// We expect to preserve the same number of edges.
|
155
|
-
(0, _assert().default)(this.#edges.count ===
|
231
|
+
(0, _assert().default)(this.#edges.count === edges.count, `Edge mismatch! ${this.#edges.count} does not match ${edges.count}.`);
|
156
232
|
|
157
233
|
// Finally, copy the new data arrays over to this graph.
|
158
|
-
this.#nodes =
|
159
|
-
this.#edges =
|
234
|
+
this.#nodes = nodes;
|
235
|
+
this.#edges = edges;
|
160
236
|
}
|
161
237
|
|
162
238
|
/**
|
163
239
|
* Adds a node to the graph.
|
164
240
|
*
|
241
|
+
* Note that this method does not increment the node count
|
242
|
+
* (that only happens in `addEdge`), it _may_ preemptively resize
|
243
|
+
* the nodes array if it is at capacity, under the asumption that
|
244
|
+
* at least 1 edge to or from this new node will be added.
|
245
|
+
*
|
165
246
|
* Returns the id of the added node.
|
166
247
|
*/
|
167
248
|
addNode() {
|
168
249
|
let id = this.#nodes.getId();
|
169
|
-
|
170
|
-
|
171
|
-
this.resizeNodes(increaseNodeCapacity(this.#nodes.capacity));
|
250
|
+
if (this.#nodes.getLoad() >= 1) {
|
251
|
+
this.resizeNodes(increaseNodeCapacity(this.#nodes.capacity, this.#params));
|
172
252
|
}
|
173
253
|
return id;
|
174
254
|
}
|
@@ -176,64 +256,54 @@ class AdjacencyList {
|
|
176
256
|
/**
|
177
257
|
* Adds an edge to the graph.
|
178
258
|
*
|
259
|
+
* This method will increment the edge count, and it _may_
|
260
|
+
* also increment the node count, if the originating or
|
261
|
+
* terminating node does not yet have any edges of the given type.
|
262
|
+
*
|
263
|
+
* If either the `nodes` or `edges` arrays are at capacity,
|
264
|
+
* this method will resize them before adding.
|
265
|
+
*
|
266
|
+
* Furthermore, if the `edges` array has a high number of
|
267
|
+
* deleted edges, it may reclaim the space before adding.
|
268
|
+
*
|
179
269
|
* Returns `true` if the edge was added,
|
180
270
|
* or `false` if the edge already exists.
|
181
271
|
*/
|
182
272
|
addEdge(from, to, type = 1) {
|
273
|
+
(0, _assert().default)(from < this.#nodes.nextId, `Node ${from} does not exist.`);
|
274
|
+
(0, _assert().default)(to < this.#nodes.nextId, `Node ${to} does not exist.`);
|
183
275
|
(0, _assert().default)(type > 0, `Unsupported edge type ${type}`);
|
184
|
-
let
|
185
|
-
let
|
276
|
+
let result;
|
277
|
+
let tries = 0;
|
278
|
+
do {
|
279
|
+
(0, _assert().default)(tries++ < MAX_LINK_TRIES, 'Failed to addEdge too many times!');
|
280
|
+
result = link(from, to, type, this.#edges, this.#nodes, this.#params.unloadFactor);
|
186
281
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
this.resizeEdges(getNextEdgeCapacity(capacity, count, this.#edges.getLoad(count)));
|
205
|
-
} else {
|
206
|
-
this.resizeEdges(getNextEdgeCapacity(capacity, total, this.#edges.getLoad(total)));
|
207
|
-
}
|
208
|
-
// We must rehash because the capacity has changed.
|
209
|
-
hash = this.#edges.hash(from, to, type);
|
210
|
-
}
|
211
|
-
let toNode = this.#nodes.addressOf(to, type);
|
212
|
-
let fromNode = this.#nodes.addressOf(from, type);
|
213
|
-
if (toNode === null || fromNode === null) {
|
214
|
-
// If we're in danger of overflowing the `nodes` array, resize it.
|
215
|
-
if (this.#nodes.load >= LOAD_FACTOR) {
|
216
|
-
this.resizeNodes(increaseNodeCapacity(this.#nodes.capacity));
|
217
|
-
// We need to update our indices since the `nodes` array has changed.
|
218
|
-
toNode = this.#nodes.addressOf(to, type);
|
219
|
-
fromNode = this.#nodes.addressOf(from, type);
|
282
|
+
// Sometimes we need to resize before we can add.
|
283
|
+
switch (result) {
|
284
|
+
case LinkResult.NodesOverloaded:
|
285
|
+
{
|
286
|
+
this.resizeNodes(increaseNodeCapacity(this.#nodes.capacity, this.#params));
|
287
|
+
break;
|
288
|
+
}
|
289
|
+
case LinkResult.EdgesOverloaded:
|
290
|
+
{
|
291
|
+
this.resizeEdges(increaseEdgeCapacity(this.#edges.capacity, this.#params));
|
292
|
+
break;
|
293
|
+
}
|
294
|
+
case LinkResult.TooManyDeletes:
|
295
|
+
{
|
296
|
+
this.resizeEdges(decreaseEdgeCapacity(this.#edges.capacity, this.#params));
|
297
|
+
break;
|
298
|
+
}
|
220
299
|
}
|
221
|
-
}
|
222
|
-
|
223
|
-
if (fromNode === null) fromNode = this.#nodes.add(from, type);
|
224
|
-
|
225
|
-
// Add our new edge to its hash bucket.
|
226
|
-
edge = this.#edges.add(hash, from, to, type);
|
227
|
-
|
228
|
-
// Link this edge to the node's list of incoming edges.
|
229
|
-
let prevIn = this.#nodes.linkIn(toNode, edge);
|
230
|
-
if (prevIn !== null) this.#edges.linkIn(prevIn, edge);
|
231
|
-
|
232
|
-
// Link this edge to the node's list of outgoing edges.
|
233
|
-
let prevOut = this.#nodes.linkOut(fromNode, edge);
|
234
|
-
if (prevOut !== null) this.#edges.linkOut(prevOut, edge);
|
235
|
-
return true;
|
300
|
+
} while (result > LinkResult.EdgeExists);
|
301
|
+
return result === LinkResult.EdgeAdded;
|
236
302
|
}
|
303
|
+
|
304
|
+
/**
|
305
|
+
* Iterate over all edges in insertion order.
|
306
|
+
*/
|
237
307
|
*getAllEdges() {
|
238
308
|
for (let edge of this.#edges) {
|
239
309
|
yield {
|
@@ -259,7 +329,12 @@ class AdjacencyList {
|
|
259
329
|
}
|
260
330
|
|
261
331
|
/**
|
332
|
+
* Remove an edge connecting the `from` and `to` nodes.
|
333
|
+
*
|
334
|
+
* Note that space for the deleted edge is not reclaimed
|
335
|
+
* until the `edges` array is resized.
|
262
336
|
*
|
337
|
+
* This method will increment the edge delete count.
|
263
338
|
*/
|
264
339
|
removeEdge(from, to, type = 1) {
|
265
340
|
let hash = this.#edges.hash(from, to, type);
|
@@ -285,6 +360,14 @@ class AdjacencyList {
|
|
285
360
|
// Finally, delete the edge.
|
286
361
|
this.#edges.delete(edge);
|
287
362
|
}
|
363
|
+
|
364
|
+
/**
|
365
|
+
* Check if the given node has any edges incoming from other nodes.
|
366
|
+
*
|
367
|
+
* Essentially, this is an orphan check. If a node has no incoming edges,
|
368
|
+
* it (and its entire subgraph) is completely disconnected from the
|
369
|
+
* rest of the graph.
|
370
|
+
*/
|
288
371
|
hasInboundEdges(to) {
|
289
372
|
let node = this.#nodes.head(to);
|
290
373
|
while (node !== null) {
|
@@ -293,6 +376,11 @@ class AdjacencyList {
|
|
293
376
|
}
|
294
377
|
return false;
|
295
378
|
}
|
379
|
+
|
380
|
+
/**
|
381
|
+
* Get a list of every node (labeled `from`) connecting _to_
|
382
|
+
* the given `to` node, along with the edge `type` connecting them.
|
383
|
+
*/
|
296
384
|
getInboundEdgesByType(to) {
|
297
385
|
let edges = [];
|
298
386
|
let node = this.#nodes.head(to);
|
@@ -311,6 +399,11 @@ class AdjacencyList {
|
|
311
399
|
}
|
312
400
|
return edges;
|
313
401
|
}
|
402
|
+
|
403
|
+
/**
|
404
|
+
* Get a list of every node (labeled `to`) connected _from_
|
405
|
+
* the given `from` node, along with the edge `type` connecting them.
|
406
|
+
*/
|
314
407
|
getOutboundEdgesByType(from) {
|
315
408
|
let edges = [];
|
316
409
|
let node = this.#nodes.head(from);
|
@@ -331,7 +424,11 @@ class AdjacencyList {
|
|
331
424
|
}
|
332
425
|
|
333
426
|
/**
|
334
|
-
* Get the list of
|
427
|
+
* Get the list of node ids connected from this node.
|
428
|
+
*
|
429
|
+
* If `type` is specified, only return nodes connected by edges of that type.
|
430
|
+
* If `type` is an array, return nodes connected by edges of any of those types.
|
431
|
+
* If `type` is `AllEdgeTypes` (`-1`), return nodes connected by edges of any type.
|
335
432
|
*/
|
336
433
|
getNodeIdsConnectedFrom(from, type = 1) {
|
337
434
|
let matches = node => type === _Graph.ALL_EDGE_TYPES || (Array.isArray(type) ? type.includes(this.#nodes.typeOf(node)) : type === this.#nodes.typeOf(node));
|
@@ -370,7 +467,11 @@ class AdjacencyList {
|
|
370
467
|
}
|
371
468
|
|
372
469
|
/**
|
373
|
-
* Get the list of
|
470
|
+
* Get the list of node ids connected to this node.
|
471
|
+
*
|
472
|
+
* If `type` is specified, only return nodes connected by edges of that type.
|
473
|
+
* If `type` is an array, return nodes connected by edges of any of those types.
|
474
|
+
* If `type` is `AllEdgeTypes` (`-1`), return nodes connected by edges of any type.
|
374
475
|
*/
|
375
476
|
getNodeIdsConnectedTo(to, type = 1) {
|
376
477
|
let matches = node => type === _Graph.ALL_EDGE_TYPES || (Array.isArray(type) ? type.includes(this.#nodes.typeOf(node)) : type === this.#nodes.typeOf(node));
|
@@ -420,7 +521,7 @@ class AdjacencyList {
|
|
420
521
|
* └──┴──┴──┴───────┴──┴──┴──┴───────┴──┴──┘
|
421
522
|
* └──┬──┘ └─────────┬─────────┘
|
422
523
|
* header items
|
423
|
-
* (HEADER_SIZE)
|
524
|
+
* (HEADER_SIZE) (capacity * ITEM_SIZE)
|
424
525
|
*
|
425
526
|
*
|
426
527
|
* An item is added with a hash key that fits within the range of the hash
|
@@ -478,29 +579,55 @@ class SharedTypeMap {
|
|
478
579
|
/** The offset at which an item's type is stored. */
|
479
580
|
static #TYPE = 1;
|
480
581
|
|
481
|
-
/** The
|
482
|
-
static
|
582
|
+
/** The largest possible capacity. */
|
583
|
+
static get MAX_CAPACITY() {
|
584
|
+
return Math.floor(
|
585
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length#what_went_wrong
|
586
|
+
(2 ** 31 - 1 - this.HEADER_SIZE) / this.ITEM_SIZE);
|
587
|
+
}
|
588
|
+
|
589
|
+
/** Assert that the given `capacity` does not exceed `MAX_CAPACITY`. */
|
590
|
+
static assertMaxCapacity(capacity) {
|
591
|
+
(0, _assert().default)(capacity <= this.MAX_CAPACITY, `${this.name} capacity overflow!`);
|
592
|
+
}
|
593
|
+
/** The total number of items that can fit in the map. */
|
483
594
|
get capacity() {
|
484
595
|
return this.data[SharedTypeMap.#CAPACITY];
|
485
596
|
}
|
597
|
+
|
598
|
+
/** The number of items in the map. */
|
486
599
|
get count() {
|
487
600
|
return this.data[SharedTypeMap.#COUNT];
|
488
601
|
}
|
602
|
+
|
603
|
+
/** The ratio of the count to the capacity. */
|
489
604
|
get load() {
|
490
605
|
return this.getLoad();
|
491
606
|
}
|
607
|
+
|
608
|
+
/** The total length of the map, in bytes. */
|
492
609
|
get length() {
|
493
610
|
return this.getLength();
|
494
611
|
}
|
612
|
+
|
613
|
+
/** The address of the first item in the map. */
|
495
614
|
get addressableLimit() {
|
496
615
|
return this.constructor.HEADER_SIZE + this.capacity;
|
497
616
|
}
|
617
|
+
|
618
|
+
/** The size of the map in mb, as a localized string. */
|
498
619
|
get bufferSize() {
|
499
620
|
return `${(this.data.byteLength / 1024 / 1024).toLocaleString(undefined, {
|
500
621
|
minimumFractionDigits: 2,
|
501
622
|
maximumFractionDigits: 2
|
502
623
|
})} mb`;
|
503
624
|
}
|
625
|
+
|
626
|
+
/**
|
627
|
+
* Create a new `SharedTypeMap` in one of two ways:
|
628
|
+
* - with a capacity of `capacityOrData` if it is a number,
|
629
|
+
* - or with `capacityOrData` as its data, if it is a `Uint32Array`.
|
630
|
+
*/
|
504
631
|
constructor(capacityOrData) {
|
505
632
|
if (typeof capacityOrData === 'number') {
|
506
633
|
let {
|
@@ -515,6 +642,14 @@ class SharedTypeMap {
|
|
515
642
|
(0, _assert().default)(this.getLength() === this.data.length, 'Data appears corrupt.');
|
516
643
|
}
|
517
644
|
}
|
645
|
+
|
646
|
+
/**
|
647
|
+
* Overwrite the data in this map with the given `data`.
|
648
|
+
*
|
649
|
+
* The `data` is expected to conform to the same
|
650
|
+
* partitioning and schema as the data in this map,
|
651
|
+
* and is expected to be of equal or smaller capacity to this map.
|
652
|
+
*/
|
518
653
|
set(data) {
|
519
654
|
let {
|
520
655
|
HEADER_SIZE,
|
@@ -547,19 +682,30 @@ class SharedTypeMap {
|
|
547
682
|
if (toItems[i + NEXT]) toItems[i + NEXT] += delta;
|
548
683
|
}
|
549
684
|
}
|
685
|
+
|
686
|
+
/**
|
687
|
+
* Given a `count` (defaulting to `this.count`),
|
688
|
+
* get the load on the map.
|
689
|
+
*
|
690
|
+
* The load is the ratio of the `count` the capacity of the map.
|
691
|
+
*
|
692
|
+
* If the load is `1`, it means the map is at capacity, and needs
|
693
|
+
* to be resized before adding more items.
|
694
|
+
*/
|
550
695
|
getLoad(count = this.count) {
|
551
|
-
|
552
|
-
BUCKET_SIZE
|
553
|
-
} = this.constructor;
|
554
|
-
return count / (this.capacity * BUCKET_SIZE);
|
696
|
+
return count / this.capacity;
|
555
697
|
}
|
698
|
+
|
699
|
+
/**
|
700
|
+
* Given a `capacity` (defaulting to `this.capacity`),
|
701
|
+
* get the length of the map, in bytes.
|
702
|
+
*/
|
556
703
|
getLength(capacity = this.capacity) {
|
557
704
|
let {
|
558
705
|
HEADER_SIZE,
|
559
|
-
ITEM_SIZE
|
560
|
-
BUCKET_SIZE
|
706
|
+
ITEM_SIZE
|
561
707
|
} = this.constructor;
|
562
|
-
return capacity + HEADER_SIZE + ITEM_SIZE *
|
708
|
+
return capacity + HEADER_SIZE + ITEM_SIZE * capacity;
|
563
709
|
}
|
564
710
|
|
565
711
|
/** Get the next available address in the map. */
|
@@ -584,9 +730,16 @@ class SharedTypeMap {
|
|
584
730
|
let NEXT = SharedTypeMap.#NEXT;
|
585
731
|
return this.data[item + NEXT] || null;
|
586
732
|
}
|
733
|
+
|
734
|
+
/** Get the type of the item at the given `item` address. */
|
587
735
|
typeOf(item) {
|
588
736
|
return this.data[item + SharedTypeMap.#TYPE];
|
589
737
|
}
|
738
|
+
|
739
|
+
/**
|
740
|
+
* Store an item of `type` at the `item` address and
|
741
|
+
* link the address to the `hash` bucket.
|
742
|
+
*/
|
590
743
|
link(hash, item, type) {
|
591
744
|
let COUNT = SharedTypeMap.#COUNT;
|
592
745
|
let NEXT = SharedTypeMap.#NEXT;
|
@@ -609,6 +762,10 @@ class SharedTypeMap {
|
|
609
762
|
}
|
610
763
|
this.data[COUNT]++;
|
611
764
|
}
|
765
|
+
|
766
|
+
/**
|
767
|
+
* Remove the link to the `item` address from the `hash` bucket.
|
768
|
+
*/
|
612
769
|
unlink(hash, item) {
|
613
770
|
let COUNT = SharedTypeMap.#COUNT;
|
614
771
|
let NEXT = SharedTypeMap.#NEXT;
|
@@ -673,16 +830,13 @@ class SharedTypeMap {
|
|
673
830
|
}
|
674
831
|
inspect() {
|
675
832
|
const {
|
676
|
-
HEADER_SIZE
|
677
|
-
ITEM_SIZE,
|
678
|
-
BUCKET_SIZE
|
833
|
+
HEADER_SIZE
|
679
834
|
} = this.constructor;
|
680
|
-
let min =
|
681
|
-
let max = min + this.capacity * BUCKET_SIZE * ITEM_SIZE;
|
835
|
+
let min = this.addressableLimit;
|
682
836
|
return {
|
683
837
|
header: this.data.subarray(0, HEADER_SIZE),
|
684
838
|
table: this.data.subarray(HEADER_SIZE, min),
|
685
|
-
data: this.data.subarray(min
|
839
|
+
data: this.data.subarray(min)
|
686
840
|
};
|
687
841
|
}
|
688
842
|
}
|
@@ -690,7 +844,17 @@ class SharedTypeMap {
|
|
690
844
|
/**
|
691
845
|
* Nodes are stored in a `SharedTypeMap`, keyed on node id plus an edge type.
|
692
846
|
* This means that for any given unique node id, there may be `e` nodes in the
|
693
|
-
* map, where `e` is the number of
|
847
|
+
* map, where `e` is the number of unique edge types in the graph.
|
848
|
+
*
|
849
|
+
* The _hash_ for a node is simply the node id (as issued by `getId`),
|
850
|
+
* and forms the head of linked list of unique _edge types_ connected
|
851
|
+
* to or from the same node id.
|
852
|
+
*
|
853
|
+
* In addition to a unique edge type, each Node contains the heads and tails
|
854
|
+
* of doubly linked lists of incoming and outgoing edges of the same type.
|
855
|
+
*
|
856
|
+
* Note that the links in the doubly linked lists are Edges (not Nodes),
|
857
|
+
* which are stored in a corresponding `EdgeTypeMap`.
|
694
858
|
*/
|
695
859
|
exports.SharedTypeMap = SharedTypeMap;
|
696
860
|
class NodeTypeMap extends SharedTypeMap {
|
@@ -707,6 +871,15 @@ class NodeTypeMap extends SharedTypeMap {
|
|
707
871
|
* ┌──────────┬───────┬─────────┐
|
708
872
|
* │ CAPACITY │ COUNT │ NEXT_ID │
|
709
873
|
* └──────────┴───────┴─────────┘
|
874
|
+
*
|
875
|
+
* The `nextId` is a count of the number of times `getId` has been called.
|
876
|
+
* This is distinct concept from the `count`, which tracks the number of times
|
877
|
+
* `add` has been called.
|
878
|
+
*
|
879
|
+
* The reason for this distinction is that `getId` is called once per node
|
880
|
+
* (to issue a _unique_ id) and will _always increment_ the `nextId` counter,
|
881
|
+
* whereas `add` is called once per edge, and will only increment the `count`
|
882
|
+
* if the _type_ of edge is new for the given node.
|
710
883
|
*/
|
711
884
|
static HEADER_SIZE = 3;
|
712
885
|
/** The offset from the header where the next available node id is stored. */
|
@@ -728,6 +901,9 @@ class NodeTypeMap extends SharedTypeMap {
|
|
728
901
|
* ┌──────┬──────┬──────────┬───────────┬─────────┬──────────┐
|
729
902
|
* │ NEXT │ TYPE │ FIRST_IN │ FIRST_OUT │ LAST_IN │ LAST_OUT │
|
730
903
|
* └──────┴──────┴──────────┴───────────┴─────────┴──────────┘
|
904
|
+
*
|
905
|
+
* The `Node` implicitly maps a node id (the hash the node was added with)
|
906
|
+
* to the first and last incoming and outgoing edges of the same _edge type_.
|
731
907
|
*/
|
732
908
|
static ITEM_SIZE = 6;
|
733
909
|
/** The offset at which a node's first incoming edge of this type is stored. */
|
@@ -738,13 +914,6 @@ class NodeTypeMap extends SharedTypeMap {
|
|
738
914
|
static #LAST_IN = 4;
|
739
915
|
/** The offset at which a node's last outgoing edge of this type is stored. */
|
740
916
|
static #LAST_OUT = 5;
|
741
|
-
|
742
|
-
/** The smallest functional node map capacity. */
|
743
|
-
static MIN_CAPACITY = 2;
|
744
|
-
/** The largest possible node map capacity. */
|
745
|
-
static MAX_CAPACITY = Math.floor(
|
746
|
-
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length#what_went_wrong
|
747
|
-
(2 ** 31 - 1 - NodeTypeMap.HEADER_SIZE) / NodeTypeMap.ITEM_SIZE / NodeTypeMap.BUCKET_SIZE);
|
748
917
|
get nextId() {
|
749
918
|
return (0, _types.toNodeId)(this.data[NodeTypeMap.#NEXT_ID]);
|
750
919
|
}
|
@@ -752,13 +921,28 @@ class NodeTypeMap extends SharedTypeMap {
|
|
752
921
|
this.data[NodeTypeMap.#NEXT_ID] = (0, _types.fromNodeId)(nextId);
|
753
922
|
}
|
754
923
|
|
755
|
-
/**
|
924
|
+
/**
|
925
|
+
* Get the load on the node map.
|
926
|
+
*
|
927
|
+
* The load is the greater of either:
|
928
|
+
* - the ratio of the number of node ids to the capacity of the map,
|
929
|
+
* - or the ratio of the `count` to the capacity of the map.
|
930
|
+
*
|
931
|
+
* if `count` is not provided, the default is the number of items
|
932
|
+
* currently added to the map.
|
933
|
+
*/
|
934
|
+
getLoad(count) {
|
935
|
+
return Math.max((0, _types.fromNodeId)(this.nextId) / this.capacity, super.getLoad(count));
|
936
|
+
}
|
937
|
+
|
938
|
+
/** Increment the node counter to get a unique node id. */
|
756
939
|
getId() {
|
757
940
|
return (0, _types.toNodeId)(this.data[NodeTypeMap.#NEXT_ID]++);
|
758
941
|
}
|
759
|
-
|
760
|
-
|
761
|
-
|
942
|
+
|
943
|
+
/**
|
944
|
+
* Add new lists of edges of the given `type` to and from the given `node`.
|
945
|
+
*/
|
762
946
|
add(node, type) {
|
763
947
|
let index = (0, _types.fromNodeId)(node);
|
764
948
|
(0, _assert().default)(index >= 0 && index < this.data[NodeTypeMap.#NEXT_ID], `Invalid node id ${String(node)} (${this.data[NodeTypeMap.#NEXT_ID]})`);
|
@@ -766,6 +950,11 @@ class NodeTypeMap extends SharedTypeMap {
|
|
766
950
|
this.link(node, address, type);
|
767
951
|
return address;
|
768
952
|
}
|
953
|
+
|
954
|
+
/**
|
955
|
+
* Get the address of the lists edges of the given `type`
|
956
|
+
* to and from the given `node`.
|
957
|
+
*/
|
769
958
|
addressOf(node, type) {
|
770
959
|
let address = this.head(node);
|
771
960
|
while (address !== null) {
|
@@ -776,18 +965,46 @@ class NodeTypeMap extends SharedTypeMap {
|
|
776
965
|
}
|
777
966
|
return null;
|
778
967
|
}
|
968
|
+
|
969
|
+
/**
|
970
|
+
* Given a `node` address, get the _head_ of the linked list
|
971
|
+
* of incoming edges of the same type to the same node.
|
972
|
+
*/
|
779
973
|
firstIn(node) {
|
780
974
|
return this.data[node + NodeTypeMap.#FIRST_IN] || null;
|
781
975
|
}
|
976
|
+
|
977
|
+
/**
|
978
|
+
* Given a `node` address, get the _head_ of the linked list
|
979
|
+
* of outgoing edges of the same type from the same node.
|
980
|
+
*/
|
782
981
|
firstOut(node) {
|
783
982
|
return this.data[node + NodeTypeMap.#FIRST_OUT] || null;
|
784
983
|
}
|
984
|
+
|
985
|
+
/**
|
986
|
+
* Given a `node` address, get the _tail_ of the linked list
|
987
|
+
* of incoming edges of the same type to the same node.
|
988
|
+
*/
|
785
989
|
lastIn(node) {
|
786
990
|
return this.data[node + NodeTypeMap.#LAST_IN] || null;
|
787
991
|
}
|
992
|
+
|
993
|
+
/**
|
994
|
+
* Given a `node` address, get the _tail_ of the linked list
|
995
|
+
* of outgoing edges of the same type from the same node.
|
996
|
+
*/
|
788
997
|
lastOut(node) {
|
789
998
|
return this.data[node + NodeTypeMap.#LAST_OUT] || null;
|
790
999
|
}
|
1000
|
+
|
1001
|
+
/**
|
1002
|
+
* Set `edge` as the last incoming edge to `node`.
|
1003
|
+
* If `node` has no incoming edges, set `edge`
|
1004
|
+
* as the first incoming edge, as well.
|
1005
|
+
*
|
1006
|
+
* Returns the address of the old last incoming edge, if any.
|
1007
|
+
*/
|
791
1008
|
linkIn(node, edge) {
|
792
1009
|
let first = this.firstIn(node);
|
793
1010
|
let last = this.lastIn(node);
|
@@ -795,6 +1012,14 @@ class NodeTypeMap extends SharedTypeMap {
|
|
795
1012
|
this.data[node + NodeTypeMap.#LAST_IN] = edge;
|
796
1013
|
return last;
|
797
1014
|
}
|
1015
|
+
|
1016
|
+
/**
|
1017
|
+
* If `edge` is the last incoming edge to `node`,
|
1018
|
+
* update the node's last incoming edge to `prev`.
|
1019
|
+
*
|
1020
|
+
* If `edge` is the first incoming edge to `node`,
|
1021
|
+
* update the node's first incoming edge to `next`.
|
1022
|
+
*/
|
798
1023
|
unlinkIn(node, edge, prev, next) {
|
799
1024
|
let first = this.firstIn(node);
|
800
1025
|
let last = this.lastIn(node);
|
@@ -805,6 +1030,14 @@ class NodeTypeMap extends SharedTypeMap {
|
|
805
1030
|
this.data[node + NodeTypeMap.#FIRST_IN] = next === null ? 0 : next;
|
806
1031
|
}
|
807
1032
|
}
|
1033
|
+
|
1034
|
+
/**
|
1035
|
+
* Set `edge` as the last outgoing edge from `node`.
|
1036
|
+
* If `node` has no outgoing edges, set `edge`
|
1037
|
+
* as the first outgoing edge, as well.
|
1038
|
+
*
|
1039
|
+
* Returns the address of the old last outgoing edge, if any.
|
1040
|
+
*/
|
808
1041
|
linkOut(node, edge) {
|
809
1042
|
let first = this.firstOut(node);
|
810
1043
|
let last = this.lastOut(node);
|
@@ -812,6 +1045,14 @@ class NodeTypeMap extends SharedTypeMap {
|
|
812
1045
|
this.data[node + NodeTypeMap.#LAST_OUT] = edge;
|
813
1046
|
return last;
|
814
1047
|
}
|
1048
|
+
|
1049
|
+
/**
|
1050
|
+
* If `edge` is the last outgoing edge from `node`,
|
1051
|
+
* update the node's last outgoing edge to `prev`.
|
1052
|
+
*
|
1053
|
+
* If `edge` is the first outgoing edge from `node`,
|
1054
|
+
* update the node's first outgoing edge to `next`.
|
1055
|
+
*/
|
815
1056
|
unlinkOut(node, edge, prev, next) {
|
816
1057
|
let first = this.firstOut(node);
|
817
1058
|
let last = this.lastOut(node);
|
@@ -827,6 +1068,14 @@ class NodeTypeMap extends SharedTypeMap {
|
|
827
1068
|
/**
|
828
1069
|
* Edges are stored in a `SharedTypeMap`,
|
829
1070
|
* keyed on the 'from' and 'to' node ids, and the edge type.
|
1071
|
+
*
|
1072
|
+
* The _hash_ for an edge is a hash of the edge's `from`, `to`, and `type` values,
|
1073
|
+
* and forms the head of linked list of edges with the same hash.
|
1074
|
+
*
|
1075
|
+
* In addition to the `from`, `to` and `type` values, each Edge contains
|
1076
|
+
* the next and previous links of doubly linked lists of the _adjacent_ edges
|
1077
|
+
* of the same type, both incoming to the `to` node, and outgoing from
|
1078
|
+
* the `from` node.
|
830
1079
|
*/
|
831
1080
|
exports.NodeTypeMap = NodeTypeMap;
|
832
1081
|
class EdgeTypeMap extends SharedTypeMap {
|
@@ -843,6 +1092,13 @@ class EdgeTypeMap extends SharedTypeMap {
|
|
843
1092
|
* ┌──────────┬───────┬─────────┐
|
844
1093
|
* │ CAPACITY │ COUNT │ DELETES │
|
845
1094
|
* └──────────┴───────┴─────────┘
|
1095
|
+
*
|
1096
|
+
* Since new edges are always appended, the space for deleted edges
|
1097
|
+
* is not reused. Instead, the `deletes` count is incremented when an
|
1098
|
+
* edge is deleted. The next available address is calculated by
|
1099
|
+
* adding the `count` and `deletes` values to the header size.
|
1100
|
+
*
|
1101
|
+
* The only way to reclaim the space used by deleted edges is to resize the map.
|
846
1102
|
*/
|
847
1103
|
static HEADER_SIZE = 3;
|
848
1104
|
/** The offset from the header where the delete count is stored. */
|
@@ -866,6 +1122,10 @@ class EdgeTypeMap extends SharedTypeMap {
|
|
866
1122
|
* ┌──────┬──────┬──────┬────┬─────────┬─────────┬──────────┬──────────┐
|
867
1123
|
* │ NEXT │ TYPE │ FROM │ TO │ NEXT_IN │ PREV_IN │ NEXT_OUT │ PREV_OUT │
|
868
1124
|
* └──────┴──────┴──────┴────┴─────────┴─────────┴──────────┴──────────┘
|
1125
|
+
*
|
1126
|
+
* The `Edge` implicitly maps an edge hash (the hash of the edge's `FROM`,
|
1127
|
+
* `TO`, and `TYPE` values) to the next and previous adjacent edges of the
|
1128
|
+
* same _edge type_.
|
869
1129
|
*/
|
870
1130
|
static ITEM_SIZE = 8;
|
871
1131
|
/** The offset at which an edge's 'from' node id is stored. */
|
@@ -881,23 +1141,23 @@ class EdgeTypeMap extends SharedTypeMap {
|
|
881
1141
|
/** The offset at which the 'from' node's previous outgoing edge is stored. */
|
882
1142
|
static #PREV_OUT = 7;
|
883
1143
|
|
884
|
-
/** The
|
885
|
-
static MIN_CAPACITY = 2;
|
886
|
-
/** The largest possible edge map capacity. */
|
887
|
-
static MAX_CAPACITY = Math.floor(
|
888
|
-
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length#what_went_wrong
|
889
|
-
(2 ** 31 - 1 - EdgeTypeMap.HEADER_SIZE) / EdgeTypeMap.ITEM_SIZE / EdgeTypeMap.BUCKET_SIZE);
|
890
|
-
/** The size after which to grow the capacity by the minimum factor. */
|
891
|
-
static PEAK_CAPACITY = 2 ** 18;
|
1144
|
+
/** The number of deleted edges currently occupying space in the map. */
|
892
1145
|
get deletes() {
|
893
1146
|
return this.data[EdgeTypeMap.#DELETES];
|
894
1147
|
}
|
1148
|
+
|
1149
|
+
/** Get the next available address in the map. */
|
895
1150
|
getNextAddress() {
|
896
1151
|
let {
|
897
1152
|
ITEM_SIZE
|
898
1153
|
} = this.constructor;
|
899
1154
|
return this.addressableLimit + (this.count + this.deletes) * ITEM_SIZE;
|
900
1155
|
}
|
1156
|
+
|
1157
|
+
/**
|
1158
|
+
* Add an edge of the given `type` between the `to` and `from` nodes
|
1159
|
+
* and link the address to the `hash` bucket.
|
1160
|
+
*/
|
901
1161
|
add(hash, from, to, type) {
|
902
1162
|
(0, _assert().default)(hash >= 0 && hash < this.capacity, `Invalid edge hash ${String(hash)}`);
|
903
1163
|
// Use the next available edge address.
|
@@ -908,11 +1168,21 @@ class EdgeTypeMap extends SharedTypeMap {
|
|
908
1168
|
this.data[edge + EdgeTypeMap.#TO] = (0, _types.fromNodeId)(to);
|
909
1169
|
return edge;
|
910
1170
|
}
|
1171
|
+
|
1172
|
+
/**
|
1173
|
+
* Remove the `to` and `from` nodes for the given `edge` address
|
1174
|
+
* and increment the `deletes` counter.
|
1175
|
+
*/
|
911
1176
|
delete(edge) {
|
912
1177
|
this.data[edge + EdgeTypeMap.#FROM] = 0;
|
913
1178
|
this.data[edge + EdgeTypeMap.#TO] = 0;
|
914
1179
|
this.data[EdgeTypeMap.#DELETES]++;
|
915
1180
|
}
|
1181
|
+
|
1182
|
+
/**
|
1183
|
+
* Get the address of the edge with the given `hash`, `from` and `to` nodes,
|
1184
|
+
* and edge `type`.
|
1185
|
+
*/
|
916
1186
|
addressOf(hash, from, to, type) {
|
917
1187
|
let address = this.head(hash);
|
918
1188
|
while (address !== null) {
|
@@ -923,22 +1193,45 @@ class EdgeTypeMap extends SharedTypeMap {
|
|
923
1193
|
}
|
924
1194
|
return null;
|
925
1195
|
}
|
1196
|
+
|
1197
|
+
/** Get the id of the 'from' node for the given `edge` address. */
|
926
1198
|
from(edge) {
|
927
1199
|
return (0, _types.toNodeId)(this.data[edge + EdgeTypeMap.#FROM]);
|
928
1200
|
}
|
1201
|
+
|
1202
|
+
/** Get the id of the 'to' node for the given `edge` address. */
|
929
1203
|
to(edge) {
|
930
1204
|
return (0, _types.toNodeId)(this.data[edge + EdgeTypeMap.#TO]);
|
931
1205
|
}
|
1206
|
+
|
1207
|
+
/**
|
1208
|
+
* Get the address of the next edge _of the same type_
|
1209
|
+
* incoming _to the same node_ as the edge at the given address.
|
1210
|
+
*/
|
932
1211
|
nextIn(edge) {
|
933
1212
|
return this.data[edge + EdgeTypeMap.#NEXT_IN] || null;
|
934
1213
|
}
|
1214
|
+
|
1215
|
+
/**
|
1216
|
+
* Get the address of the previous edge _of the same type_
|
1217
|
+
* incoming _to the same node_ as the edge at the given address.
|
1218
|
+
*/
|
935
1219
|
prevIn(edge) {
|
936
1220
|
return this.data[edge + EdgeTypeMap.#PREV_IN] || null;
|
937
1221
|
}
|
1222
|
+
|
1223
|
+
/** Link two adjacent edges of the same type incoming to the same node. */
|
938
1224
|
linkIn(edge, next) {
|
1225
|
+
(0, _assert().default)(this.typeOf(edge) === this.typeOf(next), 'Edge types must match.');
|
1226
|
+
(0, _assert().default)(this.to(edge) === this.to(next), 'To nodes must match.');
|
939
1227
|
this.data[edge + EdgeTypeMap.#NEXT_IN] = next;
|
940
1228
|
this.data[next + EdgeTypeMap.#PREV_IN] = edge;
|
941
1229
|
}
|
1230
|
+
|
1231
|
+
/**
|
1232
|
+
* Unlink an edge from the doubly linked list of incoming edges
|
1233
|
+
* to the same node.
|
1234
|
+
*/
|
942
1235
|
unlinkIn(edge) {
|
943
1236
|
let next = this.nextIn(edge);
|
944
1237
|
let prev = this.prevIn(edge);
|
@@ -953,16 +1246,35 @@ class EdgeTypeMap extends SharedTypeMap {
|
|
953
1246
|
this.data[prev + EdgeTypeMap.#NEXT_IN] = 0;
|
954
1247
|
}
|
955
1248
|
}
|
1249
|
+
|
1250
|
+
/**
|
1251
|
+
* Get the address of the next edge _of the same type_
|
1252
|
+
* outgoing _from the same node_ as the edge at the given address.
|
1253
|
+
*/
|
956
1254
|
nextOut(edge) {
|
957
1255
|
return this.data[edge + EdgeTypeMap.#NEXT_OUT] || null;
|
958
1256
|
}
|
1257
|
+
|
1258
|
+
/**
|
1259
|
+
* Get the address of the previous edge _of the same type_
|
1260
|
+
* outgoing _from the same node_ as the edge at the given address.
|
1261
|
+
*/
|
959
1262
|
prevOut(edge) {
|
960
1263
|
return this.data[edge + EdgeTypeMap.#PREV_OUT] || null;
|
961
1264
|
}
|
1265
|
+
|
1266
|
+
/** Link two adjacent edges of the same type outgoing from the same node. */
|
962
1267
|
linkOut(edge, next) {
|
1268
|
+
(0, _assert().default)(this.typeOf(edge) === this.typeOf(next), 'Edge types must match.');
|
1269
|
+
(0, _assert().default)(this.from(edge) === this.from(next), 'From nodes must match.');
|
963
1270
|
this.data[edge + EdgeTypeMap.#NEXT_OUT] = next;
|
964
1271
|
this.data[next + EdgeTypeMap.#PREV_OUT] = edge;
|
965
1272
|
}
|
1273
|
+
|
1274
|
+
/**
|
1275
|
+
* Unlink an edge from the doubly linked list of outgoing edges
|
1276
|
+
* of the same type from the same node.
|
1277
|
+
*/
|
966
1278
|
unlinkOut(edge) {
|
967
1279
|
let next = this.nextOut(edge);
|
968
1280
|
let prev = this.prevOut(edge);
|
@@ -994,8 +1306,64 @@ class EdgeTypeMap extends SharedTypeMap {
|
|
994
1306
|
}
|
995
1307
|
}
|
996
1308
|
|
997
|
-
|
1309
|
+
/**
|
1310
|
+
* Links a node to another node with an edge of the given type.
|
1311
|
+
*
|
1312
|
+
* Returns one of the following numeric status codes:
|
1313
|
+
* - `0` EdgeAdded: the edge was added
|
1314
|
+
* - `1` EdgeExists: the edge already exists
|
1315
|
+
* - `2` EdgesOverloaded: the edge map is overloaded
|
1316
|
+
* - `3` TooManyDeletes: the edge map has too many deleted edges
|
1317
|
+
* - `4` NodesOverloaded: the node map is overloaded
|
1318
|
+
*/
|
998
1319
|
exports.EdgeTypeMap = EdgeTypeMap;
|
1320
|
+
function link(from, to, type, edges, nodes, unloadFactor = DEFAULT_PARAMS.unloadFactor) {
|
1321
|
+
let hash = edges.hash(from, to, type);
|
1322
|
+
let edge = edges.addressOf(hash, from, to, type);
|
1323
|
+
|
1324
|
+
// The edge is already in the graph; do nothing.
|
1325
|
+
if (edge !== null) return LinkResult.EdgeExists;
|
1326
|
+
let toNode = nodes.addressOf(to, type);
|
1327
|
+
let fromNode = nodes.addressOf(from, type);
|
1328
|
+
let nodeCount = nodes.count;
|
1329
|
+
// add one for each node we must add.
|
1330
|
+
if (toNode === null) nodeCount++;
|
1331
|
+
if (fromNode === null) nodeCount++;
|
1332
|
+
// If we're in danger of overflowing the `nodes` array, resize it.
|
1333
|
+
if (nodes.getLoad(nodeCount) >= 1) {
|
1334
|
+
return LinkResult.NodesOverloaded;
|
1335
|
+
}
|
1336
|
+
|
1337
|
+
// We add 1 to account for the edge we are adding.
|
1338
|
+
let count = edges.count + 1;
|
1339
|
+
// Since the space occupied by deleted edges isn't reclaimed,
|
1340
|
+
// we include them in our count to avoid overflowing the `edges` array.
|
1341
|
+
let deletes = edges.deletes;
|
1342
|
+
if (edges.getLoad(count + deletes) >= 1) {
|
1343
|
+
if (edges.getLoad(deletes) >= unloadFactor && edges.getLoad(count) < unloadFactor) {
|
1344
|
+
// If we have a significant number of deletes, reclaim the space.
|
1345
|
+
return LinkResult.TooManyDeletes;
|
1346
|
+
} else {
|
1347
|
+
return LinkResult.EdgesOverloaded;
|
1348
|
+
}
|
1349
|
+
}
|
1350
|
+
if (toNode === null) toNode = nodes.add(to, type);
|
1351
|
+
if (fromNode === null) fromNode = nodes.add(from, type);
|
1352
|
+
|
1353
|
+
// Add our new edge to its hash bucket.
|
1354
|
+
edge = edges.add(hash, from, to, type);
|
1355
|
+
|
1356
|
+
// Link this edge to the node's list of incoming edges.
|
1357
|
+
let prevIn = nodes.linkIn(toNode, edge);
|
1358
|
+
if (prevIn !== null) edges.linkIn(prevIn, edge);
|
1359
|
+
|
1360
|
+
// Link this edge to the node's list of outgoing edges.
|
1361
|
+
let prevOut = nodes.linkOut(fromNode, edge);
|
1362
|
+
if (prevOut !== null) edges.linkOut(prevOut, edge);
|
1363
|
+
return LinkResult.EdgeAdded;
|
1364
|
+
}
|
1365
|
+
|
1366
|
+
// From https://gist.github.com/badboy/6267743#32-bit-mix-functions
|
999
1367
|
function hash32shift(key) {
|
1000
1368
|
key = ~key + (key << 15); // key = (key << 15) - key - 1;
|
1001
1369
|
key = key ^ key >> 12;
|
@@ -1008,35 +1376,40 @@ function hash32shift(key) {
|
|
1008
1376
|
function interpolate(x, y, t) {
|
1009
1377
|
return x + (y - x) * Math.min(1, Math.max(0, t));
|
1010
1378
|
}
|
1011
|
-
function increaseNodeCapacity(
|
1012
|
-
let
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1379
|
+
function increaseNodeCapacity(currentCapacity, params) {
|
1380
|
+
let newCapacity = Math.max(
|
1381
|
+
// Make sure we have room for at least 2 more nodes.
|
1382
|
+
currentCapacity + 2, Math.ceil(currentCapacity * params.minGrowFactor));
|
1383
|
+
if (newCapacity >= NodeTypeMap.MAX_CAPACITY) {
|
1384
|
+
if (currentCapacity > NodeTypeMap.MAX_CAPACITY - 2) {
|
1385
|
+
throw new Error('Node capacity overflow!');
|
1386
|
+
}
|
1387
|
+
return NodeTypeMap.MAX_CAPACITY;
|
1388
|
+
}
|
1389
|
+
return newCapacity;
|
1390
|
+
}
|
1391
|
+
function increaseEdgeCapacity(currentCapacity, params) {
|
1392
|
+
// This is intended to strike a balance between growing the edge capacity
|
1393
|
+
// in too small increments, which causes a lot of resizing, and growing
|
1394
|
+
// the edge capacity in too large increments, which results in a lot of
|
1395
|
+
// wasted memory.
|
1396
|
+
let pct = currentCapacity / params.peakCapacity;
|
1397
|
+
let growFactor = interpolate(params.maxGrowFactor, params.minGrowFactor, pct);
|
1398
|
+
let newCapacity = Math.max(
|
1399
|
+
// Make sure we have room for at least one more edge.
|
1400
|
+
currentCapacity + 1, Math.ceil(currentCapacity * growFactor));
|
1401
|
+
if (newCapacity >= EdgeTypeMap.MAX_CAPACITY) {
|
1402
|
+
if (currentCapacity > EdgeTypeMap.MAX_CAPACITY - 1) {
|
1403
|
+
throw new Error('Edge capacity overflow!');
|
1404
|
+
}
|
1405
|
+
return EdgeTypeMap.MAX_CAPACITY;
|
1406
|
+
}
|
1407
|
+
return newCapacity;
|
1019
1408
|
}
|
1020
|
-
function
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
let newCapacity = capacity;
|
1027
|
-
if (load > LOAD_FACTOR) {
|
1028
|
-
// This is intended to strike a balance between growing the edge capacity
|
1029
|
-
// in too small increments, which causes a lot of resizing, and growing
|
1030
|
-
// the edge capacity in too large increments, which results in a lot of
|
1031
|
-
// wasted memory.
|
1032
|
-
|
1033
|
-
let growFactor = interpolate(MAX_GROW_FACTOR, MIN_GROW_FACTOR, capacity / PEAK_CAPACITY);
|
1034
|
-
newCapacity = Math.round(capacity * growFactor);
|
1035
|
-
} else if (load < UNLOAD_FACTOR) {
|
1036
|
-
// In some cases, it may be possible to shrink the edge capacity,
|
1037
|
-
// but this is only likely to occur when a lot of edges have been removed.
|
1038
|
-
newCapacity = Math.round(capacity * SHRINK_FACTOR);
|
1039
|
-
}
|
1040
|
-
(0, _assert().default)(newCapacity <= MAX_CAPACITY, 'Edge capacity overflow!');
|
1041
|
-
return Math.max(MIN_CAPACITY, newCapacity);
|
1409
|
+
function decreaseEdgeCapacity(currentCapacity, params) {
|
1410
|
+
return Math.max(
|
1411
|
+
// Make sure we don't shrink the capacity _below_ 2.
|
1412
|
+
2, Math.min(
|
1413
|
+
// Make sure we shrink the capacity by at least 1.
|
1414
|
+
currentCapacity - 1, Math.ceil(currentCapacity * params.shrinkFactor)));
|
1042
1415
|
}
|