@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,1247 @@
1
+ /**
2
+ * The concrete GraphSnapshot class (design sections 3, 5.8, 7, 9.3 and 11.4) and its factory
3
+ * `createSnapshot(parts)`, plus the boundary helpers `isGraphSnapshot` / `equalsTopology` (design
4
+ * section 7.5) and the arena helpers a GPU consumer binds segments with (section 10.3).
5
+ *
6
+ * The object is frozen with `Object.freeze` so `snapshot.rowPtr = x` throws in strict mode; every
7
+ * lazily populated member (the identity permutations of design section 3.1, the view cache of
8
+ * section 7.2, the checksum records of section 5.8, the content hash, the edge id index) lives in
9
+ * one mutable state record reachable only through a private field, which `Object.freeze` does not
10
+ * reach. `arcToEdge` / `edgeToArc` are prototype getters that materialise an identity permutation on
11
+ * first access outside the arena (never on the wire, never in `byteLength()`); `rowPtr`, `colIdx`
12
+ * and `weights` are plain data properties so hot loops pay nothing. Every method that reads the core
13
+ * throws `E_DETACHED` once the core buffer was transferred away (`detached` is derived:
14
+ * `rowPtr.length === 0`).
15
+ *
16
+ * Views are computed by views.ts, queries by queries.ts, derived graphs by derived.ts (which returns
17
+ * SnapshotParts that this module wraps), invariant checks by validate.ts and hashes by hash.ts. The
18
+ * wire methods (`toWire`, `toBytes`, `toByteChunks`, `transferables`) call the wire module directly;
19
+ * the wire module imports this one too, and the cycle is safe because each side reaches the other
20
+ * only through hoisted function declarations invoked at call time, never at module load.
21
+ */
22
+ import { dropGpuViewCache, isColumnDetached } from "../columns/column.js";
23
+ import { columnSetVersion } from "../columns/table.js";
24
+ import { FORMAT_VERSION, INVALID_INDEX, SNAPSHOT_BRAND } from "../constants.js";
25
+ import { GraphFormatError } from "../errors.js";
26
+ import { EdgeIdIndex } from "../ids/edge-id-index.js";
27
+ import { idMapBuffers, idMapDetached } from "../ids/node-id-map.js";
28
+ import { assertOneOf } from "../util/options.js";
29
+ import { claimHolder } from "../util/shared-buffers.js";
30
+ import { toByteChunks, toBytes } from "../wire/bytes.js";
31
+ import { toWire, transferables } from "../wire/to-wire.js";
32
+ import { deriveContract, deriveFilterEdges, deriveInducedSubgraph, deriveRelabel, deriveSimplified, deriveToUndirected, deriveTranspose, deriveWithoutSelfLoops, identityDerived, withColumnsParts, } from "./derived.js";
33
+ import { contentHashOf, hashColumn, hashTypedArray } from "./hash.js";
34
+ import { arcRangeIn, arcSourceIn, findArcIn, multiplicityIn, selfLoopsAtIn } from "./queries.js";
35
+ import { validateFull, validateStructure } from "./validate.js";
36
+ import { computeCoo, computeDegreeOrder, computeEdgeList, computeMate, computeReverse, computeSelfLoops, computeSelfLoopWeight, computeSymmetric, computeTotalWeight, identityPermutation, rowLengths, rowWeightSums, sumDegrees, sumF64, viewArrays, viewByteLength, widenToF64, } from "./views.js";
37
+ // ============================================================ module state
38
+ /** The next process-unique serial (design section 5.8). */
39
+ let nextSerial = 1;
40
+ /** Every view name in the order `cachedViews()` reports them. */
41
+ const VIEW_NAMES = [
42
+ "reverse",
43
+ "coo",
44
+ "edgeList",
45
+ "outDegree",
46
+ "inDegree",
47
+ "degree",
48
+ "weightedOutDegree",
49
+ "weightedInDegree",
50
+ "weightedDegree",
51
+ "selfLoopWeight",
52
+ "totalWeight",
53
+ "selfLoopArcs",
54
+ "selfLoopsPerNode",
55
+ "mate",
56
+ "degreeOrder",
57
+ "reverseDegreeOrder",
58
+ "symmetric",
59
+ ];
60
+ /**
61
+ * The own enumerable property every snapshot carries whose value is a function, so that
62
+ * `structuredClone(snapshot)` and `postMessage(snapshot)` throw `DataCloneError` immediately (design
63
+ * section 7.5; the structured clone algorithm refuses functions and visits enumerable own
64
+ * properties only).
65
+ */
66
+ export const CLONE_GUARD_KEY = "__graphtyNoStructuredClone";
67
+ export { EMPTY_GRAPH_META } from "./graph-meta.js";
68
+ /**
69
+ * The value of the clone guard property.
70
+ * @returns nothing
71
+ */
72
+ function cloneGuard() {
73
+ return undefined;
74
+ }
75
+ /** Module-private access to the state of a snapshot for the helpers below. */
76
+ const STATES = new WeakMap();
77
+ /**
78
+ * A fresh view cache with every slot empty.
79
+ * @returns the cache
80
+ */
81
+ function emptyViewCache() {
82
+ return {
83
+ reverse: null,
84
+ coo: null,
85
+ edgeList: null,
86
+ outDegree: null,
87
+ inDegree: null,
88
+ degree: null,
89
+ weightedOutDegree: null,
90
+ weightedInDegree: null,
91
+ weightedDegree: null,
92
+ selfLoopWeight: null,
93
+ totalWeight: null,
94
+ selfLoopArcs: null,
95
+ selfLoopsPerNode: null,
96
+ mate: null,
97
+ degreeOrder: null,
98
+ reverseDegreeOrder: null,
99
+ symmetric: null,
100
+ };
101
+ }
102
+ /**
103
+ * The E_INVALID_SNAPSHOT error of a SnapshotParts inconsistency caught at construction.
104
+ * @param invariant - the invariant number
105
+ * @param message - the description
106
+ * @param details - the location
107
+ * @returns the error
108
+ */
109
+ function partsError(invariant, message, details) {
110
+ return new GraphFormatError("E_INVALID_SNAPSHOT", `invariant ${invariant} violated: ${message}`, {
111
+ invariant,
112
+ ...details,
113
+ });
114
+ }
115
+ /**
116
+ * The O(1) consistency checks the constructor runs on the parts a producer hands it (lengths and
117
+ * the identity-flag rule); everything deeper is `validate()`.
118
+ * @param parts - the parts
119
+ */
120
+ function checkParts(parts) {
121
+ const { nodeCount, edgeCount, arcCount, flags } = parts;
122
+ if (parts.rowPtr.length !== nodeCount + 1) {
123
+ throw partsError("I1", `rowPtr has ${parts.rowPtr.length} entries, expected ${nodeCount + 1}`, {
124
+ expected: nodeCount + 1,
125
+ found: parts.rowPtr.length,
126
+ });
127
+ }
128
+ if (parts.colIdx.length !== arcCount) {
129
+ throw partsError("I1", `colIdx has ${parts.colIdx.length} entries, expected ${arcCount}`, {
130
+ expected: arcCount,
131
+ found: parts.colIdx.length,
132
+ });
133
+ }
134
+ if (parts.weights !== null && parts.weights.length !== arcCount) {
135
+ throw partsError("I8", `weights has ${parts.weights.length} entries, expected ${arcCount}`, {
136
+ expected: arcCount,
137
+ found: parts.weights.length,
138
+ });
139
+ }
140
+ const identity = flags.arcToEdgeIsIdentity;
141
+ if ((parts.arcToEdge === null) !== identity || (parts.edgeToArc === null) !== identity) {
142
+ throw partsError("I9", "arcToEdge / edgeToArc must be null exactly when flags.arcToEdgeIsIdentity", {
143
+ flag: "arcToEdgeIsIdentity",
144
+ found: identity,
145
+ });
146
+ }
147
+ if (identity && (!parts.directed || arcCount !== edgeCount)) {
148
+ throw partsError("I9", "flags.arcToEdgeIsIdentity requires a directed snapshot with arcCount === edgeCount", {
149
+ flag: "arcToEdgeIsIdentity",
150
+ directed: parts.directed,
151
+ arcCount,
152
+ edgeCount,
153
+ });
154
+ }
155
+ if (parts.arcToEdge !== null && parts.arcToEdge.length !== arcCount) {
156
+ throw partsError("I5", `arcToEdge has ${parts.arcToEdge.length} entries, expected ${arcCount}`, {
157
+ expected: arcCount,
158
+ found: parts.arcToEdge.length,
159
+ });
160
+ }
161
+ if (parts.edgeToArc !== null && parts.edgeToArc.length !== edgeCount) {
162
+ throw partsError("I5", `edgeToArc has ${parts.edgeToArc.length} entries, expected ${edgeCount}`, {
163
+ expected: edgeCount,
164
+ found: parts.edgeToArc.length,
165
+ });
166
+ }
167
+ if (parts.ids.size !== nodeCount) {
168
+ throw partsError("I11", `ids.size is ${parts.ids.size}, expected ${nodeCount}`, {
169
+ expected: nodeCount,
170
+ found: parts.ids.size,
171
+ });
172
+ }
173
+ if (parts.nodes.rowCount !== nodeCount || parts.edges.rowCount !== edgeCount || parts.graph.rowCount !== 1) {
174
+ throw partsError("I12", "table row counts do not match nodeCount / edgeCount / 1", {
175
+ nodes: parts.nodes.rowCount,
176
+ edges: parts.edges.rowCount,
177
+ graph: parts.graph.rowCount,
178
+ });
179
+ }
180
+ }
181
+ /**
182
+ * Record the digests of every immutable column of a table.
183
+ * @param records - the records
184
+ * @param table - the table
185
+ */
186
+ function recordColumns(records, table) {
187
+ for (const column of table) {
188
+ if (!column.meta.mutable) {
189
+ records.columns.set(column, hashColumn(column));
190
+ }
191
+ }
192
+ }
193
+ // ============================================================ the class
194
+ /**
195
+ * The frozen graph (design section 3): a CSR core over 4-byte typed arrays, the id map, the
196
+ * attribute tables, the flags and a per-instance view cache. Topology, counts, flags, id map and
197
+ * immutable columns never change after construction (invariant I17); the column SET of `nodes` /
198
+ * `edges` / `graph` and the CONTENTS of columns declared mutable are mutable side tables (design
199
+ * section 5.8). Every view is a pure function of the core, memoised once and SHARED: a call returns
200
+ * the cached array itself, so writing into a view is a contract violation; call `.slice()` for
201
+ * scratch. Index-taking queries are total for in-range arguments and unchecked otherwise (design
202
+ * section 11.1). Instances come from `createSnapshot()` (the builder, `fromCsr`, `fromWire` and the
203
+ * derived-graph methods); `isGraphSnapshot()` recognises them structurally by brand and
204
+ * formatVersion.
205
+ */
206
+ export class GraphSnapshot {
207
+ /**
208
+ * Wrap prepared parts. Producers call `createSnapshot()`; the constructor is not part of the public
209
+ * surface. The producer has established I1-I13; the constructor checks the O(1) length rules,
210
+ * records checksums when asked, freezes the object and installs the clone guard.
211
+ * @param parts - the parts (no array may alias memory a builder can still write, invariant I18)
212
+ * @internal
213
+ */
214
+ constructor(parts) {
215
+ checkParts(parts);
216
+ this[SNAPSHOT_BRAND] = true;
217
+ this.serial = parts.serial ?? nextSerial++;
218
+ this.label = parts.label;
219
+ this.formatVersion = FORMAT_VERSION;
220
+ this.directed = parts.directed;
221
+ this.nodeCount = parts.nodeCount;
222
+ this.edgeCount = parts.edgeCount;
223
+ this.arcCount = parts.arcCount;
224
+ this.selfLoopCount = parts.selfLoopCount;
225
+ this.rowPtr = parts.rowPtr;
226
+ this.colIdx = parts.colIdx;
227
+ this.weights = parts.weights;
228
+ this.flags = Object.freeze({ ...parts.flags });
229
+ this.ids = parts.ids;
230
+ this.nodes = parts.nodes;
231
+ this.edges = parts.edges;
232
+ this.graph = parts.graph;
233
+ this.extensions = parts.extensions;
234
+ this.meta = parts.meta;
235
+ this.arena = parts.arena;
236
+ let checksums = null;
237
+ if (parts.checksum) {
238
+ checksums = { core: new Map(), columns: new WeakMap(), views: new Map() };
239
+ checksums.core.set("rowPtr", hashTypedArray(parts.rowPtr));
240
+ checksums.core.set("colIdx", hashTypedArray(parts.colIdx));
241
+ if (parts.weights !== null) {
242
+ checksums.core.set("weights", hashTypedArray(parts.weights));
243
+ }
244
+ if (parts.arcToEdge !== null) {
245
+ checksums.core.set("arcToEdge", hashTypedArray(parts.arcToEdge));
246
+ }
247
+ if (parts.edgeToArc !== null) {
248
+ checksums.core.set("edgeToArc", hashTypedArray(parts.edgeToArc));
249
+ }
250
+ recordColumns(checksums, parts.nodes);
251
+ recordColumns(checksums, parts.edges);
252
+ recordColumns(checksums, parts.graph);
253
+ for (const table of parts.extensions.values()) {
254
+ recordColumns(checksums, table);
255
+ }
256
+ }
257
+ const state = {
258
+ arcToEdge: parts.arcToEdge,
259
+ edgeToArc: parts.edgeToArc,
260
+ views: emptyViewCache(),
261
+ checksums,
262
+ contentHash: null,
263
+ edgeIds: null,
264
+ };
265
+ Object.defineProperty(this, "state", { value: state, enumerable: false, writable: false, configurable: false });
266
+ STATES.set(this, state);
267
+ claimStorage(parts);
268
+ Object.defineProperty(this, CLONE_GUARD_KEY, {
269
+ value: cloneGuard,
270
+ enumerable: true,
271
+ writable: false,
272
+ configurable: false,
273
+ });
274
+ Object.freeze(this);
275
+ }
276
+ // ---------------------------------------------------------------- core accessors
277
+ /**
278
+ * rowPtr.length === 0: the core buffer was transferred away (design section 9.4). Derived from the
279
+ * array state, never a stored bit, so every holder of the same core agrees.
280
+ * @returns true when detached
281
+ */
282
+ get detached() {
283
+ return this.rowPtr.length === 0;
284
+ }
285
+ /**
286
+ * Logical edge of every arc (invariant I5); an identity permutation is materialised on first
287
+ * access as a separate 4-byte-aligned array outside the arena. GPU code tests
288
+ * `flags.arcToEdgeIsIdentity` before reading it.
289
+ * @returns arcCount entries; E_DETACHED after a consuming transfer
290
+ */
291
+ get arcToEdge() {
292
+ this.assertAttached();
293
+ const { state } = this;
294
+ if (state.arcToEdge === null) {
295
+ state.arcToEdge = identityPermutation(this.arcCount);
296
+ state.checksums?.core.set("arcToEdge", hashTypedArray(state.arcToEdge));
297
+ }
298
+ return state.arcToEdge;
299
+ }
300
+ /**
301
+ * The arc holding the declared orientation of every logical edge (invariant I5); an identity
302
+ * permutation is materialised on first access outside the arena.
303
+ * @returns edgeCount entries; E_DETACHED after a consuming transfer
304
+ */
305
+ get edgeToArc() {
306
+ this.assertAttached();
307
+ const { state } = this;
308
+ if (state.edgeToArc === null) {
309
+ state.edgeToArc = identityPermutation(this.edgeCount);
310
+ state.checksums?.core.set("edgeToArc", hashTypedArray(state.edgeToArc));
311
+ }
312
+ return state.edgeToArc;
313
+ }
314
+ // ---------------------------------------------------------------- queries (3.9)
315
+ /**
316
+ * The out-arc range of a node: [rowPtr[u], rowPtr[u + 1]]. Allocates a tuple; hot loops read
317
+ * rowPtr directly.
318
+ * @param u - the node index
319
+ * @returns the half-open arc range as [start, end]
320
+ */
321
+ outArcs(u) {
322
+ this.assertAttached();
323
+ return [this.rowPtr[u], this.rowPtr[u + 1]];
324
+ }
325
+ /**
326
+ * rowPtr[u + 1] - rowPtr[u]: the out-arc count (a self-loop counted once).
327
+ * @param u - the node index
328
+ * @returns the out-degree
329
+ */
330
+ outDegreeOf(u) {
331
+ this.assertAttached();
332
+ return this.rowPtr[u + 1] - this.rowPtr[u];
333
+ }
334
+ /**
335
+ * Binary search for the first arc u -> v (the lowest logical edge index among parallels).
336
+ * @param u - the source node index
337
+ * @param v - the target node index
338
+ * @returns the arc index, or INVALID_INDEX when absent
339
+ */
340
+ findArc(u, v) {
341
+ this.assertAttached();
342
+ return findArcIn(this.rowPtr, this.colIdx, u, v);
343
+ }
344
+ /**
345
+ * Whether an arc u -> v exists.
346
+ * @param u - the source node index
347
+ * @param v - the target node index
348
+ * @returns findArc(u, v) !== INVALID_INDEX
349
+ */
350
+ hasArc(u, v) {
351
+ return this.findArc(u, v) !== INVALID_INDEX;
352
+ }
353
+ /**
354
+ * The half-open arc range [lo, hi) of every arc u -> v; empty when lo === hi. Allocates a tuple.
355
+ * @param u - the source node index
356
+ * @param v - the target node index
357
+ * @returns the range as [lo, hi]
358
+ */
359
+ arcsBetween(u, v) {
360
+ this.assertAttached();
361
+ return arcRangeIn(this.rowPtr, this.colIdx, u, v);
362
+ }
363
+ /**
364
+ * Number of parallel arcs u -> v.
365
+ * @param u - the source node index
366
+ * @param v - the target node index
367
+ * @returns hi - lo of arcsBetween(u, v)
368
+ */
369
+ multiplicity(u, v) {
370
+ this.assertAttached();
371
+ return multiplicityIn(this.rowPtr, this.colIdx, u, v);
372
+ }
373
+ /**
374
+ * The row containing an arc: O(1) through the cached coo() view, else a binary search on rowPtr.
375
+ * @param a - the arc index
376
+ * @returns the source node index
377
+ */
378
+ arcSource(a) {
379
+ this.assertAttached();
380
+ const { coo } = this.state.views;
381
+ if (coo !== null) {
382
+ return coo.src[a];
383
+ }
384
+ return arcSourceIn(this.rowPtr, a);
385
+ }
386
+ /**
387
+ * Declared source of a logical edge: arcSource(edgeToArc[e]).
388
+ * @param e - the logical edge index
389
+ * @returns the source node index
390
+ */
391
+ edgeSource(e) {
392
+ this.assertAttached();
393
+ const list = this.state.views.edgeList;
394
+ if (list !== null) {
395
+ return list.src[e];
396
+ }
397
+ return this.arcSource(this.edgeToArc[e]);
398
+ }
399
+ /**
400
+ * Declared target of a logical edge: colIdx[edgeToArc[e]].
401
+ * @param e - the logical edge index
402
+ * @returns the target node index
403
+ */
404
+ edgeTarget(e) {
405
+ return this.colIdx[this.edgeToArc[e]];
406
+ }
407
+ /**
408
+ * Lookup through the role "id" edge column, backed by a Map built on first call and rebuilt when the
409
+ * column is replaced or a mutable id column's version changes (design section 4.6).
410
+ * @param id - the edge id
411
+ * @returns the logical edge index, or INVALID_INDEX on a miss or when no id column exists
412
+ */
413
+ edgeIndexOf(id) {
414
+ const { state } = this;
415
+ const version = columnSetVersion(this.edges);
416
+ if (state.edgeIds === null || state.edgeIds.version !== version) {
417
+ // the role lookup is repeated only when the column set changed
418
+ const column = this.edges.byRole("id");
419
+ state.edgeIds = { version, column, index: column === null ? null : new EdgeIdIndex(column) };
420
+ }
421
+ return state.edgeIds.index === null ? INVALID_INDEX : state.edgeIds.index.indexOf(id);
422
+ }
423
+ // ---------------------------------------------------------------- views (7.2)
424
+ /**
425
+ * The in-adjacency, rows sorted by source; the forward arrays themselves when undirected. Cached
426
+ * and shared.
427
+ * @returns the reverse view
428
+ */
429
+ reverse() {
430
+ return this.cached("reverse", () => computeReverse(this));
431
+ }
432
+ /**
433
+ * Per-arc COO form; src is the only new array. Cached and shared.
434
+ * @returns the COO view
435
+ */
436
+ coo() {
437
+ return this.cached("coo", () => computeCoo(this));
438
+ }
439
+ /**
440
+ * Every logical edge once in declared orientation. Cached and shared.
441
+ * @returns the edge list view
442
+ */
443
+ edgeList() {
444
+ return this.cached("edgeList", () => computeEdgeList(this));
445
+ }
446
+ /**
447
+ * rowPtr differences materialised; a self-loop counted once. Cached and shared: call `.slice()`
448
+ * before writing.
449
+ * @returns Uint32Array(n)
450
+ */
451
+ outDegree() {
452
+ return this.cached("outDegree", () => rowLengths(this.rowPtr, this.nodeCount));
453
+ }
454
+ /**
455
+ * Arcs targeting each node, via reverse().rowPtr; the same object as outDegree() when undirected.
456
+ * Cached and shared: call `.slice()` before writing.
457
+ * @returns Uint32Array(n)
458
+ */
459
+ inDegree() {
460
+ return this.cached("inDegree", () => this.directed ? rowLengths(this.reverse().rowPtr, this.nodeCount) : this.outDegree());
461
+ }
462
+ /**
463
+ * Graph-theoretic degree (NetworkX convention, design section 3.4): in + out when directed; out
464
+ * plus self-loops when undirected. Cached and shared: call `.slice()` before writing.
465
+ * @returns Uint32Array(n)
466
+ */
467
+ degree() {
468
+ return this.cached("degree", () => sumDegrees(this.outDegree(), this.directed ? this.inDegree() : this.selfLoopsPerNode()));
469
+ }
470
+ /**
471
+ * Row sums of weights (a self-loop arc counted once); outDegree widened when unweighted. F64 for
472
+ * CPU precision; may be 0 for a node with out-arcs. Cached and shared: call `.slice()` before
473
+ * writing.
474
+ * @returns Float64Array(n)
475
+ */
476
+ weightedOutDegree() {
477
+ return this.cached("weightedOutDegree", () => this.weights === null
478
+ ? widenToF64(this.outDegree())
479
+ : rowWeightSums(this.rowPtr, this.weights, this.nodeCount));
480
+ }
481
+ /**
482
+ * Weight sums over incoming arcs; the same object as weightedOutDegree() when undirected. Cached
483
+ * and shared: call `.slice()` before writing.
484
+ * @returns Float64Array(n)
485
+ */
486
+ weightedInDegree() {
487
+ return this.cached("weightedInDegree", () => {
488
+ if (!this.directed) {
489
+ return this.weightedOutDegree();
490
+ }
491
+ const reverse = this.reverse();
492
+ return reverse.weights === null
493
+ ? widenToF64(this.inDegree())
494
+ : rowWeightSums(reverse.rowPtr, reverse.weights, this.nodeCount);
495
+ });
496
+ }
497
+ /**
498
+ * NetworkX weighted degree: weightedOutDegree + (directed ? weightedInDegree : selfLoopWeight), so
499
+ * the sum is 2 * totalWeight() when undirected. Cached and shared: call `.slice()` before writing.
500
+ * @returns Float64Array(n)
501
+ */
502
+ weightedDegree() {
503
+ return this.cached("weightedDegree", () => sumF64(this.weightedOutDegree(), this.directed ? this.weightedInDegree() : this.selfLoopWeight()));
504
+ }
505
+ /**
506
+ * Sum of weights over each node's self-loop arcs (selfLoopsPerNode widened when unweighted).
507
+ * Cached and shared: call `.slice()` before writing.
508
+ * @returns Float64Array(n)
509
+ */
510
+ selfLoopWeight() {
511
+ return this.cached("selfLoopWeight", () => computeSelfLoopWeight(this, {
512
+ selfLoopArcs: this.selfLoopArcs(),
513
+ selfLoopsPerNode: this.selfLoopsPerNode(),
514
+ }));
515
+ }
516
+ /**
517
+ * Sum of weights over logical edges (each undirected edge once). Cached.
518
+ * @returns the total weight
519
+ */
520
+ totalWeight() {
521
+ return this.cached("totalWeight", () => computeTotalWeight(this));
522
+ }
523
+ /**
524
+ * The arcs a with colIdx[a] === row(a), found by binary search per row. Cached and shared: call
525
+ * `.slice()` before writing.
526
+ * @returns Uint32Array(selfLoopCount)
527
+ */
528
+ selfLoopArcs() {
529
+ return this.cached("selfLoopArcs", () => this.materialiseSelfLoops().selfLoopArcs);
530
+ }
531
+ /**
532
+ * Self-loop arcs per node. Cached and shared: call `.slice()` before writing.
533
+ * @returns Uint32Array(n)
534
+ */
535
+ selfLoopsPerNode() {
536
+ return this.cached("selfLoopsPerNode", () => this.materialiseSelfLoops().selfLoopsPerNode);
537
+ }
538
+ /**
539
+ * Point query: self-loop arcs at one node, O(log d).
540
+ * @param u - the node index
541
+ * @returns the count
542
+ */
543
+ selfLoopsAt(u) {
544
+ this.assertAttached();
545
+ return selfLoopsAtIn(this.rowPtr, this.colIdx, u);
546
+ }
547
+ /**
548
+ * For every arc, the arc storing the opposite orientation of the same edge (a self-loop maps to
549
+ * itself); the lockstep walk of design section 6.4. Undirected only. Cached and shared: call
550
+ * `.slice()` before writing.
551
+ * @returns Uint32Array(arcCount); E_DIRECTED on a directed snapshot
552
+ */
553
+ mate() {
554
+ return this.cached("mate", () => computeMate(this));
555
+ }
556
+ /**
557
+ * Nodes ordered by descending out-degree of the forward or reverse adjacency with the cuGraph tier
558
+ * boundaries. Both variants cached; on an undirected snapshot they are the same object.
559
+ * @param options - which adjacency's degree to order by
560
+ * @returns the permutation and its tier offsets
561
+ */
562
+ degreeOrder(options) {
563
+ const of = assertOneOf("of", options?.of, ["forward", "reverse"]) ?? "forward";
564
+ if (of === "reverse" && this.directed) {
565
+ return this.cached("reverseDegreeOrder", () => computeDegreeOrder(this.reverse().rowPtr, this.nodeCount));
566
+ }
567
+ const forward = this.cached("degreeOrder", () => computeDegreeOrder(this.rowPtr, this.nodeCount));
568
+ if (!this.directed) {
569
+ this.state.views.reverseDegreeOrder = forward;
570
+ }
571
+ return forward;
572
+ }
573
+ /**
574
+ * Whether the arc set is closed under reversal with equal weights (forward row v equals reverse row
575
+ * v for every v); true without work when undirected. Cached.
576
+ * @returns true when symmetric
577
+ */
578
+ isSymmetric() {
579
+ return this.cached("symmetric", () => (this.directed ? computeSymmetric(this, this.reverse()) : true));
580
+ }
581
+ /**
582
+ * Compute a set of views eagerly (inside freeze() through FreezeOptions.prepare, or off the critical
583
+ * path).
584
+ * @param views - the views to materialise
585
+ * @returns this snapshot
586
+ */
587
+ prepare(views) {
588
+ for (const name of views) {
589
+ this.materialiseView(name);
590
+ }
591
+ return this;
592
+ }
593
+ /**
594
+ * Release every cached view (and every identity permutation materialised on demand) and every
595
+ * cached `gpuView()` f32 copy of an f64 column (design section 7.2); they are recomputed on
596
+ * demand. The checksum records of the dropped views are dropped with them.
597
+ */
598
+ dropCaches() {
599
+ const { state } = this;
600
+ for (const name of VIEW_NAMES) {
601
+ state.views[name] = null;
602
+ }
603
+ state.checksums?.views.clear();
604
+ if (this.flags.arcToEdgeIsIdentity) {
605
+ state.arcToEdge = null;
606
+ state.edgeToArc = null;
607
+ state.checksums?.core.delete("arcToEdge");
608
+ state.checksums?.core.delete("edgeToArc");
609
+ }
610
+ for (const table of [this.nodes, this.edges, this.graph, ...this.extensions.values()]) {
611
+ for (const column of table) {
612
+ dropGpuViewCache(column);
613
+ }
614
+ }
615
+ }
616
+ /**
617
+ * Which views are resident.
618
+ * @returns the names of the cached views, in the fixed ViewName order
619
+ */
620
+ cachedViews() {
621
+ if (this.detached) {
622
+ // design section 9.1: the view caches of a detached snapshot are dropped
623
+ this.dropCaches();
624
+ return [];
625
+ }
626
+ const { views } = this.state;
627
+ return VIEW_NAMES.filter((name) => views[name] !== null);
628
+ }
629
+ // ---------------------------------------------------------------- derived graphs (7.3)
630
+ /**
631
+ * Every directed edge becomes undirected; reciprocal pairs collapse to one edge keeping the lower
632
+ * index's row (keep-first). Returns `{ snapshot: this, null maps }` on an undirected snapshot.
633
+ * @param options - reciprocal filtering and the weight reducer
634
+ * @returns the derived graph
635
+ */
636
+ toUndirected(options) {
637
+ this.assertAttached();
638
+ if (!this.directed) {
639
+ return this.wrapDerived(identityDerived());
640
+ }
641
+ return this.wrapDerived(deriveToUndirected(this, this.edgeEndpoints(), options));
642
+ }
643
+ /**
644
+ * Orientation of every edge swapped: the reverse view's arrays become the core. Returns this on an
645
+ * undirected snapshot.
646
+ * @returns the derived graph (same node and edge spaces)
647
+ */
648
+ transpose() {
649
+ this.assertAttached();
650
+ if (!this.directed) {
651
+ return this.wrapDerived(identityDerived());
652
+ }
653
+ return this.wrapDerived(deriveTranspose(this, this.reverse()));
654
+ }
655
+ /**
656
+ * One edge per (u, v) group (parallels are adjacent by invariant I4), survivor = lowest index;
657
+ * flags.multigraph is false afterwards.
658
+ * @param options - reducers and self-loop policy
659
+ * @returns the derived graph
660
+ */
661
+ simplified(options) {
662
+ this.assertAttached();
663
+ return this.wrapDerived(deriveSimplified(this, this.edgeEndpoints(), options));
664
+ }
665
+ /**
666
+ * Every edge with source !== target.
667
+ * @returns the derived graph
668
+ */
669
+ withoutSelfLoops() {
670
+ this.assertAttached();
671
+ return this.wrapDerived(deriveWithoutSelfLoops(this, this.edgeEndpoints()));
672
+ }
673
+ /**
674
+ * Keep the logical edges whose mask bit is set.
675
+ * @param keep - packed bitmap over logical edges; E_MASK_LENGTH when shorter than ceil(edgeCount / 32) words
676
+ * @returns the derived graph
677
+ */
678
+ filterEdges(keep) {
679
+ this.assertAttached();
680
+ return this.wrapDerived(deriveFilterEdges(this, this.edgeEndpoints(), keep));
681
+ }
682
+ /**
683
+ * The subgraph induced by a node selection: an index list (order = new index order; E_INDEX_RANGE
684
+ * for an out-of-range or repeated index) or a packed mask (ascending order; E_MASK_LENGTH when
685
+ * short). Edges with both endpoints kept.
686
+ * @param selection - the node indices or a mask
687
+ * @returns the derived graph
688
+ */
689
+ inducedSubgraph(selection) {
690
+ this.assertAttached();
691
+ return this.wrapDerived(deriveInducedSubgraph(this, this.edgeEndpoints(), selection));
692
+ }
693
+ /**
694
+ * Contract the nodes of each partition block into one node (design section 7.3).
695
+ * @param partition - one label per node; E_PARTITION for a wrong length or an INVALID_INDEX label
696
+ * @param options - weight reducer, self-loop and parallel policies, column reducers
697
+ * @returns the derived graph with blockSizes and an identity id map
698
+ */
699
+ contract(partition, options) {
700
+ this.assertAttached();
701
+ return this.wrapDerived(deriveContract(this, this.edgeEndpoints(), partition, options));
702
+ }
703
+ /**
704
+ * Permute the node space: perm[newIndex] = oldIndex; the id map follows; edge order is preserved.
705
+ * @param perm - a permutation of 0..n-1; E_INVALID_PERMUTATION otherwise
706
+ * @returns the derived graph
707
+ */
708
+ relabel(perm) {
709
+ this.assertAttached();
710
+ return this.wrapDerived(deriveRelabel(this, this.edgeEndpoints(), perm));
711
+ }
712
+ /**
713
+ * A new snapshot object sharing the core, the id map and the serial with a CLONED column set plus
714
+ * the given columns (the only operation that clones the column set).
715
+ * @param nodes - node columns to add, keyed by name
716
+ * @param edges - edge columns to add, keyed by name
717
+ * @returns the new snapshot
718
+ */
719
+ withColumns(nodes, edges) {
720
+ this.assertAttached();
721
+ const identity = this.flags.arcToEdgeIsIdentity;
722
+ const parts = withColumnsParts(this, nodes, edges, identity ? null : this.arcToEdge, identity ? null : this.edgeToArc);
723
+ return new GraphSnapshot({ ...parts, checksum: this.state.checksums !== null });
724
+ }
725
+ // ---------------------------------------------------------------- memory, transfer, checks (9, 11)
726
+ /**
727
+ * Resident bytes of the core (an identity permutation counts zero even when materialised), plus
728
+ * the side structures selected by the options.
729
+ * @param options - which side structures to include
730
+ * @returns the byte count
731
+ */
732
+ byteLength(options = {}) {
733
+ let bytes = this.rowPtr.byteLength + this.colIdx.byteLength;
734
+ if (this.weights !== null) {
735
+ bytes += this.weights.byteLength;
736
+ }
737
+ if (!this.flags.arcToEdgeIsIdentity) {
738
+ bytes += this.arcToEdge.byteLength + this.edgeToArc.byteLength;
739
+ }
740
+ if (options.views === true) {
741
+ const { views } = this.state;
742
+ for (const name of VIEW_NAMES) {
743
+ const value = views[name];
744
+ if (value !== null && !(name === "reverseDegreeOrder" && value === views.degreeOrder)) {
745
+ bytes += viewByteLength(this, name, value);
746
+ }
747
+ }
748
+ }
749
+ if (options.columns === true) {
750
+ for (const table of [this.nodes, this.edges, this.graph, ...this.extensions.values()]) {
751
+ for (const column of table) {
752
+ bytes += column.byteLength;
753
+ }
754
+ }
755
+ }
756
+ if (options.ids === true) {
757
+ bytes += this.ids.byteLength();
758
+ }
759
+ return bytes;
760
+ }
761
+ /**
762
+ * A 64-bit content hash of the core arrays (two 32-bit FNV-1a lanes) as 16 hex characters,
763
+ * computed lazily and cached (design section 9.3).
764
+ * @returns the hash
765
+ */
766
+ contentHash() {
767
+ this.assertAttached();
768
+ const { state } = this;
769
+ state.contentHash ?? (state.contentHash = contentHashOf(this));
770
+ return state.contentHash;
771
+ }
772
+ /**
773
+ * The distinct, exclusively owned backing buffers: the postMessage transfer list (design section
774
+ * 9.1). Buffers shared with another holder (a withColumns() sibling, a derived graph sharing the
775
+ * node table) are excluded.
776
+ * @returns the buffers
777
+ */
778
+ transferables() {
779
+ this.assertAttached();
780
+ return transferables(this);
781
+ }
782
+ /**
783
+ * The plain-object wire form (design section 9.1): a JSON-serialisable manifest plus the backing
784
+ * buffers. With `transfer: true` only exclusively owned buffers are listed as transferables and
785
+ * shared ones are copied.
786
+ * @param options - transfer mode, views and columns to include
787
+ * @returns the wire snapshot
788
+ */
789
+ toWire(options) {
790
+ this.assertAttached();
791
+ return toWire(this, options);
792
+ }
793
+ /**
794
+ * The GSNP byte container as one contiguous buffer (design section 9.2).
795
+ * @param options - views to include
796
+ * @returns the container bytes
797
+ */
798
+ toBytes(options) {
799
+ this.assertAttached();
800
+ return toBytes(this, options);
801
+ }
802
+ /**
803
+ * The GSNP container as a sequence of chunks (design section 9.2): the header plus manifest,
804
+ * then one chunk per non-empty segment.
805
+ * @param options - views to include
806
+ * @returns the chunks
807
+ */
808
+ toByteChunks(options) {
809
+ this.assertAttached();
810
+ return toByteChunks(this, options);
811
+ }
812
+ /**
813
+ * Check the invariants (design section 11.4): the recorded checksums first when `checksum` is set
814
+ * (a mutated frozen array is the most useful diagnosis, details.reason "checksum"; "no-checksum"
815
+ * when none were recorded), then "structure" or "full" (default) per the level table of design
816
+ * section 9.5. Throws E_INVALID_SNAPSHOT with details.invariant and the location on the first
817
+ * violation.
818
+ * @param options - level and checksum comparison
819
+ */
820
+ validate(options = {}) {
821
+ this.assertAttached();
822
+ const level = assertOneOf("level", options.level, ["structure", "full"]) ?? "full";
823
+ if (typeof options.checksum !== "boolean" && options.checksum !== undefined) {
824
+ throw new GraphFormatError("E_UNSUPPORTED", "validate option checksum must be a boolean", {
825
+ field: "checksum",
826
+ found: options.checksum,
827
+ reason: "unsupported option",
828
+ });
829
+ }
830
+ // a column or id store transferred away underneath the snapshot (a holder outside the owner
831
+ // count, design section 9.1) is E_DETACHED, not a length-rule violation
832
+ if (idMapDetached(this.ids)) {
833
+ throw new GraphFormatError("E_DETACHED", "the id map's storage was transferred away", {
834
+ serial: this.serial,
835
+ });
836
+ }
837
+ for (const table of [this.nodes, this.edges, this.graph, ...this.extensions.values()]) {
838
+ for (const column of table) {
839
+ if (isColumnDetached(column)) {
840
+ throw new GraphFormatError("E_DETACHED", `column "${column.meta.name}" was transferred away`, {
841
+ column: column.meta.name,
842
+ serial: this.serial,
843
+ });
844
+ }
845
+ }
846
+ }
847
+ if (options.checksum === true) {
848
+ this.verifyChecksums();
849
+ }
850
+ if (level === "structure") {
851
+ validateStructure(this);
852
+ }
853
+ else {
854
+ validateFull(this);
855
+ }
856
+ }
857
+ // ---------------------------------------------------------------- private helpers
858
+ /**
859
+ * Throw E_DETACHED when the core was transferred away.
860
+ */
861
+ assertAttached() {
862
+ if (this.rowPtr.length === 0) {
863
+ throw new GraphFormatError("E_DETACHED", "the snapshot's core buffer was transferred away", {
864
+ serial: this.serial,
865
+ });
866
+ }
867
+ }
868
+ /**
869
+ * Return the cached value of a view, computing and recording it on first use.
870
+ * @param name - the view name
871
+ * @param compute - the computation
872
+ * @returns the cached value
873
+ */
874
+ cached(name, compute) {
875
+ this.assertAttached();
876
+ const { views } = this.state;
877
+ const existing = views[name];
878
+ if (existing !== null) {
879
+ return existing;
880
+ }
881
+ const value = compute();
882
+ views[name] = value;
883
+ this.recordView(name, value);
884
+ return value;
885
+ }
886
+ /**
887
+ * Compute both self-loop views in one pass and seed the slot the caller did not ask for.
888
+ * @returns the two views
889
+ */
890
+ materialiseSelfLoops() {
891
+ const loops = computeSelfLoops(this);
892
+ const { views } = this.state;
893
+ if (views.selfLoopArcs === null) {
894
+ views.selfLoopArcs = loops.selfLoopArcs;
895
+ this.recordView("selfLoopArcs", loops.selfLoopArcs);
896
+ }
897
+ if (views.selfLoopsPerNode === null) {
898
+ views.selfLoopsPerNode = loops.selfLoopsPerNode;
899
+ this.recordView("selfLoopsPerNode", loops.selfLoopsPerNode);
900
+ }
901
+ return { selfLoopArcs: views.selfLoopArcs, selfLoopsPerNode: views.selfLoopsPerNode };
902
+ }
903
+ /**
904
+ * Record the digests of a freshly materialised view when checksums are on.
905
+ * @param name - the view name
906
+ * @param value - the view
907
+ */
908
+ recordView(name, value) {
909
+ recordViewDigests(this, this.state, name, value);
910
+ }
911
+ /**
912
+ * Compare every recorded checksum with the current bytes (design section 5.8).
913
+ */
914
+ verifyChecksums() {
915
+ const records = this.state.checksums;
916
+ if (records === null) {
917
+ throw new GraphFormatError("E_INVALID_SNAPSHOT", "no checksums were recorded for this snapshot", {
918
+ reason: "no-checksum",
919
+ });
920
+ }
921
+ const mismatch = (what, details) => new GraphFormatError("E_INVALID_SNAPSHOT", `checksum mismatch: ${what} was modified after freeze`, {
922
+ reason: "checksum",
923
+ ...details,
924
+ });
925
+ const { state } = this;
926
+ const core = [
927
+ ["rowPtr", this.rowPtr],
928
+ ["colIdx", this.colIdx],
929
+ ["weights", this.weights],
930
+ ["arcToEdge", state.arcToEdge],
931
+ ["edgeToArc", state.edgeToArc],
932
+ ];
933
+ for (const [name, array] of core) {
934
+ const recorded = records.core.get(name);
935
+ if (recorded !== undefined && array !== null && hashTypedArray(array) !== recorded) {
936
+ throw mismatch(`core array ${name}`, { array: name });
937
+ }
938
+ }
939
+ const tables = [
940
+ ["nodes", this.nodes],
941
+ ["edges", this.edges],
942
+ ["graph", this.graph],
943
+ ...[...this.extensions].map(([name, table]) => [`extensions[${name}]`, table]),
944
+ ];
945
+ for (const [tableName, table] of tables) {
946
+ for (const column of table) {
947
+ const recorded = records.columns.get(column);
948
+ if (recorded !== undefined && hashColumn(column) !== recorded) {
949
+ throw mismatch(`column "${column.meta.name}" of ${tableName}`, {
950
+ table: tableName,
951
+ column: column.meta.name,
952
+ });
953
+ }
954
+ }
955
+ }
956
+ for (const name of VIEW_NAMES) {
957
+ const value = state.views[name];
958
+ if (value === null) {
959
+ continue;
960
+ }
961
+ for (const [member, array] of Object.entries(viewArrays(this, name, value))) {
962
+ const recorded = records.views.get(`${name}.${member}`);
963
+ if (recorded !== undefined && hashTypedArray(array) !== recorded) {
964
+ throw mismatch(`view ${name}.${member}`, { view: name, member });
965
+ }
966
+ }
967
+ }
968
+ }
969
+ /**
970
+ * The edge endpoints in declared orientation for the derived-graph functions: the cached edge list
971
+ * when resident, else a fresh one that is NOT cached (derived graphs must not grow the source's
972
+ * resident size, design section 7.3).
973
+ * @returns an edge list view
974
+ */
975
+ edgeEndpoints() {
976
+ return this.state.views.edgeList ?? computeEdgeList(this);
977
+ }
978
+ /**
979
+ * Wrap derived parts into a DerivedGraph, creating the new snapshot (with this snapshot's checksum
980
+ * setting) or returning this for the identity cases.
981
+ * @param derived - the derived parts
982
+ * @returns the derived graph
983
+ */
984
+ wrapDerived(derived) {
985
+ const snapshot = derived.parts === null
986
+ ? this
987
+ : new GraphSnapshot({ ...derived.parts, checksum: this.state.checksums !== null });
988
+ return {
989
+ snapshot,
990
+ nodeOrigin: derived.nodeOrigin,
991
+ edgeOrigin: derived.edgeOrigin,
992
+ nodeRemap: derived.nodeRemap,
993
+ edgeRemap: derived.edgeRemap,
994
+ blockSizes: derived.blockSizes,
995
+ report: derived.report,
996
+ };
997
+ }
998
+ /**
999
+ * Materialise one named view (the prepare() dispatch).
1000
+ * @param name - the view name
1001
+ */
1002
+ materialiseView(name) {
1003
+ switch (name) {
1004
+ case "reverse":
1005
+ this.reverse();
1006
+ break;
1007
+ case "coo":
1008
+ this.coo();
1009
+ break;
1010
+ case "edgeList":
1011
+ this.edgeList();
1012
+ break;
1013
+ case "outDegree":
1014
+ this.outDegree();
1015
+ break;
1016
+ case "inDegree":
1017
+ this.inDegree();
1018
+ break;
1019
+ case "degree":
1020
+ this.degree();
1021
+ break;
1022
+ case "weightedOutDegree":
1023
+ this.weightedOutDegree();
1024
+ break;
1025
+ case "weightedInDegree":
1026
+ this.weightedInDegree();
1027
+ break;
1028
+ case "weightedDegree":
1029
+ this.weightedDegree();
1030
+ break;
1031
+ case "selfLoopWeight":
1032
+ this.selfLoopWeight();
1033
+ break;
1034
+ case "totalWeight":
1035
+ this.totalWeight();
1036
+ break;
1037
+ case "selfLoopArcs":
1038
+ this.selfLoopArcs();
1039
+ break;
1040
+ case "selfLoopsPerNode":
1041
+ this.selfLoopsPerNode();
1042
+ break;
1043
+ case "mate":
1044
+ this.mate();
1045
+ break;
1046
+ case "degreeOrder":
1047
+ this.degreeOrder();
1048
+ break;
1049
+ case "reverseDegreeOrder":
1050
+ this.degreeOrder({ of: "reverse" });
1051
+ break;
1052
+ case "symmetric":
1053
+ this.isSymmetric();
1054
+ break;
1055
+ default: {
1056
+ const unknown = name;
1057
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown view ${String(unknown)}`, { view: unknown });
1058
+ }
1059
+ }
1060
+ }
1061
+ }
1062
+ /**
1063
+ * Record this snapshot as a holder of every buffer, table and id map it keeps (design section 9.1):
1064
+ * a second snapshot claiming the same storage (a withColumns() sibling, a derived graph sharing the
1065
+ * node table) marks it shared, so a transfer copies it instead of emptying the sibling. Each distinct
1066
+ * buffer is claimed once per snapshot (the arena backs several core arrays).
1067
+ * @param parts - the parts the snapshot was built from
1068
+ */
1069
+ function claimStorage(parts) {
1070
+ const buffers = new Set([parts.rowPtr.buffer, parts.colIdx.buffer]);
1071
+ if (parts.weights !== null) {
1072
+ buffers.add(parts.weights.buffer);
1073
+ }
1074
+ if (parts.arcToEdge !== null) {
1075
+ buffers.add(parts.arcToEdge.buffer);
1076
+ }
1077
+ if (parts.edgeToArc !== null) {
1078
+ buffers.add(parts.edgeToArc.buffer);
1079
+ }
1080
+ for (const buffer of idMapBuffers(parts.ids)) {
1081
+ buffers.add(buffer);
1082
+ }
1083
+ for (const buffer of buffers) {
1084
+ claimHolder(buffer);
1085
+ }
1086
+ claimHolder(parts.ids);
1087
+ claimHolder(parts.nodes);
1088
+ claimHolder(parts.edges);
1089
+ claimHolder(parts.graph);
1090
+ for (const table of parts.extensions.values()) {
1091
+ claimHolder(table);
1092
+ }
1093
+ }
1094
+ // ============================================================ factory and helpers
1095
+ /**
1096
+ * Create a snapshot from prepared parts (design section 6.3 step 12): the constructor freezes the
1097
+ * object, records checksums when `parts.checksum` is set and allocates the per-instance view cache.
1098
+ * @param parts - the parts; I1-I13 established by the producer, O(1) length rules checked here
1099
+ * @returns the frozen snapshot
1100
+ */
1101
+ export function createSnapshot(parts) {
1102
+ return new GraphSnapshot(parts);
1103
+ }
1104
+ /**
1105
+ * Structural check for a snapshot (design section 7.5): the SNAPSHOT_BRAND property plus the
1106
+ * formatVersion, never instanceof, so a duplicated package copy still interoperates.
1107
+ * @param x - any value
1108
+ * @returns true when x is a GraphSnapshot of this format version
1109
+ */
1110
+ export function isGraphSnapshot(x) {
1111
+ if (typeof x !== "object" || x === null) {
1112
+ return false;
1113
+ }
1114
+ const candidate = x;
1115
+ return candidate[SNAPSHOT_BRAND] === true && candidate.formatVersion === FORMAT_VERSION;
1116
+ }
1117
+ /**
1118
+ * Whether two typed arrays hold the same elements.
1119
+ * @param a - the first array
1120
+ * @param b - the second array
1121
+ * @returns true when equal element by element
1122
+ */
1123
+ function sameElements(a, b) {
1124
+ if (a.length !== b.length) {
1125
+ return false;
1126
+ }
1127
+ for (let i = 0; i < a.length; i++) {
1128
+ if (a[i] !== b[i]) {
1129
+ return false;
1130
+ }
1131
+ }
1132
+ return true;
1133
+ }
1134
+ /**
1135
+ * Compare the core arrays and id maps of two snapshots element by element (design section 7.5):
1136
+ * directedness, counts, rowPtr, colIdx, weights (both absent or equal), arcToEdge / edgeToArc
1137
+ * (identity compared without materialising) and every id (SameValueZero).
1138
+ * @param a - the first snapshot
1139
+ * @param b - the second snapshot
1140
+ * @returns true when the topologies and id maps are equal
1141
+ */
1142
+ export function equalsTopology(a, b) {
1143
+ if (a.directed !== b.directed ||
1144
+ a.nodeCount !== b.nodeCount ||
1145
+ a.edgeCount !== b.edgeCount ||
1146
+ a.arcCount !== b.arcCount ||
1147
+ a.selfLoopCount !== b.selfLoopCount) {
1148
+ return false;
1149
+ }
1150
+ if (!sameElements(a.rowPtr, b.rowPtr) || !sameElements(a.colIdx, b.colIdx)) {
1151
+ return false;
1152
+ }
1153
+ if ((a.weights === null) !== (b.weights === null)) {
1154
+ return false;
1155
+ }
1156
+ if (a.weights !== null && b.weights !== null && !sameElements(a.weights, b.weights)) {
1157
+ return false;
1158
+ }
1159
+ const identityA = a.flags.arcToEdgeIsIdentity;
1160
+ const identityB = b.flags.arcToEdgeIsIdentity;
1161
+ if (!identityA || !identityB) {
1162
+ if (!sameElements(a.arcToEdge, b.arcToEdge) || !sameElements(a.edgeToArc, b.edgeToArc)) {
1163
+ return false;
1164
+ }
1165
+ }
1166
+ if (a.ids.size !== b.ids.size) {
1167
+ return false;
1168
+ }
1169
+ for (let i = 0; i < a.nodeCount; i++) {
1170
+ const idA = a.ids.idOf(i);
1171
+ const idB = b.ids.idOf(i);
1172
+ if (idA !== idB &&
1173
+ !(typeof idA === "number" && typeof idB === "number" && Number.isNaN(idA) && Number.isNaN(idB))) {
1174
+ return false;
1175
+ }
1176
+ }
1177
+ return true;
1178
+ }
1179
+ /**
1180
+ * Record the digests of a materialised view in the checksum records, when the snapshot keeps any.
1181
+ * @param snapshot - the snapshot
1182
+ * @param state - its state record
1183
+ * @param name - the view name
1184
+ * @param value - the view
1185
+ */
1186
+ function recordViewDigests(snapshot, state, name, value) {
1187
+ const records = state.checksums;
1188
+ if (records === null) {
1189
+ return;
1190
+ }
1191
+ for (const [member, array] of Object.entries(viewArrays(snapshot, name, value))) {
1192
+ records.views.set(`${name}.${member}`, hashTypedArray(array));
1193
+ }
1194
+ }
1195
+ /**
1196
+ * Install a view value the wire carried (`includeViews`, design section 9.1) into the snapshot's
1197
+ * cache, so the receiver does not recompute it. The caller has already checked that the value has
1198
+ * the shape of the view; the slot is only filled when it is still empty.
1199
+ * @param snapshot - the snapshot
1200
+ * @param name - the view name
1201
+ * @param value - the view value
1202
+ * @returns true when the view was installed, false when the slot was already resident
1203
+ */
1204
+ export function seedView(snapshot, name, value) {
1205
+ const state = STATES.get(snapshot);
1206
+ if (state === undefined || state.views[name] !== null) {
1207
+ return false;
1208
+ }
1209
+ state.views[name] = value;
1210
+ recordViewDigests(snapshot, state, name, value);
1211
+ return true;
1212
+ }
1213
+ /**
1214
+ * The cached value of a view without computing it (for tests and for the wire module's
1215
+ * `includeViews`).
1216
+ * @param snapshot - the snapshot
1217
+ * @param name - the view name
1218
+ * @returns the cached value, or null when not resident
1219
+ */
1220
+ export function peekView(snapshot, name) {
1221
+ const state = STATES.get(snapshot);
1222
+ if (state === undefined) {
1223
+ return null;
1224
+ }
1225
+ return state.views[name];
1226
+ }
1227
+ /**
1228
+ * Whether the snapshot's permutation arrays are held (materialised) rather than lazy, for tests of
1229
+ * the identity rule and for the wire module (which never serialises an identity permutation).
1230
+ * @param snapshot - the snapshot
1231
+ * @returns true when arcToEdge has been materialised or was supplied
1232
+ */
1233
+ export function permutationMaterialised(snapshot) {
1234
+ const state = STATES.get(snapshot);
1235
+ return state !== undefined && state.arcToEdge !== null;
1236
+ }
1237
+ /**
1238
+ * Whether checksums were recorded at construction (design section 5.8).
1239
+ * @param snapshot - the snapshot
1240
+ * @returns true when validate({ checksum: true }) can compare
1241
+ */
1242
+ export function hasChecksums(snapshot) {
1243
+ const state = STATES.get(snapshot);
1244
+ return state !== undefined && state.checksums !== null;
1245
+ }
1246
+ // ============================================================ arena helpers (10.3)
1247
+ //# sourceMappingURL=graph-snapshot.js.map