@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.
@@ -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
- edgeCapacity?: number,
26
- nodeCapacity?: number,
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
- /** The upper bound above which capacity should be increased. */
30
- const LOAD_FACTOR = 0.7;
31
- /** The lower bound below which capacity should be decreased. */
32
- const UNLOAD_FACTOR = 0.3;
33
- /** The max amount by which to grow the capacity. */
34
- const MAX_GROW_FACTOR = 8;
35
- /** The min amount by which to grow the capacity. */
36
- const MIN_GROW_FACTOR = 2;
37
- /** The amount by which to shrink the capacity. */
38
- const SHRINK_FACTOR = 0.5;
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
- let {
58
- nodeCapacity = NodeTypeMap.MIN_CAPACITY,
59
- edgeCapacity = EdgeTypeMap.MIN_CAPACITY,
60
- } = opts ?? {};
61
- assert(
62
- nodeCapacity <= NodeTypeMap.MAX_CAPACITY,
63
- 'Node capacity overflow!',
64
- );
65
- assert(
66
- edgeCapacity <= EdgeTypeMap.MAX_CAPACITY,
67
- 'Edge capacity overflow!',
68
- );
69
- this.#nodes = new NodeTypeMap(nodeCapacity);
70
- this.#edges = new EdgeTypeMap(edgeCapacity);
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` from the given options.
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 graph.
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 maximum number of edges the graph can contain. */
109
- edgeCapacity: number,
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
- /** The likelihood of uniform distribution. ~1.0 indicates certainty. */
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
- edgeCapacity: this.#edges.capacity,
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: Math.round((collisions / buckets.size) * 100) / 100 || 0,
169
- uniformity: Math.round(uniformity * 100) / 100 || 0,
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` when the `numNodes` meets or exceeds
177
- * the allocated size of the `nodes` array.
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 `numEdges` meets or exceeds
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 copy = new AdjacencyList({
196
- nodeCapacity: this.#nodes.capacity,
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
- copy.#nodes.nextId = this.#nodes.nextId;
302
+ nodes.nextId = this.#nodes.nextId;
202
303
  this.#edges.forEach(
203
304
  edge =>
204
- void copy.addEdge(
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 === copy.#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 = copy.#nodes;
221
- this.#edges = copy.#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
- // If we're in danger of overflowing the `nodes` array, resize it.
232
- if (this.#nodes.load > LOAD_FACTOR) {
233
- this.resizeNodes(increaseNodeCapacity(this.#nodes.capacity));
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 hash = this.#edges.hash(from, to, type);
252
- let edge = this.#edges.addressOf(hash, from, to, type);
372
+ let result;
373
+ let tries = 0;
253
374
 
254
- // The edge is already in the graph; do nothing.
255
- if (edge !== null) return false;
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
- // Link this edge to the node's list of incoming edges.
302
- let prevIn = this.#nodes.linkIn(toNode, edge);
303
- if (prevIn !== null) this.#edges.linkIn(prevIn, edge);
378
+ result = link(
379
+ from,
380
+ to,
381
+ type,
382
+ this.#edges,
383
+ this.#nodes,
384
+ this.#params.unloadFactor,
385
+ );
304
386
 
305
- // Link this edge to the node's list of outgoing edges.
306
- let prevOut = this.#nodes.linkOut(fromNode, edge);
307
- if (prevOut !== null) this.#edges.linkOut(prevOut, edge);
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 true;
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 nodes connected from this node.
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 nodes connected to this node.
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) (capacity * ITEM_SIZE * BUCKET_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 number of items to accommodate per hash bucket. */
612
- static BUCKET_SIZE: number = 2;
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
- let {BUCKET_SIZE} = this.constructor;
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, BUCKET_SIZE} = this.constructor;
696
- return capacity + HEADER_SIZE + ITEM_SIZE * BUCKET_SIZE * capacity;
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, ITEM_SIZE, BUCKET_SIZE} = this.constructor;
819
- let min = HEADER_SIZE + this.capacity;
820
- let max = min + this.capacity * BUCKET_SIZE * ITEM_SIZE;
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, max),
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 possible edge types in the graph.
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
- /** Get a unique node id. */
902
- getId(): NodeId {
903
- return toNodeId(this.data[NodeTypeMap.#NEXT_ID]++);
904
- }
905
-
906
- getLoad(count: number = this.count): number {
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 smallest functional edge map capacity. */
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(nodeCapacity: number): number {
1217
- let {MIN_CAPACITY, MAX_CAPACITY} = NodeTypeMap;
1218
- let newCapacity = Math.round(nodeCapacity * MIN_GROW_FACTOR);
1219
- assert(newCapacity <= MAX_CAPACITY, 'Node capacity overflow!');
1220
- return Math.max(MIN_CAPACITY, newCapacity);
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 getNextEdgeCapacity(
1224
- capacity: number,
1225
- count: number,
1226
- load: number,
1614
+ function increaseEdgeCapacity(
1615
+ currentCapacity: number,
1616
+ params: AdjacencyListParams,
1227
1617
  ): number {
1228
- let {MIN_CAPACITY, MAX_CAPACITY, PEAK_CAPACITY} = EdgeTypeMap;
1229
- let newCapacity = capacity;
1230
- if (load > LOAD_FACTOR) {
1231
- // This is intended to strike a balance between growing the edge capacity
1232
- // in too small increments, which causes a lot of resizing, and growing
1233
- // the edge capacity in too large increments, which results in a lot of
1234
- // wasted memory.
1235
- let pct = capacity / PEAK_CAPACITY;
1236
- let growFactor = interpolate(MAX_GROW_FACTOR, MIN_GROW_FACTOR, pct);
1237
- newCapacity = Math.round(capacity * growFactor);
1238
- } else if (load < UNLOAD_FACTOR) {
1239
- // In some cases, it may be possible to shrink the edge capacity,
1240
- // but this is only likely to occur when a lot of edges have been removed.
1241
- newCapacity = Math.round(capacity * SHRINK_FACTOR);
1242
- }
1243
- assert(newCapacity <= MAX_CAPACITY, 'Edge capacity overflow!');
1244
- return Math.max(MIN_CAPACITY, newCapacity);
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
  }