@graphty/graph-format 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +216 -43
- package/dist/graph-format.d.ts +1 -0
- package/dist/graph-format.d.ts.map +1 -0
- package/dist/graph-format.js +15798 -0
- package/dist/graph-format.js.map +1 -0
- package/dist/src/builder/arena.d.ts +85 -0
- package/dist/src/builder/arena.d.ts.map +1 -0
- package/dist/src/builder/arena.js +154 -0
- package/dist/src/builder/arena.js.map +1 -0
- package/dist/src/builder/compact.d.ts +515 -0
- package/dist/src/builder/compact.d.ts.map +1 -0
- package/dist/src/builder/compact.js +1553 -0
- package/dist/src/builder/compact.js.map +1 -0
- package/dist/src/builder/counting-sort.d.ts +96 -0
- package/dist/src/builder/counting-sort.d.ts.map +1 -0
- package/dist/src/builder/counting-sort.js +430 -0
- package/dist/src/builder/counting-sort.js.map +1 -0
- package/dist/src/builder/freeze.d.ts +72 -0
- package/dist/src/builder/freeze.d.ts.map +1 -0
- package/dist/src/builder/freeze.js +548 -0
- package/dist/src/builder/freeze.js.map +1 -0
- package/dist/src/builder/graph-builder.d.ts +602 -0
- package/dist/src/builder/graph-builder.d.ts.map +1 -0
- package/dist/src/builder/graph-builder.js +1940 -0
- package/dist/src/builder/graph-builder.js.map +1 -0
- package/dist/src/builder/options.d.ts +16 -0
- package/dist/src/builder/options.d.ts.map +1 -0
- package/dist/src/builder/options.js +21 -0
- package/dist/src/builder/options.js.map +1 -0
- package/dist/src/columns/bitmap.d.ts +96 -0
- package/dist/src/columns/bitmap.d.ts.map +1 -0
- package/dist/src/columns/bitmap.js +184 -0
- package/dist/src/columns/bitmap.js.map +1 -0
- package/dist/src/columns/column.d.ts +220 -0
- package/dist/src/columns/column.d.ts.map +1 -0
- package/dist/src/columns/column.js +2109 -0
- package/dist/src/columns/column.js.map +1 -0
- package/dist/src/columns/dictionary.d.ts +60 -0
- package/dist/src/columns/dictionary.d.ts.map +1 -0
- package/dist/src/columns/dictionary.js +102 -0
- package/dist/src/columns/dictionary.js.map +1 -0
- package/dist/src/columns/growable.d.ts +198 -0
- package/dist/src/columns/growable.d.ts.map +1 -0
- package/dist/src/columns/growable.js +296 -0
- package/dist/src/columns/growable.js.map +1 -0
- package/dist/src/columns/infer.d.ts +121 -0
- package/dist/src/columns/infer.d.ts.map +1 -0
- package/dist/src/columns/infer.js +268 -0
- package/dist/src/columns/infer.js.map +1 -0
- package/dist/src/columns/remap.d.ts +143 -0
- package/dist/src/columns/remap.d.ts.map +1 -0
- package/dist/src/columns/remap.js +588 -0
- package/dist/src/columns/remap.js.map +1 -0
- package/dist/src/columns/table.d.ts +184 -0
- package/dist/src/columns/table.d.ts.map +1 -0
- package/dist/src/columns/table.js +485 -0
- package/dist/src/columns/table.js.map +1 -0
- package/dist/src/constants.d.ts +34 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +79 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/errors.d.ts +76 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +32 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/ids/edge-id-index.d.ts +77 -0
- package/dist/src/ids/edge-id-index.d.ts.map +1 -0
- package/dist/src/ids/edge-id-index.js +155 -0
- package/dist/src/ids/edge-id-index.js.map +1 -0
- package/dist/src/ids/node-id-map.d.ts +280 -0
- package/dist/src/ids/node-id-map.d.ts.map +1 -0
- package/dist/src/ids/node-id-map.js +1139 -0
- package/dist/src/ids/node-id-map.js.map +1 -0
- package/dist/src/ids/string-store.d.ts +219 -0
- package/dist/src/ids/string-store.d.ts.map +1 -0
- package/dist/src/ids/string-store.js +526 -0
- package/dist/src/ids/string-store.js.map +1 -0
- package/dist/src/index.d.ts +30 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +29 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/populate/from-csr.d.ts +61 -0
- package/dist/src/populate/from-csr.d.ts.map +1 -0
- package/dist/src/populate/from-csr.js +706 -0
- package/dist/src/populate/from-csr.js.map +1 -0
- package/dist/src/populate/from-edge-arrays.d.ts +51 -0
- package/dist/src/populate/from-edge-arrays.d.ts.map +1 -0
- package/dist/src/populate/from-edge-arrays.js +217 -0
- package/dist/src/populate/from-edge-arrays.js.map +1 -0
- package/dist/src/populate/from-records.d.ts +51 -0
- package/dist/src/populate/from-records.d.ts.map +1 -0
- package/dist/src/populate/from-records.js +447 -0
- package/dist/src/populate/from-records.js.map +1 -0
- package/dist/src/snapshot/derived.d.ts +211 -0
- package/dist/src/snapshot/derived.d.ts.map +1 -0
- package/dist/src/snapshot/derived.js +1254 -0
- package/dist/src/snapshot/derived.js.map +1 -0
- package/dist/src/snapshot/graph-meta.d.ts +23 -0
- package/dist/src/snapshot/graph-meta.d.ts.map +1 -0
- package/dist/src/snapshot/graph-meta.js +159 -0
- package/dist/src/snapshot/graph-meta.js.map +1 -0
- package/dist/src/snapshot/graph-snapshot.d.ts +509 -0
- package/dist/src/snapshot/graph-snapshot.d.ts.map +1 -0
- package/dist/src/snapshot/graph-snapshot.js +1247 -0
- package/dist/src/snapshot/graph-snapshot.js.map +1 -0
- package/dist/src/snapshot/hash.d.ts +90 -0
- package/dist/src/snapshot/hash.d.ts.map +1 -0
- package/dist/src/snapshot/hash.js +228 -0
- package/dist/src/snapshot/hash.js.map +1 -0
- package/dist/src/snapshot/queries.d.ts +74 -0
- package/dist/src/snapshot/queries.d.ts.map +1 -0
- package/dist/src/snapshot/queries.js +131 -0
- package/dist/src/snapshot/queries.js.map +1 -0
- package/dist/src/snapshot/validate.d.ts +192 -0
- package/dist/src/snapshot/validate.d.ts.map +1 -0
- package/dist/src/snapshot/validate.js +990 -0
- package/dist/src/snapshot/validate.js.map +1 -0
- package/dist/src/snapshot/views.d.ts +274 -0
- package/dist/src/snapshot/views.d.ts.map +1 -0
- package/dist/src/snapshot/views.js +856 -0
- package/dist/src/snapshot/views.js.map +1 -0
- package/dist/src/types/builder.d.ts +456 -0
- package/dist/src/types/builder.d.ts.map +1 -0
- package/dist/src/types/builder.js +11 -0
- package/dist/src/types/builder.js.map +1 -0
- package/dist/src/types/columns.d.ts +615 -0
- package/dist/src/types/columns.d.ts.map +1 -0
- package/dist/src/types/columns.js +14 -0
- package/dist/src/types/columns.js.map +1 -0
- package/dist/src/types/index.d.ts +14 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +11 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/internal.d.ts +172 -0
- package/dist/src/types/internal.d.ts.map +1 -0
- package/dist/src/types/internal.js +13 -0
- package/dist/src/types/internal.js.map +1 -0
- package/dist/src/types/snapshot.d.ts +801 -0
- package/dist/src/types/snapshot.d.ts.map +1 -0
- package/dist/src/types/snapshot.js +14 -0
- package/dist/src/types/snapshot.js.map +1 -0
- package/dist/src/types/wire.d.ts +196 -0
- package/dist/src/types/wire.d.ts.map +1 -0
- package/dist/src/types/wire.js +8 -0
- package/dist/src/types/wire.js.map +1 -0
- package/dist/src/util/mask.d.ts +55 -0
- package/dist/src/util/mask.d.ts.map +1 -0
- package/dist/src/util/mask.js +71 -0
- package/dist/src/util/mask.js.map +1 -0
- package/dist/src/util/options.d.ts +22 -0
- package/dist/src/util/options.d.ts.map +1 -0
- package/dist/src/util/options.js +49 -0
- package/dist/src/util/options.js.map +1 -0
- package/dist/src/util/shared-buffers.d.ts +42 -0
- package/dist/src/util/shared-buffers.d.ts.map +1 -0
- package/dist/src/util/shared-buffers.js +52 -0
- package/dist/src/util/shared-buffers.js.map +1 -0
- package/dist/src/util/typed-array.d.ts +205 -0
- package/dist/src/util/typed-array.d.ts.map +1 -0
- package/dist/src/util/typed-array.js +289 -0
- package/dist/src/util/typed-array.js.map +1 -0
- package/dist/src/wire/bytes.d.ts +69 -0
- package/dist/src/wire/bytes.d.ts.map +1 -0
- package/dist/src/wire/bytes.js +361 -0
- package/dist/src/wire/bytes.js.map +1 -0
- package/dist/src/wire/carried-views.d.ts +111 -0
- package/dist/src/wire/carried-views.d.ts.map +1 -0
- package/dist/src/wire/carried-views.js +286 -0
- package/dist/src/wire/carried-views.js.map +1 -0
- package/dist/src/wire/from-wire.d.ts +144 -0
- package/dist/src/wire/from-wire.d.ts.map +1 -0
- package/dist/src/wire/from-wire.js +1425 -0
- package/dist/src/wire/from-wire.js.map +1 -0
- package/dist/src/wire/to-wire.d.ts +171 -0
- package/dist/src/wire/to-wire.d.ts.map +1 -0
- package/dist/src/wire/to-wire.js +652 -0
- package/dist/src/wire/to-wire.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +72 -7
- package/src/builder/arena.ts +192 -0
- package/src/builder/compact.ts +1811 -0
- package/src/builder/counting-sort.ts +506 -0
- package/src/builder/freeze.ts +689 -0
- package/src/builder/graph-builder.ts +2264 -0
- package/src/builder/options.ts +29 -0
- package/src/columns/bitmap.ts +196 -0
- package/src/columns/column.ts +2574 -0
- package/src/columns/dictionary.ts +114 -0
- package/src/columns/growable.ts +350 -0
- package/src/columns/infer.ts +285 -0
- package/src/columns/remap.ts +663 -0
- package/src/columns/table.ts +568 -0
- package/src/constants.ts +89 -0
- package/src/errors.ts +115 -0
- package/src/ids/edge-id-index.ts +179 -0
- package/src/ids/node-id-map.ts +1340 -0
- package/src/ids/string-store.ts +589 -0
- package/src/index.ts +139 -0
- package/src/lib-resizable-array-buffer.d.ts +34 -0
- package/src/populate/from-csr.ts +802 -0
- package/src/populate/from-edge-arrays.ts +257 -0
- package/src/populate/from-records.ts +536 -0
- package/src/snapshot/derived.ts +1486 -0
- package/src/snapshot/graph-meta.ts +167 -0
- package/src/snapshot/graph-snapshot.ts +1503 -0
- package/src/snapshot/hash.ts +241 -0
- package/src/snapshot/queries.ts +136 -0
- package/src/snapshot/validate.ts +1108 -0
- package/src/snapshot/views.ts +943 -0
- package/src/types/builder.ts +480 -0
- package/src/types/columns.ts +727 -0
- package/src/types/index.ts +14 -0
- package/src/types/internal.ts +195 -0
- package/src/types/snapshot.ts +886 -0
- package/src/types/wire.ts +207 -0
- package/src/util/mask.ts +89 -0
- package/src/util/options.ts +57 -0
- package/src/util/shared-buffers.ts +56 -0
- package/src/util/typed-array.ts +371 -0
- package/src/wire/bytes.ts +440 -0
- package/src/wire/carried-views.ts +327 -0
- package/src/wire/from-wire.ts +1690 -0
- package/src/wire/to-wire.ts +755 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Arrow Utf8 string store (design sections 4.2 and 5.1): `Uint32Array(rows + 1)` offsets plus one
|
|
3
|
+
* `Uint8Array` of concatenated UTF-8 bytes, kept next to a decoded JS-string representation. The two
|
|
4
|
+
* are caches of one logical value and are materialised lazily in either direction:
|
|
5
|
+
*
|
|
6
|
+
* - a store built from strings (`Utf8Store.fromStrings`, what the builder has) shares the source
|
|
7
|
+
* array by reference and encodes on the first read of `offsets` / `utf8` (toWire / toBytes /
|
|
8
|
+
* transferables), caching the result;
|
|
9
|
+
* - a store built from a wire buffer (`Utf8Store.fromEncoded`) decodes per row on `at()`, cached in
|
|
10
|
+
* a sparse array, and in bulk on `slice()` / `decodeAll()`, which are not cached.
|
|
11
|
+
*
|
|
12
|
+
* Nothing is encoded or decoded at construction. Row `i` occupies the byte range
|
|
13
|
+
* `[offsets[i], offsets[i + 1])`; `offsets[0]` need not be zero so that a zero-copy slice of a
|
|
14
|
+
* string column can share its parent's arrays. The encoder is a plain JS UTF-8 encoder that
|
|
15
|
+
* produces exactly the bytes `TextEncoder` would (a lone surrogate becomes U+FFFD); the decoder is
|
|
16
|
+
* `TextDecoder` behind an ASCII fast path.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { GraphFormatError } from "../errors.js";
|
|
20
|
+
import { type U8, type U32 } from "../types/index.js";
|
|
21
|
+
import { claimHolder } from "../util/shared-buffers.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Rows of at most this many bytes are decoded by a char-code loop when they are pure ASCII, which is
|
|
25
|
+
* about three times faster than TextDecoder for short ids; longer rows go straight to TextDecoder.
|
|
26
|
+
*/
|
|
27
|
+
const ASCII_FAST_PATH_MAX_BYTES = 32;
|
|
28
|
+
|
|
29
|
+
/** The instance type of the host's TextDecoder (a global value in Node and in browsers; typed through its constructor). */
|
|
30
|
+
type Decoder = InstanceType<typeof TextDecoder>;
|
|
31
|
+
|
|
32
|
+
let sharedDecoder: Decoder | null = null;
|
|
33
|
+
let fatalDecoder: Decoder | null = null;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The lazily created shared (replacing) decoder.
|
|
37
|
+
* @returns a TextDecoder that replaces malformed sequences with U+FFFD
|
|
38
|
+
*/
|
|
39
|
+
function replacingDecoder(): Decoder {
|
|
40
|
+
sharedDecoder ??= new TextDecoder("utf-8");
|
|
41
|
+
return sharedDecoder;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The lazily created fatal decoder used by validation.
|
|
46
|
+
* @returns a TextDecoder that throws on a malformed sequence
|
|
47
|
+
*/
|
|
48
|
+
function strictDecoder(): Decoder {
|
|
49
|
+
fatalDecoder ??= new TextDecoder("utf-8", { fatal: true });
|
|
50
|
+
return fatalDecoder;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Number of UTF-8 bytes `encodeUtf8Into` writes for a string (a lone surrogate counts as the three
|
|
55
|
+
* bytes of U+FFFD, matching TextEncoder).
|
|
56
|
+
* @param s - the string
|
|
57
|
+
* @returns the encoded byte length
|
|
58
|
+
*/
|
|
59
|
+
export function utf8ByteLength(s: string): number {
|
|
60
|
+
let bytes = 0;
|
|
61
|
+
const n = s.length;
|
|
62
|
+
for (let i = 0; i < n; i++) {
|
|
63
|
+
const c = s.charCodeAt(i);
|
|
64
|
+
if (c < 0x80) {
|
|
65
|
+
bytes += 1;
|
|
66
|
+
} else if (c < 0x800) {
|
|
67
|
+
bytes += 2;
|
|
68
|
+
} else if (c >= 0xd800 && c <= 0xdbff && i + 1 < n) {
|
|
69
|
+
const d = s.charCodeAt(i + 1);
|
|
70
|
+
if (d >= 0xdc00 && d <= 0xdfff) {
|
|
71
|
+
bytes += 4;
|
|
72
|
+
i++;
|
|
73
|
+
} else {
|
|
74
|
+
bytes += 3;
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
bytes += 3;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return bytes;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Encode a string as UTF-8 into `out` starting at `position`; the caller has sized `out` with
|
|
85
|
+
* `utf8ByteLength`. Produces the bytes TextEncoder would, including U+FFFD for a lone surrogate.
|
|
86
|
+
* @param s - the string
|
|
87
|
+
* @param out - the destination bytes
|
|
88
|
+
* @param position - the first byte to write
|
|
89
|
+
* @returns one past the last byte written
|
|
90
|
+
*/
|
|
91
|
+
export function encodeUtf8Into(s: string, out: Uint8Array, position: number): number {
|
|
92
|
+
let p = position;
|
|
93
|
+
const n = s.length;
|
|
94
|
+
for (let i = 0; i < n; i++) {
|
|
95
|
+
let c = s.charCodeAt(i);
|
|
96
|
+
if (c < 0x80) {
|
|
97
|
+
out[p++] = c;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (c < 0x800) {
|
|
101
|
+
out[p++] = 0xc0 | (c >> 6);
|
|
102
|
+
out[p++] = 0x80 | (c & 0x3f);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (c >= 0xd800 && c <= 0xdfff) {
|
|
106
|
+
if (c <= 0xdbff && i + 1 < n) {
|
|
107
|
+
const d = s.charCodeAt(i + 1);
|
|
108
|
+
if (d >= 0xdc00 && d <= 0xdfff) {
|
|
109
|
+
c = 0x10000 + ((c - 0xd800) << 10) + (d - 0xdc00);
|
|
110
|
+
i++;
|
|
111
|
+
out[p++] = 0xf0 | (c >> 18);
|
|
112
|
+
out[p++] = 0x80 | ((c >> 12) & 0x3f);
|
|
113
|
+
out[p++] = 0x80 | ((c >> 6) & 0x3f);
|
|
114
|
+
out[p++] = 0x80 | (c & 0x3f);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
c = 0xfffd;
|
|
119
|
+
}
|
|
120
|
+
out[p++] = 0xe0 | (c >> 12);
|
|
121
|
+
out[p++] = 0x80 | ((c >> 6) & 0x3f);
|
|
122
|
+
out[p++] = 0x80 | (c & 0x3f);
|
|
123
|
+
}
|
|
124
|
+
return p;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The length from which V8 represents a substring as a SlicedString that keeps its parent alive
|
|
129
|
+
* (`SlicedString::kMinLength`); shorter substrings are copied.
|
|
130
|
+
*/
|
|
131
|
+
const SLICED_STRING_MIN_LENGTH = 13;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A copy of a string that no longer references the buffer it was sliced from. Importers cut ids
|
|
135
|
+
* and cell texts out of decoded input chunks, and V8 represents such a substring of 13 or more
|
|
136
|
+
* characters as a view on the chunk, so a retained id or value would keep the whole chunk alive
|
|
137
|
+
* for the life of the builder and the snapshot (measured: a 40 MiB CSV kept 47 MiB of decoded
|
|
138
|
+
* text resident through 100k retained ids). Concatenating one character and slicing it off makes
|
|
139
|
+
* V8 flatten the string into fresh storage; shorter strings are already copies and are returned
|
|
140
|
+
* as they are.
|
|
141
|
+
* @param s - the string
|
|
142
|
+
* @returns an equal string that owns its characters
|
|
143
|
+
*/
|
|
144
|
+
export function detachString(s: string): string {
|
|
145
|
+
return s.length < SLICED_STRING_MIN_LENGTH ? s : ` ${s}`.slice(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Whether a string contains a lone UTF-16 surrogate, which TextEncoder would replace with U+FFFD on
|
|
150
|
+
* the wire (design section 4.1: such an id is E_INVALID_ID).
|
|
151
|
+
* @param s - the string
|
|
152
|
+
* @returns true when a high surrogate lacks its low partner or a low surrogate stands alone
|
|
153
|
+
*/
|
|
154
|
+
export function hasLoneSurrogate(s: string): boolean {
|
|
155
|
+
const n = s.length;
|
|
156
|
+
for (let i = 0; i < n; i++) {
|
|
157
|
+
const c = s.charCodeAt(i);
|
|
158
|
+
if (c < 0xd800 || c > 0xdfff) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (c > 0xdbff) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (i + 1 >= n) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
const d = s.charCodeAt(i + 1);
|
|
168
|
+
if (d < 0xdc00 || d > 0xdfff) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
i++;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Decode one row of UTF-8 bytes with an ASCII fast path.
|
|
178
|
+
* @param utf8 - the byte store
|
|
179
|
+
* @param start - first byte of the row
|
|
180
|
+
* @param end - one past the last byte of the row
|
|
181
|
+
* @returns the decoded string (malformed sequences become U+FFFD)
|
|
182
|
+
*/
|
|
183
|
+
export function decodeUtf8Row(utf8: Uint8Array, start: number, end: number): string {
|
|
184
|
+
const len = end - start;
|
|
185
|
+
if (len === 0) {
|
|
186
|
+
return "";
|
|
187
|
+
}
|
|
188
|
+
if (len <= ASCII_FAST_PATH_MAX_BYTES) {
|
|
189
|
+
let s = "";
|
|
190
|
+
let i = start;
|
|
191
|
+
for (; i < end; i++) {
|
|
192
|
+
const b = utf8[i];
|
|
193
|
+
if (b >= 0x80) {
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
s += String.fromCharCode(b);
|
|
197
|
+
}
|
|
198
|
+
if (i === end) {
|
|
199
|
+
return s;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return replacingDecoder().decode(utf8.subarray(start, end));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Whether every byte of a range is ASCII.
|
|
207
|
+
* @param utf8 - the byte store
|
|
208
|
+
* @param start - first byte
|
|
209
|
+
* @param end - one past the last byte
|
|
210
|
+
* @returns true when no byte is >= 0x80
|
|
211
|
+
*/
|
|
212
|
+
function isAsciiRange(utf8: Uint8Array, start: number, end: number): boolean {
|
|
213
|
+
for (let i = start; i < end; i++) {
|
|
214
|
+
if (utf8[i] >= 0x80) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Decode a range of rows in one pass: when the whole byte range is ASCII, one TextDecoder call plus
|
|
223
|
+
* one substring per row (byte offsets equal char offsets); otherwise one decode per row.
|
|
224
|
+
* @param utf8 - the byte store
|
|
225
|
+
* @param offsets - the row offsets
|
|
226
|
+
* @param from - first row
|
|
227
|
+
* @param to - one past the last row
|
|
228
|
+
* @returns the decoded rows in order
|
|
229
|
+
*/
|
|
230
|
+
export function decodeUtf8Rows(utf8: Uint8Array, offsets: Uint32Array, from: number, to: number): string[] {
|
|
231
|
+
const out = new Array<string>(to - from);
|
|
232
|
+
if (to <= from) {
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
const base = offsets[from];
|
|
236
|
+
const end = offsets[to];
|
|
237
|
+
if (isAsciiRange(utf8, base, end)) {
|
|
238
|
+
const whole = replacingDecoder().decode(utf8.subarray(base, end));
|
|
239
|
+
for (let i = from; i < to; i++) {
|
|
240
|
+
out[i - from] = whole.substring(offsets[i] - base, offsets[i + 1] - base);
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
for (let i = from; i < to; i++) {
|
|
245
|
+
out[i - from] = decodeUtf8Row(utf8, offsets[i], offsets[i + 1]);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Whether one row of bytes is well-formed UTF-8 (a fatal decode).
|
|
252
|
+
* @param utf8 - the byte store
|
|
253
|
+
* @param start - first byte of the row
|
|
254
|
+
* @param end - one past the last byte of the row
|
|
255
|
+
* @returns true when the row decodes without error
|
|
256
|
+
*/
|
|
257
|
+
function isWellFormedUtf8Row(utf8: Uint8Array, start: number, end: number): boolean {
|
|
258
|
+
let ascii = true;
|
|
259
|
+
for (let i = start; i < end; i++) {
|
|
260
|
+
if (utf8[i] >= 0x80) {
|
|
261
|
+
ascii = false;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (ascii) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
strictDecoder().decode(utf8.subarray(start, end));
|
|
270
|
+
return true;
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Encode the first `length` entries of an array as an Arrow Utf8 store in one sizing pass and one
|
|
278
|
+
* writing pass. Entries that are not strings (the number rows of a mixed id map) encode as empty
|
|
279
|
+
* rows; offsets start at 0.
|
|
280
|
+
* @param source - the rows; only entries below `length` are read
|
|
281
|
+
* @param length - the number of rows to encode
|
|
282
|
+
* @returns fresh, exactly sized offsets (length + 1) and utf8 arrays
|
|
283
|
+
*/
|
|
284
|
+
export function encodeUtf8Rows(source: ArrayLike<unknown>, length: number): EncodedUtf8 {
|
|
285
|
+
const offsets = new Uint32Array(length + 1);
|
|
286
|
+
let total = 0;
|
|
287
|
+
for (let i = 0; i < length; i++) {
|
|
288
|
+
const v = source[i];
|
|
289
|
+
if (typeof v === "string") {
|
|
290
|
+
total += utf8ByteLength(v);
|
|
291
|
+
}
|
|
292
|
+
offsets[i + 1] = total;
|
|
293
|
+
}
|
|
294
|
+
const utf8 = new Uint8Array(total);
|
|
295
|
+
let p = 0;
|
|
296
|
+
for (let i = 0; i < length; i++) {
|
|
297
|
+
const v = source[i];
|
|
298
|
+
if (typeof v === "string") {
|
|
299
|
+
p = encodeUtf8Into(v, utf8, p);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { offsets, utf8 };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Structure-level check of a Utf8 store's layout (design section 9.5): `rows + 1` offsets,
|
|
307
|
+
* non-decreasing, ending inside the byte store.
|
|
308
|
+
* @param offsets - the offsets array
|
|
309
|
+
* @param utf8 - the byte store
|
|
310
|
+
* @param rows - the expected row count
|
|
311
|
+
* @param ref - the manifest path named in the error, e.g. "ids.offsets"
|
|
312
|
+
*/
|
|
313
|
+
export function checkUtf8Layout(offsets: Uint32Array, utf8: Uint8Array, rows: number, ref: string): void {
|
|
314
|
+
if (offsets.length !== rows + 1) {
|
|
315
|
+
throw new GraphFormatError(
|
|
316
|
+
"E_BAD_SERIALIZATION",
|
|
317
|
+
`${ref}: expected ${rows + 1} offsets, found ${offsets.length}`,
|
|
318
|
+
{ ref, reason: "offsets length", expected: rows + 1, found: offsets.length },
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
let previous = offsets[0];
|
|
322
|
+
for (let i = 1; i <= rows; i++) {
|
|
323
|
+
const current = offsets[i];
|
|
324
|
+
if (current < previous) {
|
|
325
|
+
throw new GraphFormatError("E_BAD_SERIALIZATION", `${ref}: offsets decrease at row ${i - 1}`, {
|
|
326
|
+
ref,
|
|
327
|
+
reason: "offsets not monotonic",
|
|
328
|
+
row: i - 1,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
previous = current;
|
|
332
|
+
}
|
|
333
|
+
if (previous > utf8.length) {
|
|
334
|
+
throw new GraphFormatError(
|
|
335
|
+
"E_BAD_SERIALIZATION",
|
|
336
|
+
`${ref}: offsets end at ${previous} but the byte store has ${utf8.length} bytes`,
|
|
337
|
+
{ ref, reason: "offsets exceed utf8", end: previous, byteLength: utf8.length },
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** The encoded representation of a store: offsets plus bytes, always materialised together. */
|
|
343
|
+
export interface EncodedUtf8 {
|
|
344
|
+
/** rows + 1 non-decreasing offsets. */
|
|
345
|
+
readonly offsets: U32;
|
|
346
|
+
/** The concatenated UTF-8 bytes. */
|
|
347
|
+
readonly utf8: U8;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* An Arrow Utf8 string store with lazily materialised encoded and decoded representations (design
|
|
352
|
+
* sections 4.2 and 5.1). Immutable once built; every lazily computed representation is cached.
|
|
353
|
+
*/
|
|
354
|
+
export class Utf8Store {
|
|
355
|
+
/** Number of rows. */
|
|
356
|
+
readonly length: number;
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* The decoded rows, shared by reference for a store built from strings (only entries below
|
|
360
|
+
* `length` are read) or produced by `materialiseDecoded()` for an encoded store.
|
|
361
|
+
*/
|
|
362
|
+
private source: ArrayLike<string> | null;
|
|
363
|
+
|
|
364
|
+
/** Sparse per-row decode cache of an encoded-only store; allocated on the first decode. */
|
|
365
|
+
private cache: (string | undefined)[] | null;
|
|
366
|
+
|
|
367
|
+
/** The offsets and bytes, once encoded or when adopted. */
|
|
368
|
+
private encodedArrays: EncodedUtf8 | null;
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Construct a store; use the static factories. At least one representation is always given.
|
|
372
|
+
* @param length - the row count
|
|
373
|
+
* @param source - the decoded rows or null
|
|
374
|
+
* @param encoded - the offsets and bytes or null
|
|
375
|
+
*/
|
|
376
|
+
private constructor(length: number, source: ArrayLike<string> | null, encoded: EncodedUtf8 | null) {
|
|
377
|
+
this.length = length;
|
|
378
|
+
this.source = source;
|
|
379
|
+
this.cache = null;
|
|
380
|
+
this.encodedArrays = encoded;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* A store over decoded strings, shared by reference: the builder-side representation. Encoding
|
|
385
|
+
* happens on the first read of `offsets` / `utf8`.
|
|
386
|
+
* @param source - the rows; only entries below `length` are ever read, so a builder array that
|
|
387
|
+
* keeps growing can be shared (design section 4.2)
|
|
388
|
+
* @param length - the row count; default source.length
|
|
389
|
+
* @returns the store
|
|
390
|
+
*/
|
|
391
|
+
static fromStrings(source: ArrayLike<string>, length: number = source.length): Utf8Store {
|
|
392
|
+
return new Utf8Store(length, source, null);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* A store over an encoded Utf8 layout, adopted by reference: the wire-side representation.
|
|
397
|
+
* Decoding happens per row on `at()` and in bulk on `slice()` / `decodeAll()`. The layout is not
|
|
398
|
+
* checked here; `checkUtf8Layout` does that for untrusted input.
|
|
399
|
+
* @param offsets - rows + 1 non-decreasing offsets
|
|
400
|
+
* @param utf8 - the concatenated bytes
|
|
401
|
+
* @returns the store
|
|
402
|
+
*/
|
|
403
|
+
static fromEncoded(offsets: U32, utf8: U8): Utf8Store {
|
|
404
|
+
return new Utf8Store(Math.max(0, offsets.length - 1), null, { offsets, utf8 });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Whether the encoded representation exists (adopted or already materialised).
|
|
409
|
+
* @returns true when `offsets` / `utf8` can be read without encoding
|
|
410
|
+
*/
|
|
411
|
+
get encoded(): boolean {
|
|
412
|
+
return this.encodedArrays !== null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Whether the store is backed by a decoded source array, so `at()` never decodes.
|
|
417
|
+
* @returns true for a store built from strings
|
|
418
|
+
*/
|
|
419
|
+
get decoded(): boolean {
|
|
420
|
+
return this.source !== null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Whether the encoded arrays were transferred away while rows still depend on them (design
|
|
425
|
+
* section 9.1): a transferred buffer leaves zero-length views.
|
|
426
|
+
* @returns true when detached
|
|
427
|
+
*/
|
|
428
|
+
get detached(): boolean {
|
|
429
|
+
return (
|
|
430
|
+
this.source === null &&
|
|
431
|
+
this.length > 0 &&
|
|
432
|
+
this.encodedArrays !== null &&
|
|
433
|
+
this.encodedArrays.offsets.length === 0
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* The `length + 1` offsets, encoding the source on first access.
|
|
439
|
+
* @returns the offsets array (cached)
|
|
440
|
+
*/
|
|
441
|
+
get offsets(): U32 {
|
|
442
|
+
return this.ensureEncoded().offsets;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* The concatenated UTF-8 bytes, encoding the source on first access.
|
|
447
|
+
* @returns the byte array (cached)
|
|
448
|
+
*/
|
|
449
|
+
get utf8(): U8 {
|
|
450
|
+
return this.ensureEncoded().utf8;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The string of one row, decoded on first access and cached per row.
|
|
455
|
+
* @param row - the row index
|
|
456
|
+
* @returns the string; E_INDEX_RANGE when row is out of range
|
|
457
|
+
*/
|
|
458
|
+
at(row: number): string {
|
|
459
|
+
if (!(row >= 0 && row < this.length) || !Number.isInteger(row)) {
|
|
460
|
+
throw new GraphFormatError("E_INDEX_RANGE", `row ${row} out of range (length ${this.length})`, {
|
|
461
|
+
index: row,
|
|
462
|
+
size: this.length,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (this.source !== null) {
|
|
466
|
+
return this.source[row];
|
|
467
|
+
}
|
|
468
|
+
this.cache ??= new Array<string | undefined>(this.length);
|
|
469
|
+
const cached = this.cache[row];
|
|
470
|
+
if (cached !== undefined) {
|
|
471
|
+
return cached;
|
|
472
|
+
}
|
|
473
|
+
const { offsets, utf8 } = this.ensureEncoded();
|
|
474
|
+
const value = decodeUtf8Row(utf8, offsets[row], offsets[row + 1]);
|
|
475
|
+
this.cache[row] = value;
|
|
476
|
+
return value;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* The strings of a row range decoded in one pass (one TextDecoder call for an all-ASCII range);
|
|
481
|
+
* nothing is cached. Bounds follow Array.prototype.slice (negative values count from the end,
|
|
482
|
+
* out-of-range values are clamped).
|
|
483
|
+
* @param start - first row (default 0)
|
|
484
|
+
* @param end - one past the last row (default length)
|
|
485
|
+
* @returns a fresh array of the rows in order
|
|
486
|
+
*/
|
|
487
|
+
slice(start = 0, end = this.length): string[] {
|
|
488
|
+
const [from, to] = resolveRange(start, end, this.length);
|
|
489
|
+
const { source } = this;
|
|
490
|
+
if (source !== null) {
|
|
491
|
+
const out = new Array<string>(to - from);
|
|
492
|
+
for (let i = from; i < to; i++) {
|
|
493
|
+
out[i - from] = source[i];
|
|
494
|
+
}
|
|
495
|
+
return out;
|
|
496
|
+
}
|
|
497
|
+
const { offsets, utf8 } = this.ensureEncoded();
|
|
498
|
+
return decodeUtf8Rows(utf8, offsets, from, to);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Every row decoded in one pass; not cached.
|
|
503
|
+
* @returns a fresh array of all rows
|
|
504
|
+
*/
|
|
505
|
+
decodeAll(): string[] {
|
|
506
|
+
return this.slice(0, this.length);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Decode every row of an encoded store once, in bulk, and keep the result as the decoded
|
|
511
|
+
* representation so that later `at()` calls are O(1) without a per-row decode. For a store that
|
|
512
|
+
* already has its decoded rows this is a no-op. Used before a pass that will touch every row
|
|
513
|
+
* anyway (building the reverse map of a wire-decoded id map).
|
|
514
|
+
*/
|
|
515
|
+
materialiseDecoded(): void {
|
|
516
|
+
if (this.source === null) {
|
|
517
|
+
this.source = this.decodeAll();
|
|
518
|
+
this.cache = null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Bytes of typed storage: the offsets and utf8 arrays when they exist, 0 for a store that has not
|
|
524
|
+
* been encoded yet. JS strings are never counted.
|
|
525
|
+
* @returns the byte count
|
|
526
|
+
*/
|
|
527
|
+
byteLength(): number {
|
|
528
|
+
const enc = this.encodedArrays;
|
|
529
|
+
return enc === null ? 0 : enc.offsets.byteLength + enc.utf8.byteLength;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* The first row whose bytes are not well-formed UTF-8, for full validation of untrusted input
|
|
534
|
+
* (design section 9.5: string ids are decoded with a fatal decoder). A source-backed store is
|
|
535
|
+
* always well-formed.
|
|
536
|
+
* @returns the row index, or -1 when every row is well-formed
|
|
537
|
+
*/
|
|
538
|
+
firstMalformedRow(): number {
|
|
539
|
+
const enc = this.encodedArrays;
|
|
540
|
+
if (this.source !== null || enc === null) {
|
|
541
|
+
return -1;
|
|
542
|
+
}
|
|
543
|
+
const { offsets, utf8 } = enc;
|
|
544
|
+
for (let i = 0; i < this.length; i++) {
|
|
545
|
+
if (!isWellFormedUtf8Row(utf8, offsets[i], offsets[i + 1])) {
|
|
546
|
+
return i;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return -1;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* The encoded representation, encoding the source once on first use. A store always has a source
|
|
554
|
+
* when it has no encoded arrays (the private constructor is only reached through the factories).
|
|
555
|
+
* @returns the offsets and bytes
|
|
556
|
+
*/
|
|
557
|
+
private ensureEncoded(): EncodedUtf8 {
|
|
558
|
+
let enc = this.encodedArrays;
|
|
559
|
+
if (enc === null) {
|
|
560
|
+
enc = encodeUtf8Rows(this.source as ArrayLike<string>, this.length);
|
|
561
|
+
this.encodedArrays = enc;
|
|
562
|
+
// the store is the first holder of the buffers it materialises (design section 9.1)
|
|
563
|
+
claimHolder(enc.offsets.buffer);
|
|
564
|
+
claimHolder(enc.utf8.buffer);
|
|
565
|
+
}
|
|
566
|
+
return enc;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Resolve Array.prototype.slice-style bounds against a length.
|
|
572
|
+
* @param start - the requested start (negative counts from the end)
|
|
573
|
+
* @param end - the requested end (negative counts from the end)
|
|
574
|
+
* @param length - the length of the sequence
|
|
575
|
+
* @returns the clamped [from, to] with from <= to
|
|
576
|
+
*/
|
|
577
|
+
export function resolveRange(start: number, end: number, length: number): [from: number, to: number] {
|
|
578
|
+
let from = Math.trunc(start);
|
|
579
|
+
let to = Math.trunc(end);
|
|
580
|
+
if (Number.isNaN(from)) {
|
|
581
|
+
from = 0;
|
|
582
|
+
}
|
|
583
|
+
if (Number.isNaN(to)) {
|
|
584
|
+
to = 0;
|
|
585
|
+
}
|
|
586
|
+
from = from < 0 ? Math.max(length + from, 0) : Math.min(from, length);
|
|
587
|
+
to = to < 0 ? Math.max(length + to, 0) : Math.min(to, length);
|
|
588
|
+
return [from, Math.max(from, to)];
|
|
589
|
+
}
|