@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,1690 @@
1
+ /**
2
+ * The wire decoder (design sections 4.5, 5.9, 9.1, 9.5 and 11.3): `fromWire()` rebuilds a snapshot
3
+ * from a WireSnapshot, and `decodeManifest()` is the shared reader the GSNP container functions of
4
+ * bytes.ts drive with their own buffer regions.
5
+ *
6
+ * Rules (design section 9.1): a manifest whose `formatVersion` is not the reader's or whose wire
7
+ * major is unknown is refused with E_UNSUPPORTED_VERSION; a newer wire minor is read by ignoring
8
+ * unknown manifest fields; an unknown column dtype is E_UNSUPPORTED unless `unknownColumns: "skip"`
9
+ * drops the column and names it in `meta.extra["graphty.skippedColumns"]`; an unknown id-map kind
10
+ * is always E_UNSUPPORTED; unknown view entries are ignored and recomputed. Every WireBufferRef is
11
+ * checked against its buffer before a typed array is built over it (E_BAD_SERIALIZATION with
12
+ * details.ref), whatever the validation level, because a RangeError from a typed-array constructor
13
+ * is never an acceptable failure mode; the "structure" and "full" levels add the checks of design
14
+ * section 9.5 and then run the snapshot's own validate().
15
+ *
16
+ * Buffers are adopted by reference by default: the arrays are views into the caller's buffers and
17
+ * the arena descriptor is honoured. A SharedArrayBuffer is copied (decision D-SAB), and `copy: true`
18
+ * copies every buffer before adoption.
19
+ */
20
+
21
+ import { CORE_ORDER } from "../builder/arena.js";
22
+ import { createColumn, emptyParts, isPlainObject, resolveColumnMeta } from "../columns/column.js";
23
+ import { AttributeTable } from "../columns/table.js";
24
+ import { ALIGNMENT, FORMAT_VERSION, MAX_COUNT, WIRE_FORMAT, WIRE_MAJOR } from "../constants.js";
25
+ import { GraphFormatError } from "../errors.js";
26
+ import { type NodeIdMap, nodeIdMapFromTyped, type NodeIdMapTypedParts } from "../ids/node-id-map.js";
27
+ import { checkUtf8Layout, decodeUtf8Rows, Utf8Store } from "../ids/string-store.js";
28
+ import { createSnapshot, type GraphSnapshot, seedView } from "../snapshot/graph-snapshot.js";
29
+ import { cooViewOf, edgeListViewOf, ReverseAdjacency, rowLengths } from "../snapshot/views.js";
30
+ import {
31
+ type ArenaLayout,
32
+ type ArenaSegment,
33
+ type Column,
34
+ type ColumnDeclPatch,
35
+ type ColumnDomain,
36
+ type ColumnMeta,
37
+ type ColumnOrigin,
38
+ type ColumnOriginInput,
39
+ type CoreArrayName,
40
+ type Dtype,
41
+ type F32,
42
+ type F64,
43
+ type FromWireOptions,
44
+ type GraphMeta,
45
+ type NodeIdMapKind,
46
+ type ScalarDtype,
47
+ type SnapshotFlags,
48
+ type TypedArrayData,
49
+ type U8,
50
+ type U32,
51
+ type ValidationLevel,
52
+ type WireBufferRef,
53
+ type WireDtype,
54
+ type WireSnapshot,
55
+ } from "../types/index.js";
56
+ import { type MutableColumnParts, type SnapshotParts } from "../types/internal.js";
57
+ import { canViewAsPaddedU32, copyToPaddedStore, padTo4 } from "../util/typed-array.js";
58
+ import {
59
+ checkCooSrc,
60
+ checkDegree,
61
+ checkDegreeOrder,
62
+ checkEdgeList,
63
+ checkF64View,
64
+ checkInDegree,
65
+ checkMate,
66
+ checkOutDegree,
67
+ checkReverse,
68
+ checkSelfLoopArcs,
69
+ checkSelfLoopsPerNode,
70
+ CoreFacts,
71
+ } from "./carried-views.js";
72
+ import { defineJsonKey } from "./to-wire.js";
73
+
74
+ // ============================================================ vocabularies
75
+
76
+ const WIRE_DTYPES: ReadonlySet<string> = new Set(["u32", "i32", "f32", "f64", "u8", "utf8"]);
77
+ const COLUMN_DTYPES: ReadonlySet<string> = new Set([
78
+ "f32",
79
+ "f64",
80
+ "i32",
81
+ "u32",
82
+ "u8",
83
+ "bool",
84
+ "dict",
85
+ "string",
86
+ "list",
87
+ "json",
88
+ ]);
89
+ const ID_MAP_KINDS: ReadonlySet<string> = new Set(["identity", "dense", "numeric", "string", "mixed"]);
90
+ const ID_TYPES: ReadonlySet<GraphMeta["idType"] & string> = new Set(["string", "integer", "mixed"] as const);
91
+ const TIME_FORMATS: ReadonlySet<GraphMeta["timeFormat"] & string> = new Set([
92
+ "integer",
93
+ "double",
94
+ "date",
95
+ "dateTime",
96
+ ] as const);
97
+ const TIME_REPRESENTATIONS: ReadonlySet<GraphMeta["timeRepresentation"] & string> = new Set([
98
+ "interval",
99
+ "timestamp",
100
+ ] as const);
101
+ const MODES: ReadonlySet<GraphMeta["mode"] & string> = new Set(["static", "dynamic", "slice"] as const);
102
+ const FORBIDDEN_KEYS: ReadonlySet<string> = new Set(["__proto__", "constructor", "prototype"]);
103
+ const NUM_TAG = "$num";
104
+ const ESC_TAG = "$esc";
105
+ /** The deepest JSON nesting a manifest value may have; deeper input is refused instead of overflowing the stack. */
106
+ const MAX_JSON_DEPTH = 256;
107
+
108
+ /**
109
+ * A short plain-ASCII rendering of an untrusted manifest value for an error message: strings are
110
+ * quoted, everything else is named by type (a bigint or a null-prototype object cannot be
111
+ * stringified, and a structuredClone-delivered manifest can carry both).
112
+ * @param value - the value
113
+ * @returns the rendering
114
+ */
115
+ function describe(value: unknown): string {
116
+ if (typeof value === "string") {
117
+ return JSON.stringify(value.length > 40 ? `${value.slice(0, 37)}...` : value);
118
+ }
119
+ if (typeof value === "number" || typeof value === "boolean" || value === null) {
120
+ return String(value);
121
+ }
122
+ return value === undefined ? "undefined" : `a ${typeof value}`;
123
+ }
124
+
125
+ /** The meta.extra key naming the columns dropped under unknownColumns: "skip" (design section 9.1). */
126
+ export const SKIPPED_COLUMNS_KEY = "graphty.skippedColumns";
127
+
128
+ // ============================================================ errors and shape readers
129
+
130
+ /**
131
+ * The E_BAD_SERIALIZATION error of a malformed manifest member or buffer reference.
132
+ * @param ref - the manifest path, e.g. "nodeColumns[2].data"
133
+ * @param reason - what is wrong
134
+ * @param details - extra context
135
+ * @returns the error
136
+ */
137
+ export function badWire(
138
+ ref: string,
139
+ reason: string,
140
+ details: Readonly<Record<string, unknown>> = {},
141
+ ): GraphFormatError {
142
+ return new GraphFormatError("E_BAD_SERIALIZATION", `${ref}: ${reason}`, { ref, reason, ...details });
143
+ }
144
+
145
+ function asObject(value: unknown, path: string): Record<string, unknown> {
146
+ if (!isPlainObject(value)) {
147
+ throw badWire(path, "expected an object");
148
+ }
149
+ return value;
150
+ }
151
+
152
+ function asArray(value: unknown, path: string): readonly unknown[] {
153
+ if (!Array.isArray(value)) {
154
+ throw badWire(path, "expected an array");
155
+ }
156
+ return value as readonly unknown[];
157
+ }
158
+
159
+ function asString(value: unknown, path: string): string {
160
+ if (typeof value !== "string") {
161
+ throw badWire(path, "expected a string");
162
+ }
163
+ return value;
164
+ }
165
+
166
+ function asStringOrNull(value: unknown, path: string): string | null {
167
+ if (value === undefined || value === null) {
168
+ return null;
169
+ }
170
+ return asString(value, path);
171
+ }
172
+
173
+ function asBoolean(value: unknown, path: string): boolean {
174
+ if (typeof value !== "boolean") {
175
+ throw badWire(path, "expected a boolean");
176
+ }
177
+ return value;
178
+ }
179
+
180
+ function asBooleanOrNull(value: unknown, path: string): boolean | null {
181
+ if (value === undefined || value === null) {
182
+ return null;
183
+ }
184
+ return asBoolean(value, path);
185
+ }
186
+
187
+ function asCount(value: unknown, path: string): number {
188
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > MAX_COUNT) {
189
+ throw badWire(path, `expected an integer in [0, ${MAX_COUNT}]`, { found: value });
190
+ }
191
+ // JSON.parse("-0") is -0; a count is stored as 0 (design section 4.1)
192
+ return value === 0 ? 0 : value;
193
+ }
194
+
195
+ function asEnum<T extends string>(value: unknown, path: string, allowed: ReadonlySet<T>): T | null {
196
+ if (value === undefined || value === null) {
197
+ return null;
198
+ }
199
+ if (typeof value !== "string" || !(allowed as ReadonlySet<string>).has(value)) {
200
+ throw badWire(path, `unexpected value ${describe(value)}`, { found: value });
201
+ }
202
+ return value as T;
203
+ }
204
+
205
+ /**
206
+ * Whether a string names a column dtype this reader knows.
207
+ * @param value - the dtype text
208
+ * @returns true for one of the ten dtypes
209
+ */
210
+ function isKnownDtype(value: unknown): value is Dtype {
211
+ return typeof value === "string" && COLUMN_DTYPES.has(value);
212
+ }
213
+
214
+ // ============================================================ JSON values (5.9)
215
+
216
+ /**
217
+ * Decode a manifest JSON value (design section 5.9): `{ "$num": "Infinity" | "-Infinity" | "NaN" |
218
+ * "-0" }` becomes the number, `{ "$esc": { ... } }` unwraps a user object whose only key collided
219
+ * with a tag (the writer's escape), arrays and plain objects are rebuilt recursively (a key named
220
+ * `__proto__`, `constructor` or `prototype` is refused), everything else is returned as is. Nesting
221
+ * deeper than MAX_JSON_DEPTH is E_BAD_SERIALIZATION rather than a stack overflow.
222
+ * @param value - the value from the manifest
223
+ * @param path - the manifest path for error messages
224
+ * @param depth - the current nesting depth
225
+ * @returns the decoded value
226
+ */
227
+ export function decodeJsonValue(value: unknown, path: string, depth = 0): unknown {
228
+ if (depth > MAX_JSON_DEPTH) {
229
+ throw badWire(path, `JSON nesting deeper than ${MAX_JSON_DEPTH}`, { reason: "nesting" });
230
+ }
231
+ if (Array.isArray(value)) {
232
+ return value.map((item: unknown, i) => decodeJsonValue(item, `${path}[${i}]`, depth + 1));
233
+ }
234
+ if (!isPlainObject(value)) {
235
+ return value;
236
+ }
237
+ const keys = Object.keys(value);
238
+ if (keys.length === 1 && keys[0] === NUM_TAG) {
239
+ const tag: unknown = value[NUM_TAG];
240
+ switch (tag) {
241
+ case "NaN":
242
+ return Number.NaN;
243
+ case "Infinity":
244
+ return Infinity;
245
+ case "-Infinity":
246
+ return -Infinity;
247
+ case "-0":
248
+ return -0;
249
+ default:
250
+ throw badWire(path, `unknown number tag ${describe(tag)}`, { found: tag });
251
+ }
252
+ }
253
+ if (keys.length === 1 && keys[0] === ESC_TAG) {
254
+ const inner: unknown = value[ESC_TAG];
255
+ if (!isPlainObject(inner)) {
256
+ throw badWire(path, "an escaped object must hold a plain object", { found: typeof inner });
257
+ }
258
+ return decodeJsonObject(inner, path, depth);
259
+ }
260
+ return decodeJsonObject(value, path, depth);
261
+ }
262
+
263
+ /**
264
+ * Rebuild a plain object's members (no tag interpretation of the object itself).
265
+ * @param value - the plain object
266
+ * @param path - the manifest path
267
+ * @param depth - the nesting depth of the object
268
+ * @returns the rebuilt object
269
+ */
270
+ function decodeJsonObject(value: Record<string, unknown>, path: string, depth: number): Record<string, unknown> {
271
+ const out: Record<string, unknown> = {};
272
+ for (const key of Object.keys(value)) {
273
+ if (FORBIDDEN_KEYS.has(key)) {
274
+ throw badWire(path, `forbidden key "${key}"`, { key });
275
+ }
276
+ defineJsonKey(out, key, decodeJsonValue(value[key], `${path}.${key}`, depth + 1));
277
+ }
278
+ return out;
279
+ }
280
+
281
+ /**
282
+ * The JSON.parse reviver of design section 9.5: refuses the prototype-pollution keys.
283
+ * @param key - the property name being revived
284
+ * @param value - the revived value
285
+ * @returns the value unchanged
286
+ */
287
+ function guardReviver(this: unknown, key: string, value: unknown): unknown {
288
+ if (FORBIDDEN_KEYS.has(key)) {
289
+ throw badWire("manifest", `forbidden key "${key}"`, { key });
290
+ }
291
+ return value;
292
+ }
293
+
294
+ /**
295
+ * Parse JSON text from a container (the manifest or one json column row) with the guarding
296
+ * reviver. A syntax error is E_BAD_SERIALIZATION.
297
+ * @param text - the JSON text
298
+ * @param path - the manifest path for error messages
299
+ * @returns the parsed value
300
+ */
301
+ export function parseGuardedJson(text: string, path: string): unknown {
302
+ try {
303
+ return JSON.parse(text, guardReviver) as unknown;
304
+ } catch (err) {
305
+ if (err instanceof GraphFormatError) {
306
+ throw err;
307
+ }
308
+ throw badWire(path, `not valid JSON (${err instanceof Error ? err.message : String(err)})`);
309
+ }
310
+ }
311
+
312
+ // ============================================================ options and regions
313
+
314
+ /** The options of fromWire / fromBytes after defaults were applied. */
315
+ interface ResolvedFromWireOptions {
316
+ /** The validation level. */
317
+ readonly level: ValidationLevel;
318
+ /** Whether buffers are copied before adoption. */
319
+ readonly copy: boolean;
320
+ /** What to do with an unknown column dtype. */
321
+ readonly unknownColumns: "error" | "skip";
322
+ }
323
+
324
+ /**
325
+ * Apply defaults to FromWireOptions and reject values outside their documented sets (E_UNSUPPORTED).
326
+ * @param options - the caller's options
327
+ * @param defaultLevel - "structure" for fromWire, "full" for fromBytes
328
+ * @returns the resolved options
329
+ */
330
+ export function resolveFromWireOptions(
331
+ options: FromWireOptions | undefined,
332
+ defaultLevel: ValidationLevel,
333
+ ): ResolvedFromWireOptions {
334
+ const level = options?.validate ?? defaultLevel;
335
+ if (level !== "none" && level !== "structure" && level !== "full") {
336
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown validation level ${String(level)}`, {
337
+ option: "validate",
338
+ found: level,
339
+ });
340
+ }
341
+ const unknownColumns = options?.unknownColumns ?? "error";
342
+ if (unknownColumns !== "error" && unknownColumns !== "skip") {
343
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown unknownColumns policy ${String(unknownColumns)}`, {
344
+ option: "unknownColumns",
345
+ found: unknownColumns,
346
+ });
347
+ }
348
+ const copy = options?.copy ?? false;
349
+ if (typeof copy !== "boolean") {
350
+ throw new GraphFormatError("E_UNSUPPORTED", "copy must be a boolean", { option: "copy", found: copy });
351
+ }
352
+ return { level, copy, unknownColumns };
353
+ }
354
+
355
+ /** Where a located byte range lives. */
356
+ export interface LocatedRange {
357
+ /** The plain ArrayBuffer holding the bytes. */
358
+ readonly buffer: ArrayBuffer;
359
+ /** The absolute byte offset of the range inside `buffer`. */
360
+ readonly byteOffset: number;
361
+ }
362
+
363
+ /**
364
+ * One buffer index of a manifest as the decoder sees it: a byte length for range checks, a locator
365
+ * that finds a range contiguously in one plain buffer (null when it spans chunks or lives in a
366
+ * SharedArrayBuffer) and a reader that copies a range out.
367
+ */
368
+ export interface WireRegion {
369
+ /** The byte length every reference is checked against. */
370
+ readonly byteLength: number;
371
+ /**
372
+ * Find a byte range in one plain buffer.
373
+ * @param byteOffset - region-relative start
374
+ * @param byteLength - length
375
+ * @returns the buffer and absolute offset, or null when the range cannot be adopted
376
+ */
377
+ locate(byteOffset: number, byteLength: number): LocatedRange | null;
378
+ /**
379
+ * Copy a byte range into a fresh buffer.
380
+ * @param byteOffset - region-relative start
381
+ * @param byteLength - length
382
+ * @returns a Uint8Array at offset 0 of a fresh ArrayBuffer of at least byteLength bytes
383
+ */
384
+ read(byteOffset: number, byteLength: number): U8;
385
+ }
386
+
387
+ /**
388
+ * A region over one plain ArrayBuffer.
389
+ * @param buffer - the buffer
390
+ * @param base - the absolute byte offset of the region's start inside the buffer
391
+ * @param byteLength - the region's byte length
392
+ * @returns the region
393
+ */
394
+ export function bufferRegion(buffer: ArrayBuffer, base: number, byteLength: number): WireRegion {
395
+ return {
396
+ byteLength,
397
+ locate(byteOffset: number): LocatedRange {
398
+ return { buffer, byteOffset: base + byteOffset };
399
+ },
400
+ read(byteOffset: number, length: number): U8 {
401
+ const out = new Uint8Array(new ArrayBuffer(padTo4(length)), 0, length);
402
+ out.set(new Uint8Array(buffer, base + byteOffset, length));
403
+ return out;
404
+ },
405
+ };
406
+ }
407
+
408
+ /**
409
+ * Copy an ArrayBuffer or SharedArrayBuffer into a fresh ArrayBuffer.
410
+ * @param buffer - the source
411
+ * @returns the copy
412
+ */
413
+ function copyBuffer(buffer: ArrayBufferLike): ArrayBuffer {
414
+ const out = new ArrayBuffer(buffer.byteLength);
415
+ new Uint8Array(out).set(new Uint8Array(buffer));
416
+ return out;
417
+ }
418
+
419
+ /**
420
+ * Whether a value is a SharedArrayBuffer (never adopted, decision D-SAB).
421
+ * @param value - the value
422
+ * @returns true for a SharedArrayBuffer
423
+ */
424
+ export function isSharedArrayBuffer(value: unknown): value is SharedArrayBuffer {
425
+ return typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer;
426
+ }
427
+
428
+ // ============================================================ the decoder
429
+
430
+ /** Who a placed byte range belongs to, for the overlap rule of design section 9.5. */
431
+ type RangeKind = "core" | "immutable" | "mutable" | "view";
432
+
433
+ /** An adopted byte range, keyed by the ArrayBuffer it lives in (absolute offsets), for the overlap rule. */
434
+ interface PlacedRange {
435
+ readonly buffer: ArrayBuffer;
436
+ readonly start: number;
437
+ readonly end: number;
438
+ readonly kind: RangeKind;
439
+ readonly path: string;
440
+ }
441
+
442
+ interface SkippedColumn {
443
+ readonly domain: ColumnDomain;
444
+ readonly table: string | null;
445
+ readonly name: string;
446
+ readonly dtype: string;
447
+ }
448
+
449
+ /**
450
+ * Bytes per element and constructor of a wire dtype.
451
+ * @param dtype - the wire dtype
452
+ * @returns the element size
453
+ */
454
+ function wireElementSize(dtype: WireDtype): number {
455
+ switch (dtype) {
456
+ case "u32":
457
+ case "i32":
458
+ case "f32":
459
+ return 4;
460
+ case "f64":
461
+ return 8;
462
+ case "u8":
463
+ case "utf8":
464
+ return 1;
465
+ default: {
466
+ const name: string = dtype;
467
+ throw badWire("dtype", `unknown wire dtype ${name}`, { dtype: name });
468
+ }
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Build the typed array of a wire dtype over a buffer.
474
+ * @param dtype - the wire dtype
475
+ * @param buffer - the buffer
476
+ * @param byteOffset - the absolute start
477
+ * @param length - the element count
478
+ * @returns the array
479
+ */
480
+ function buildArray(dtype: WireDtype, buffer: ArrayBuffer, byteOffset: number, length: number): TypedArrayData {
481
+ switch (dtype) {
482
+ case "u32":
483
+ return new Uint32Array(buffer, byteOffset, length);
484
+ case "i32":
485
+ return new Int32Array(buffer, byteOffset, length);
486
+ case "f32":
487
+ return new Float32Array(buffer, byteOffset, length);
488
+ case "f64":
489
+ return new Float64Array(buffer, byteOffset, length);
490
+ case "u8":
491
+ case "utf8":
492
+ return new Uint8Array(buffer, byteOffset, length);
493
+ default: {
494
+ const name: string = dtype;
495
+ throw badWire("dtype", `unknown wire dtype ${name}`, { dtype: name });
496
+ }
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Read a WireBufferRef's shape from an untrusted manifest member.
502
+ * @param value - the member
503
+ * @param path - its manifest path
504
+ * @returns the reference
505
+ */
506
+ function readRef(value: unknown, path: string): WireBufferRef {
507
+ const o = asObject(value, path);
508
+ const { dtype } = o;
509
+ if (typeof dtype !== "string" || !WIRE_DTYPES.has(dtype)) {
510
+ throw badWire(path, `unknown wire dtype ${describe(dtype)}`, { found: dtype });
511
+ }
512
+ const fields = ["buffer", "byteOffset", "byteLength", "length"] as const;
513
+ for (const field of fields) {
514
+ const v = o[field];
515
+ if (typeof v !== "number" || !Number.isInteger(v) || v < 0) {
516
+ throw badWire(`${path}.${field}`, "expected a non-negative integer", { found: v });
517
+ }
518
+ }
519
+ return {
520
+ buffer: o.buffer as number,
521
+ byteOffset: o.byteOffset as number,
522
+ byteLength: o.byteLength as number,
523
+ dtype: dtype as WireDtype,
524
+ length: o.length as number,
525
+ };
526
+ }
527
+
528
+ /**
529
+ * The error to throw for a failure while building a column or table from untrusted input: a
530
+ * column-construction error (E_COLUMN_*, and E_DUPLICATE_ROLE from the table constructor) becomes
531
+ * E_BAD_SERIALIZATION naming the manifest path, every other Error passes through, and a non-Error
532
+ * value is wrapped.
533
+ * @param err - the caught value
534
+ * @param path - the manifest path of the column or table
535
+ * @returns the error to throw
536
+ */
537
+ function asWireError(err: unknown, path: string): Error {
538
+ if (err instanceof GraphFormatError && (err.code.startsWith("E_COLUMN") || err.code === "E_DUPLICATE_ROLE")) {
539
+ return new GraphFormatError("E_BAD_SERIALIZATION", `${path}: ${err.message}`, {
540
+ ref: path,
541
+ reason: err.message,
542
+ cause: err.code,
543
+ ...err.details,
544
+ });
545
+ }
546
+ if (err instanceof Error) {
547
+ return err;
548
+ }
549
+ return badWire(path, String(err));
550
+ }
551
+
552
+ /**
553
+ * The stateful reader of one manifest: resolves references against the regions, records placed
554
+ * ranges for the overlap rule and the columns skipped under unknownColumns: "skip".
555
+ */
556
+ class WireDecoder {
557
+ private readonly regions: readonly WireRegion[];
558
+ private readonly options: ResolvedFromWireOptions;
559
+ private readonly ranges: PlacedRange[] = [];
560
+ readonly skipped: SkippedColumn[] = [];
561
+
562
+ /**
563
+ * Create a decoder.
564
+ * @param regions - one region per buffer index
565
+ * @param options - the resolved options
566
+ */
567
+ constructor(regions: readonly WireRegion[], options: ResolvedFromWireOptions) {
568
+ this.regions = regions;
569
+ this.options = options;
570
+ }
571
+
572
+ /**
573
+ * Whether the level includes the structure checks.
574
+ * @returns true for "structure" and "full"
575
+ */
576
+ get structure(): boolean {
577
+ return this.options.level !== "none";
578
+ }
579
+
580
+ /**
581
+ * Whether the level is "full".
582
+ * @returns true for "full"
583
+ */
584
+ get full(): boolean {
585
+ return this.options.level === "full";
586
+ }
587
+
588
+ /**
589
+ * Check a reference against its region (design section 9.5) and build the typed array over the
590
+ * referenced bytes, adopting them when they are contiguous and aligned and copying otherwise.
591
+ * @param value - the manifest member holding the reference
592
+ * @param path - its manifest path
593
+ * @param expected - the wire dtype the slot requires
594
+ * @param kind - who the range belongs to
595
+ * @returns the typed array
596
+ */
597
+ array(value: unknown, path: string, expected: WireDtype, kind: RangeKind): TypedArrayData {
598
+ const ref = readRef(value, path);
599
+ if (ref.dtype !== expected) {
600
+ throw badWire(path, `expected dtype ${expected}, found ${ref.dtype}`, {
601
+ expected,
602
+ found: ref.dtype,
603
+ });
604
+ }
605
+ const region = this.regions[ref.buffer] as WireRegion | undefined;
606
+ if (region === undefined) {
607
+ throw badWire(path, `buffer index ${ref.buffer} is out of range (${this.regions.length} buffers)`, {
608
+ found: ref.buffer,
609
+ buffers: this.regions.length,
610
+ });
611
+ }
612
+ const elementSize = wireElementSize(ref.dtype);
613
+ if (ref.byteOffset % elementSize !== 0) {
614
+ throw badWire(path, `byteOffset ${ref.byteOffset} is not a multiple of ${elementSize}`, {
615
+ byteOffset: ref.byteOffset,
616
+ elementSize,
617
+ });
618
+ }
619
+ if (ref.byteLength !== ref.length * elementSize) {
620
+ throw badWire(path, `byteLength ${ref.byteLength} is not length ${ref.length} * ${elementSize}`, {
621
+ byteLength: ref.byteLength,
622
+ length: ref.length,
623
+ elementSize,
624
+ });
625
+ }
626
+ if (ref.byteOffset + ref.byteLength > region.byteLength) {
627
+ throw badWire(path, `range [${ref.byteOffset}, ${ref.byteOffset + ref.byteLength}) exceeds the buffer`, {
628
+ byteOffset: ref.byteOffset,
629
+ byteLength: ref.byteLength,
630
+ bufferByteLength: region.byteLength,
631
+ });
632
+ }
633
+ const located = region.locate(ref.byteOffset, ref.byteLength);
634
+ if (located !== null && located.byteOffset % elementSize === 0) {
635
+ // adopted: the overlap rule is checked on the ArrayBuffer itself, so listing one buffer
636
+ // twice in wire.buffers cannot hide an alias
637
+ this.ranges.push({
638
+ buffer: located.buffer,
639
+ start: located.byteOffset,
640
+ end: located.byteOffset + ref.byteLength,
641
+ kind,
642
+ path,
643
+ });
644
+ return buildArray(ref.dtype, located.buffer, located.byteOffset, ref.length);
645
+ }
646
+ // copied: a private buffer, nothing to overlap
647
+ const copy = region.read(ref.byteOffset, ref.byteLength);
648
+ return buildArray(ref.dtype, copy.buffer, 0, ref.length);
649
+ }
650
+
651
+ /**
652
+ * A u32 array slot.
653
+ * @param value - the manifest member
654
+ * @param path - its path
655
+ * @param kind - the range owner
656
+ * @returns the array
657
+ */
658
+ u32(value: unknown, path: string, kind: RangeKind): U32 {
659
+ return this.array(value, path, "u32", kind) as U32;
660
+ }
661
+
662
+ /**
663
+ * A Utf8 store slot (offsets + utf8), its layout checked at the structure level and its bytes at
664
+ * the full level.
665
+ * @param value - the manifest member (a WireUtf8)
666
+ * @param path - its path
667
+ * @param rows - the row count the store must have, or null to take it from the offsets (a dictionary)
668
+ * @param kind - the range owner
669
+ * @param always - check the layout at every level (stores decoded eagerly)
670
+ * @returns the offsets and bytes
671
+ */
672
+ utf8(
673
+ value: unknown,
674
+ path: string,
675
+ rows: number | null,
676
+ kind: RangeKind,
677
+ always = false,
678
+ ): { offsets: U32; utf8: U8 } {
679
+ const o = asObject(value, path);
680
+ const offsets = this.u32(o.offsets, `${path}.offsets`, kind);
681
+ const utf8 = this.array(o.utf8, `${path}.utf8`, "utf8", kind) as U8;
682
+ if (offsets.length === 0) {
683
+ throw badWire(`${path}.offsets`, "a Utf8 store needs at least one offset");
684
+ }
685
+ if (this.structure || always) {
686
+ checkUtf8Layout(offsets, utf8, rows ?? offsets.length - 1, path);
687
+ }
688
+ if (this.full) {
689
+ const bad = Utf8Store.fromEncoded(offsets, utf8).firstMalformedRow();
690
+ if (bad !== -1) {
691
+ throw badWire(path, `malformed UTF-8 in row ${bad}`, { row: bad });
692
+ }
693
+ }
694
+ return { offsets, utf8 };
695
+ }
696
+
697
+ /**
698
+ * Enforce the overlap rule of design section 9.5: no byte overlap between a mutable column and
699
+ * any core or immutable segment.
700
+ */
701
+ checkOverlaps(): void {
702
+ const fixed = this.ranges.filter((r) => (r.kind === "core" || r.kind === "immutable") && r.end > r.start);
703
+ for (const range of this.ranges) {
704
+ if (range.kind !== "mutable" || range.end === range.start) {
705
+ continue;
706
+ }
707
+ for (const other of fixed) {
708
+ if (other.buffer === range.buffer && range.start < other.end && other.start < range.end) {
709
+ throw badWire(range.path, `mutable column overlaps ${other.path}`, { overlaps: other.path });
710
+ }
711
+ }
712
+ }
713
+ }
714
+
715
+ // ---------------------------------------------------------------- columns
716
+
717
+ /**
718
+ * Decode one column (design section 9.1), or drop it under unknownColumns: "skip".
719
+ * @param value - the WireColumn
720
+ * @param domain - the table's domain
721
+ * @param table - the extension table name, or null
722
+ * @param rows - the row count
723
+ * @param path - the manifest path
724
+ * @returns the column, or null when skipped
725
+ */
726
+ column(value: unknown, domain: ColumnDomain, table: string | null, rows: number, path: string): Column | null {
727
+ const w = asObject(value, path);
728
+ const metaRaw = asObject(w.meta, `${path}.meta`);
729
+ const name = asString(metaRaw.name, `${path}.meta.name`);
730
+ const { dtype, itemDtype } = metaRaw;
731
+ const unknown = !isKnownDtype(dtype) || (dtype === "list" && !isKnownDtype(itemDtype));
732
+ if (unknown) {
733
+ const found = dtype === "list" ? itemDtype : dtype;
734
+ if (this.options.unknownColumns === "skip") {
735
+ this.skipped.push({ domain, table, name, dtype: String(found) });
736
+ return null;
737
+ }
738
+ throw new GraphFormatError("E_UNSUPPORTED", `column "${name}" has unknown dtype ${String(found)}`, {
739
+ dtype: found,
740
+ column: name,
741
+ ref: path,
742
+ });
743
+ }
744
+ const meta = this.columnMeta(metaRaw, name, domain, path);
745
+ const kind: RangeKind = meta.mutable ? "mutable" : "immutable";
746
+ const parts: MutableColumnParts = emptyParts(
747
+ meta,
748
+ rows,
749
+ w.validity === null || w.validity === undefined ? null : this.u32(w.validity, `${path}.validity`, kind),
750
+ );
751
+ const declaredNullCount = w.nullCount;
752
+ switch (meta.dtype) {
753
+ case "f32":
754
+ case "f64":
755
+ case "i32":
756
+ case "u32":
757
+ parts.data = this.array(w.data, `${path}.data`, meta.dtype, kind);
758
+ break;
759
+ case "u8": {
760
+ const data = this.array(w.data, `${path}.data`, "u8", kind) as U8;
761
+ parts.data = canViewAsPaddedU32(data) ? data : copyToPaddedStore(data);
762
+ break;
763
+ }
764
+ case "bool":
765
+ parts.data = this.u32(w.data, `${path}.data`, kind);
766
+ break;
767
+ case "dict": {
768
+ parts.data = this.u32(w.data, `${path}.data`, kind);
769
+ const dict = this.utf8(w.dictionary, `${path}.dictionary`, null, kind, true);
770
+ parts.dictionary = decodeUtf8Rows(dict.utf8, dict.offsets, 0, dict.offsets.length - 1);
771
+ break;
772
+ }
773
+ case "string": {
774
+ const store = this.utf8(w.strings, `${path}.strings`, rows, kind);
775
+ parts.offsets = store.offsets;
776
+ parts.utf8 = store.utf8;
777
+ break;
778
+ }
779
+ case "list": {
780
+ const offsets = this.u32(w.offsets, `${path}.offsets`, kind);
781
+ if (offsets.length !== rows + 1) {
782
+ throw badWire(`${path}.offsets`, `expected ${rows + 1} offsets, found ${offsets.length}`, {
783
+ expected: rows + 1,
784
+ found: offsets.length,
785
+ });
786
+ }
787
+ parts.offsets = offsets;
788
+ const child = this.column(w.child, domain, table, offsets[rows], `${path}.child`);
789
+ if (child === null || child.dtype === "list") {
790
+ throw badWire(`${path}.child`, "a list column needs a non-list child column");
791
+ }
792
+ parts.child = child;
793
+ break;
794
+ }
795
+ case "json":
796
+ parts.values = this.jsonRows(w.jsonText, rows, `${path}.jsonText`, kind);
797
+ break;
798
+ default: {
799
+ const dtypeName: string = meta.dtype;
800
+ throw badWire(`${path}.meta.dtype`, `unknown dtype ${dtypeName}`, { dtype: dtypeName });
801
+ }
802
+ }
803
+ let column: Column;
804
+ try {
805
+ column = createColumn(parts);
806
+ } catch (err) {
807
+ throw asWireError(err, path);
808
+ }
809
+ if (this.structure && column.nullCount !== declaredNullCount) {
810
+ throw badWire(
811
+ `${path}.nullCount`,
812
+ `manifest says ${String(declaredNullCount)}, the bitmap says ${column.nullCount}`,
813
+ {
814
+ expected: column.nullCount,
815
+ found: declaredNullCount,
816
+ },
817
+ );
818
+ }
819
+ return column;
820
+ }
821
+
822
+ /**
823
+ * Resolve a column's metadata from its manifest form: the tagged JSON fields decoded, then the
824
+ * declaration resolver of the column module applied so every rule of design section 5.5 holds.
825
+ * @param raw - the manifest meta
826
+ * @param name - the column name
827
+ * @param domain - the table's domain
828
+ * @param path - the column's manifest path
829
+ * @returns the resolved metadata
830
+ */
831
+ private columnMeta(raw: Record<string, unknown>, name: string, domain: ColumnDomain, path: string): ColumnMeta {
832
+ const metaPath = `${path}.meta`;
833
+ const optional = <T>(key: string, check: (v: unknown, p: string) => T): T | undefined => {
834
+ const v = raw[key];
835
+ return v === undefined || v === null ? undefined : check(v, `${metaPath}.${key}`);
836
+ };
837
+ const numberOf = (v: unknown, p: string): number => {
838
+ if (typeof v !== "number") {
839
+ throw badWire(p, "expected a number", { found: v });
840
+ }
841
+ return v;
842
+ };
843
+ const decl: ColumnDeclPatch = {
844
+ name,
845
+ dtype: raw.dtype as Dtype,
846
+ components: optional("components", numberOf),
847
+ itemDtype: optional("itemDtype", (v) => v as ScalarDtype),
848
+ itemComponents: optional("itemComponents", numberOf),
849
+ nullable: optional("nullable", asBoolean),
850
+ mutable: optional("mutable", asBoolean),
851
+ role: optional("role", asString),
852
+ refersTo: optional("refersTo", (v) => v as "node" | "edge"),
853
+ unique: optional("unique", asBoolean),
854
+ default: decodeJsonValue(raw.default, `${metaPath}.default`),
855
+ fill: decodeJsonValue(raw.fill, `${metaPath}.fill`) as number | string | boolean | undefined,
856
+ options: optional("options", (v, p) => decodeJsonValue(v, p) as readonly unknown[]),
857
+ origin: optional("origin", (v) => v as ColumnOriginInput),
858
+ dynamic: optional("dynamic", asBoolean),
859
+ extra: optional("extra", (v, p) => decodeJsonValue(v, p) as Readonly<Record<string, unknown>>),
860
+ };
861
+ try {
862
+ return resolveColumnMeta(name, domain, decl);
863
+ } catch (err) {
864
+ throw asWireError(err, path);
865
+ }
866
+ }
867
+
868
+ /**
869
+ * The values of a json column from its JSON text store: empty text is an unset row (undefined),
870
+ * every other row is parsed with the guarding reviver and untagged.
871
+ * @param value - the WireUtf8
872
+ * @param rows - the row count
873
+ * @param path - the manifest path
874
+ * @param kind - the range owner
875
+ * @returns one value per row
876
+ */
877
+ private jsonRows(value: unknown, rows: number, path: string, kind: RangeKind): unknown[] {
878
+ const store = this.utf8(value, path, rows, kind, true);
879
+ const texts = decodeUtf8Rows(store.utf8, store.offsets, 0, rows);
880
+ const values = new Array<unknown>(rows);
881
+ for (let row = 0; row < rows; row++) {
882
+ const text = texts[row];
883
+ values[row] =
884
+ text === "" ? undefined : decodeJsonValue(parseGuardedJson(text, `${path}[${row}]`), `${path}[${row}]`);
885
+ }
886
+ return values;
887
+ }
888
+
889
+ /**
890
+ * Decode a table's columns in declaration order.
891
+ * @param value - the WireColumn array
892
+ * @param domain - the domain
893
+ * @param table - the extension table name, or null
894
+ * @param rows - the row count
895
+ * @param path - the manifest path
896
+ * @returns the table
897
+ */
898
+ table(value: unknown, domain: ColumnDomain, table: string | null, rows: number, path: string): AttributeTable {
899
+ const list = asArray(value, path);
900
+ const columns: Column[] = [];
901
+ const names = new Set<string>();
902
+ for (let i = 0; i < list.length; i++) {
903
+ const column = this.column(list[i], domain, table, rows, `${path}[${i}]`);
904
+ if (column === null) {
905
+ continue;
906
+ }
907
+ if (names.has(column.meta.name)) {
908
+ throw badWire(`${path}[${i}].meta.name`, `column "${column.meta.name}" is declared twice`, {
909
+ column: column.meta.name,
910
+ });
911
+ }
912
+ names.add(column.meta.name);
913
+ columns.push(column);
914
+ }
915
+ try {
916
+ return new AttributeTable({ domain, rowCount: rows, columns });
917
+ } catch (err) {
918
+ throw asWireError(err, path);
919
+ }
920
+ }
921
+
922
+ // ---------------------------------------------------------------- id map
923
+
924
+ /**
925
+ * Decode the id map (design section 4.5). An unknown kind is E_UNSUPPORTED with details.kind.
926
+ * @param value - the WireIdMap
927
+ * @returns the map
928
+ */
929
+ ids(value: unknown): NodeIdMap {
930
+ const o = asObject(value, "ids");
931
+ const kindRaw = o.kind;
932
+ if (typeof kindRaw !== "string" || !ID_MAP_KINDS.has(kindRaw)) {
933
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown id map kind ${describe(kindRaw)}`, {
934
+ kind: kindRaw,
935
+ ref: "ids.kind",
936
+ });
937
+ }
938
+ const kind = kindRaw as NodeIdMapKind;
939
+ const size = asCount(o.size, "ids.size");
940
+ const offsetRaw = o.offset ?? 0;
941
+ if (typeof offsetRaw !== "number") {
942
+ throw badWire("ids.offset", "expected a number", { found: offsetRaw });
943
+ }
944
+ const parts: NodeIdMapTypedParts = {
945
+ kind,
946
+ size,
947
+ offset: kind === "identity" ? offsetRaw : 0,
948
+ values: null,
949
+ tags: null,
950
+ numbers: null,
951
+ offsets: null,
952
+ utf8: null,
953
+ };
954
+ const typed = parts as { -readonly [K in keyof NodeIdMapTypedParts]: NodeIdMapTypedParts[K] };
955
+ switch (kind) {
956
+ case "identity":
957
+ break;
958
+ case "dense":
959
+ typed.values = this.u32(o.values, "ids.values", "immutable");
960
+ break;
961
+ case "numeric":
962
+ typed.values = this.array(o.values, "ids.values", "f64", "immutable") as F64;
963
+ break;
964
+ case "string": {
965
+ typed.offsets = this.u32(o.offsets, "ids.offsets", "immutable");
966
+ typed.utf8 = this.array(o.utf8, "ids.utf8", "utf8", "immutable") as U8;
967
+ break;
968
+ }
969
+ case "mixed": {
970
+ typed.tags = this.array(o.tags, "ids.tags", "u8", "immutable") as U8;
971
+ typed.numbers = this.array(o.numbers, "ids.numbers", "f64", "immutable") as F64;
972
+ typed.offsets = this.u32(o.offsets, "ids.offsets", "immutable");
973
+ typed.utf8 = this.array(o.utf8, "ids.utf8", "utf8", "immutable") as U8;
974
+ break;
975
+ }
976
+ default: {
977
+ const name: string = kind;
978
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown id map kind ${name}`, { kind: name });
979
+ }
980
+ }
981
+ return nodeIdMapFromTyped(typed, this.options.level);
982
+ }
983
+
984
+ // ---------------------------------------------------------------- views
985
+
986
+ /**
987
+ * Resolve the member arrays of the carried views (design section 9.1): every reference is
988
+ * checked like any other, and the arrays are installed on the snapshot by `installViews()`.
989
+ * @param value - the manifest views member
990
+ * @returns the member arrays per view name (unknown names included; they are ignored later)
991
+ */
992
+ views(value: unknown): CarriedViews {
993
+ const out: CarriedViews = new Map();
994
+ if (value === null || value === undefined) {
995
+ return out;
996
+ }
997
+ const views = asObject(value, "views");
998
+ for (const name of Object.keys(views)) {
999
+ const members = asObject(views[name], `views.${name}`);
1000
+ const arrays = new Map<string, TypedArrayData>();
1001
+ for (const member of Object.keys(members)) {
1002
+ const path = `views.${name}.${member}`;
1003
+ const ref = readRef(members[member], path);
1004
+ arrays.set(member, this.array(ref, path, ref.dtype, "view"));
1005
+ }
1006
+ out.set(name, arrays);
1007
+ }
1008
+ return out;
1009
+ }
1010
+ }
1011
+
1012
+ // ============================================================ carried views
1013
+
1014
+ /** The member arrays of the views a manifest carried, keyed by view name then member name. */
1015
+ type CarriedViews = Map<string, Map<string, TypedArrayData>>;
1016
+
1017
+ /**
1018
+ * One member array of a carried view, checked for class and length.
1019
+ * @param members - the view's member arrays
1020
+ * @param view - the view name (for the error path)
1021
+ * @param member - the member name
1022
+ * @param ctor - the required typed-array class
1023
+ * @param length - the required length
1024
+ * @param required - whether an absent member is an error
1025
+ * @returns the array, or null when absent and not required
1026
+ */
1027
+ function viewMember<T extends TypedArrayData>(
1028
+ members: ReadonlyMap<string, TypedArrayData>,
1029
+ view: string,
1030
+ member: string,
1031
+ ctor: new (length: number) => T,
1032
+ length: number,
1033
+ required: boolean,
1034
+ ): T | null {
1035
+ const path = `views.${view}.${member}`;
1036
+ const array = members.get(member);
1037
+ if (array === undefined) {
1038
+ if (required) {
1039
+ throw badWire(path, "the carried view is missing this member");
1040
+ }
1041
+ return null;
1042
+ }
1043
+ if (!(array instanceof ctor)) {
1044
+ throw badWire(path, `expected ${ctor.name}, found ${array.constructor.name}`, {
1045
+ expected: ctor.name,
1046
+ found: array.constructor.name,
1047
+ });
1048
+ }
1049
+ if (array.length !== length) {
1050
+ throw badWire(path, `expected ${length} entries, found ${array.length}`, {
1051
+ expected: length,
1052
+ found: array.length,
1053
+ });
1054
+ }
1055
+ return array;
1056
+ }
1057
+
1058
+ /**
1059
+ * The per-node or per-arc array of a single-member view.
1060
+ * @param members - the view's member arrays
1061
+ * @param view - the view name
1062
+ * @param ctor - the required typed-array class
1063
+ * @param length - the required length
1064
+ * @returns the array
1065
+ */
1066
+ function dataMember<T extends TypedArrayData>(
1067
+ members: ReadonlyMap<string, TypedArrayData>,
1068
+ view: string,
1069
+ ctor: new (length: number) => T,
1070
+ length: number,
1071
+ ): T {
1072
+ return viewMember(members, view, "data", ctor, length, true) as T;
1073
+ }
1074
+
1075
+ /**
1076
+ * Install the views a manifest carried into the snapshot's cache (design section 9.1), so the
1077
+ * receiver does not recompute them. Views that alias another view on an undirected snapshot
1078
+ * (`reverse`, `inDegree`, `weightedInDegree`, `reverseDegreeOrder`) are left to the snapshot, which
1079
+ * produces the alias itself; `mate` on a directed snapshot and unknown view names are ignored.
1080
+ * Every installed member must have the class and length of the view it claims to be
1081
+ * (E_BAD_SERIALIZATION otherwise); at the "structure" level (`verify`) every carried view is also
1082
+ * checked against the core before it is installed (src/wire/carried-views.ts), so a corrupt or
1083
+ * forged view never enters the cache below "full".
1084
+ * @param snapshot - the freshly built snapshot
1085
+ * @param carried - the carried member arrays
1086
+ * @param verify - whether to check the contents (the "structure" level)
1087
+ */
1088
+ function installViews(snapshot: GraphSnapshot, carried: CarriedViews, verify: boolean): void {
1089
+ const { directed, nodeCount, edgeCount, arcCount, selfLoopCount } = snapshot;
1090
+ let facts: CoreFacts | null = null;
1091
+ const factsOf = (): CoreFacts => {
1092
+ facts ??= new CoreFacts(snapshot);
1093
+ return facts;
1094
+ };
1095
+ /**
1096
+ * Install an f64 view; under `verify` it is compared with the snapshot's own computation instead
1097
+ * (which then stays cached), so a corrupt value array never enters the cache.
1098
+ * @param name - the view
1099
+ */
1100
+ const seedF64 = (name: "weightedOutDegree" | "weightedInDegree" | "weightedDegree" | "selfLoopWeight"): void => {
1101
+ const data = dataMember(
1102
+ carried.get(name) as ReadonlyMap<string, TypedArrayData>,
1103
+ name,
1104
+ Float64Array,
1105
+ nodeCount,
1106
+ );
1107
+ if (verify) {
1108
+ checkF64View(name, data, snapshot[name]());
1109
+ return;
1110
+ }
1111
+ seedView(snapshot, name, data);
1112
+ };
1113
+ for (const [name, members] of carried) {
1114
+ switch (name) {
1115
+ case "reverse": {
1116
+ if (!directed) {
1117
+ break;
1118
+ }
1119
+ const rowPtr = viewMember(members, name, "rowPtr", Uint32Array, nodeCount + 1, true) as U32;
1120
+ const colIdx = viewMember(members, name, "colIdx", Uint32Array, arcCount, true) as U32;
1121
+ const fwdArc = viewMember(members, name, "fwdArc", Uint32Array, arcCount, true) as U32;
1122
+ const weights = viewMember(members, name, "weights", Float32Array, arcCount, snapshot.weights !== null);
1123
+ if (weights !== null && snapshot.weights === null) {
1124
+ throw badWire(`views.${name}.weights`, "weights carried for an unweighted snapshot");
1125
+ }
1126
+ const reverse = new ReverseAdjacency(snapshot, rowPtr, colIdx, weights, fwdArc);
1127
+ if (verify) {
1128
+ checkReverse(snapshot, factsOf(), reverse);
1129
+ }
1130
+ seedView(snapshot, "reverse", reverse);
1131
+ break;
1132
+ }
1133
+ case "coo": {
1134
+ const src = viewMember(members, name, "src", Uint32Array, arcCount, true) as U32;
1135
+ if (verify) {
1136
+ checkCooSrc(factsOf(), src);
1137
+ }
1138
+ seedView(snapshot, "coo", cooViewOf(snapshot, src));
1139
+ break;
1140
+ }
1141
+ case "edgeList": {
1142
+ const identity = snapshot.flags.arcToEdgeIsIdentity;
1143
+ const src = viewMember(members, name, "src", Uint32Array, edgeCount, true) as U32;
1144
+ const dst = viewMember(members, name, "dst", Uint32Array, edgeCount, !identity);
1145
+ const weights = viewMember(
1146
+ members,
1147
+ name,
1148
+ "weights",
1149
+ Float32Array,
1150
+ edgeCount,
1151
+ snapshot.weights !== null && !identity,
1152
+ );
1153
+ if (weights !== null && snapshot.weights === null) {
1154
+ throw badWire(`views.${name}.weights`, "weights carried for an unweighted snapshot");
1155
+ }
1156
+ if (verify) {
1157
+ checkEdgeList(snapshot, factsOf(), src, dst, weights);
1158
+ }
1159
+ seedView(
1160
+ snapshot,
1161
+ "edgeList",
1162
+ edgeListViewOf(snapshot, src, dst ?? snapshot.colIdx, weights ?? snapshot.weights),
1163
+ );
1164
+ break;
1165
+ }
1166
+ case "outDegree": {
1167
+ const data = dataMember(members, name, Uint32Array, nodeCount);
1168
+ if (verify) {
1169
+ checkOutDegree(snapshot, data);
1170
+ }
1171
+ seedView(snapshot, name, data);
1172
+ break;
1173
+ }
1174
+ case "degree": {
1175
+ const data = dataMember(members, name, Uint32Array, nodeCount);
1176
+ if (verify) {
1177
+ checkDegree(snapshot, factsOf(), data);
1178
+ }
1179
+ seedView(snapshot, name, data);
1180
+ break;
1181
+ }
1182
+ case "selfLoopsPerNode": {
1183
+ const data = dataMember(members, name, Uint32Array, nodeCount);
1184
+ if (verify) {
1185
+ checkSelfLoopsPerNode(factsOf(), data);
1186
+ }
1187
+ seedView(snapshot, name, data);
1188
+ break;
1189
+ }
1190
+ case "inDegree":
1191
+ if (directed) {
1192
+ const data = dataMember(members, name, Uint32Array, nodeCount);
1193
+ if (verify) {
1194
+ checkInDegree(factsOf(), data);
1195
+ }
1196
+ seedView(snapshot, name, data);
1197
+ }
1198
+ break;
1199
+ case "weightedOutDegree":
1200
+ case "weightedDegree":
1201
+ case "selfLoopWeight":
1202
+ seedF64(name);
1203
+ break;
1204
+ case "weightedInDegree":
1205
+ if (directed) {
1206
+ seedF64(name);
1207
+ }
1208
+ break;
1209
+ case "selfLoopArcs": {
1210
+ const data = dataMember(members, name, Uint32Array, selfLoopCount);
1211
+ if (verify) {
1212
+ checkSelfLoopArcs(snapshot, factsOf(), data);
1213
+ }
1214
+ seedView(snapshot, name, data);
1215
+ break;
1216
+ }
1217
+ case "mate":
1218
+ if (!directed) {
1219
+ const data = dataMember(members, name, Uint32Array, arcCount);
1220
+ if (verify) {
1221
+ checkMate(snapshot, factsOf(), data);
1222
+ }
1223
+ seedView(snapshot, name, data);
1224
+ }
1225
+ break;
1226
+ case "degreeOrder":
1227
+ case "reverseDegreeOrder": {
1228
+ if (name === "reverseDegreeOrder" && !directed) {
1229
+ break;
1230
+ }
1231
+ const perm = viewMember(members, name, "perm", Uint32Array, nodeCount, true) as U32;
1232
+ const segmentOffsets = viewMember(members, name, "segmentOffsets", Uint32Array, 5, true) as U32;
1233
+ if (verify) {
1234
+ const degree = name === "degreeOrder" ? rowLengths(snapshot.rowPtr, nodeCount) : factsOf().inDegree;
1235
+ checkDegreeOrder(name, degree, perm, segmentOffsets);
1236
+ }
1237
+ seedView(snapshot, name, Object.freeze({ perm, segmentOffsets }));
1238
+ break;
1239
+ }
1240
+ default:
1241
+ // An unknown view name (a newer minor) or a scalar view: ignored and recomputed.
1242
+ break;
1243
+ }
1244
+ }
1245
+ }
1246
+
1247
+ // ============================================================ core, arena, meta
1248
+
1249
+ interface CoreRefs {
1250
+ readonly rowPtr: WireBufferRef;
1251
+ readonly colIdx: WireBufferRef | null;
1252
+ readonly weights: WireBufferRef | null;
1253
+ readonly arcToEdge: WireBufferRef | null;
1254
+ readonly edgeToArc: WireBufferRef | null;
1255
+ }
1256
+
1257
+ /** The counts a manifest declares. */
1258
+ interface WireCounts {
1259
+ readonly nodes: number;
1260
+ readonly edges: number;
1261
+ readonly arcs: number;
1262
+ readonly selfLoops: number;
1263
+ }
1264
+
1265
+ interface CoreArrays {
1266
+ readonly rowPtr: U32;
1267
+ readonly colIdx: U32;
1268
+ readonly weights: F32 | null;
1269
+ readonly arcToEdge: U32 | null;
1270
+ readonly edgeToArc: U32 | null;
1271
+ }
1272
+
1273
+ /**
1274
+ * Read the core references from the manifest, applying the null rules of design section 9.1.
1275
+ * @param value - manifest.core
1276
+ * @returns the references
1277
+ */
1278
+ function readCoreRefs(value: unknown): CoreRefs {
1279
+ const core = asObject(value, "core");
1280
+ const optionalRef = (name: string): WireBufferRef | null => {
1281
+ const v = core[name];
1282
+ return v === null || v === undefined ? null : readRef(v, `core.${name}`);
1283
+ };
1284
+ return {
1285
+ rowPtr: readRef(core.rowPtr, "core.rowPtr"),
1286
+ colIdx: optionalRef("colIdx"),
1287
+ weights: optionalRef("weights"),
1288
+ arcToEdge: optionalRef("arcToEdge"),
1289
+ edgeToArc: optionalRef("edgeToArc"),
1290
+ };
1291
+ }
1292
+
1293
+ /**
1294
+ * Build the core arrays: a null colIdx is a zero-arc graph, a null weights array an unweighted (or
1295
+ * zero-arc weighted) graph, a null permutation the identity (when the flag says so) or zero length.
1296
+ * @param decoder - the decoder
1297
+ * @param refs - the core references
1298
+ * @param counts - the manifest counts
1299
+ * @param flags - the manifest flags
1300
+ * @returns the arrays
1301
+ */
1302
+ function decodeCore(decoder: WireDecoder, refs: CoreRefs, counts: WireCounts, flags: SnapshotFlags): CoreArrays {
1303
+ const rowPtr = decoder.u32(refs.rowPtr, "core.rowPtr", "core");
1304
+ let colIdx: U32;
1305
+ if (refs.colIdx === null) {
1306
+ if (counts.arcs !== 0) {
1307
+ throw badWire("core.colIdx", `null although counts.arcs is ${counts.arcs}`);
1308
+ }
1309
+ colIdx = new Uint32Array(0);
1310
+ } else {
1311
+ colIdx = decoder.u32(refs.colIdx, "core.colIdx", "core");
1312
+ }
1313
+ let weights: F32 | null = null;
1314
+ if (refs.weights !== null) {
1315
+ if (!flags.weighted) {
1316
+ throw badWire("core.weights", "present although flags.weighted is false");
1317
+ }
1318
+ weights = decoder.array(refs.weights, "core.weights", "f32", "core") as F32;
1319
+ } else if (flags.weighted) {
1320
+ if (counts.arcs !== 0) {
1321
+ throw badWire("core.weights", `null although flags.weighted is true and counts.arcs is ${counts.arcs}`);
1322
+ }
1323
+ weights = new Float32Array(0);
1324
+ }
1325
+ const identity = flags.arcToEdgeIsIdentity;
1326
+ const permutation = (ref: WireBufferRef | null, name: "arcToEdge" | "edgeToArc", count: number): U32 | null => {
1327
+ if (ref !== null) {
1328
+ if (identity) {
1329
+ throw badWire(`core.${name}`, "present although flags.arcToEdgeIsIdentity is true");
1330
+ }
1331
+ return decoder.u32(ref, `core.${name}`, "core");
1332
+ }
1333
+ if (identity) {
1334
+ return null;
1335
+ }
1336
+ if (count !== 0) {
1337
+ throw badWire(
1338
+ `core.${name}`,
1339
+ `null although the permutation is not the identity and the count is ${count}`,
1340
+ );
1341
+ }
1342
+ return new Uint32Array(0);
1343
+ };
1344
+ return {
1345
+ rowPtr,
1346
+ colIdx,
1347
+ weights,
1348
+ arcToEdge: permutation(refs.arcToEdge, "arcToEdge", counts.arcs),
1349
+ edgeToArc: permutation(refs.edgeToArc, "edgeToArc", counts.edges),
1350
+ };
1351
+ }
1352
+
1353
+ /**
1354
+ * Honour the manifest's arena descriptor (design sections 9.1 and 10.3): the core arrays lying at
1355
+ * 256-aligned offsets inside the described range become its segments; hotByteLength is recomputed
1356
+ * and, at the structure level, compared with the manifest.
1357
+ * @param value - manifest.arena
1358
+ * @param regions - the buffer regions
1359
+ * @param refs - the core references
1360
+ * @param arrays - the core arrays
1361
+ * @param structure - whether the structure checks run
1362
+ * @returns the arena layout, or null when the manifest carries none or the region is chunked
1363
+ */
1364
+ function decodeArena(
1365
+ value: unknown,
1366
+ regions: readonly WireRegion[],
1367
+ refs: CoreRefs,
1368
+ arrays: CoreArrays,
1369
+ structure: boolean,
1370
+ ): ArenaLayout | null {
1371
+ if (value === null || value === undefined) {
1372
+ return null;
1373
+ }
1374
+ const a = asObject(value, "arena");
1375
+ const buffer = asCount(a.buffer, "arena.buffer");
1376
+ const byteOffset = asCount(a.byteOffset, "arena.byteOffset");
1377
+ const byteLength = asCount(a.byteLength, "arena.byteLength");
1378
+ const region = regions[buffer] as WireRegion | undefined;
1379
+ if (region === undefined) {
1380
+ throw badWire("arena.buffer", `buffer index ${buffer} is out of range`, { found: buffer });
1381
+ }
1382
+ if (byteOffset + byteLength > region.byteLength) {
1383
+ throw badWire("arena", `range [${byteOffset}, ${byteOffset + byteLength}) exceeds the buffer`, {
1384
+ byteOffset,
1385
+ byteLength,
1386
+ bufferByteLength: region.byteLength,
1387
+ });
1388
+ }
1389
+ const located = region.locate(byteOffset, byteLength);
1390
+ if (located === null) {
1391
+ return null;
1392
+ }
1393
+ const segments: Record<CoreArrayName, ArenaSegment | null> = {
1394
+ rowPtr: null,
1395
+ colIdx: null,
1396
+ weights: null,
1397
+ arcToEdge: null,
1398
+ edgeToArc: null,
1399
+ };
1400
+ let hotByteLength = 0;
1401
+ const names: readonly CoreArrayName[] = CORE_ORDER;
1402
+ for (const name of names) {
1403
+ const ref = refs[name];
1404
+ const array = arrays[name];
1405
+ if (ref === null || array === null || array.length === 0) {
1406
+ continue;
1407
+ }
1408
+ const inside =
1409
+ ref.buffer === buffer &&
1410
+ ref.byteOffset >= byteOffset &&
1411
+ ref.byteOffset + ref.byteLength <= byteOffset + byteLength;
1412
+ if (!inside) {
1413
+ continue;
1414
+ }
1415
+ const relative = ref.byteOffset - byteOffset;
1416
+ if (relative % ALIGNMENT !== 0) {
1417
+ throw badWire(`core.${name}`, `inside the arena at offset ${relative}, not a multiple of ${ALIGNMENT}`, {
1418
+ byteOffset: relative,
1419
+ });
1420
+ }
1421
+ if (array.buffer !== located.buffer || array.byteOffset !== located.byteOffset + relative) {
1422
+ throw badWire(`core.${name}`, "the arena describes a segment the array is not a view of");
1423
+ }
1424
+ segments[name] = Object.freeze({ byteOffset: located.byteOffset + relative, byteLength: ref.byteLength });
1425
+ if (name === "rowPtr" || name === "colIdx" || name === "weights") {
1426
+ hotByteLength = relative + ref.byteLength;
1427
+ }
1428
+ }
1429
+ if (structure && a.hotByteLength !== hotByteLength) {
1430
+ throw badWire(
1431
+ "arena.hotByteLength",
1432
+ `manifest says ${String(a.hotByteLength)}, the segments say ${hotByteLength}`,
1433
+ {
1434
+ expected: hotByteLength,
1435
+ found: a.hotByteLength,
1436
+ },
1437
+ );
1438
+ }
1439
+ return Object.freeze({
1440
+ buffer: located.buffer,
1441
+ byteOffset: located.byteOffset,
1442
+ byteLength,
1443
+ alignment: ALIGNMENT,
1444
+ segments: Object.freeze(segments),
1445
+ hotByteLength,
1446
+ });
1447
+ }
1448
+
1449
+ /**
1450
+ * Read the flags (every member a boolean).
1451
+ * @param value - manifest.flags
1452
+ * @returns the flags
1453
+ */
1454
+ function readFlags(value: unknown): SnapshotFlags {
1455
+ const o = asObject(value, "flags");
1456
+ const flag = (name: keyof SnapshotFlags): boolean => asBoolean(o[name], `flags.${name}`);
1457
+ return {
1458
+ multigraph: flag("multigraph"),
1459
+ hasSelfLoops: flag("hasSelfLoops"),
1460
+ arcToEdgeIsIdentity: flag("arcToEdgeIsIdentity"),
1461
+ weighted: flag("weighted"),
1462
+ allWeightsOne: flag("allWeightsOne"),
1463
+ nonNegativeWeights: flag("nonNegativeWeights"),
1464
+ finiteWeights: flag("finiteWeights"),
1465
+ };
1466
+ }
1467
+
1468
+ /**
1469
+ * Read a column origin (every field a string or null).
1470
+ * @param value - the manifest member
1471
+ * @param path - its path
1472
+ * @returns the origin, or null
1473
+ */
1474
+ function readOrigin(value: unknown, path: string): ColumnOrigin | null {
1475
+ if (value === null || value === undefined) {
1476
+ return null;
1477
+ }
1478
+ const o = asObject(value, path);
1479
+ const field = (key: keyof ColumnOrigin): string | null => asStringOrNull(o[key], `${path}.${key}`);
1480
+ return {
1481
+ format: field("format"),
1482
+ id: field("id"),
1483
+ title: field("title"),
1484
+ type: field("type"),
1485
+ namespace: field("namespace"),
1486
+ };
1487
+ }
1488
+
1489
+ /**
1490
+ * Read the graph metadata (design section 5.9); unknown fields are ignored, missing ones are null.
1491
+ * @param value - manifest.meta
1492
+ * @returns the frozen metadata
1493
+ */
1494
+ function readGraphMeta(value: unknown): GraphMeta {
1495
+ const o = asObject(value, "meta");
1496
+ const keywordsRaw = o.keywords ?? [];
1497
+ const keywords = asArray(keywordsRaw, "meta.keywords").map((k, i) => asString(k, `meta.keywords[${i}]`));
1498
+ const extraRaw = decodeJsonValue(o.extra ?? {}, "meta.extra");
1499
+ if (!isPlainObject(extraRaw)) {
1500
+ throw badWire("meta.extra", "expected an object");
1501
+ }
1502
+ return Object.freeze({
1503
+ name: asStringOrNull(o.name, "meta.name"),
1504
+ description: asStringOrNull(o.description, "meta.description"),
1505
+ creator: asStringOrNull(o.creator, "meta.creator"),
1506
+ created: asStringOrNull(o.created, "meta.created"),
1507
+ modified: asStringOrNull(o.modified, "meta.modified"),
1508
+ keywords: Object.freeze(keywords),
1509
+ sourceFormat: asStringOrNull(o.sourceFormat, "meta.sourceFormat"),
1510
+ sourceVersion: asStringOrNull(o.sourceVersion, "meta.sourceVersion"),
1511
+ idType: asEnum(o.idType, "meta.idType", ID_TYPES),
1512
+ timeFormat: asEnum(o.timeFormat, "meta.timeFormat", TIME_FORMATS),
1513
+ timeRepresentation: asEnum(o.timeRepresentation, "meta.timeRepresentation", TIME_REPRESENTATIONS),
1514
+ mode: asEnum(o.mode, "meta.mode", MODES),
1515
+ declaredMultigraph: asBooleanOrNull(o.declaredMultigraph, "meta.declaredMultigraph"),
1516
+ weightOrigin: readOrigin(o.weightOrigin, "meta.weightOrigin"),
1517
+ extra: Object.freeze(extraRaw),
1518
+ });
1519
+ }
1520
+
1521
+ /**
1522
+ * Check the manifest's discriminator and versions (design sections 9.1 and 13.5): `format`, the
1523
+ * wire major (E_UNSUPPORTED_VERSION kind "wire") and `formatVersion` (kind "format"). A newer wire
1524
+ * minor is accepted.
1525
+ * @param manifest - the manifest object
1526
+ * @returns the wire version pair
1527
+ */
1528
+ export function checkManifestVersions(manifest: Record<string, unknown>): readonly [number, number] {
1529
+ if (manifest.format !== WIRE_FORMAT) {
1530
+ throw badWire("format", `expected "${WIRE_FORMAT}", found ${describe(manifest.format)}`, {
1531
+ found: manifest.format,
1532
+ });
1533
+ }
1534
+ const wire = asArray(manifest.wire, "wire");
1535
+ const major = wire[0];
1536
+ const minor = wire[1];
1537
+ if (
1538
+ typeof major !== "number" ||
1539
+ typeof minor !== "number" ||
1540
+ !Number.isInteger(major) ||
1541
+ !Number.isInteger(minor)
1542
+ ) {
1543
+ throw badWire("wire", "expected [major, minor] integers", { found: manifest.wire });
1544
+ }
1545
+ if (major !== WIRE_MAJOR) {
1546
+ throw new GraphFormatError(
1547
+ "E_UNSUPPORTED_VERSION",
1548
+ `wire major ${major} is not supported (reader: ${WIRE_MAJOR})`,
1549
+ {
1550
+ kind: "wire",
1551
+ found: major,
1552
+ supported: WIRE_MAJOR,
1553
+ },
1554
+ );
1555
+ }
1556
+ if (manifest.formatVersion !== FORMAT_VERSION) {
1557
+ throw new GraphFormatError(
1558
+ "E_UNSUPPORTED_VERSION",
1559
+ `formatVersion ${describe(manifest.formatVersion)} is not supported (reader: ${FORMAT_VERSION})`,
1560
+ { kind: "format", found: manifest.formatVersion, supported: FORMAT_VERSION },
1561
+ );
1562
+ }
1563
+ return [major, minor];
1564
+ }
1565
+
1566
+ // ============================================================ the manifest reader
1567
+
1568
+ /**
1569
+ * Rebuild a snapshot from a manifest and the regions its references index (design sections 9.1 and
1570
+ * 9.5): versions checked, core arrays and arena adopted, id map and tables decoded, then the
1571
+ * snapshot validated at the requested level.
1572
+ * @param manifestRaw - the manifest (an object; already parsed for a container)
1573
+ * @param regions - one region per buffer index
1574
+ * @param options - the resolved options
1575
+ * @param honourArena - whether the manifest's arena descriptor is adopted (false for chunked input)
1576
+ * @returns the snapshot
1577
+ */
1578
+ export function decodeManifest(
1579
+ manifestRaw: unknown,
1580
+ regions: readonly WireRegion[],
1581
+ options: ResolvedFromWireOptions,
1582
+ honourArena: boolean,
1583
+ ): GraphSnapshot {
1584
+ const manifest = asObject(manifestRaw, "manifest");
1585
+ checkManifestVersions(manifest);
1586
+ const decoder = new WireDecoder(regions, options);
1587
+ const directed = asBoolean(manifest.directed, "directed");
1588
+ const countsRaw = asObject(manifest.counts, "counts");
1589
+ const counts: WireCounts = {
1590
+ nodes: asCount(countsRaw.nodes, "counts.nodes"),
1591
+ edges: asCount(countsRaw.edges, "counts.edges"),
1592
+ arcs: asCount(countsRaw.arcs, "counts.arcs"),
1593
+ selfLoops: asCount(countsRaw.selfLoops, "counts.selfLoops"),
1594
+ };
1595
+ const flags = readFlags(manifest.flags);
1596
+ if (flags.arcToEdgeIsIdentity && (!directed || counts.arcs !== counts.edges)) {
1597
+ throw badWire("flags.arcToEdgeIsIdentity", "set on an undirected graph or with arcs !== edges");
1598
+ }
1599
+ const coreRefs = readCoreRefs(manifest.core);
1600
+ const core = decodeCore(decoder, coreRefs, counts, flags);
1601
+ const arena = honourArena ? decodeArena(manifest.arena, regions, coreRefs, core, decoder.structure) : null;
1602
+ const ids = decoder.ids(manifest.ids);
1603
+ const nodes = decoder.table(manifest.nodeColumns ?? [], "node", null, counts.nodes, "nodeColumns");
1604
+ const edges = decoder.table(manifest.edgeColumns ?? [], "edge", null, counts.edges, "edgeColumns");
1605
+ const graph = decoder.table(manifest.graphColumns ?? [], "graph", null, 1, "graphColumns");
1606
+ const extensions = new Map<string, AttributeTable>();
1607
+ const extensionsRaw = asArray(manifest.extensions ?? [], "extensions");
1608
+ for (let i = 0; i < extensionsRaw.length; i++) {
1609
+ const path = `extensions[${i}]`;
1610
+ const ext = asObject(extensionsRaw[i], path);
1611
+ const name = asString(ext.name, `${path}.name`);
1612
+ if (extensions.has(name)) {
1613
+ throw badWire(`${path}.name`, `extension table "${name}" appears twice`, { table: name });
1614
+ }
1615
+ const rowCount = asCount(ext.rowCount, `${path}.rowCount`);
1616
+ extensions.set(name, decoder.table(ext.columns, "extension", name, rowCount, `${path}.columns`));
1617
+ }
1618
+ let meta = readGraphMeta(manifest.meta ?? {});
1619
+ if (decoder.skipped.length > 0) {
1620
+ const skipped = Object.freeze(decoder.skipped.map((s) => Object.freeze({ ...s })));
1621
+ meta = Object.freeze({ ...meta, extra: Object.freeze({ ...meta.extra, [SKIPPED_COLUMNS_KEY]: skipped }) });
1622
+ }
1623
+ const carried = decoder.views(manifest.views);
1624
+ if (decoder.structure) {
1625
+ decoder.checkOverlaps();
1626
+ }
1627
+ const parts: SnapshotParts = {
1628
+ label: asStringOrNull(manifest.label, "label"),
1629
+ serial: null,
1630
+ directed,
1631
+ nodeCount: counts.nodes,
1632
+ edgeCount: counts.edges,
1633
+ arcCount: counts.arcs,
1634
+ selfLoopCount: counts.selfLoops,
1635
+ rowPtr: core.rowPtr,
1636
+ colIdx: core.colIdx,
1637
+ weights: core.weights,
1638
+ arcToEdge: core.arcToEdge,
1639
+ edgeToArc: core.edgeToArc,
1640
+ flags,
1641
+ ids,
1642
+ nodes,
1643
+ edges,
1644
+ graph,
1645
+ extensions,
1646
+ meta,
1647
+ arena,
1648
+ checksum: false,
1649
+ };
1650
+ const snapshot = createSnapshot(parts);
1651
+ if (options.level === "full") {
1652
+ snapshot.validate({ level: "full" });
1653
+ } else if (options.level === "structure") {
1654
+ snapshot.validate({ level: "structure" });
1655
+ }
1656
+ if (options.level !== "full") {
1657
+ installViews(snapshot, carried, options.level === "structure");
1658
+ }
1659
+ return snapshot;
1660
+ }
1661
+
1662
+ /**
1663
+ * Rebuild a snapshot from its wire form (design section 9.1). Defaults: `validate: "structure"`
1664
+ * (the manifest is trusted to have come from this package; the cheap checks still run) and `copy:
1665
+ * false` (the arrays are views into `wire.buffers`; a SharedArrayBuffer is copied). The reverse id
1666
+ * Map and decoded strings are rebuilt lazily; carried views (`includeViews`) are installed on the
1667
+ * receiver below "full" and recomputed under "full", where nothing carried is trusted.
1668
+ * @param wire - the wire snapshot
1669
+ * @param options - validation level, copy and unknown-column policy
1670
+ * @returns the snapshot; E_UNSUPPORTED_VERSION, E_UNSUPPORTED, E_BAD_SERIALIZATION or E_INVALID_SNAPSHOT on bad input
1671
+ */
1672
+ export function fromWire(wire: WireSnapshot, options?: FromWireOptions): GraphSnapshot {
1673
+ const resolved = resolveFromWireOptions(options, "structure");
1674
+ const w = asObject(wire, "wire");
1675
+ const buffersRaw = asArray(w.buffers, "wire.buffers");
1676
+ const regions: WireRegion[] = [];
1677
+ for (let i = 0; i < buffersRaw.length; i++) {
1678
+ const b: unknown = buffersRaw[i];
1679
+ let buffer: ArrayBuffer;
1680
+ if (b instanceof ArrayBuffer) {
1681
+ buffer = resolved.copy ? copyBuffer(b) : b;
1682
+ } else if (isSharedArrayBuffer(b)) {
1683
+ buffer = copyBuffer(b);
1684
+ } else {
1685
+ throw badWire(`wire.buffers[${i}]`, "expected an ArrayBuffer");
1686
+ }
1687
+ regions.push(bufferRegion(buffer, 0, buffer.byteLength));
1688
+ }
1689
+ return decodeManifest(w.manifest, regions, resolved, true);
1690
+ }