@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,755 @@
1
+ /**
2
+ * The wire encoder (design sections 4.5, 5.9, 9.1 and 12.2): `toWire()` turns a snapshot into a
3
+ * JSON-serialisable manifest plus a list of ArrayBuffers, `transferables()` lists the exclusively
4
+ * owned buffers a worker hand-off may transfer, and the segment collector shared with the GSNP
5
+ * container writer of bytes.ts lays every typed array out as a WireBufferRef.
6
+ *
7
+ * Two layouts exist. In REFERENCE mode (toWire) every distinct backing ArrayBuffer becomes one entry
8
+ * of `buffers` and a reference carries the array's own byteOffset, so the core stays views over the
9
+ * arena (one buffer) and nothing is copied unless `transfer: true` meets a shared buffer. In
10
+ * CONTAINER mode (toBytes / toByteChunks) every array is placed at a 256-byte-aligned offset of one
11
+ * buffer region in manifest order, core arrays first so the region's prefix is the arena.
12
+ *
13
+ * toWire allocates nothing for typed data except the first materialisation of a lazily kept
14
+ * representation: the Utf8 store of string ids and string columns and the f64 / tag arrays of a
15
+ * numeric or mixed id map are cached by their owners, the encoded dictionaries and the JSON text of
16
+ * json columns are cached here. Non-finite numbers and -0 inside metadata are carried as the tagged
17
+ * strings of design section 5.9 so the manifest survives JSON.stringify unchanged.
18
+ */
19
+
20
+ import { CORE_ORDER } from "../builder/arena.js";
21
+ import { isPlainObject } from "../columns/column.js";
22
+ import { ALIGNMENT, FORMAT_VERSION, IS_LITTLE_ENDIAN, WIRE_FORMAT, WIRE_MAJOR, WIRE_MINOR } from "../constants.js";
23
+ import { GraphFormatError } from "../errors.js";
24
+ import { NodeIdMap, nodeIdMapToTyped } from "../ids/node-id-map.js";
25
+ import { type EncodedUtf8, encodeUtf8Rows } from "../ids/string-store.js";
26
+ import { type GraphSnapshot, peekView } from "../snapshot/graph-snapshot.js";
27
+ import { viewArrays } from "../snapshot/views.js";
28
+ import {
29
+ type AttributeTable,
30
+ type Column,
31
+ type ColumnMeta,
32
+ type CoreArrayName,
33
+ type GraphMeta,
34
+ type JsonColumn,
35
+ type ToWireOptions,
36
+ type TypedArrayData,
37
+ type ViewName,
38
+ type WireArena,
39
+ type WireBufferRef,
40
+ type WireColumn,
41
+ type WireDtype,
42
+ type WireIdMap,
43
+ type WireManifest,
44
+ type WireSnapshot,
45
+ type WireUtf8,
46
+ } from "../types/index.js";
47
+ import { isShared, noteShared } from "../util/shared-buffers.js";
48
+ import { alignUp } from "../util/typed-array.js";
49
+
50
+ // ============================================================ constants
51
+
52
+ /**
53
+ * The `producer` string written into every manifest (design section 9.1): the package name and its
54
+ * npm version. Golden fixtures mask it when compared. Bumped together with package.json.
55
+ */
56
+ export const WIRE_PRODUCER = "@graphty/graph-format@0.1.0";
57
+
58
+ /** The views the wire carries when named by includeViews; the scalar views are never carried. */
59
+ const SCALAR_VIEWS: ReadonlySet<ViewName> = new Set<ViewName>(["totalWeight", "symmetric"]);
60
+
61
+ /** The core arrays in arena order (design section 10.3). */
62
+
63
+ const encoder = new TextEncoder();
64
+
65
+ // ============================================================ host endianness
66
+
67
+ /**
68
+ * Refuse to emit wire data on a big-endian host (design section 9.2): every supported platform is
69
+ * little-endian and there is no byte-swapping reader, so a big-endian host must never produce a file
70
+ * other hosts reject. The parameter exists so the refusal path is testable on a little-endian host.
71
+ * @param littleEndian - whether the host is little-endian; defaults to the detected value
72
+ */
73
+ export function assertLittleEndianHost(littleEndian: boolean = IS_LITTLE_ENDIAN): void {
74
+ if (!littleEndian) {
75
+ throw new GraphFormatError("E_UNSUPPORTED", "the wire format cannot be written on a big-endian host", {
76
+ reason: "big-endian host",
77
+ });
78
+ }
79
+ }
80
+
81
+ // ============================================================ shared-buffer bookkeeping (9.1)
82
+
83
+ // ============================================================ JSON tagging (5.9)
84
+
85
+ /** The tag key of a non-finite or negative-zero number on the wire. */
86
+ const NUM_TAG = "$num";
87
+ const ESC_TAG = "$esc";
88
+
89
+ /**
90
+ * Encode a JSON value for the manifest (design section 5.9): `Infinity`, `-Infinity`, `NaN` and `-0`
91
+ * become `{ "$num": "..." }`, arrays and plain objects are copied recursively, everything else is
92
+ * returned as is. A user object whose ONLY key is `$num` or `$esc` would collide with the tags, so
93
+ * it is wrapped as `{ "$esc": { ...the object... } }` and unwrapped by the reader.
94
+ * The input has already passed assertJsonValue (design section 5.9), so no other shape can occur.
95
+ * @param value - the JSON value
96
+ * @returns the tagged copy
97
+ */
98
+ export function encodeJsonValue(value: unknown): unknown {
99
+ if (typeof value === "number") {
100
+ if (Number.isNaN(value)) {
101
+ return { [NUM_TAG]: "NaN" };
102
+ }
103
+ if (value === Infinity) {
104
+ return { [NUM_TAG]: "Infinity" };
105
+ }
106
+ if (value === -Infinity) {
107
+ return { [NUM_TAG]: "-Infinity" };
108
+ }
109
+ if (Object.is(value, -0)) {
110
+ return { [NUM_TAG]: "-0" };
111
+ }
112
+ return value;
113
+ }
114
+ if (Array.isArray(value)) {
115
+ return value.map((item: unknown) => encodeJsonValue(item));
116
+ }
117
+ if (isPlainObject(value)) {
118
+ const keys = Object.keys(value);
119
+ const out: Record<string, unknown> = {};
120
+ for (const key of keys) {
121
+ defineJsonKey(out, key, encodeJsonValue(value[key]));
122
+ }
123
+ if (keys.length === 1 && (keys[0] === NUM_TAG || keys[0] === ESC_TAG)) {
124
+ return { [ESC_TAG]: out };
125
+ }
126
+ return out;
127
+ }
128
+ return value;
129
+ }
130
+
131
+ /**
132
+ * Add an own enumerable property to a plain object without going through a setter, so a key such as
133
+ * "__proto__" stays a data property (the manifest reader rejects it anyway).
134
+ * @param target - the object
135
+ * @param key - the property name
136
+ * @param value - the value
137
+ */
138
+ export function defineJsonKey(target: Record<string, unknown>, key: string, value: unknown): void {
139
+ Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
140
+ }
141
+
142
+ // ============================================================ the segment collector
143
+
144
+ /** How references are laid out. */
145
+ type WireMode = "reference" | "container";
146
+
147
+ /** One typed array placed in the container region: the source view and where it goes. */
148
+ export interface ContainerSegment {
149
+ /** The source array. */
150
+ readonly view: TypedArrayData;
151
+ /** Its reference (buffer 0, region-relative byteOffset). */
152
+ readonly ref: WireBufferRef;
153
+ }
154
+
155
+ /**
156
+ * Collects typed arrays into WireBufferRefs (design sections 9.1 and 9.2). In reference mode
157
+ * `buffers` is the list of distinct backing buffers (a shared buffer copied under `transfer`, its
158
+ * index recorded in `copied`); in container mode `segments` is the placement of every distinct array
159
+ * at a 256-aligned offset and `regionLength` the padded length of the region.
160
+ */
161
+ export class WireLayout {
162
+ /** The layout mode. */
163
+ readonly mode: WireMode;
164
+ /** Reference mode: whether shared buffers are copied so the rest can be transferred. */
165
+ readonly transfer: boolean;
166
+ /** Reference mode: the distinct buffers, or their copies. */
167
+ readonly buffers: ArrayBuffer[] = [];
168
+ /** Reference mode: the indices of `buffers` holding copies of shared buffers. */
169
+ readonly copied: number[] = [];
170
+ /** Container mode: every placed array in region order. */
171
+ readonly segments: ContainerSegment[] = [];
172
+
173
+ private readonly bufferIndex = new Map<ArrayBuffer, number>();
174
+ private readonly placed = new Map<string, WireBufferRef>();
175
+ private cursor = 0;
176
+
177
+ /**
178
+ * Create a collector.
179
+ * @param mode - reference (toWire) or container (toBytes)
180
+ * @param transfer - reference mode only: copy shared buffers so the others can be transferred
181
+ */
182
+ constructor(mode: WireMode, transfer = false) {
183
+ this.mode = mode;
184
+ this.transfer = transfer;
185
+ }
186
+
187
+ /**
188
+ * Container mode: the padded byte length of the region (every segment padded to 256 bytes).
189
+ * @returns the region length
190
+ */
191
+ get regionLength(): number {
192
+ return alignUp(this.cursor, ALIGNMENT);
193
+ }
194
+
195
+ /**
196
+ * Reference mode: the index a buffer was assigned, or null when no array over it was placed.
197
+ * @param buffer - the buffer
198
+ * @returns the index into `buffers`, or null
199
+ */
200
+ indexOf(buffer: ArrayBuffer): number | null {
201
+ return this.bufferIndex.get(buffer) ?? null;
202
+ }
203
+
204
+ /**
205
+ * Place a typed array and return its reference. The same byte range is placed once. When the
206
+ * holder of the array (its column, table or id map) is shared, the buffer is recorded as shared
207
+ * so `finalise()` copies it under `transfer` (design section 9.1).
208
+ * @param view - the array
209
+ * @param dtype - its wire dtype ("utf8" for byte stores, "u8" for u8 columns and tags)
210
+ * @param sharedHolder - whether the object holding the array is held by more than one snapshot or table
211
+ * @returns the reference
212
+ */
213
+ ref(view: TypedArrayData, dtype: WireDtype, sharedHolder = false): WireBufferRef {
214
+ if (sharedHolder) {
215
+ noteShared(view.buffer);
216
+ }
217
+ if (this.mode === "reference") {
218
+ return {
219
+ buffer: this.bufferIndexOf(view.buffer),
220
+ byteOffset: view.byteOffset,
221
+ byteLength: view.byteLength,
222
+ dtype,
223
+ length: view.length,
224
+ };
225
+ }
226
+ const key = `${this.bufferIndexOf(view.buffer)}:${view.byteOffset}:${view.byteLength}:${dtype}`;
227
+ const existing = this.placed.get(key);
228
+ if (existing !== undefined) {
229
+ return existing;
230
+ }
231
+ const byteOffset = alignUp(this.cursor, ALIGNMENT);
232
+ this.cursor = byteOffset + view.byteLength;
233
+ const ref: WireBufferRef = { buffer: 0, byteOffset, byteLength: view.byteLength, dtype, length: view.length };
234
+ this.placed.set(key, ref);
235
+ this.segments.push({ view, ref });
236
+ return ref;
237
+ }
238
+
239
+ /**
240
+ * Reference mode with `transfer`: replace every buffer recorded as shared by a copy and list its
241
+ * index in `copied` (design section 9.1). Called once after every array was placed, so the
242
+ * decision does not depend on the order in which shared holders were met.
243
+ */
244
+ finalise(): void {
245
+ if (this.mode !== "reference" || !this.transfer) {
246
+ return;
247
+ }
248
+ for (let i = 0; i < this.buffers.length; i++) {
249
+ const buffer = this.buffers[i];
250
+ if (isShared(buffer)) {
251
+ this.buffers[i] = buffer.slice(0);
252
+ this.copied.push(i);
253
+ }
254
+ }
255
+ }
256
+
257
+ /**
258
+ * The index of a backing buffer, assigning the next one on first sight.
259
+ * @param buffer - the buffer
260
+ * @returns the index
261
+ */
262
+ private bufferIndexOf(buffer: ArrayBuffer): number {
263
+ const known = this.bufferIndex.get(buffer);
264
+ if (known !== undefined) {
265
+ return known;
266
+ }
267
+ const index = this.buffers.length;
268
+ this.bufferIndex.set(buffer, index);
269
+ this.buffers.push(buffer);
270
+ return index;
271
+ }
272
+ }
273
+
274
+ // ============================================================ dtype helpers
275
+
276
+ /**
277
+ * The wire dtype of a typed array by its class.
278
+ * @param view - the array
279
+ * @returns the dtype ("u8" for a Uint8Array; byte stores pass "utf8" explicitly)
280
+ */
281
+ function wireDtypeOf(view: TypedArrayData): WireDtype {
282
+ if (view instanceof Uint32Array) {
283
+ return "u32";
284
+ }
285
+ if (view instanceof Int32Array) {
286
+ return "i32";
287
+ }
288
+ if (view instanceof Float32Array) {
289
+ return "f32";
290
+ }
291
+ if (view instanceof Float64Array) {
292
+ return "f64";
293
+ }
294
+ return "u8";
295
+ }
296
+
297
+ // ============================================================ caches of lazily kept representations
298
+
299
+ /** Encoded dictionaries, keyed by the dictionary array (shared between a static column and its temporal table). */
300
+ const DICTIONARY_CACHE = new WeakMap<readonly string[], EncodedUtf8>();
301
+
302
+ /** JSON text of json columns, keyed by the column, with the column version it was built from. */
303
+ const JSON_TEXT_CACHE = new WeakMap<Column, { readonly version: number; readonly encoded: EncodedUtf8 }>();
304
+
305
+ /**
306
+ * Whether a cached Utf8 store is still usable: the same row count and not detached by a transfer.
307
+ * @param encoded - the cached store
308
+ * @param rows - the expected row count
309
+ * @returns true when the cache can be reused
310
+ */
311
+ function cacheUsable(encoded: EncodedUtf8, rows: number): boolean {
312
+ return encoded.offsets.length === rows + 1;
313
+ }
314
+
315
+ /**
316
+ * The encoded form of a dictionary, built once per dictionary array.
317
+ * @param dictionary - the strings in code order
318
+ * @returns the offsets and bytes
319
+ */
320
+ function encodedDictionary(dictionary: readonly string[]): EncodedUtf8 {
321
+ const cached = DICTIONARY_CACHE.get(dictionary);
322
+ if (cached !== undefined && cacheUsable(cached, dictionary.length)) {
323
+ return cached;
324
+ }
325
+ const encoded = encodeUtf8Rows(dictionary, dictionary.length);
326
+ DICTIONARY_CACHE.set(dictionary, encoded);
327
+ return encoded;
328
+ }
329
+
330
+ /**
331
+ * The JSON text of every row of a json column as a Utf8 store (an unset row is empty text), built
332
+ * once per column version.
333
+ * @param column - the json column
334
+ * @returns the offsets and bytes
335
+ */
336
+ function encodedJsonText(column: JsonColumn): EncodedUtf8 {
337
+ const cached = JSON_TEXT_CACHE.get(column);
338
+ if (cached !== undefined && cached.version === column.version && cacheUsable(cached.encoded, column.length)) {
339
+ return cached.encoded;
340
+ }
341
+ const texts = new Array<string>(column.length);
342
+ for (let row = 0; row < column.length; row++) {
343
+ const value = column.values[row];
344
+ texts[row] = value === undefined || !column.isSet(row) ? "" : JSON.stringify(encodeJsonValue(value));
345
+ }
346
+ const encoded = encodeUtf8Rows(texts, texts.length);
347
+ JSON_TEXT_CACHE.set(column, { version: column.version, encoded });
348
+ return encoded;
349
+ }
350
+
351
+ // ============================================================ column encoding
352
+
353
+ /**
354
+ * A Utf8 store as two references.
355
+ * @param layout - the collector
356
+ * @param encoded - the offsets and bytes
357
+ * @param shared - whether the holder of the store is shared
358
+ * @returns the wire store
359
+ */
360
+ function utf8Ref(layout: WireLayout, encoded: EncodedUtf8, shared: boolean): WireUtf8 {
361
+ return { offsets: layout.ref(encoded.offsets, "u32", shared), utf8: layout.ref(encoded.utf8, "utf8", shared) };
362
+ }
363
+
364
+ /**
365
+ * The manifest copy of a column's metadata: the JSON fields tagged per design section 5.9, the rest
366
+ * copied as is. The tagged fields no longer match ColumnMeta's static types when a value was
367
+ * non-finite; the reader decodes them before resolving the metadata again.
368
+ * @param meta - the resolved metadata
369
+ * @returns the manifest form
370
+ */
371
+ function encodeColumnMeta(meta: ColumnMeta): ColumnMeta {
372
+ return {
373
+ name: meta.name,
374
+ domain: meta.domain,
375
+ dtype: meta.dtype,
376
+ components: meta.components,
377
+ itemDtype: meta.itemDtype,
378
+ itemComponents: meta.itemComponents,
379
+ nullable: meta.nullable,
380
+ mutable: meta.mutable,
381
+ role: meta.role,
382
+ refersTo: meta.refersTo,
383
+ unique: meta.unique,
384
+ default: encodeJsonValue(meta.default),
385
+ fill: encodeJsonValue(meta.fill) as number | string | boolean,
386
+ options: meta.options === null ? null : (encodeJsonValue(meta.options) as readonly unknown[]),
387
+ origin: meta.origin === null ? null : { ...meta.origin },
388
+ dynamic: meta.dynamic,
389
+ extra: encodeJsonValue(meta.extra) as Readonly<Record<string, unknown>>,
390
+ };
391
+ }
392
+
393
+ /**
394
+ * Encode one column (design section 9.1): the buffers its dtype uses, null for the rest. A column
395
+ * held by more than one table (or belonging to a table held by more than one snapshot) has every
396
+ * buffer recorded as shared, including the ones it materialises lazily while being encoded.
397
+ * @param layout - the collector
398
+ * @param column - the column
399
+ * @param tableShared - whether the table holding the column is shared
400
+ * @returns the wire column
401
+ */
402
+ function encodeColumn(layout: WireLayout, column: Column, tableShared: boolean): WireColumn {
403
+ const shared = tableShared || isShared(column);
404
+ const base = {
405
+ meta: encodeColumnMeta(column.meta),
406
+ data: null as WireBufferRef | null,
407
+ validity: column.validity === null ? null : layout.ref(column.validity, "u32", shared),
408
+ nullCount: column.nullCount,
409
+ dictionary: null as WireUtf8 | null,
410
+ strings: null as WireUtf8 | null,
411
+ offsets: null as WireBufferRef | null,
412
+ child: null as WireColumn | null,
413
+ jsonText: null as WireUtf8 | null,
414
+ };
415
+ const { dtype } = column;
416
+ switch (dtype) {
417
+ case "f32":
418
+ case "f64":
419
+ case "i32":
420
+ case "u32":
421
+ case "u8":
422
+ base.data = layout.ref(column.data, dtype, shared);
423
+ break;
424
+ case "bool":
425
+ base.data = layout.ref(column.data, "u32", shared);
426
+ break;
427
+ case "dict":
428
+ base.data = layout.ref(column.codes, "u32", shared);
429
+ base.dictionary = utf8Ref(layout, encodedDictionary(column.dictionary), shared);
430
+ break;
431
+ case "string":
432
+ base.strings = {
433
+ offsets: layout.ref(column.offsets, "u32", shared),
434
+ utf8: layout.ref(column.utf8, "utf8", shared),
435
+ };
436
+ break;
437
+ case "list":
438
+ base.offsets = layout.ref(column.offsets, "u32", shared);
439
+ base.child = encodeColumn(layout, column.child, shared);
440
+ break;
441
+ case "json":
442
+ base.jsonText = utf8Ref(layout, encodedJsonText(column), shared);
443
+ break;
444
+ default: {
445
+ const name: string = dtype;
446
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown dtype ${name}`, { dtype: name });
447
+ }
448
+ }
449
+ return base;
450
+ }
451
+
452
+ /**
453
+ * Encode every column of a table in declaration order.
454
+ * @param layout - the collector
455
+ * @param table - the table
456
+ * @returns the wire columns
457
+ */
458
+ function encodeTable(layout: WireLayout, table: AttributeTable): WireColumn[] {
459
+ const out: WireColumn[] = [];
460
+ const tableShared = isShared(table);
461
+ for (const column of table) {
462
+ out.push(encodeColumn(layout, column, tableShared));
463
+ }
464
+ return out;
465
+ }
466
+
467
+ // ============================================================ id map, meta, views
468
+
469
+ /**
470
+ * Encode the id map (design section 4.5), materialising its typed form on first use.
471
+ * @param layout - the collector
472
+ * @param snapshot - the snapshot
473
+ * @returns the wire id map
474
+ */
475
+ function encodeIds(layout: WireLayout, snapshot: GraphSnapshot): WireIdMap {
476
+ const { ids } = snapshot;
477
+ if (!(ids instanceof NodeIdMap)) {
478
+ throw new GraphFormatError("E_UNSUPPORTED", "the snapshot's id map is not a NodeIdMap of this package", {
479
+ reason: "foreign id map",
480
+ });
481
+ }
482
+ const typed = nodeIdMapToTyped(ids);
483
+ const shared = isShared(ids);
484
+ return {
485
+ kind: typed.kind,
486
+ size: typed.size,
487
+ offset: typed.offset,
488
+ values: typed.values === null ? null : layout.ref(typed.values, wireDtypeOf(typed.values), shared),
489
+ tags: typed.tags === null ? null : layout.ref(typed.tags, "u8", shared),
490
+ numbers: typed.numbers === null ? null : layout.ref(typed.numbers, "f64", shared),
491
+ offsets: typed.offsets === null ? null : layout.ref(typed.offsets, "u32", shared),
492
+ utf8: typed.utf8 === null ? null : layout.ref(typed.utf8, "utf8", shared),
493
+ };
494
+ }
495
+
496
+ /**
497
+ * The manifest copy of the graph metadata with `extra` tagged per design section 5.9.
498
+ * @param meta - the metadata
499
+ * @returns the manifest form
500
+ */
501
+ function encodeGraphMeta(meta: GraphMeta): GraphMeta {
502
+ return {
503
+ name: meta.name,
504
+ description: meta.description,
505
+ creator: meta.creator,
506
+ created: meta.created,
507
+ modified: meta.modified,
508
+ keywords: [...meta.keywords],
509
+ sourceFormat: meta.sourceFormat,
510
+ sourceVersion: meta.sourceVersion,
511
+ idType: meta.idType,
512
+ timeFormat: meta.timeFormat,
513
+ timeRepresentation: meta.timeRepresentation,
514
+ mode: meta.mode,
515
+ declaredMultigraph: meta.declaredMultigraph,
516
+ weightOrigin: meta.weightOrigin === null ? null : { ...meta.weightOrigin },
517
+ extra: encodeJsonValue(meta.extra) as Readonly<Record<string, unknown>>,
518
+ };
519
+ }
520
+
521
+ /**
522
+ * The cached, non-scalar views named by includeViews, each as its materialised member arrays
523
+ * (design section 9.1). A view that is not resident is skipped: the receiver recomputes it.
524
+ * @param layout - the collector
525
+ * @param snapshot - the snapshot
526
+ * @param names - the requested views
527
+ * @returns the wire views, or null when none was carried
528
+ */
529
+ function encodeViews(
530
+ layout: WireLayout,
531
+ snapshot: GraphSnapshot,
532
+ names: readonly ViewName[] | undefined,
533
+ ): Record<string, Record<string, WireBufferRef>> | null {
534
+ if (names === undefined || names.length === 0) {
535
+ return null;
536
+ }
537
+ const out: Record<string, Record<string, WireBufferRef>> = {};
538
+ let count = 0;
539
+ for (const name of new Set(names)) {
540
+ if (SCALAR_VIEWS.has(name)) {
541
+ continue;
542
+ }
543
+ const value: unknown = peekView(snapshot, name);
544
+ if (value === null) {
545
+ continue;
546
+ }
547
+ const members: Record<string, WireBufferRef> = {};
548
+ let any = false;
549
+ for (const [member, array] of Object.entries(viewArrays(snapshot, name, value))) {
550
+ const view = array as TypedArrayData;
551
+ members[member] = layout.ref(view, wireDtypeOf(view));
552
+ any = true;
553
+ }
554
+ if (any) {
555
+ out[name] = members;
556
+ count++;
557
+ }
558
+ }
559
+ return count === 0 ? null : out;
560
+ }
561
+
562
+ // ============================================================ the manifest
563
+
564
+ /** The result of collecting a snapshot: the manifest and the layout that placed its arrays. */
565
+ interface CollectedWire {
566
+ /** The manifest. */
567
+ readonly manifest: WireManifest;
568
+ /** The collector holding the buffers (reference mode) or the segments (container mode). */
569
+ readonly layout: WireLayout;
570
+ }
571
+
572
+ /**
573
+ * Build the manifest of a snapshot over a collector (design section 9.1): core arrays first (an
574
+ * identity permutation, an absent weights array and a zero-length array are null), then the id map,
575
+ * the tables, the extension tables, the metadata and the requested views.
576
+ * @param snapshot - the snapshot
577
+ * @param layout - the collector
578
+ * @param includeViews - the views to carry
579
+ * @param includeColumns - whether attribute and extension tables are carried (default true)
580
+ * @returns the manifest and the collector
581
+ */
582
+ export function collectWire(
583
+ snapshot: GraphSnapshot,
584
+ layout: WireLayout,
585
+ includeViews: readonly ViewName[] | undefined,
586
+ includeColumns = true,
587
+ ): CollectedWire {
588
+ assertLittleEndianHost();
589
+ const identity = snapshot.flags.arcToEdgeIsIdentity;
590
+ const coreArrays: Record<CoreArrayName, TypedArrayData | null> = {
591
+ rowPtr: snapshot.rowPtr,
592
+ colIdx: snapshot.colIdx,
593
+ weights: snapshot.weights,
594
+ arcToEdge: identity ? null : snapshot.arcToEdge,
595
+ edgeToArc: identity ? null : snapshot.edgeToArc,
596
+ };
597
+ const coreRefs: Record<CoreArrayName, WireBufferRef | null> = {
598
+ rowPtr: null,
599
+ colIdx: null,
600
+ weights: null,
601
+ arcToEdge: null,
602
+ edgeToArc: null,
603
+ };
604
+ const rowPtrRef = layout.ref(snapshot.rowPtr, "u32");
605
+ coreRefs.rowPtr = rowPtrRef;
606
+ for (const name of CORE_ORDER) {
607
+ const array = coreArrays[name];
608
+ if (name !== "rowPtr" && array !== null && array.length > 0) {
609
+ coreRefs[name] = layout.ref(array, wireDtypeOf(array));
610
+ }
611
+ }
612
+ const arena = encodeArena(snapshot, layout, coreRefs);
613
+ const ids = encodeIds(layout, snapshot);
614
+ const nodeColumns = includeColumns ? encodeTable(layout, snapshot.nodes) : [];
615
+ const edgeColumns = includeColumns ? encodeTable(layout, snapshot.edges) : [];
616
+ const graphColumns = includeColumns ? encodeTable(layout, snapshot.graph) : [];
617
+ const extensions: { name: string; rowCount: number; columns: WireColumn[] }[] = [];
618
+ if (includeColumns) {
619
+ for (const [name, table] of snapshot.extensions) {
620
+ extensions.push({ name, rowCount: table.rowCount, columns: encodeTable(layout, table) });
621
+ }
622
+ }
623
+ const views = encodeViews(layout, snapshot, includeViews);
624
+ layout.finalise();
625
+ const manifest: WireManifest = {
626
+ format: WIRE_FORMAT,
627
+ wire: [WIRE_MAJOR, WIRE_MINOR],
628
+ producer: WIRE_PRODUCER,
629
+ formatVersion: FORMAT_VERSION,
630
+ directed: snapshot.directed,
631
+ counts: {
632
+ nodes: snapshot.nodeCount,
633
+ edges: snapshot.edgeCount,
634
+ arcs: snapshot.arcCount,
635
+ selfLoops: snapshot.selfLoopCount,
636
+ },
637
+ flags: { ...snapshot.flags },
638
+ core: {
639
+ rowPtr: rowPtrRef,
640
+ colIdx: coreRefs.colIdx,
641
+ weights: coreRefs.weights,
642
+ arcToEdge: coreRefs.arcToEdge,
643
+ edgeToArc: coreRefs.edgeToArc,
644
+ },
645
+ arena,
646
+ ids,
647
+ nodeColumns,
648
+ edgeColumns,
649
+ graphColumns,
650
+ extensions,
651
+ meta: encodeGraphMeta(snapshot.meta),
652
+ views,
653
+ copied: layout.copied,
654
+ label: snapshot.label,
655
+ };
656
+ return { manifest, layout };
657
+ }
658
+
659
+ /**
660
+ * The arena descriptor: in container mode the prefix of the region holding the core arrays (always
661
+ * present); in reference mode the snapshot's own arena when it has one.
662
+ * @param snapshot - the snapshot
663
+ * @param layout - the collector, after the core arrays were placed
664
+ * @param coreRefs - the core references
665
+ * @returns the descriptor, or null
666
+ */
667
+ function encodeArena(
668
+ snapshot: GraphSnapshot,
669
+ layout: WireLayout,
670
+ coreRefs: Readonly<Record<CoreArrayName, WireBufferRef | null>>,
671
+ ): WireArena | null {
672
+ if (layout.mode === "container") {
673
+ let end = 0;
674
+ let hotByteLength = 0;
675
+ for (const name of CORE_ORDER) {
676
+ const ref = coreRefs[name];
677
+ if (ref === null) {
678
+ continue;
679
+ }
680
+ end = ref.byteOffset + ref.byteLength;
681
+ if (name === "rowPtr" || name === "colIdx" || name === "weights") {
682
+ hotByteLength = end;
683
+ }
684
+ }
685
+ return { buffer: 0, byteOffset: 0, byteLength: end, hotByteLength };
686
+ }
687
+ const { arena } = snapshot;
688
+ if (arena === null) {
689
+ return null;
690
+ }
691
+ const buffer = layout.indexOf(arena.buffer);
692
+ if (buffer === null) {
693
+ return null;
694
+ }
695
+ return { buffer, byteOffset: arena.byteOffset, byteLength: arena.byteLength, hotByteLength: arena.hotByteLength };
696
+ }
697
+
698
+ // ============================================================ public entry points
699
+
700
+ /**
701
+ * The plain-object wire form of a snapshot (design section 9.1): a JSON-serialisable manifest plus
702
+ * the distinct backing ArrayBuffers of the core, the id map, the columns and the extension tables.
703
+ * Without `transfer` every buffer is a plain reference and postMessage clones; with `transfer: true`
704
+ * every buffer held by more than one snapshot, table or column (design section 9.1, tracked by
705
+ * src/util/shared-buffers.ts) is copied (its index listed in `manifest.copied`) so the remaining
706
+ * buffers can be transferred with `transferables()`.
707
+ * @param snapshot - the snapshot
708
+ * @param options - transfer mode, the cached views to carry and whether columns are carried
709
+ * @returns the wire snapshot; E_UNSUPPORTED on a big-endian host
710
+ */
711
+ export function toWire(snapshot: GraphSnapshot, options: ToWireOptions = {}): WireSnapshot {
712
+ const transfer = options.transfer === true;
713
+ const layout = new WireLayout("reference", transfer);
714
+ const { manifest } = collectWire(snapshot, layout, options.includeViews, options.includeColumns !== false);
715
+ if (transfer) {
716
+ // the transfer list of THIS wire: exclusive originals plus the fresh copies of shared buffers
717
+ TRANSFER_LISTS.set(snapshot, [...layout.buffers]);
718
+ }
719
+ return { manifest, buffers: layout.buffers };
720
+ }
721
+
722
+ /** Per snapshot, the buffers its most recent `toWire({ transfer: true })` put into `wire.buffers`. */
723
+ const TRANSFER_LISTS = new WeakMap<GraphSnapshot, ArrayBuffer[]>();
724
+
725
+ /**
726
+ * The postMessage transfer list of design section 9.1: the same set `toWire({ transfer: true })`
727
+ * put into `wire.buffers` -- exclusively owned originals plus the fresh copies made for shared
728
+ * buffers -- so `postMessage(wire, snapshot.transferables())` transfers exactly the buffers the
729
+ * wire carries, whatever `includeColumns` / `includeViews` selected (a buffer the wire does not
730
+ * reference is never listed, so no column is lost on either side). Before any transfer-mode toWire()
731
+ * call the list is computed for the default wire shape: the distinct exclusively owned buffers of
732
+ * the core, the id map, the typed columns, the string stores and the extension tables; a buffer
733
+ * held by more than one snapshot, table or column is excluded and an identity permutation is never
734
+ * included.
735
+ * @param snapshot - the snapshot
736
+ * @returns the buffers, in manifest order
737
+ */
738
+ export function transferables(snapshot: GraphSnapshot): ArrayBuffer[] {
739
+ const recorded = TRANSFER_LISTS.get(snapshot);
740
+ if (recorded !== undefined) {
741
+ return [...recorded];
742
+ }
743
+ const layout = new WireLayout("reference", false);
744
+ collectWire(snapshot, layout, undefined, true);
745
+ return layout.buffers.filter((buffer) => !isShared(buffer));
746
+ }
747
+
748
+ /**
749
+ * The UTF-8 bytes of a manifest as the container stores them (design section 9.2).
750
+ * @param manifest - the manifest
751
+ * @returns the encoded JSON text
752
+ */
753
+ export function encodeManifest(manifest: WireManifest): Uint8Array {
754
+ return encoder.encode(JSON.stringify(manifest));
755
+ }