@graphty/graph-format 0.0.0 → 0.2.0
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/LICENSE +21 -0
- package/README.md +216 -43
- package/dist/graph-format.d.ts +1 -0
- package/dist/graph-format.d.ts.map +1 -0
- package/dist/graph-format.js +15798 -0
- package/dist/graph-format.js.map +1 -0
- package/dist/src/builder/arena.d.ts +85 -0
- package/dist/src/builder/arena.d.ts.map +1 -0
- package/dist/src/builder/arena.js +154 -0
- package/dist/src/builder/arena.js.map +1 -0
- package/dist/src/builder/compact.d.ts +515 -0
- package/dist/src/builder/compact.d.ts.map +1 -0
- package/dist/src/builder/compact.js +1553 -0
- package/dist/src/builder/compact.js.map +1 -0
- package/dist/src/builder/counting-sort.d.ts +96 -0
- package/dist/src/builder/counting-sort.d.ts.map +1 -0
- package/dist/src/builder/counting-sort.js +430 -0
- package/dist/src/builder/counting-sort.js.map +1 -0
- package/dist/src/builder/freeze.d.ts +72 -0
- package/dist/src/builder/freeze.d.ts.map +1 -0
- package/dist/src/builder/freeze.js +548 -0
- package/dist/src/builder/freeze.js.map +1 -0
- package/dist/src/builder/graph-builder.d.ts +602 -0
- package/dist/src/builder/graph-builder.d.ts.map +1 -0
- package/dist/src/builder/graph-builder.js +1940 -0
- package/dist/src/builder/graph-builder.js.map +1 -0
- package/dist/src/builder/options.d.ts +16 -0
- package/dist/src/builder/options.d.ts.map +1 -0
- package/dist/src/builder/options.js +21 -0
- package/dist/src/builder/options.js.map +1 -0
- package/dist/src/columns/bitmap.d.ts +96 -0
- package/dist/src/columns/bitmap.d.ts.map +1 -0
- package/dist/src/columns/bitmap.js +184 -0
- package/dist/src/columns/bitmap.js.map +1 -0
- package/dist/src/columns/column.d.ts +220 -0
- package/dist/src/columns/column.d.ts.map +1 -0
- package/dist/src/columns/column.js +2109 -0
- package/dist/src/columns/column.js.map +1 -0
- package/dist/src/columns/dictionary.d.ts +60 -0
- package/dist/src/columns/dictionary.d.ts.map +1 -0
- package/dist/src/columns/dictionary.js +102 -0
- package/dist/src/columns/dictionary.js.map +1 -0
- package/dist/src/columns/growable.d.ts +198 -0
- package/dist/src/columns/growable.d.ts.map +1 -0
- package/dist/src/columns/growable.js +296 -0
- package/dist/src/columns/growable.js.map +1 -0
- package/dist/src/columns/infer.d.ts +121 -0
- package/dist/src/columns/infer.d.ts.map +1 -0
- package/dist/src/columns/infer.js +268 -0
- package/dist/src/columns/infer.js.map +1 -0
- package/dist/src/columns/remap.d.ts +143 -0
- package/dist/src/columns/remap.d.ts.map +1 -0
- package/dist/src/columns/remap.js +588 -0
- package/dist/src/columns/remap.js.map +1 -0
- package/dist/src/columns/table.d.ts +184 -0
- package/dist/src/columns/table.d.ts.map +1 -0
- package/dist/src/columns/table.js +485 -0
- package/dist/src/columns/table.js.map +1 -0
- package/dist/src/constants.d.ts +34 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +79 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/errors.d.ts +76 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +32 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/ids/edge-id-index.d.ts +77 -0
- package/dist/src/ids/edge-id-index.d.ts.map +1 -0
- package/dist/src/ids/edge-id-index.js +155 -0
- package/dist/src/ids/edge-id-index.js.map +1 -0
- package/dist/src/ids/node-id-map.d.ts +280 -0
- package/dist/src/ids/node-id-map.d.ts.map +1 -0
- package/dist/src/ids/node-id-map.js +1139 -0
- package/dist/src/ids/node-id-map.js.map +1 -0
- package/dist/src/ids/string-store.d.ts +219 -0
- package/dist/src/ids/string-store.d.ts.map +1 -0
- package/dist/src/ids/string-store.js +526 -0
- package/dist/src/ids/string-store.js.map +1 -0
- package/dist/src/index.d.ts +30 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +29 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/populate/from-csr.d.ts +61 -0
- package/dist/src/populate/from-csr.d.ts.map +1 -0
- package/dist/src/populate/from-csr.js +706 -0
- package/dist/src/populate/from-csr.js.map +1 -0
- package/dist/src/populate/from-edge-arrays.d.ts +51 -0
- package/dist/src/populate/from-edge-arrays.d.ts.map +1 -0
- package/dist/src/populate/from-edge-arrays.js +217 -0
- package/dist/src/populate/from-edge-arrays.js.map +1 -0
- package/dist/src/populate/from-records.d.ts +51 -0
- package/dist/src/populate/from-records.d.ts.map +1 -0
- package/dist/src/populate/from-records.js +447 -0
- package/dist/src/populate/from-records.js.map +1 -0
- package/dist/src/snapshot/derived.d.ts +211 -0
- package/dist/src/snapshot/derived.d.ts.map +1 -0
- package/dist/src/snapshot/derived.js +1254 -0
- package/dist/src/snapshot/derived.js.map +1 -0
- package/dist/src/snapshot/graph-meta.d.ts +23 -0
- package/dist/src/snapshot/graph-meta.d.ts.map +1 -0
- package/dist/src/snapshot/graph-meta.js +159 -0
- package/dist/src/snapshot/graph-meta.js.map +1 -0
- package/dist/src/snapshot/graph-snapshot.d.ts +509 -0
- package/dist/src/snapshot/graph-snapshot.d.ts.map +1 -0
- package/dist/src/snapshot/graph-snapshot.js +1247 -0
- package/dist/src/snapshot/graph-snapshot.js.map +1 -0
- package/dist/src/snapshot/hash.d.ts +90 -0
- package/dist/src/snapshot/hash.d.ts.map +1 -0
- package/dist/src/snapshot/hash.js +228 -0
- package/dist/src/snapshot/hash.js.map +1 -0
- package/dist/src/snapshot/queries.d.ts +74 -0
- package/dist/src/snapshot/queries.d.ts.map +1 -0
- package/dist/src/snapshot/queries.js +131 -0
- package/dist/src/snapshot/queries.js.map +1 -0
- package/dist/src/snapshot/validate.d.ts +192 -0
- package/dist/src/snapshot/validate.d.ts.map +1 -0
- package/dist/src/snapshot/validate.js +990 -0
- package/dist/src/snapshot/validate.js.map +1 -0
- package/dist/src/snapshot/views.d.ts +274 -0
- package/dist/src/snapshot/views.d.ts.map +1 -0
- package/dist/src/snapshot/views.js +856 -0
- package/dist/src/snapshot/views.js.map +1 -0
- package/dist/src/types/builder.d.ts +456 -0
- package/dist/src/types/builder.d.ts.map +1 -0
- package/dist/src/types/builder.js +11 -0
- package/dist/src/types/builder.js.map +1 -0
- package/dist/src/types/columns.d.ts +615 -0
- package/dist/src/types/columns.d.ts.map +1 -0
- package/dist/src/types/columns.js +14 -0
- package/dist/src/types/columns.js.map +1 -0
- package/dist/src/types/index.d.ts +14 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +11 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/internal.d.ts +172 -0
- package/dist/src/types/internal.d.ts.map +1 -0
- package/dist/src/types/internal.js +13 -0
- package/dist/src/types/internal.js.map +1 -0
- package/dist/src/types/snapshot.d.ts +801 -0
- package/dist/src/types/snapshot.d.ts.map +1 -0
- package/dist/src/types/snapshot.js +14 -0
- package/dist/src/types/snapshot.js.map +1 -0
- package/dist/src/types/wire.d.ts +196 -0
- package/dist/src/types/wire.d.ts.map +1 -0
- package/dist/src/types/wire.js +8 -0
- package/dist/src/types/wire.js.map +1 -0
- package/dist/src/util/mask.d.ts +55 -0
- package/dist/src/util/mask.d.ts.map +1 -0
- package/dist/src/util/mask.js +71 -0
- package/dist/src/util/mask.js.map +1 -0
- package/dist/src/util/options.d.ts +22 -0
- package/dist/src/util/options.d.ts.map +1 -0
- package/dist/src/util/options.js +49 -0
- package/dist/src/util/options.js.map +1 -0
- package/dist/src/util/shared-buffers.d.ts +42 -0
- package/dist/src/util/shared-buffers.d.ts.map +1 -0
- package/dist/src/util/shared-buffers.js +52 -0
- package/dist/src/util/shared-buffers.js.map +1 -0
- package/dist/src/util/typed-array.d.ts +205 -0
- package/dist/src/util/typed-array.d.ts.map +1 -0
- package/dist/src/util/typed-array.js +289 -0
- package/dist/src/util/typed-array.js.map +1 -0
- package/dist/src/wire/bytes.d.ts +69 -0
- package/dist/src/wire/bytes.d.ts.map +1 -0
- package/dist/src/wire/bytes.js +361 -0
- package/dist/src/wire/bytes.js.map +1 -0
- package/dist/src/wire/carried-views.d.ts +111 -0
- package/dist/src/wire/carried-views.d.ts.map +1 -0
- package/dist/src/wire/carried-views.js +286 -0
- package/dist/src/wire/carried-views.js.map +1 -0
- package/dist/src/wire/from-wire.d.ts +144 -0
- package/dist/src/wire/from-wire.d.ts.map +1 -0
- package/dist/src/wire/from-wire.js +1425 -0
- package/dist/src/wire/from-wire.js.map +1 -0
- package/dist/src/wire/to-wire.d.ts +171 -0
- package/dist/src/wire/to-wire.d.ts.map +1 -0
- package/dist/src/wire/to-wire.js +652 -0
- package/dist/src/wire/to-wire.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +72 -7
- package/src/builder/arena.ts +192 -0
- package/src/builder/compact.ts +1811 -0
- package/src/builder/counting-sort.ts +506 -0
- package/src/builder/freeze.ts +689 -0
- package/src/builder/graph-builder.ts +2264 -0
- package/src/builder/options.ts +29 -0
- package/src/columns/bitmap.ts +196 -0
- package/src/columns/column.ts +2574 -0
- package/src/columns/dictionary.ts +114 -0
- package/src/columns/growable.ts +350 -0
- package/src/columns/infer.ts +285 -0
- package/src/columns/remap.ts +663 -0
- package/src/columns/table.ts +568 -0
- package/src/constants.ts +89 -0
- package/src/errors.ts +115 -0
- package/src/ids/edge-id-index.ts +179 -0
- package/src/ids/node-id-map.ts +1340 -0
- package/src/ids/string-store.ts +589 -0
- package/src/index.ts +139 -0
- package/src/lib-resizable-array-buffer.d.ts +34 -0
- package/src/populate/from-csr.ts +802 -0
- package/src/populate/from-edge-arrays.ts +257 -0
- package/src/populate/from-records.ts +536 -0
- package/src/snapshot/derived.ts +1486 -0
- package/src/snapshot/graph-meta.ts +167 -0
- package/src/snapshot/graph-snapshot.ts +1503 -0
- package/src/snapshot/hash.ts +241 -0
- package/src/snapshot/queries.ts +136 -0
- package/src/snapshot/validate.ts +1108 -0
- package/src/snapshot/views.ts +943 -0
- package/src/types/builder.ts +480 -0
- package/src/types/columns.ts +727 -0
- package/src/types/index.ts +14 -0
- package/src/types/internal.ts +195 -0
- package/src/types/snapshot.ts +886 -0
- package/src/types/wire.ts +207 -0
- package/src/util/mask.ts +89 -0
- package/src/util/options.ts +57 -0
- package/src/util/shared-buffers.ts +56 -0
- package/src/util/typed-array.ts +371 -0
- package/src/wire/bytes.ts +440 -0
- package/src/wire/carried-views.ts +327 -0
- package/src/wire/from-wire.ts +1690 -0
- package/src/wire/to-wire.ts +755 -0
|
@@ -0,0 +1,1139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The immutable id-to-index bijection of a snapshot (design section 4): five storage kinds chosen by
|
|
3
|
+
* inspecting the ids once in O(n), SameValueZero equality, `INVALID_INDEX` on a miss, a dense-integer
|
|
4
|
+
* fast path with no Map at all, lazily materialised typed / decoded representations in either
|
|
5
|
+
* direction, and a reverse `Map` that is shared with the builder (guarded by `index < size`) or built
|
|
6
|
+
* lazily on the first lookup.
|
|
7
|
+
*
|
|
8
|
+
* The class exposes exactly the public surface of design section 12.2. Everything the sibling
|
|
9
|
+
* modules need beyond it (construction from ids, typed arrays or the wire; the typed form for the
|
|
10
|
+
* wire; gather and remap for derived graphs and compaction) is a module-level function that reaches
|
|
11
|
+
* the private storage through a module-private WeakMap, so nothing internal appears on the instance
|
|
12
|
+
* type.
|
|
13
|
+
*/
|
|
14
|
+
import { INVALID_INDEX, MAX_COUNT } from "../constants.js";
|
|
15
|
+
import { GraphFormatError } from "../errors.js";
|
|
16
|
+
import { assertOneOf } from "../util/options.js";
|
|
17
|
+
import { claimHolder } from "../util/shared-buffers.js";
|
|
18
|
+
import { checkUtf8Layout, encodeUtf8Rows, hasLoneSurrogate, resolveRange, Utf8Store } from "./string-store.js";
|
|
19
|
+
// ============================================================ id validation
|
|
20
|
+
/**
|
|
21
|
+
* A short, plain-ASCII rendering of an id for error messages.
|
|
22
|
+
* @param id - the value
|
|
23
|
+
* @returns the number, a quoted (possibly truncated) string, or the type name
|
|
24
|
+
*/
|
|
25
|
+
export function describeId(id) {
|
|
26
|
+
if (typeof id === "number") {
|
|
27
|
+
return String(id);
|
|
28
|
+
}
|
|
29
|
+
if (typeof id === "string") {
|
|
30
|
+
const text = id.length > 40 ? `${id.slice(0, 37)}...` : id;
|
|
31
|
+
return JSON.stringify(text);
|
|
32
|
+
}
|
|
33
|
+
if (id === null) {
|
|
34
|
+
return "null";
|
|
35
|
+
}
|
|
36
|
+
if (typeof id === "bigint") {
|
|
37
|
+
return `${String(id)}n`;
|
|
38
|
+
}
|
|
39
|
+
return `a value of type ${typeof id}`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check a value as a node id and normalise it (design section 4.1): a finite number (`-0` becomes
|
|
43
|
+
* `0`) or a string without a lone surrogate. NaN, non-finite numbers, bigints, objects, null and
|
|
44
|
+
* undefined are E_INVALID_ID; a lone surrogate is E_INVALID_ID with details.reason "lone surrogate".
|
|
45
|
+
* @param id - the value to check
|
|
46
|
+
* @returns the id as stored
|
|
47
|
+
*/
|
|
48
|
+
export function validateNodeId(id) {
|
|
49
|
+
if (typeof id === "number") {
|
|
50
|
+
if (!Number.isFinite(id)) {
|
|
51
|
+
throw new GraphFormatError("E_INVALID_ID", `invalid node id ${String(id)}: not a finite number`, {
|
|
52
|
+
id,
|
|
53
|
+
reason: "non-finite",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return id === 0 ? 0 : id;
|
|
57
|
+
}
|
|
58
|
+
if (typeof id === "string") {
|
|
59
|
+
if (hasLoneSurrogate(id)) {
|
|
60
|
+
throw new GraphFormatError("E_INVALID_ID", `invalid node id ${describeId(id)}: lone surrogate`, {
|
|
61
|
+
id,
|
|
62
|
+
reason: "lone surrogate",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return id;
|
|
66
|
+
}
|
|
67
|
+
throw new GraphFormatError("E_INVALID_ID", `invalid node id: ${describeId(id)} is not a string or a number`, {
|
|
68
|
+
reason: "unsupported type",
|
|
69
|
+
type: typeof id,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Storage behind every instance, reachable by the module-level functions but not by consumers. */
|
|
73
|
+
const INTERNALS = new WeakMap();
|
|
74
|
+
/**
|
|
75
|
+
* The storage of an instance.
|
|
76
|
+
* @param map - the id map
|
|
77
|
+
* @returns its private storage
|
|
78
|
+
*/
|
|
79
|
+
function storageOf(map) {
|
|
80
|
+
const storage = INTERNALS.get(map);
|
|
81
|
+
if (storage === undefined) {
|
|
82
|
+
throw new GraphFormatError("E_UNSUPPORTED", "not a NodeIdMap of this package", { reason: "foreign id map" });
|
|
83
|
+
}
|
|
84
|
+
return storage;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A storage record with every slot null except the given ones.
|
|
88
|
+
* @param kind - the storage kind
|
|
89
|
+
* @param size - the id count
|
|
90
|
+
* @param slots - the non-null slots
|
|
91
|
+
* @returns the storage
|
|
92
|
+
*/
|
|
93
|
+
function makeStorage(kind, size, slots = {}) {
|
|
94
|
+
return {
|
|
95
|
+
kind,
|
|
96
|
+
size,
|
|
97
|
+
offset: 0,
|
|
98
|
+
denseValues: null,
|
|
99
|
+
denseInverse: null,
|
|
100
|
+
decoded: null,
|
|
101
|
+
numbers: null,
|
|
102
|
+
tags: null,
|
|
103
|
+
strings: null,
|
|
104
|
+
map: null,
|
|
105
|
+
stringIndex: null,
|
|
106
|
+
...slots,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Whether a typed array the storage still reads from was transferred away.
|
|
111
|
+
* @param storage - the storage
|
|
112
|
+
* @returns true when detached
|
|
113
|
+
*/
|
|
114
|
+
function storageDetached(storage) {
|
|
115
|
+
if (storage.size === 0) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return ((storage.denseValues !== null && storage.denseValues.length === 0) ||
|
|
119
|
+
(storage.decoded === null && storage.numbers !== null && storage.numbers.length === 0) ||
|
|
120
|
+
(storage.decoded === null && storage.tags !== null && storage.tags.length === 0) ||
|
|
121
|
+
(storage.decoded === null && storage.strings !== null && storage.strings.detached));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Whether an id map's typed storage was transferred away (design section 9.1), for validate().
|
|
125
|
+
* @param map - the id map
|
|
126
|
+
* @returns true when detached
|
|
127
|
+
*/
|
|
128
|
+
export function idMapDetached(map) {
|
|
129
|
+
const storage = INTERNALS.get(map);
|
|
130
|
+
return storage !== undefined && storageDetached(storage);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* -0 is stored and reported as 0 (SameValueZero, design section 4.1).
|
|
134
|
+
* @param value - a numeric id
|
|
135
|
+
* @returns the id with -0 folded into 0
|
|
136
|
+
*/
|
|
137
|
+
function normaliseZero(value) {
|
|
138
|
+
return value === 0 ? 0 : value;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The E_DUPLICATE_ID error for two indices holding the same id.
|
|
142
|
+
* @param id - the id
|
|
143
|
+
* @param first - the lower index
|
|
144
|
+
* @param second - the higher index
|
|
145
|
+
* @returns the error
|
|
146
|
+
*/
|
|
147
|
+
function duplicateIdError(id, first, second) {
|
|
148
|
+
return new GraphFormatError("E_DUPLICATE_ID", `duplicate node id ${describeId(id)} at indices ${first} and ${second}`, {
|
|
149
|
+
id,
|
|
150
|
+
indices: [first, second],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The E_INVALID_SNAPSHOT error for an id bijection failure of untrusted typed input (invariant I11).
|
|
155
|
+
* @param reason - what failed
|
|
156
|
+
* @param details - extra context
|
|
157
|
+
* @returns the error
|
|
158
|
+
*/
|
|
159
|
+
function bijectionError(reason, details) {
|
|
160
|
+
return new GraphFormatError("E_INVALID_SNAPSHOT", `id map violates invariant I11: ${reason}`, {
|
|
161
|
+
invariant: "I11",
|
|
162
|
+
reason,
|
|
163
|
+
...details,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The E_BAD_SERIALIZATION error for a malformed typed id map.
|
|
168
|
+
* @param ref - the manifest member
|
|
169
|
+
* @param reason - what is wrong
|
|
170
|
+
* @param details - extra context
|
|
171
|
+
* @returns the error
|
|
172
|
+
*/
|
|
173
|
+
function layoutError(ref, reason, details = {}) {
|
|
174
|
+
return new GraphFormatError("E_BAD_SERIALIZATION", `${ref}: ${reason}`, { ref, reason, ...details });
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Build the dense inverse array (id -> index) from the dense values, detecting duplicates.
|
|
178
|
+
* @param values - the id of every index, integers in [0, MAX_COUNT)
|
|
179
|
+
* @param maxId - the largest id
|
|
180
|
+
* @param onDuplicate - builds the error thrown for a repeated id
|
|
181
|
+
* @returns a fresh inverse of maxId + 1 entries filled with INVALID_INDEX where absent
|
|
182
|
+
*/
|
|
183
|
+
function buildDenseInverse(values, maxId, onDuplicate) {
|
|
184
|
+
const inverse = new Uint32Array(maxId + 1).fill(INVALID_INDEX);
|
|
185
|
+
for (let i = 0; i < values.length; i++) {
|
|
186
|
+
const id = values[i];
|
|
187
|
+
if (inverse[id] !== INVALID_INDEX) {
|
|
188
|
+
throw onDuplicate(id, inverse[id], i);
|
|
189
|
+
}
|
|
190
|
+
inverse[id] = i;
|
|
191
|
+
}
|
|
192
|
+
return inverse;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Build the reverse map of `size` decoded ids, detecting duplicates.
|
|
196
|
+
* @param ids - the ids in index order
|
|
197
|
+
* @param size - how many entries to index
|
|
198
|
+
* @param onDuplicate - builds the error thrown for a repeated id
|
|
199
|
+
* @returns a fresh Map from id to index
|
|
200
|
+
*/
|
|
201
|
+
function buildReverseMap(ids, size, onDuplicate) {
|
|
202
|
+
const map = new Map();
|
|
203
|
+
for (let i = 0; i < size; i++) {
|
|
204
|
+
const id = ids[i];
|
|
205
|
+
const seen = map.get(id);
|
|
206
|
+
if (seen !== undefined) {
|
|
207
|
+
throw onDuplicate(id, seen, i);
|
|
208
|
+
}
|
|
209
|
+
map.set(id, i);
|
|
210
|
+
}
|
|
211
|
+
return map;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* One pass over the first `size` ids that decides the storage kind. The ids are assumed valid
|
|
215
|
+
* (finite numbers or strings).
|
|
216
|
+
* @param ids - the ids in index order
|
|
217
|
+
* @param size - how many to inspect
|
|
218
|
+
* @returns the scan result
|
|
219
|
+
*/
|
|
220
|
+
function scanIds(ids, size) {
|
|
221
|
+
let allNumbers = true;
|
|
222
|
+
let allStrings = true;
|
|
223
|
+
let allDenseIntegers = true;
|
|
224
|
+
let maxId = -1;
|
|
225
|
+
let offset = 0;
|
|
226
|
+
let identity = true;
|
|
227
|
+
if (size > 0) {
|
|
228
|
+
const first = ids[0];
|
|
229
|
+
if (typeof first === "number" && Number.isSafeInteger(first)) {
|
|
230
|
+
offset = first;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
identity = false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
for (let i = 0; i < size; i++) {
|
|
237
|
+
const id = ids[i];
|
|
238
|
+
if (typeof id === "number") {
|
|
239
|
+
allStrings = false;
|
|
240
|
+
if (identity && id !== i + offset) {
|
|
241
|
+
identity = false;
|
|
242
|
+
}
|
|
243
|
+
if (allDenseIntegers) {
|
|
244
|
+
if (Number.isInteger(id) && id >= 0 && id < MAX_COUNT) {
|
|
245
|
+
if (id > maxId) {
|
|
246
|
+
maxId = id;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
allDenseIntegers = false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
allNumbers = false;
|
|
256
|
+
identity = false;
|
|
257
|
+
allDenseIntegers = false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (!allNumbers) {
|
|
261
|
+
maxId = -1;
|
|
262
|
+
}
|
|
263
|
+
return { allNumbers, allStrings, identity, offset, allDenseIntegers, maxId };
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* The kind a scan selects (design section 4.2): identity, else dense when the inverse array would be
|
|
267
|
+
* at most twice the node count, else numeric, string or mixed.
|
|
268
|
+
* @param scan - the scan result
|
|
269
|
+
* @param size - the id count
|
|
270
|
+
* @returns the kind
|
|
271
|
+
*/
|
|
272
|
+
function kindOf(scan, size) {
|
|
273
|
+
if (size === 0 || scan.identity) {
|
|
274
|
+
return "identity";
|
|
275
|
+
}
|
|
276
|
+
if (scan.allDenseIntegers && scan.maxId + 1 <= 2 * size) {
|
|
277
|
+
return "dense";
|
|
278
|
+
}
|
|
279
|
+
if (scan.allNumbers) {
|
|
280
|
+
return "numeric";
|
|
281
|
+
}
|
|
282
|
+
if (scan.allStrings) {
|
|
283
|
+
return "string";
|
|
284
|
+
}
|
|
285
|
+
return "mixed";
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Copy the first `size` ids into a fresh U32 (dense values).
|
|
289
|
+
* @param ids - the ids, all integers in [0, MAX_COUNT)
|
|
290
|
+
* @param size - how many
|
|
291
|
+
* @returns the values
|
|
292
|
+
*/
|
|
293
|
+
function denseValuesOf(ids, size) {
|
|
294
|
+
const values = new Uint32Array(size);
|
|
295
|
+
for (let i = 0; i < size; i++) {
|
|
296
|
+
const id = ids[i];
|
|
297
|
+
values[i] = typeof id === "number" ? id : INVALID_INDEX;
|
|
298
|
+
}
|
|
299
|
+
return values;
|
|
300
|
+
}
|
|
301
|
+
// ============================================================ the class
|
|
302
|
+
/**
|
|
303
|
+
* Bijection between node ids and node indices for one snapshot (design section 4, invariant I11):
|
|
304
|
+
* `indexOf(idOf(i)) === i` for every i, `size === nodeCount`, SameValueZero equality (so `1` and
|
|
305
|
+
* `"1"` are distinct, `-0` is `0`, `1.5` is legal and `NaN` never occurs). Immutable. Lookups by id
|
|
306
|
+
* are total and return `INVALID_INDEX` on a miss (decision C9); `requireIndex` is the checked form.
|
|
307
|
+
*
|
|
308
|
+
* Instances are created by the module-level factories (`nodeIdMapFromIds`, `nodeIdMapFromF64`,
|
|
309
|
+
* `identityNodeIdMap`, `nodeIdMapFromTyped`) and by `gatherNodeIdMap` / `remapNodeIdMap`; the
|
|
310
|
+
* constructor takes a prepared storage record and is not meant to be called by consumers.
|
|
311
|
+
*/
|
|
312
|
+
export class NodeIdMap {
|
|
313
|
+
/**
|
|
314
|
+
* Wrap a prepared storage record. Sibling modules use the factories of this module instead.
|
|
315
|
+
* @param storage - the storage; the kind's slots must be filled as documented on NodeIdMapStorage
|
|
316
|
+
* @internal
|
|
317
|
+
*/
|
|
318
|
+
constructor(storage) {
|
|
319
|
+
this.kind = storage.kind;
|
|
320
|
+
this.size = storage.size;
|
|
321
|
+
this.offset = storage.kind === "identity" ? storage.offset : 0;
|
|
322
|
+
this.storage = storage;
|
|
323
|
+
INTERNALS.set(this, storage);
|
|
324
|
+
Object.freeze(this);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* The id of a node index.
|
|
328
|
+
* @param index - the node index
|
|
329
|
+
* @returns the id; E_INDEX_RANGE when index is not an integer in [0, size)
|
|
330
|
+
*/
|
|
331
|
+
idOf(index) {
|
|
332
|
+
if (!(index >= 0 && index < this.size) || !Number.isInteger(index)) {
|
|
333
|
+
throw new GraphFormatError("E_INDEX_RANGE", `node index ${index} out of range (size ${this.size})`, {
|
|
334
|
+
index,
|
|
335
|
+
size: this.size,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return this.idAt(index);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Total lookup by id: SameValueZero, never coerces, so `indexOf("1")` misses a map of numbers.
|
|
342
|
+
* @param id - the node id
|
|
343
|
+
* @returns the node index, or INVALID_INDEX when absent
|
|
344
|
+
*/
|
|
345
|
+
indexOf(id) {
|
|
346
|
+
const { storage } = this;
|
|
347
|
+
switch (storage.kind) {
|
|
348
|
+
case "identity": {
|
|
349
|
+
if (typeof id !== "number") {
|
|
350
|
+
return INVALID_INDEX;
|
|
351
|
+
}
|
|
352
|
+
const index = id - storage.offset;
|
|
353
|
+
if (!Number.isInteger(index) || index < 0 || index >= storage.size) {
|
|
354
|
+
return INVALID_INDEX;
|
|
355
|
+
}
|
|
356
|
+
return index === 0 ? 0 : index;
|
|
357
|
+
}
|
|
358
|
+
case "dense": {
|
|
359
|
+
const inverse = storage.denseInverse;
|
|
360
|
+
if (typeof id !== "number" || !Number.isInteger(id) || id < 0 || id >= inverse.length) {
|
|
361
|
+
return INVALID_INDEX;
|
|
362
|
+
}
|
|
363
|
+
return inverse[id];
|
|
364
|
+
}
|
|
365
|
+
case "numeric":
|
|
366
|
+
case "string":
|
|
367
|
+
case "mixed": {
|
|
368
|
+
const index = this.reverseMap().get(id);
|
|
369
|
+
return index === undefined || index >= storage.size ? INVALID_INDEX : index;
|
|
370
|
+
}
|
|
371
|
+
default:
|
|
372
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown id map kind ${String(storage.kind)}`, {
|
|
373
|
+
kind: storage.kind,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Whether an id is present.
|
|
379
|
+
* @param id - the node id
|
|
380
|
+
* @returns true when present
|
|
381
|
+
*/
|
|
382
|
+
has(id) {
|
|
383
|
+
return this.indexOf(id) !== INVALID_INDEX;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Checked lookup by id, for algorithms that today throw "node not found".
|
|
387
|
+
* @param id - the node id
|
|
388
|
+
* @returns the node index; E_UNKNOWN_NODE when absent
|
|
389
|
+
*/
|
|
390
|
+
requireIndex(id) {
|
|
391
|
+
const index = this.indexOf(id);
|
|
392
|
+
if (index === INVALID_INDEX) {
|
|
393
|
+
throw new GraphFormatError("E_UNKNOWN_NODE", `unknown node id ${describeId(id)}`, { id });
|
|
394
|
+
}
|
|
395
|
+
return index;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Bulk lookup.
|
|
399
|
+
* @param ids - the ids to resolve
|
|
400
|
+
* @param onMissing - "invalid" (default) writes INVALID_INDEX for a miss; "throw" raises E_UNKNOWN_NODE
|
|
401
|
+
* @returns a fresh U32 of indices in input order
|
|
402
|
+
*/
|
|
403
|
+
indicesOf(ids, onMissing = "invalid") {
|
|
404
|
+
assertOneOf("onMissing", onMissing, ["invalid", "throw"]);
|
|
405
|
+
const lookup = onMissing === "throw"
|
|
406
|
+
? (id) => this.requireIndex(id)
|
|
407
|
+
: (id) => this.indexOf(id);
|
|
408
|
+
if (Array.isArray(ids)) {
|
|
409
|
+
const list = ids;
|
|
410
|
+
const out = new Uint32Array(list.length);
|
|
411
|
+
for (let i = 0; i < list.length; i++) {
|
|
412
|
+
out[i] = lookup(list[i]);
|
|
413
|
+
}
|
|
414
|
+
return out;
|
|
415
|
+
}
|
|
416
|
+
let out = new Uint32Array(16);
|
|
417
|
+
let count = 0;
|
|
418
|
+
for (const id of ids) {
|
|
419
|
+
if (count === out.length) {
|
|
420
|
+
const grown = new Uint32Array(out.length * 2);
|
|
421
|
+
grown.set(out);
|
|
422
|
+
out = grown;
|
|
423
|
+
}
|
|
424
|
+
out[count++] = lookup(id);
|
|
425
|
+
}
|
|
426
|
+
return out.slice(0, count);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Bulk decode of a range of ids in one pass. Bounds follow Array.prototype.slice (negative values
|
|
430
|
+
* count from the end, out-of-range values are clamped).
|
|
431
|
+
* @param start - first index (default 0)
|
|
432
|
+
* @param end - one past the last index (default size)
|
|
433
|
+
* @returns a fresh array of the ids in index order
|
|
434
|
+
*/
|
|
435
|
+
idsSlice(start = 0, end = this.size) {
|
|
436
|
+
const [from, to] = resolveRange(start, end, this.size);
|
|
437
|
+
const { storage } = this;
|
|
438
|
+
this.assertAttached();
|
|
439
|
+
switch (storage.kind) {
|
|
440
|
+
case "identity": {
|
|
441
|
+
const out = new Array(to - from);
|
|
442
|
+
for (let i = from; i < to; i++) {
|
|
443
|
+
out[i - from] = i + storage.offset;
|
|
444
|
+
}
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
case "dense":
|
|
448
|
+
return Array.from(storage.denseValues.subarray(from, to));
|
|
449
|
+
case "numeric":
|
|
450
|
+
if (storage.decoded !== null) {
|
|
451
|
+
return storage.decoded.slice(from, to);
|
|
452
|
+
}
|
|
453
|
+
return Array.from(storage.numbers.subarray(from, to), normaliseZero);
|
|
454
|
+
case "string":
|
|
455
|
+
return storage.strings.slice(from, to);
|
|
456
|
+
case "mixed": {
|
|
457
|
+
if (storage.decoded !== null) {
|
|
458
|
+
return storage.decoded.slice(from, to);
|
|
459
|
+
}
|
|
460
|
+
const tags = storage.tags;
|
|
461
|
+
const numbers = storage.numbers;
|
|
462
|
+
const strings = storage.strings;
|
|
463
|
+
const out = new Array(to - from);
|
|
464
|
+
for (let i = from; i < to; i++) {
|
|
465
|
+
out[i - from] = tags[i] === 0 ? normaliseZero(numbers[i]) : strings.at(i);
|
|
466
|
+
}
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
default:
|
|
470
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown id map kind ${String(storage.kind)}`, {
|
|
471
|
+
kind: storage.kind,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Every id in index order.
|
|
477
|
+
* @returns a fresh array
|
|
478
|
+
*/
|
|
479
|
+
toArray() {
|
|
480
|
+
return this.idsSlice(0, this.size);
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Iterate the ids in index order.
|
|
484
|
+
* @yields each id, decoding lazily
|
|
485
|
+
*/
|
|
486
|
+
*[Symbol.iterator]() {
|
|
487
|
+
for (let i = 0; i < this.size; i++) {
|
|
488
|
+
yield this.idAt(i);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Key an index-aligned result vector by id (boundary helper, decision C11).
|
|
493
|
+
* @param values - one value per node index
|
|
494
|
+
* @returns a Map from id to value, in index order
|
|
495
|
+
*/
|
|
496
|
+
toMap(values) {
|
|
497
|
+
const out = new Map();
|
|
498
|
+
for (let i = 0; i < this.size; i++) {
|
|
499
|
+
out.set(this.idAt(i), values[i]);
|
|
500
|
+
}
|
|
501
|
+
return out;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Key an index-aligned result vector by String(id), for legacy Map<string, T> result shapes. When
|
|
505
|
+
* two ids share a string form (1 and "1") the HIGHER index wins, exactly as the legacy
|
|
506
|
+
* algorithms' assignment in node order does (design section 14.2).
|
|
507
|
+
* @param values - one value per node index
|
|
508
|
+
* @returns a Map from String(id) to value
|
|
509
|
+
*/
|
|
510
|
+
toStringMap(values) {
|
|
511
|
+
const out = new Map();
|
|
512
|
+
for (let i = 0; i < this.size; i++) {
|
|
513
|
+
out.set(String(this.idAt(i)), values[i]);
|
|
514
|
+
}
|
|
515
|
+
return out;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Key an index-aligned result vector by String(id), for legacy record result shapes only. The
|
|
519
|
+
* record has a null prototype so ids such as "__proto__" are ordinary keys; when two ids share a
|
|
520
|
+
* string form the HIGHER index wins (the legacy assignment order, design section 14.2).
|
|
521
|
+
* @param values - one value per node index
|
|
522
|
+
* @returns a record from String(id) to value
|
|
523
|
+
*/
|
|
524
|
+
toRecord(values) {
|
|
525
|
+
const out = Object.create(null);
|
|
526
|
+
for (let i = 0; i < this.size; i++) {
|
|
527
|
+
out[String(this.idAt(i))] = values[i];
|
|
528
|
+
}
|
|
529
|
+
return out;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Iterate [id, value] pairs of an index-aligned result vector.
|
|
533
|
+
* @param values - one value per node index
|
|
534
|
+
* @yields the pairs in index order
|
|
535
|
+
*/
|
|
536
|
+
*entries(values) {
|
|
537
|
+
for (let i = 0; i < this.size; i++) {
|
|
538
|
+
yield [this.idAt(i), values[i]];
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* String(idOf(i)) -> i, built lazily once, for legacy string-typed id parameters. When two ids
|
|
543
|
+
* share a string form (always one number and one string, since ids are distinct) the STRING id
|
|
544
|
+
* wins: a legacy string parameter "1" addressed the node whose id is the string "1".
|
|
545
|
+
* @returns the read-only string index
|
|
546
|
+
*/
|
|
547
|
+
stringIndex() {
|
|
548
|
+
const { storage } = this;
|
|
549
|
+
if (storage.stringIndex === null) {
|
|
550
|
+
const index = new Map();
|
|
551
|
+
for (let i = 0; i < storage.size; i++) {
|
|
552
|
+
const id = this.idAt(i);
|
|
553
|
+
const key = String(id);
|
|
554
|
+
if (typeof id === "string" || !index.has(key)) {
|
|
555
|
+
index.set(key, i);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
storage.stringIndex = index;
|
|
559
|
+
}
|
|
560
|
+
return storage.stringIndex;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Bytes of typed storage currently materialised; excludes the reverse Map and JS strings.
|
|
564
|
+
* @returns the byte count
|
|
565
|
+
*/
|
|
566
|
+
byteLength() {
|
|
567
|
+
const { storage } = this;
|
|
568
|
+
let bytes = 0;
|
|
569
|
+
if (storage.denseValues !== null) {
|
|
570
|
+
bytes += storage.denseValues.byteLength;
|
|
571
|
+
}
|
|
572
|
+
if (storage.denseInverse !== null) {
|
|
573
|
+
bytes += storage.denseInverse.byteLength;
|
|
574
|
+
}
|
|
575
|
+
if (storage.numbers !== null) {
|
|
576
|
+
bytes += storage.numbers.byteLength;
|
|
577
|
+
}
|
|
578
|
+
if (storage.tags !== null) {
|
|
579
|
+
bytes += storage.tags.byteLength;
|
|
580
|
+
}
|
|
581
|
+
if (storage.strings !== null) {
|
|
582
|
+
bytes += storage.strings.byteLength();
|
|
583
|
+
}
|
|
584
|
+
return bytes;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Unchecked id of an in-range index.
|
|
588
|
+
* @param index - a node index in [0, size)
|
|
589
|
+
* @returns the id
|
|
590
|
+
*/
|
|
591
|
+
idAt(index) {
|
|
592
|
+
const { storage } = this;
|
|
593
|
+
this.assertAttached();
|
|
594
|
+
switch (storage.kind) {
|
|
595
|
+
case "identity":
|
|
596
|
+
return index + storage.offset;
|
|
597
|
+
case "dense":
|
|
598
|
+
return storage.denseValues[index];
|
|
599
|
+
case "numeric":
|
|
600
|
+
return storage.decoded !== null
|
|
601
|
+
? storage.decoded[index]
|
|
602
|
+
: normaliseZero(storage.numbers[index]);
|
|
603
|
+
case "string":
|
|
604
|
+
return storage.strings.at(index);
|
|
605
|
+
case "mixed":
|
|
606
|
+
if (storage.decoded !== null) {
|
|
607
|
+
return storage.decoded[index];
|
|
608
|
+
}
|
|
609
|
+
return storage.tags[index] === 0
|
|
610
|
+
? normaliseZero(storage.numbers[index])
|
|
611
|
+
: storage.strings.at(index);
|
|
612
|
+
default:
|
|
613
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown id map kind ${String(storage.kind)}`, {
|
|
614
|
+
kind: storage.kind,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* E_DETACHED when a typed array the map reads from was transferred away (design sections 9.1
|
|
620
|
+
* and 11.3): derived from the array state (a transferred buffer leaves zero-length views).
|
|
621
|
+
*/
|
|
622
|
+
assertAttached() {
|
|
623
|
+
if (storageDetached(this.storage)) {
|
|
624
|
+
throw new GraphFormatError("E_DETACHED", "the id map's storage was transferred away", {
|
|
625
|
+
kind: this.storage.kind,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* The reverse map of a numeric / string / mixed map, built on first use (one pass over the ids,
|
|
631
|
+
* after a bulk decode of a wire-decoded Utf8 store) when it was not shared by the builder.
|
|
632
|
+
* @returns the map from id to index
|
|
633
|
+
*/
|
|
634
|
+
reverseMap() {
|
|
635
|
+
const { storage } = this;
|
|
636
|
+
if (storage.map === null) {
|
|
637
|
+
// the bulk decode below reads the Utf8 store: E_DETACHED, never a TypeError, after a transfer
|
|
638
|
+
this.assertAttached();
|
|
639
|
+
if (storage.decoded === null && storage.strings !== null) {
|
|
640
|
+
storage.strings.materialiseDecoded();
|
|
641
|
+
}
|
|
642
|
+
const map = new Map();
|
|
643
|
+
for (let i = 0; i < storage.size; i++) {
|
|
644
|
+
map.set(this.idAt(i), i);
|
|
645
|
+
}
|
|
646
|
+
storage.map = map;
|
|
647
|
+
}
|
|
648
|
+
return storage.map;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// ============================================================ factories
|
|
652
|
+
/**
|
|
653
|
+
* An identity map: id === index + offset, no storage at all (design section 4.2). What
|
|
654
|
+
* `fromEdgeArrays` without ids and `contract()` produce; offset 1 covers 1-based files.
|
|
655
|
+
* @param size - the node count
|
|
656
|
+
* @param offset - the integer added to every index; default 0
|
|
657
|
+
* @returns the map
|
|
658
|
+
*/
|
|
659
|
+
export function identityNodeIdMap(size, offset = 0) {
|
|
660
|
+
if (!Number.isInteger(size) || size < 0 || size > MAX_COUNT) {
|
|
661
|
+
throw new GraphFormatError("E_TOO_LARGE", `node count ${size} is not an integer in [0, MAX_COUNT]`, {
|
|
662
|
+
count: size,
|
|
663
|
+
max: MAX_COUNT,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
if (!Number.isSafeInteger(offset)) {
|
|
667
|
+
throw new GraphFormatError("E_INVALID_ID", `identity offset ${offset} is not a safe integer`, {
|
|
668
|
+
offset,
|
|
669
|
+
reason: "non-integer offset",
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
return new NodeIdMap(makeStorage("identity", size, { offset }));
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Build a map from decoded ids in index order, choosing the kind in one O(n) pass (design section
|
|
676
|
+
* 4.2, freeze step 9). With `options.map` (the builder's Map) the array and the Map are shared by
|
|
677
|
+
* reference and nothing is validated; identity and dense maps drop the Map. Without it the ids are
|
|
678
|
+
* validated and copied unless `validate` is false.
|
|
679
|
+
* @param ids - the ids; only entries below `size` are read
|
|
680
|
+
* @param size - the node count; default ids.length
|
|
681
|
+
* @param options - the shared map and validation switch
|
|
682
|
+
* @returns the map
|
|
683
|
+
*/
|
|
684
|
+
export function nodeIdMapFromIds(ids, size = ids.length, options = {}) {
|
|
685
|
+
if (!Number.isInteger(size) || size < 0 || size > MAX_COUNT) {
|
|
686
|
+
throw new GraphFormatError("E_TOO_LARGE", `node count ${size} is not an integer in [0, MAX_COUNT]`, {
|
|
687
|
+
count: size,
|
|
688
|
+
max: MAX_COUNT,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
const trusted = options.map !== undefined || options.validate === false;
|
|
692
|
+
let source = ids;
|
|
693
|
+
if (!trusted) {
|
|
694
|
+
const copy = new Array(size);
|
|
695
|
+
for (let i = 0; i < size; i++) {
|
|
696
|
+
copy[i] = validateNodeId(ids[i]);
|
|
697
|
+
}
|
|
698
|
+
source = copy;
|
|
699
|
+
}
|
|
700
|
+
const scan = scanIds(source, size);
|
|
701
|
+
const kind = kindOf(scan, size);
|
|
702
|
+
switch (kind) {
|
|
703
|
+
case "identity":
|
|
704
|
+
return new NodeIdMap(makeStorage("identity", size, { offset: size === 0 ? 0 : scan.offset }));
|
|
705
|
+
case "dense": {
|
|
706
|
+
const denseValues = denseValuesOf(source, size);
|
|
707
|
+
const denseInverse = buildDenseInverse(denseValues, scan.maxId, duplicateIdError);
|
|
708
|
+
return new NodeIdMap(makeStorage("dense", size, { denseValues, denseInverse }));
|
|
709
|
+
}
|
|
710
|
+
case "numeric":
|
|
711
|
+
case "mixed": {
|
|
712
|
+
const map = options.map ?? (trusted ? null : buildReverseMap(source, size, duplicateIdError));
|
|
713
|
+
return new NodeIdMap(makeStorage(kind, size, { decoded: source, map }));
|
|
714
|
+
}
|
|
715
|
+
case "string": {
|
|
716
|
+
const map = options.map ?? (trusted ? null : buildReverseMap(source, size, duplicateIdError));
|
|
717
|
+
const strings = Utf8Store.fromStrings(source, size);
|
|
718
|
+
return new NodeIdMap(makeStorage("string", size, { strings, map }));
|
|
719
|
+
}
|
|
720
|
+
default:
|
|
721
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown id map kind ${String(kind)}`, { kind });
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Build a map from numeric ids in a Float64Array (the `ids: F64` form of `fromEdgeArrays` /
|
|
726
|
+
* `fromCsr`), choosing identity, dense or numeric in one pass. A numeric map adopts the array as its
|
|
727
|
+
* typed representation.
|
|
728
|
+
* @param values - one id per index
|
|
729
|
+
* @param options - the validation switch
|
|
730
|
+
* @returns the map
|
|
731
|
+
*/
|
|
732
|
+
export function nodeIdMapFromF64(values, options = {}) {
|
|
733
|
+
const size = values.length;
|
|
734
|
+
if (size > MAX_COUNT) {
|
|
735
|
+
throw new GraphFormatError("E_TOO_LARGE", `node count ${size} exceeds MAX_COUNT`, {
|
|
736
|
+
count: size,
|
|
737
|
+
max: MAX_COUNT,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
const validate = options.validate !== false;
|
|
741
|
+
let numbers = values;
|
|
742
|
+
if (validate) {
|
|
743
|
+
let negativeZero = false;
|
|
744
|
+
for (let i = 0; i < size; i++) {
|
|
745
|
+
const v = values[i];
|
|
746
|
+
if (!Number.isFinite(v)) {
|
|
747
|
+
throw new GraphFormatError("E_INVALID_ID", `invalid node id ${String(v)} at index ${i}: not a finite number`, {
|
|
748
|
+
id: v,
|
|
749
|
+
index: i,
|
|
750
|
+
reason: "non-finite",
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
if (Object.is(v, -0)) {
|
|
754
|
+
negativeZero = true;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (negativeZero) {
|
|
758
|
+
numbers = values.slice();
|
|
759
|
+
for (let i = 0; i < size; i++) {
|
|
760
|
+
if (numbers[i] === 0) {
|
|
761
|
+
numbers[i] = 0;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const scan = scanIds(numbers, size);
|
|
767
|
+
const kind = kindOf(scan, size);
|
|
768
|
+
switch (kind) {
|
|
769
|
+
case "identity":
|
|
770
|
+
return new NodeIdMap(makeStorage("identity", size, { offset: size === 0 ? 0 : scan.offset }));
|
|
771
|
+
case "dense": {
|
|
772
|
+
const denseValues = denseValuesOf(numbers, size);
|
|
773
|
+
const denseInverse = buildDenseInverse(denseValues, scan.maxId, duplicateIdError);
|
|
774
|
+
return new NodeIdMap(makeStorage("dense", size, { denseValues, denseInverse }));
|
|
775
|
+
}
|
|
776
|
+
case "numeric": {
|
|
777
|
+
const map = validate ? buildReverseMap(numbers, size, duplicateIdError) : null;
|
|
778
|
+
return new NodeIdMap(makeStorage("numeric", size, { numbers, map }));
|
|
779
|
+
}
|
|
780
|
+
default:
|
|
781
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unexpected id map kind ${kind} for numeric ids`, { kind });
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* The distinct backing buffers of an id map's materialised typed storage (dense values and inverse,
|
|
786
|
+
* numbers, tags, an encoded Utf8 store), for the owner count of design section 9.1.
|
|
787
|
+
* @param map - the id map
|
|
788
|
+
* @returns the buffers, each once
|
|
789
|
+
*/
|
|
790
|
+
export function idMapBuffers(map) {
|
|
791
|
+
const storage = INTERNALS.get(map);
|
|
792
|
+
const out = new Set();
|
|
793
|
+
if (storage === undefined) {
|
|
794
|
+
// a foreign object standing in for a map (tests wrap one): nothing typed to claim
|
|
795
|
+
return [];
|
|
796
|
+
}
|
|
797
|
+
for (const view of [storage.denseValues, storage.denseInverse, storage.numbers, storage.tags]) {
|
|
798
|
+
if (view !== null) {
|
|
799
|
+
out.add(view.buffer);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (storage.strings !== null && storage.strings.encoded) {
|
|
803
|
+
out.add(storage.strings.offsets.buffer);
|
|
804
|
+
out.add(storage.strings.utf8.buffer);
|
|
805
|
+
}
|
|
806
|
+
return [...out];
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Build a map from its typed (wire) representation (design sections 4.5 and 9.5), adopting the arrays
|
|
810
|
+
* by reference. "structure" checks lengths, tag values, the dense bound and the Utf8 layout
|
|
811
|
+
* (E_BAD_SERIALIZATION with details.ref); "full" additionally checks invariant I11: finite numbers,
|
|
812
|
+
* well-formed UTF-8 (a fatal decode) and distinct ids (E_INVALID_SNAPSHOT with details.invariant
|
|
813
|
+
* "I11"), building the reverse map eagerly. A dense duplicate is always detected because the inverse
|
|
814
|
+
* array is rebuilt on load. An unknown kind is E_UNSUPPORTED with details.kind.
|
|
815
|
+
* @param parts - the typed representation
|
|
816
|
+
* @param validate - the validation level; default "structure"
|
|
817
|
+
* @returns the map
|
|
818
|
+
*/
|
|
819
|
+
export function nodeIdMapFromTyped(parts, validate = "structure") {
|
|
820
|
+
const { kind, size } = parts;
|
|
821
|
+
const structure = validate !== "none";
|
|
822
|
+
const full = validate === "full";
|
|
823
|
+
if (structure && (!Number.isInteger(size) || size < 0 || size > MAX_COUNT)) {
|
|
824
|
+
throw layoutError("ids.size", `size ${size} is not an integer in [0, MAX_COUNT]`, { found: size });
|
|
825
|
+
}
|
|
826
|
+
switch (kind) {
|
|
827
|
+
case "identity": {
|
|
828
|
+
// O(1) at every level: every id index + offset (the largest is offset + size - 1) must be a
|
|
829
|
+
// safe integer (I11)
|
|
830
|
+
if (!Number.isSafeInteger(parts.offset) || (size > 0 && !Number.isSafeInteger(parts.offset + size - 1))) {
|
|
831
|
+
const reason = `offset ${parts.offset} with ${size} ids leaves the safe-integer range`;
|
|
832
|
+
throw full
|
|
833
|
+
? bijectionError(reason, { found: parts.offset, size })
|
|
834
|
+
: layoutError("ids.offset", reason, { found: parts.offset, size });
|
|
835
|
+
}
|
|
836
|
+
return new NodeIdMap(makeStorage("identity", size, { offset: normaliseZero(parts.offset) }));
|
|
837
|
+
}
|
|
838
|
+
case "dense": {
|
|
839
|
+
const { values } = parts;
|
|
840
|
+
if (!(values instanceof Uint32Array)) {
|
|
841
|
+
throw layoutError("ids.values", "a dense id map needs u32 values");
|
|
842
|
+
}
|
|
843
|
+
if (structure && values.length !== size) {
|
|
844
|
+
throw layoutError("ids.values", `expected ${size} values, found ${values.length}`, {
|
|
845
|
+
expected: size,
|
|
846
|
+
found: values.length,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
let maxId = -1;
|
|
850
|
+
for (let i = 0; i < values.length; i++) {
|
|
851
|
+
if (values[i] > maxId) {
|
|
852
|
+
maxId = values[i];
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// the dense bounds are checked at every level: the scan is already paid, and the inverse
|
|
856
|
+
// array below is allocated over [0, maxId], which an unchecked id could make enormous
|
|
857
|
+
if (maxId >= MAX_COUNT) {
|
|
858
|
+
throw layoutError("ids.values", `dense id ${maxId} is not below MAX_COUNT`, { found: maxId });
|
|
859
|
+
}
|
|
860
|
+
if (maxId + 1 > 2 * Math.max(size, values.length)) {
|
|
861
|
+
throw layoutError("ids.values", `dense bound violated: max id ${maxId} with ${size} nodes`, {
|
|
862
|
+
maxId,
|
|
863
|
+
size,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
const denseInverse = buildDenseInverse(values, maxId, (id, first, second) => bijectionError(`duplicate id ${id}`, { id, indices: [first, second] }));
|
|
867
|
+
return new NodeIdMap(makeStorage("dense", size, { denseValues: values, denseInverse }));
|
|
868
|
+
}
|
|
869
|
+
case "numeric": {
|
|
870
|
+
const numbers = parts.values;
|
|
871
|
+
if (!(numbers instanceof Float64Array)) {
|
|
872
|
+
throw layoutError("ids.values", "a numeric id map needs f64 values");
|
|
873
|
+
}
|
|
874
|
+
if (structure && numbers.length !== size) {
|
|
875
|
+
throw layoutError("ids.values", `expected ${size} values, found ${numbers.length}`, {
|
|
876
|
+
expected: size,
|
|
877
|
+
found: numbers.length,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
let map = null;
|
|
881
|
+
if (full) {
|
|
882
|
+
for (let i = 0; i < size; i++) {
|
|
883
|
+
if (!Number.isFinite(numbers[i])) {
|
|
884
|
+
throw bijectionError(`non-finite id at index ${i}`, { index: i, id: numbers[i] });
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
map = buildReverseMap(numbers, size, (id, first, second) => bijectionError(`duplicate id ${describeId(id)}`, { id, indices: [first, second] }));
|
|
888
|
+
}
|
|
889
|
+
return new NodeIdMap(makeStorage("numeric", size, { numbers, map }));
|
|
890
|
+
}
|
|
891
|
+
case "string": {
|
|
892
|
+
const { offsets, utf8 } = parts;
|
|
893
|
+
if (offsets === null || utf8 === null) {
|
|
894
|
+
throw layoutError("ids.offsets", "a string id map needs offsets and utf8");
|
|
895
|
+
}
|
|
896
|
+
if (structure) {
|
|
897
|
+
checkUtf8Layout(offsets, utf8, size, "ids.offsets");
|
|
898
|
+
}
|
|
899
|
+
const strings = Utf8Store.fromEncoded(offsets, utf8);
|
|
900
|
+
let map = null;
|
|
901
|
+
if (full) {
|
|
902
|
+
const bad = strings.firstMalformedRow();
|
|
903
|
+
if (bad !== -1) {
|
|
904
|
+
throw bijectionError(`malformed UTF-8 in id at index ${bad}`, { index: bad });
|
|
905
|
+
}
|
|
906
|
+
map = buildReverseMap(strings.decodeAll(), size, (id, first, second) => bijectionError(`duplicate id ${describeId(id)}`, { id, indices: [first, second] }));
|
|
907
|
+
}
|
|
908
|
+
return new NodeIdMap(makeStorage("string", size, { strings, map }));
|
|
909
|
+
}
|
|
910
|
+
case "mixed": {
|
|
911
|
+
const { tags, numbers, offsets, utf8 } = parts;
|
|
912
|
+
if (tags === null || numbers === null || offsets === null || utf8 === null) {
|
|
913
|
+
throw layoutError("ids.tags", "a mixed id map needs tags, numbers, offsets and utf8");
|
|
914
|
+
}
|
|
915
|
+
if (structure) {
|
|
916
|
+
if (tags.length !== size) {
|
|
917
|
+
throw layoutError("ids.tags", `expected ${size} tags, found ${tags.length}`, {
|
|
918
|
+
expected: size,
|
|
919
|
+
found: tags.length,
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
if (numbers.length !== size) {
|
|
923
|
+
throw layoutError("ids.numbers", `expected ${size} numbers, found ${numbers.length}`, {
|
|
924
|
+
expected: size,
|
|
925
|
+
found: numbers.length,
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
for (let i = 0; i < size; i++) {
|
|
929
|
+
if (tags[i] > 1) {
|
|
930
|
+
throw layoutError("ids.tags", `tag ${tags[i]} at index ${i} is not 0 or 1`, {
|
|
931
|
+
index: i,
|
|
932
|
+
found: tags[i],
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
checkUtf8Layout(offsets, utf8, size, "ids.offsets");
|
|
937
|
+
}
|
|
938
|
+
const strings = Utf8Store.fromEncoded(offsets, utf8);
|
|
939
|
+
let map = null;
|
|
940
|
+
if (full) {
|
|
941
|
+
const bad = strings.firstMalformedRow();
|
|
942
|
+
if (bad !== -1) {
|
|
943
|
+
throw bijectionError(`malformed UTF-8 in id at index ${bad}`, { index: bad });
|
|
944
|
+
}
|
|
945
|
+
const decoded = new Array(size);
|
|
946
|
+
for (let i = 0; i < size; i++) {
|
|
947
|
+
if (tags[i] === 0) {
|
|
948
|
+
if (!Number.isFinite(numbers[i])) {
|
|
949
|
+
throw bijectionError(`non-finite id at index ${i}`, { index: i, id: numbers[i] });
|
|
950
|
+
}
|
|
951
|
+
decoded[i] = numbers[i];
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
decoded[i] = strings.at(i);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
map = buildReverseMap(decoded, size, (id, first, second) => bijectionError(`duplicate id ${describeId(id)}`, { id, indices: [first, second] }));
|
|
958
|
+
}
|
|
959
|
+
return new NodeIdMap(makeStorage("mixed", size, { tags, numbers, strings, map }));
|
|
960
|
+
}
|
|
961
|
+
default:
|
|
962
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown id map kind ${String(kind)}`, { kind });
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
// ============================================================ typed form, gather, remap
|
|
966
|
+
/**
|
|
967
|
+
* The typed (wire) representation of a map (design section 4.5), materialising and caching whatever
|
|
968
|
+
* the map only held in decoded form: the F64 of a numeric map, the Utf8 store of a string map, the
|
|
969
|
+
* tags + numbers + Utf8 store of a mixed map. The dense inverse and the reverse Map are never part
|
|
970
|
+
* of it. Repeated calls return the same arrays.
|
|
971
|
+
* @param map - the id map
|
|
972
|
+
* @returns the typed parts; every slot the kind does not use is null
|
|
973
|
+
*/
|
|
974
|
+
export function nodeIdMapToTyped(map) {
|
|
975
|
+
const storage = storageOf(map);
|
|
976
|
+
const { kind, size } = storage;
|
|
977
|
+
switch (kind) {
|
|
978
|
+
case "identity":
|
|
979
|
+
return {
|
|
980
|
+
kind,
|
|
981
|
+
size,
|
|
982
|
+
offset: storage.offset,
|
|
983
|
+
values: null,
|
|
984
|
+
tags: null,
|
|
985
|
+
numbers: null,
|
|
986
|
+
offsets: null,
|
|
987
|
+
utf8: null,
|
|
988
|
+
};
|
|
989
|
+
case "dense":
|
|
990
|
+
return {
|
|
991
|
+
kind,
|
|
992
|
+
size,
|
|
993
|
+
offset: 0,
|
|
994
|
+
values: storage.denseValues,
|
|
995
|
+
tags: null,
|
|
996
|
+
numbers: null,
|
|
997
|
+
offsets: null,
|
|
998
|
+
utf8: null,
|
|
999
|
+
};
|
|
1000
|
+
case "numeric": {
|
|
1001
|
+
if (storage.numbers === null) {
|
|
1002
|
+
const decoded = storage.decoded;
|
|
1003
|
+
const numbers = new Float64Array(size);
|
|
1004
|
+
for (let i = 0; i < size; i++) {
|
|
1005
|
+
const id = decoded[i];
|
|
1006
|
+
if (typeof id !== "number") {
|
|
1007
|
+
throw new GraphFormatError("E_INVALID_ID", `numeric id map holds a ${typeof id} at index ${i}`, {
|
|
1008
|
+
index: i,
|
|
1009
|
+
found: typeof id,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
numbers[i] = id;
|
|
1013
|
+
}
|
|
1014
|
+
storage.numbers = numbers;
|
|
1015
|
+
claimHolder(numbers.buffer);
|
|
1016
|
+
}
|
|
1017
|
+
return {
|
|
1018
|
+
kind,
|
|
1019
|
+
size,
|
|
1020
|
+
offset: 0,
|
|
1021
|
+
values: storage.numbers,
|
|
1022
|
+
tags: null,
|
|
1023
|
+
numbers: null,
|
|
1024
|
+
offsets: null,
|
|
1025
|
+
utf8: null,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
case "string": {
|
|
1029
|
+
const strings = storage.strings;
|
|
1030
|
+
return {
|
|
1031
|
+
kind,
|
|
1032
|
+
size,
|
|
1033
|
+
offset: 0,
|
|
1034
|
+
values: null,
|
|
1035
|
+
tags: null,
|
|
1036
|
+
numbers: null,
|
|
1037
|
+
offsets: strings.offsets,
|
|
1038
|
+
utf8: strings.utf8,
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
case "mixed": {
|
|
1042
|
+
if (storage.tags === null || storage.numbers === null || storage.strings === null) {
|
|
1043
|
+
const decoded = storage.decoded;
|
|
1044
|
+
const tags = new Uint8Array(size);
|
|
1045
|
+
const numbers = new Float64Array(size);
|
|
1046
|
+
for (let i = 0; i < size; i++) {
|
|
1047
|
+
const id = decoded[i];
|
|
1048
|
+
if (typeof id === "number") {
|
|
1049
|
+
numbers[i] = id;
|
|
1050
|
+
}
|
|
1051
|
+
else {
|
|
1052
|
+
tags[i] = 1;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const { offsets, utf8 } = encodeUtf8Rows(decoded, size);
|
|
1056
|
+
storage.tags = tags;
|
|
1057
|
+
storage.numbers = numbers;
|
|
1058
|
+
storage.strings = Utf8Store.fromEncoded(offsets, utf8);
|
|
1059
|
+
// the map is the first holder of the buffers it materialises (design section 9.1)
|
|
1060
|
+
for (const buffer of [tags.buffer, numbers.buffer, offsets.buffer, utf8.buffer]) {
|
|
1061
|
+
claimHolder(buffer);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return {
|
|
1065
|
+
kind,
|
|
1066
|
+
size,
|
|
1067
|
+
offset: 0,
|
|
1068
|
+
values: null,
|
|
1069
|
+
tags: storage.tags,
|
|
1070
|
+
numbers: storage.numbers,
|
|
1071
|
+
offsets: storage.strings.offsets,
|
|
1072
|
+
utf8: storage.strings.utf8,
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
default:
|
|
1076
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown id map kind ${String(kind)}`, { kind });
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* The map of a gathered node space: `out.idOf(i) === map.idOf(indexMap[i])` (design section 7.3:
|
|
1081
|
+
* inducedSubgraph, relabel). The kind is re-detected from the gathered ids, so an identity map
|
|
1082
|
+
* gathered through a prefix stays identity and otherwise becomes dense or numeric per the rules of
|
|
1083
|
+
* design section 4.2. The caller guarantees indexMap has no repeats (a permutation or a selection),
|
|
1084
|
+
* so the result is a bijection without re-validation.
|
|
1085
|
+
* @param map - the source map
|
|
1086
|
+
* @param indexMap - new index -> source index; E_INDEX_RANGE when an entry is out of range
|
|
1087
|
+
* @returns the gathered map
|
|
1088
|
+
*/
|
|
1089
|
+
export function gatherNodeIdMap(map, indexMap) {
|
|
1090
|
+
const n = indexMap.length;
|
|
1091
|
+
const ids = new Array(n);
|
|
1092
|
+
for (let i = 0; i < n; i++) {
|
|
1093
|
+
ids[i] = map.idOf(indexMap[i]);
|
|
1094
|
+
}
|
|
1095
|
+
return nodeIdMapFromIds(ids, n, { validate: false });
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* The map of a compacted node space: `out.idOf(remap[old]) === map.idOf(old)` for every old index
|
|
1099
|
+
* whose entry is not INVALID_INDEX (design sections 4.4 and 6.3 step 1). Every new index must be hit
|
|
1100
|
+
* exactly once.
|
|
1101
|
+
* @param map - the source map
|
|
1102
|
+
* @param remap - old index -> new index or INVALID_INDEX; length === map.size
|
|
1103
|
+
* @param newSize - the compacted node count
|
|
1104
|
+
* @returns the remapped map; E_INDEX_RANGE when remap is not a surjection onto [0, newSize)
|
|
1105
|
+
*/
|
|
1106
|
+
export function remapNodeIdMap(map, remap, newSize) {
|
|
1107
|
+
if (remap.length !== map.size) {
|
|
1108
|
+
throw new GraphFormatError("E_INDEX_RANGE", `remap has ${remap.length} entries for ${map.size} ids`, {
|
|
1109
|
+
expected: map.size,
|
|
1110
|
+
found: remap.length,
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
const ids = new Array(newSize);
|
|
1114
|
+
const seen = new Uint8Array(newSize);
|
|
1115
|
+
let filled = 0;
|
|
1116
|
+
for (let old = 0; old < remap.length; old++) {
|
|
1117
|
+
const target = remap[old];
|
|
1118
|
+
if (target === INVALID_INDEX) {
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
if (target >= newSize || seen[target] === 1) {
|
|
1122
|
+
throw new GraphFormatError("E_INDEX_RANGE", `remap entry ${target} at ${old} is out of range or repeated`, {
|
|
1123
|
+
index: target,
|
|
1124
|
+
size: newSize,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
seen[target] = 1;
|
|
1128
|
+
ids[target] = map.idOf(old);
|
|
1129
|
+
filled++;
|
|
1130
|
+
}
|
|
1131
|
+
if (filled !== newSize) {
|
|
1132
|
+
throw new GraphFormatError("E_INDEX_RANGE", `remap fills ${filled} of ${newSize} new indices`, {
|
|
1133
|
+
filled,
|
|
1134
|
+
size: newSize,
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
return nodeIdMapFromIds(ids, newSize, { validate: false });
|
|
1138
|
+
}
|
|
1139
|
+
//# sourceMappingURL=node-id-map.js.map
|