@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.
Files changed (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -43
  3. package/dist/graph-format.d.ts +1 -0
  4. package/dist/graph-format.d.ts.map +1 -0
  5. package/dist/graph-format.js +15798 -0
  6. package/dist/graph-format.js.map +1 -0
  7. package/dist/src/builder/arena.d.ts +85 -0
  8. package/dist/src/builder/arena.d.ts.map +1 -0
  9. package/dist/src/builder/arena.js +154 -0
  10. package/dist/src/builder/arena.js.map +1 -0
  11. package/dist/src/builder/compact.d.ts +515 -0
  12. package/dist/src/builder/compact.d.ts.map +1 -0
  13. package/dist/src/builder/compact.js +1553 -0
  14. package/dist/src/builder/compact.js.map +1 -0
  15. package/dist/src/builder/counting-sort.d.ts +96 -0
  16. package/dist/src/builder/counting-sort.d.ts.map +1 -0
  17. package/dist/src/builder/counting-sort.js +430 -0
  18. package/dist/src/builder/counting-sort.js.map +1 -0
  19. package/dist/src/builder/freeze.d.ts +72 -0
  20. package/dist/src/builder/freeze.d.ts.map +1 -0
  21. package/dist/src/builder/freeze.js +548 -0
  22. package/dist/src/builder/freeze.js.map +1 -0
  23. package/dist/src/builder/graph-builder.d.ts +602 -0
  24. package/dist/src/builder/graph-builder.d.ts.map +1 -0
  25. package/dist/src/builder/graph-builder.js +1940 -0
  26. package/dist/src/builder/graph-builder.js.map +1 -0
  27. package/dist/src/builder/options.d.ts +16 -0
  28. package/dist/src/builder/options.d.ts.map +1 -0
  29. package/dist/src/builder/options.js +21 -0
  30. package/dist/src/builder/options.js.map +1 -0
  31. package/dist/src/columns/bitmap.d.ts +96 -0
  32. package/dist/src/columns/bitmap.d.ts.map +1 -0
  33. package/dist/src/columns/bitmap.js +184 -0
  34. package/dist/src/columns/bitmap.js.map +1 -0
  35. package/dist/src/columns/column.d.ts +220 -0
  36. package/dist/src/columns/column.d.ts.map +1 -0
  37. package/dist/src/columns/column.js +2109 -0
  38. package/dist/src/columns/column.js.map +1 -0
  39. package/dist/src/columns/dictionary.d.ts +60 -0
  40. package/dist/src/columns/dictionary.d.ts.map +1 -0
  41. package/dist/src/columns/dictionary.js +102 -0
  42. package/dist/src/columns/dictionary.js.map +1 -0
  43. package/dist/src/columns/growable.d.ts +198 -0
  44. package/dist/src/columns/growable.d.ts.map +1 -0
  45. package/dist/src/columns/growable.js +296 -0
  46. package/dist/src/columns/growable.js.map +1 -0
  47. package/dist/src/columns/infer.d.ts +121 -0
  48. package/dist/src/columns/infer.d.ts.map +1 -0
  49. package/dist/src/columns/infer.js +268 -0
  50. package/dist/src/columns/infer.js.map +1 -0
  51. package/dist/src/columns/remap.d.ts +143 -0
  52. package/dist/src/columns/remap.d.ts.map +1 -0
  53. package/dist/src/columns/remap.js +588 -0
  54. package/dist/src/columns/remap.js.map +1 -0
  55. package/dist/src/columns/table.d.ts +184 -0
  56. package/dist/src/columns/table.d.ts.map +1 -0
  57. package/dist/src/columns/table.js +485 -0
  58. package/dist/src/columns/table.js.map +1 -0
  59. package/dist/src/constants.d.ts +34 -0
  60. package/dist/src/constants.d.ts.map +1 -0
  61. package/dist/src/constants.js +79 -0
  62. package/dist/src/constants.js.map +1 -0
  63. package/dist/src/errors.d.ts +76 -0
  64. package/dist/src/errors.d.ts.map +1 -0
  65. package/dist/src/errors.js +32 -0
  66. package/dist/src/errors.js.map +1 -0
  67. package/dist/src/ids/edge-id-index.d.ts +77 -0
  68. package/dist/src/ids/edge-id-index.d.ts.map +1 -0
  69. package/dist/src/ids/edge-id-index.js +155 -0
  70. package/dist/src/ids/edge-id-index.js.map +1 -0
  71. package/dist/src/ids/node-id-map.d.ts +280 -0
  72. package/dist/src/ids/node-id-map.d.ts.map +1 -0
  73. package/dist/src/ids/node-id-map.js +1139 -0
  74. package/dist/src/ids/node-id-map.js.map +1 -0
  75. package/dist/src/ids/string-store.d.ts +219 -0
  76. package/dist/src/ids/string-store.d.ts.map +1 -0
  77. package/dist/src/ids/string-store.js +526 -0
  78. package/dist/src/ids/string-store.js.map +1 -0
  79. package/dist/src/index.d.ts +30 -0
  80. package/dist/src/index.d.ts.map +1 -0
  81. package/dist/src/index.js +29 -0
  82. package/dist/src/index.js.map +1 -0
  83. package/dist/src/populate/from-csr.d.ts +61 -0
  84. package/dist/src/populate/from-csr.d.ts.map +1 -0
  85. package/dist/src/populate/from-csr.js +706 -0
  86. package/dist/src/populate/from-csr.js.map +1 -0
  87. package/dist/src/populate/from-edge-arrays.d.ts +51 -0
  88. package/dist/src/populate/from-edge-arrays.d.ts.map +1 -0
  89. package/dist/src/populate/from-edge-arrays.js +217 -0
  90. package/dist/src/populate/from-edge-arrays.js.map +1 -0
  91. package/dist/src/populate/from-records.d.ts +51 -0
  92. package/dist/src/populate/from-records.d.ts.map +1 -0
  93. package/dist/src/populate/from-records.js +447 -0
  94. package/dist/src/populate/from-records.js.map +1 -0
  95. package/dist/src/snapshot/derived.d.ts +211 -0
  96. package/dist/src/snapshot/derived.d.ts.map +1 -0
  97. package/dist/src/snapshot/derived.js +1254 -0
  98. package/dist/src/snapshot/derived.js.map +1 -0
  99. package/dist/src/snapshot/graph-meta.d.ts +23 -0
  100. package/dist/src/snapshot/graph-meta.d.ts.map +1 -0
  101. package/dist/src/snapshot/graph-meta.js +159 -0
  102. package/dist/src/snapshot/graph-meta.js.map +1 -0
  103. package/dist/src/snapshot/graph-snapshot.d.ts +509 -0
  104. package/dist/src/snapshot/graph-snapshot.d.ts.map +1 -0
  105. package/dist/src/snapshot/graph-snapshot.js +1247 -0
  106. package/dist/src/snapshot/graph-snapshot.js.map +1 -0
  107. package/dist/src/snapshot/hash.d.ts +90 -0
  108. package/dist/src/snapshot/hash.d.ts.map +1 -0
  109. package/dist/src/snapshot/hash.js +228 -0
  110. package/dist/src/snapshot/hash.js.map +1 -0
  111. package/dist/src/snapshot/queries.d.ts +74 -0
  112. package/dist/src/snapshot/queries.d.ts.map +1 -0
  113. package/dist/src/snapshot/queries.js +131 -0
  114. package/dist/src/snapshot/queries.js.map +1 -0
  115. package/dist/src/snapshot/validate.d.ts +192 -0
  116. package/dist/src/snapshot/validate.d.ts.map +1 -0
  117. package/dist/src/snapshot/validate.js +990 -0
  118. package/dist/src/snapshot/validate.js.map +1 -0
  119. package/dist/src/snapshot/views.d.ts +274 -0
  120. package/dist/src/snapshot/views.d.ts.map +1 -0
  121. package/dist/src/snapshot/views.js +856 -0
  122. package/dist/src/snapshot/views.js.map +1 -0
  123. package/dist/src/types/builder.d.ts +456 -0
  124. package/dist/src/types/builder.d.ts.map +1 -0
  125. package/dist/src/types/builder.js +11 -0
  126. package/dist/src/types/builder.js.map +1 -0
  127. package/dist/src/types/columns.d.ts +615 -0
  128. package/dist/src/types/columns.d.ts.map +1 -0
  129. package/dist/src/types/columns.js +14 -0
  130. package/dist/src/types/columns.js.map +1 -0
  131. package/dist/src/types/index.d.ts +14 -0
  132. package/dist/src/types/index.d.ts.map +1 -0
  133. package/dist/src/types/index.js +11 -0
  134. package/dist/src/types/index.js.map +1 -0
  135. package/dist/src/types/internal.d.ts +172 -0
  136. package/dist/src/types/internal.d.ts.map +1 -0
  137. package/dist/src/types/internal.js +13 -0
  138. package/dist/src/types/internal.js.map +1 -0
  139. package/dist/src/types/snapshot.d.ts +801 -0
  140. package/dist/src/types/snapshot.d.ts.map +1 -0
  141. package/dist/src/types/snapshot.js +14 -0
  142. package/dist/src/types/snapshot.js.map +1 -0
  143. package/dist/src/types/wire.d.ts +196 -0
  144. package/dist/src/types/wire.d.ts.map +1 -0
  145. package/dist/src/types/wire.js +8 -0
  146. package/dist/src/types/wire.js.map +1 -0
  147. package/dist/src/util/mask.d.ts +55 -0
  148. package/dist/src/util/mask.d.ts.map +1 -0
  149. package/dist/src/util/mask.js +71 -0
  150. package/dist/src/util/mask.js.map +1 -0
  151. package/dist/src/util/options.d.ts +22 -0
  152. package/dist/src/util/options.d.ts.map +1 -0
  153. package/dist/src/util/options.js +49 -0
  154. package/dist/src/util/options.js.map +1 -0
  155. package/dist/src/util/shared-buffers.d.ts +42 -0
  156. package/dist/src/util/shared-buffers.d.ts.map +1 -0
  157. package/dist/src/util/shared-buffers.js +52 -0
  158. package/dist/src/util/shared-buffers.js.map +1 -0
  159. package/dist/src/util/typed-array.d.ts +205 -0
  160. package/dist/src/util/typed-array.d.ts.map +1 -0
  161. package/dist/src/util/typed-array.js +289 -0
  162. package/dist/src/util/typed-array.js.map +1 -0
  163. package/dist/src/wire/bytes.d.ts +69 -0
  164. package/dist/src/wire/bytes.d.ts.map +1 -0
  165. package/dist/src/wire/bytes.js +361 -0
  166. package/dist/src/wire/bytes.js.map +1 -0
  167. package/dist/src/wire/carried-views.d.ts +111 -0
  168. package/dist/src/wire/carried-views.d.ts.map +1 -0
  169. package/dist/src/wire/carried-views.js +286 -0
  170. package/dist/src/wire/carried-views.js.map +1 -0
  171. package/dist/src/wire/from-wire.d.ts +144 -0
  172. package/dist/src/wire/from-wire.d.ts.map +1 -0
  173. package/dist/src/wire/from-wire.js +1425 -0
  174. package/dist/src/wire/from-wire.js.map +1 -0
  175. package/dist/src/wire/to-wire.d.ts +171 -0
  176. package/dist/src/wire/to-wire.d.ts.map +1 -0
  177. package/dist/src/wire/to-wire.js +652 -0
  178. package/dist/src/wire/to-wire.js.map +1 -0
  179. package/dist/tsconfig.build.tsbuildinfo +1 -0
  180. package/package.json +72 -7
  181. package/src/builder/arena.ts +192 -0
  182. package/src/builder/compact.ts +1811 -0
  183. package/src/builder/counting-sort.ts +506 -0
  184. package/src/builder/freeze.ts +689 -0
  185. package/src/builder/graph-builder.ts +2264 -0
  186. package/src/builder/options.ts +29 -0
  187. package/src/columns/bitmap.ts +196 -0
  188. package/src/columns/column.ts +2574 -0
  189. package/src/columns/dictionary.ts +114 -0
  190. package/src/columns/growable.ts +350 -0
  191. package/src/columns/infer.ts +285 -0
  192. package/src/columns/remap.ts +663 -0
  193. package/src/columns/table.ts +568 -0
  194. package/src/constants.ts +89 -0
  195. package/src/errors.ts +115 -0
  196. package/src/ids/edge-id-index.ts +179 -0
  197. package/src/ids/node-id-map.ts +1340 -0
  198. package/src/ids/string-store.ts +589 -0
  199. package/src/index.ts +139 -0
  200. package/src/lib-resizable-array-buffer.d.ts +34 -0
  201. package/src/populate/from-csr.ts +802 -0
  202. package/src/populate/from-edge-arrays.ts +257 -0
  203. package/src/populate/from-records.ts +536 -0
  204. package/src/snapshot/derived.ts +1486 -0
  205. package/src/snapshot/graph-meta.ts +167 -0
  206. package/src/snapshot/graph-snapshot.ts +1503 -0
  207. package/src/snapshot/hash.ts +241 -0
  208. package/src/snapshot/queries.ts +136 -0
  209. package/src/snapshot/validate.ts +1108 -0
  210. package/src/snapshot/views.ts +943 -0
  211. package/src/types/builder.ts +480 -0
  212. package/src/types/columns.ts +727 -0
  213. package/src/types/index.ts +14 -0
  214. package/src/types/internal.ts +195 -0
  215. package/src/types/snapshot.ts +886 -0
  216. package/src/types/wire.ts +207 -0
  217. package/src/util/mask.ts +89 -0
  218. package/src/util/options.ts +57 -0
  219. package/src/util/shared-buffers.ts +56 -0
  220. package/src/util/typed-array.ts +371 -0
  221. package/src/wire/bytes.ts +440 -0
  222. package/src/wire/carried-views.ts +327 -0
  223. package/src/wire/from-wire.ts +1690 -0
  224. 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