@minnowdb/core 0.6.9 → 0.7.1
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/dist/block-format/block.js +254 -325
- package/dist/block-format/checksum.js +30 -52
- package/dist/block-format/codecs.js +112 -125
- package/dist/block-format/column.js +187 -205
- package/dist/block-format/index.js +5 -1
- package/dist/block-format/physical.js +371 -387
- package/dist/block-format/types.js +4 -1
- package/dist/block-format/unicode.js +30 -39
- package/dist/date-value.d.ts +5 -0
- package/dist/date-value.js +71 -41
- package/dist/engine/artifact-cache.js +106 -116
- package/dist/engine/batch.js +36 -44
- package/dist/engine/buffered-writer.js +156 -158
- package/dist/engine/byte-estimates.js +20 -22
- package/dist/engine/cache-limits.js +24 -25
- package/dist/engine/cancellation.js +5 -3
- package/dist/engine/catalog.js +64 -73
- package/dist/engine/client.js +715 -780
- package/dist/engine/database.js +15952 -18465
- package/dist/engine/defaults.js +104 -111
- package/dist/engine/errors.js +113 -128
- package/dist/engine/fts.js +233 -280
- package/dist/engine/group-index.js +287 -321
- package/dist/engine/index.js +23 -12
- package/dist/engine/join-index.js +177 -186
- package/dist/engine/keyed-live.js +168 -173
- package/dist/engine/live-api.js +0 -1
- package/dist/engine/live-equal.js +32 -37
- package/dist/engine/live.js +603 -647
- package/dist/engine/memory.js +109 -111
- package/dist/engine/optimizer.d.ts +19 -1
- package/dist/engine/optimizer.js +2642 -2515
- package/dist/engine/point-read.d.ts +4 -2
- package/dist/engine/point-read.js +121 -171
- package/dist/engine/query-api.js +12 -3
- package/dist/engine/query-cache.js +61 -69
- package/dist/engine/query.d.ts +45 -0
- package/dist/engine/query.js +8506 -9375
- package/dist/engine/result-wire.js +221 -252
- package/dist/engine/schema-wire.js +97 -112
- package/dist/engine/schema.js +1099 -1254
- package/dist/engine/sort-keys.js +315 -389
- package/dist/engine/sql-domains.d.ts +7 -1
- package/dist/engine/sql-domains.js +648 -734
- package/dist/engine/sql-driver.js +0 -1
- package/dist/engine/sql-functions.d.ts +11 -0
- package/dist/engine/sql-functions.js +1092 -0
- package/dist/engine/sql-json.js +199 -218
- package/dist/engine/sql-semantics.d.ts +25 -4
- package/dist/engine/sql-semantics.js +579 -513
- package/dist/engine/typed-live.js +271 -293
- package/dist/engine/vector.d.ts +7 -1
- package/dist/engine/vector.js +4513 -4580
- package/dist/engine/worker-host.d.ts +8 -63
- package/dist/engine/worker-host.js +28 -1130
- package/dist/engine/worker-indexeddb.d.ts +1 -0
- package/dist/engine/worker-indexeddb.js +3 -0
- package/dist/engine/worker-memory.d.ts +1 -0
- package/dist/engine/worker-memory.js +3 -0
- package/dist/engine/worker-opfs.d.ts +1 -0
- package/dist/engine/worker-opfs.js +3 -0
- package/dist/engine/worker-server.d.ts +93 -0
- package/dist/engine/worker-server.js +999 -0
- package/dist/engine/worker-store-indexeddb.d.ts +2 -0
- package/dist/engine/worker-store-indexeddb.js +11 -0
- package/dist/engine/worker-store-memory.d.ts +2 -0
- package/dist/engine/worker-store-memory.js +6 -0
- package/dist/engine/worker-store-opfs.d.ts +2 -0
- package/dist/engine/worker-store-opfs.js +9 -0
- package/dist/engine/worker.js +0 -12
- package/dist/engine/write-block-planner.js +93 -100
- package/dist/plan/index.js +15 -16
- package/dist/plan/model.d.ts +9 -1
- package/dist/plan/model.js +20 -27
- package/dist/storage/index.js +0 -12
- package/dist/storage/indexeddb.js +11776 -13171
- package/dist/storage/memory.js +1198 -1241
- package/dist/storage/opfs/files.js +238 -295
- package/dist/storage/opfs/index.js +9 -3
- package/dist/storage/opfs/leader.js +4386 -4856
- package/dist/storage/opfs/rpc.js +223 -259
- package/dist/storage/opfs/snapshot-ledger.js +219 -260
- package/dist/storage/opfs/store.js +884 -1003
- package/dist/storage/persistence.js +59 -74
- package/dist/storage/snapshot-stream.js +788 -826
- package/dist/storage/snapshot.js +36 -37
- package/dist/storage/toolkit/extents.js +444 -520
- package/dist/storage/toolkit/index.js +28 -29
- package/dist/storage/toolkit/record-core.js +5792 -6377
- package/dist/storage/toolkit/sync-file.js +34 -42
- package/dist/storage/toolkit/wal.js +92 -127
- package/dist/storage/toolkit/wire.js +232 -256
- package/dist/storage/types.js +2952 -3228
- package/dist/testing/block-store-conformance.js +1556 -1629
- package/dist/testing/index.js +310 -291
- package/dist/testing/opfs-shim.js +280 -312
- package/dist/testing/simulator.js +496 -534
- package/dist/testing/sqllogictest.js +425 -365
- package/dist/transactions/index.js +1464 -1712
- package/dist/worker-protocol/index.js +70 -69
- package/package.json +20 -2
- package/postgres-feature-profile.json +15 -5
- package/sql-feature-matrix.json +252 -2
|
@@ -1,897 +1,859 @@
|
|
|
1
|
-
import { assertWellFormedString, crc32, crc32Continue, MAX_STORED_BLOCK_BYTE_LENGTH
|
|
1
|
+
import { assertWellFormedString, crc32, crc32Continue, MAX_STORED_BLOCK_BYTE_LENGTH } from "../block-format/index.js";
|
|
2
2
|
import { dateIsoString, dateMilliseconds } from "../date-value.js";
|
|
3
|
-
import { MAX_SNAPSHOT_FRAME_ITEMS, MAX_SNAPSHOT_METADATA_FRAME_BYTES, MAX_STORAGE_ID_CHARACTERS, MAX_ROW_ID_EXCLUSIVE_END, MAX_AUTO_INCREMENT_EXCLUSIVE_END, MAX_UNIQUE_KEY_BUILD_CHUNK_BYTES, MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_BASE_CHUNKS, MAX_FTS_TOKENS_PER_DOCUMENT, SNAPSHOT_FRAME_KINDS, normalizeSegmentRecord, uniqueKeyBuildChunkRetainedBytes, validateTableRecordBounds, validateStorageId
|
|
4
|
-
const MAGIC = Uint8Array.of(
|
|
3
|
+
import { MAX_SNAPSHOT_FRAME_ITEMS, MAX_SNAPSHOT_METADATA_FRAME_BYTES, MAX_STORAGE_ID_CHARACTERS, MAX_ROW_ID_EXCLUSIVE_END, MAX_AUTO_INCREMENT_EXCLUSIVE_END, MAX_UNIQUE_KEY_BUILD_CHUNK_BYTES, MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_BASE_CHUNKS, MAX_FTS_TOKENS_PER_DOCUMENT, SNAPSHOT_FRAME_KINDS, normalizeSegmentRecord, uniqueKeyBuildChunkRetainedBytes, validateTableRecordBounds, validateStorageId } from "./types.js";
|
|
4
|
+
const MAGIC = Uint8Array.of(77, 73, 78, 83, 78, 65, 80, 49);
|
|
5
5
|
const HEADER_PREFIX_BYTES = 20;
|
|
6
|
-
const FRAME_MARKER =
|
|
6
|
+
const FRAME_MARKER = 1179798833;
|
|
7
7
|
const FRAME_PREFIX_BYTES = 40;
|
|
8
|
-
const FOOTER_MARKER =
|
|
8
|
+
const FOOTER_MARKER = 1179931185;
|
|
9
9
|
const FOOTER_BYTES = 40;
|
|
10
10
|
const MAX_HEADER_BYTES = 64 * 1024;
|
|
11
|
-
const MAX_WIRE_ENTRIES =
|
|
11
|
+
const MAX_WIRE_ENTRIES = 262144;
|
|
12
12
|
const MAX_WIRE_DEPTH = 128;
|
|
13
13
|
const MAX_UINT64_DECIMAL_BYTES = 20;
|
|
14
14
|
const textEncoder = new TextEncoder();
|
|
15
15
|
const textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
16
16
|
function safeWhole(value, label) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
18
|
+
throw new RangeError(`${label} must be a non-negative safe integer`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
21
|
}
|
|
22
22
|
function canonicalTimestamp(value, label) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
if (typeof value !== "string")
|
|
24
|
+
throw new TypeError(`${label} must be a string`);
|
|
25
|
+
const date = new Date(value);
|
|
26
|
+
if (!Number.isFinite(dateMilliseconds(date)) || dateIsoString(date) !== value) {
|
|
27
|
+
throw new TypeError(`${label} must be a canonical UTC timestamp`);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
30
|
}
|
|
31
31
|
function sameKeys(value, expected, label) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
const keys = Object.keys(value).sort();
|
|
33
|
+
const canonical = [...expected].sort();
|
|
34
|
+
if (keys.length !== canonical.length || keys.some((key, index) => key !== canonical[index])) {
|
|
35
|
+
throw new TypeError(`${label} has unknown or missing fields`);
|
|
36
|
+
}
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
38
|
+
function prepareSnapshotFrameStreamHeader(input) {
|
|
39
|
+
const runtime = input;
|
|
40
|
+
if (typeof runtime !== "object" || runtime === null) {
|
|
41
|
+
throw new TypeError("Snapshot stream header must be an object");
|
|
42
|
+
}
|
|
43
|
+
sameKeys(runtime, ["formatVersion", "databaseVersion", "createdAt", "kinds"], "Snapshot header");
|
|
44
|
+
const fields = runtime;
|
|
45
|
+
if (fields.formatVersion !== 1)
|
|
46
|
+
throw new TypeError("Unsupported snapshot format version");
|
|
47
|
+
const databaseVersion = safeWhole(fields.databaseVersion, "Snapshot database version");
|
|
48
|
+
const createdAt = canonicalTimestamp(fields.createdAt, "Snapshot creation time");
|
|
49
|
+
if (typeof fields.kinds !== "object" || fields.kinds === null) {
|
|
50
|
+
throw new TypeError("Snapshot kind summaries must be an object");
|
|
51
|
+
}
|
|
52
|
+
sameKeys(fields.kinds, SNAPSHOT_FRAME_KINDS, "Snapshot kind summaries");
|
|
53
|
+
const rawKinds = fields.kinds;
|
|
54
|
+
const kinds = /* @__PURE__ */ Object.create(null);
|
|
55
|
+
for (const kind of SNAPSHOT_FRAME_KINDS) {
|
|
56
|
+
const summary = rawKinds[kind];
|
|
57
|
+
if (typeof summary !== "object" || summary === null) {
|
|
58
|
+
throw new TypeError(`Snapshot ${kind} summary must be an object`);
|
|
59
|
+
}
|
|
60
|
+
sameKeys(summary, ["frameCount", "itemCount", "storedBytes"], `Snapshot ${kind} summary`);
|
|
61
|
+
const rawSummary = summary;
|
|
62
|
+
const frameCount = safeWhole(rawSummary.frameCount, `Snapshot ${kind} frame count`);
|
|
63
|
+
const itemCount = safeWhole(rawSummary.itemCount, `Snapshot ${kind} item count`);
|
|
64
|
+
const storedBytes = safeWhole(rawSummary.storedBytes, `Snapshot ${kind} stored bytes`);
|
|
65
|
+
if (frameCount === 0 !== (itemCount === 0 && storedBytes === 0)) {
|
|
66
|
+
throw new TypeError(`Snapshot ${kind} empty summary is inconsistent`);
|
|
67
|
+
}
|
|
68
|
+
kinds[kind] = { frameCount, itemCount, storedBytes };
|
|
69
|
+
}
|
|
70
|
+
return { formatVersion: 1, databaseVersion, createdAt, kinds };
|
|
71
71
|
}
|
|
72
72
|
function canonicalHeaderJson(header) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
const prepared = prepareSnapshotFrameStreamHeader(header);
|
|
74
|
+
return JSON.stringify({
|
|
75
|
+
formatVersion: 1,
|
|
76
|
+
databaseVersion: prepared.databaseVersion,
|
|
77
|
+
createdAt: prepared.createdAt,
|
|
78
|
+
kinds: Object.fromEntries(SNAPSHOT_FRAME_KINDS.map((kind) => [kind, prepared.kinds[kind]]))
|
|
79
|
+
});
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
81
|
+
function encodeSnapshotFrameStreamHeader(header) {
|
|
82
|
+
const bytes = textEncoder.encode(canonicalHeaderJson(header));
|
|
83
|
+
if (bytes.byteLength > MAX_HEADER_BYTES)
|
|
84
|
+
throw new RangeError("Snapshot header is too large");
|
|
85
|
+
const encoded = new Uint8Array(HEADER_PREFIX_BYTES + bytes.byteLength);
|
|
86
|
+
encoded.set(MAGIC, 0);
|
|
87
|
+
const view = new DataView(encoded.buffer);
|
|
88
|
+
view.setUint32(8, 1, true);
|
|
89
|
+
view.setUint32(12, bytes.byteLength, true);
|
|
90
|
+
view.setUint32(16, crc32(bytes), true);
|
|
91
|
+
encoded.set(bytes, HEADER_PREFIX_BYTES);
|
|
92
|
+
return encoded;
|
|
93
93
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
function snapshotFrameStreamHeaderIdentity(header) {
|
|
95
|
+
const bytes = encodeSnapshotFrameStreamHeader(header);
|
|
96
|
+
return `snapshot-v1/${String(bytes.byteLength)}/${crc32(bytes).toString(16).padStart(8, "0")}`;
|
|
97
97
|
}
|
|
98
98
|
function utf8Bytes(value, label) {
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
assertWellFormedString(value, label);
|
|
100
|
+
return textEncoder.encode(value);
|
|
101
101
|
}
|
|
102
102
|
function stringFromUtf8(bytes) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
103
|
+
try {
|
|
104
|
+
return textDecoder.decode(bytes);
|
|
105
|
+
} catch {
|
|
106
|
+
throw new TypeError("Snapshot string is not valid UTF-8");
|
|
107
|
+
}
|
|
109
108
|
}
|
|
110
109
|
function frameKindIndex(kind) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
const index = SNAPSHOT_FRAME_KINDS.indexOf(kind);
|
|
111
|
+
if (index < 0)
|
|
112
|
+
throw new TypeError(`Unknown snapshot frame kind: ${kind}`);
|
|
113
|
+
return index;
|
|
115
114
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
view.setUint32(36, 0, true);
|
|
146
|
-
return keyBytes.byteLength === 0 ? [prefix, frame.payload] : [prefix, keyBytes, frame.payload];
|
|
115
|
+
function snapshotFrameEnvelopeParts(frame) {
|
|
116
|
+
const sequence = safeWhole(frame.sequence, "Snapshot frame sequence");
|
|
117
|
+
const itemCount = safeWhole(frame.itemCount, "Snapshot frame item count");
|
|
118
|
+
if (itemCount < 1 || itemCount > MAX_SNAPSHOT_FRAME_ITEMS) {
|
|
119
|
+
throw new RangeError("Snapshot frame item count is out of bounds");
|
|
120
|
+
}
|
|
121
|
+
const isBlock = frame.kind === "block";
|
|
122
|
+
if (isBlock !== (frame.key !== null) || isBlock && itemCount !== 1) {
|
|
123
|
+
throw new TypeError("Snapshot block frame identity is inconsistent");
|
|
124
|
+
}
|
|
125
|
+
const keyBytes = frame.key === null ? new Uint8Array() : utf8Bytes(validateStorageId(frame.key), "Snapshot frame key");
|
|
126
|
+
const payloadLimit = isBlock ? MAX_STORED_BLOCK_BYTE_LENGTH : MAX_SNAPSHOT_METADATA_FRAME_BYTES;
|
|
127
|
+
if (frame.payload.byteLength < 1 || frame.payload.byteLength > payloadLimit) {
|
|
128
|
+
throw new RangeError("Snapshot frame payload length is out of bounds");
|
|
129
|
+
}
|
|
130
|
+
if (!Number.isInteger(frame.checksum) || frame.checksum < 0 || frame.checksum > 4294967295) {
|
|
131
|
+
throw new RangeError("Snapshot frame checksum is invalid");
|
|
132
|
+
}
|
|
133
|
+
const prefix = new Uint8Array(FRAME_PREFIX_BYTES);
|
|
134
|
+
const view = new DataView(prefix.buffer);
|
|
135
|
+
view.setUint32(0, FRAME_MARKER, true);
|
|
136
|
+
view.setUint32(4, frameKindIndex(frame.kind), true);
|
|
137
|
+
view.setBigUint64(8, BigInt(sequence), true);
|
|
138
|
+
view.setUint32(16, itemCount, true);
|
|
139
|
+
view.setUint32(20, keyBytes.byteLength, true);
|
|
140
|
+
view.setBigUint64(24, BigInt(frame.payload.byteLength), true);
|
|
141
|
+
view.setUint32(32, frame.checksum, true);
|
|
142
|
+
view.setUint32(36, 0, true);
|
|
143
|
+
return keyBytes.byteLength === 0 ? [prefix, frame.payload] : [prefix, keyBytes, frame.payload];
|
|
147
144
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
145
|
+
function extendSnapshotFrameStreamChecksum(checksum, parts) {
|
|
146
|
+
let next = checksum;
|
|
147
|
+
for (const part of parts)
|
|
148
|
+
next = crc32Continue(next, part);
|
|
149
|
+
return next;
|
|
153
150
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
151
|
+
function encodeSnapshotFrameStreamFooter(footer) {
|
|
152
|
+
const frameCount = safeWhole(footer.frameCount, "Snapshot footer frame count");
|
|
153
|
+
const itemCount = safeWhole(footer.itemCount, "Snapshot footer item count");
|
|
154
|
+
const storedBytes = safeWhole(footer.storedBytes, "Snapshot footer stored bytes");
|
|
155
|
+
if (!Number.isInteger(footer.checksum) || footer.checksum < 0 || footer.checksum > 4294967295) {
|
|
156
|
+
throw new RangeError("Snapshot footer checksum is invalid");
|
|
157
|
+
}
|
|
158
|
+
const bytes = new Uint8Array(FOOTER_BYTES);
|
|
159
|
+
const view = new DataView(bytes.buffer);
|
|
160
|
+
view.setUint32(0, FOOTER_MARKER, true);
|
|
161
|
+
view.setBigUint64(4, BigInt(frameCount), true);
|
|
162
|
+
view.setBigUint64(12, BigInt(itemCount), true);
|
|
163
|
+
view.setBigUint64(20, BigInt(storedBytes), true);
|
|
164
|
+
view.setUint32(28, footer.checksum, true);
|
|
165
|
+
return bytes;
|
|
169
166
|
}
|
|
170
167
|
class WireWriter {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
168
|
+
#bytes = new Uint8Array(256);
|
|
169
|
+
#length = 0;
|
|
170
|
+
#reserve(count) {
|
|
171
|
+
const next = this.#length + count;
|
|
172
|
+
if (next > MAX_SNAPSHOT_METADATA_FRAME_BYTES) {
|
|
173
|
+
throw new RangeError("Snapshot metadata frame exceeds its byte limit");
|
|
174
|
+
}
|
|
175
|
+
if (next > this.#bytes.byteLength) {
|
|
176
|
+
let capacity = this.#bytes.byteLength;
|
|
177
|
+
while (capacity < next)
|
|
178
|
+
capacity = Math.min(MAX_SNAPSHOT_METADATA_FRAME_BYTES, capacity * 2);
|
|
179
|
+
const grown = new Uint8Array(capacity);
|
|
180
|
+
grown.set(this.#bytes.subarray(0, this.#length));
|
|
181
|
+
this.#bytes = grown;
|
|
182
|
+
}
|
|
183
|
+
const offset = this.#length;
|
|
184
|
+
this.#length = next;
|
|
185
|
+
return offset;
|
|
186
|
+
}
|
|
187
|
+
byte(value) {
|
|
188
|
+
const offset = this.#reserve(1);
|
|
189
|
+
this.#bytes[offset] = value;
|
|
190
|
+
}
|
|
191
|
+
uint32(value) {
|
|
192
|
+
const offset = this.#reserve(4);
|
|
193
|
+
new DataView(this.#bytes.buffer).setUint32(offset, value, true);
|
|
194
|
+
}
|
|
195
|
+
float64(value) {
|
|
196
|
+
const offset = this.#reserve(8);
|
|
197
|
+
new DataView(this.#bytes.buffer).setFloat64(offset, value, true);
|
|
198
|
+
}
|
|
199
|
+
bytes(value) {
|
|
200
|
+
const offset = this.#reserve(value.byteLength);
|
|
201
|
+
this.#bytes.set(value, offset);
|
|
202
|
+
}
|
|
203
|
+
finish() {
|
|
204
|
+
return this.#bytes.slice(0, this.#length);
|
|
205
|
+
}
|
|
206
|
+
get remaining() {
|
|
207
|
+
return MAX_SNAPSHOT_METADATA_FRAME_BYTES - this.#length;
|
|
208
|
+
}
|
|
212
209
|
}
|
|
213
210
|
function writeWireValue(writer, value, seen, state, depth) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
writeWireValue(writer, record[key], seen, state, depth + 1);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
211
|
+
if (depth > MAX_WIRE_DEPTH || ++state.entries > MAX_WIRE_ENTRIES) {
|
|
212
|
+
throw new RangeError("Snapshot metadata value is too complex");
|
|
213
|
+
}
|
|
214
|
+
if (value === null)
|
|
215
|
+
return writer.byte(0);
|
|
216
|
+
if (value === false)
|
|
217
|
+
return writer.byte(1);
|
|
218
|
+
if (value === true)
|
|
219
|
+
return writer.byte(2);
|
|
220
|
+
if (typeof value === "number") {
|
|
221
|
+
if (!Number.isFinite(value))
|
|
222
|
+
throw new TypeError("Snapshot metadata numbers must be finite");
|
|
223
|
+
writer.byte(3);
|
|
224
|
+
writer.float64(value);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (typeof value === "string") {
|
|
228
|
+
if (value.length > writer.remaining)
|
|
229
|
+
throw new RangeError("Snapshot metadata string is too large");
|
|
230
|
+
const bytes = utf8Bytes(value, "Snapshot metadata string");
|
|
231
|
+
writer.byte(4);
|
|
232
|
+
writer.uint32(bytes.byteLength);
|
|
233
|
+
writer.bytes(bytes);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (typeof value === "bigint") {
|
|
237
|
+
if (value < 0n || value >= MAX_ROW_ID_EXCLUSIVE_END) {
|
|
238
|
+
throw new RangeError("Snapshot metadata bigint is outside the unsigned 64-bit range");
|
|
239
|
+
}
|
|
240
|
+
const bytes = utf8Bytes(value.toString(), "Snapshot metadata bigint");
|
|
241
|
+
writer.byte(5);
|
|
242
|
+
writer.uint32(bytes.byteLength);
|
|
243
|
+
writer.bytes(bytes);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (typeof value !== "object") {
|
|
247
|
+
throw new TypeError(`Unsupported snapshot metadata value: ${typeof value}`);
|
|
248
|
+
}
|
|
249
|
+
if (seen.has(value))
|
|
250
|
+
throw new TypeError("Snapshot metadata contains a cycle or alias");
|
|
251
|
+
seen.add(value);
|
|
252
|
+
if (value instanceof Date) {
|
|
253
|
+
const milliseconds = dateMilliseconds(value);
|
|
254
|
+
if (!Number.isSafeInteger(milliseconds)) {
|
|
255
|
+
throw new TypeError("Snapshot metadata date is invalid");
|
|
256
|
+
}
|
|
257
|
+
writer.byte(8);
|
|
258
|
+
writer.float64(milliseconds);
|
|
259
|
+
} else if (value instanceof Uint8Array) {
|
|
260
|
+
if (value.byteLength > writer.remaining - 5) {
|
|
261
|
+
throw new RangeError("Snapshot metadata byte array is too large");
|
|
262
|
+
}
|
|
263
|
+
writer.byte(9);
|
|
264
|
+
writer.uint32(value.byteLength);
|
|
265
|
+
writer.bytes(value);
|
|
266
|
+
} else if (Array.isArray(value)) {
|
|
267
|
+
if (value.length > MAX_WIRE_ENTRIES) {
|
|
268
|
+
throw new RangeError("Snapshot metadata array is too large");
|
|
269
|
+
}
|
|
270
|
+
writer.byte(6);
|
|
271
|
+
writer.uint32(value.length);
|
|
272
|
+
for (const item of value)
|
|
273
|
+
writeWireValue(writer, item, seen, state, depth + 1);
|
|
274
|
+
} else {
|
|
275
|
+
const prototype = Object.getPrototypeOf(value);
|
|
276
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
277
|
+
throw new TypeError("Snapshot metadata must contain plain objects");
|
|
278
|
+
}
|
|
279
|
+
const record = value;
|
|
280
|
+
const keys = Object.keys(record).sort();
|
|
281
|
+
if (keys.length > MAX_WIRE_ENTRIES) {
|
|
282
|
+
throw new RangeError("Snapshot metadata object is too large");
|
|
283
|
+
}
|
|
284
|
+
writer.byte(7);
|
|
285
|
+
writer.uint32(keys.length);
|
|
286
|
+
for (const key of keys) {
|
|
287
|
+
writeWireValue(writer, key, seen, state, depth + 1);
|
|
288
|
+
writeWireValue(writer, record[key], seen, state, depth + 1);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
297
291
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return writer.finish();
|
|
292
|
+
function encodeSnapshotMetadataPage(items) {
|
|
293
|
+
if (items.length < 1 || items.length > MAX_SNAPSHOT_FRAME_ITEMS) {
|
|
294
|
+
throw new RangeError("Snapshot metadata page item count is out of bounds");
|
|
295
|
+
}
|
|
296
|
+
const writer = new WireWriter();
|
|
297
|
+
writeWireValue(writer, items, /* @__PURE__ */ new WeakSet(), { entries: 0 }, 0);
|
|
298
|
+
return writer.finish();
|
|
306
299
|
}
|
|
307
300
|
class WireReader {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
301
|
+
bytes;
|
|
302
|
+
#offset = 0;
|
|
303
|
+
#entries = 0;
|
|
304
|
+
constructor(bytes) {
|
|
305
|
+
this.bytes = bytes;
|
|
306
|
+
}
|
|
307
|
+
get done() {
|
|
308
|
+
return this.#offset === this.bytes.byteLength;
|
|
309
|
+
}
|
|
310
|
+
get remaining() {
|
|
311
|
+
return this.bytes.byteLength - this.#offset;
|
|
312
|
+
}
|
|
313
|
+
byte() {
|
|
314
|
+
if (this.#offset >= this.bytes.byteLength)
|
|
315
|
+
throw new Error("Snapshot metadata is truncated");
|
|
316
|
+
return this.bytes[this.#offset++] ?? 0;
|
|
317
|
+
}
|
|
318
|
+
uint32() {
|
|
319
|
+
if (this.#offset + 4 > this.bytes.byteLength)
|
|
320
|
+
throw new Error("Snapshot metadata is truncated");
|
|
321
|
+
const value = new DataView(this.bytes.buffer, this.bytes.byteOffset).getUint32(this.#offset, true);
|
|
322
|
+
this.#offset += 4;
|
|
323
|
+
return value;
|
|
324
|
+
}
|
|
325
|
+
float64() {
|
|
326
|
+
if (this.#offset + 8 > this.bytes.byteLength)
|
|
327
|
+
throw new Error("Snapshot metadata is truncated");
|
|
328
|
+
const value = new DataView(this.bytes.buffer, this.bytes.byteOffset).getFloat64(this.#offset, true);
|
|
329
|
+
this.#offset += 8;
|
|
330
|
+
return value;
|
|
331
|
+
}
|
|
332
|
+
take(length) {
|
|
333
|
+
if (length > this.bytes.byteLength - this.#offset)
|
|
334
|
+
throw new Error("Snapshot metadata is truncated");
|
|
335
|
+
const value = this.bytes.slice(this.#offset, this.#offset + length);
|
|
336
|
+
this.#offset += length;
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
339
|
+
entry(depth) {
|
|
340
|
+
if (depth > MAX_WIRE_DEPTH || ++this.#entries > MAX_WIRE_ENTRIES) {
|
|
341
|
+
throw new RangeError("Snapshot metadata value is too complex");
|
|
350
342
|
}
|
|
343
|
+
}
|
|
351
344
|
}
|
|
352
345
|
function readWireValue(reader, depth) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
return value;
|
|
383
|
-
}
|
|
384
|
-
if (tag === 8) {
|
|
385
|
-
const milliseconds = reader.float64();
|
|
386
|
-
if (!Number.isSafeInteger(milliseconds))
|
|
387
|
-
throw new TypeError("Snapshot date is invalid");
|
|
388
|
-
const value = new Date(milliseconds);
|
|
389
|
-
if (dateMilliseconds(value) !== milliseconds)
|
|
390
|
-
throw new TypeError("Snapshot date is invalid");
|
|
391
|
-
return value;
|
|
392
|
-
}
|
|
393
|
-
if (tag === 9) {
|
|
394
|
-
const length = reader.uint32();
|
|
395
|
-
if (length > MAX_SNAPSHOT_METADATA_FRAME_BYTES || length > reader.remaining) {
|
|
396
|
-
throw new RangeError("Snapshot metadata byte array is too large");
|
|
397
|
-
}
|
|
398
|
-
return reader.take(length);
|
|
399
|
-
}
|
|
400
|
-
if (tag === 6) {
|
|
401
|
-
const length = reader.uint32();
|
|
402
|
-
if (length > MAX_WIRE_ENTRIES || length > reader.bytes.byteLength) {
|
|
403
|
-
throw new RangeError("Snapshot array is too large");
|
|
404
|
-
}
|
|
405
|
-
return Array.from({ length }, () => readWireValue(reader, depth + 1));
|
|
406
|
-
}
|
|
407
|
-
if (tag === 7) {
|
|
408
|
-
const length = reader.uint32();
|
|
409
|
-
if (length > MAX_WIRE_ENTRIES)
|
|
410
|
-
throw new RangeError("Snapshot object is too large");
|
|
411
|
-
const value = {};
|
|
412
|
-
let previous = "";
|
|
413
|
-
for (let index = 0; index < length; index += 1) {
|
|
414
|
-
const key = readWireValue(reader, depth + 1);
|
|
415
|
-
if (typeof key !== "string" || (index > 0 && key <= previous)) {
|
|
416
|
-
throw new TypeError("Snapshot object keys are not canonical");
|
|
417
|
-
}
|
|
418
|
-
previous = key;
|
|
419
|
-
const item = readWireValue(reader, depth + 1);
|
|
420
|
-
if (key === "__proto__") {
|
|
421
|
-
Object.defineProperty(value, key, { value: item, enumerable: true, writable: true });
|
|
422
|
-
}
|
|
423
|
-
else
|
|
424
|
-
value[key] = item;
|
|
425
|
-
}
|
|
426
|
-
return value;
|
|
427
|
-
}
|
|
428
|
-
throw new TypeError(`Unknown snapshot metadata tag: ${String(tag)}`);
|
|
429
|
-
}
|
|
430
|
-
export function decodeSnapshotMetadataPage(payload) {
|
|
431
|
-
if (payload.byteLength < 1 || payload.byteLength > MAX_SNAPSHOT_METADATA_FRAME_BYTES) {
|
|
432
|
-
throw new RangeError("Snapshot metadata page length is out of bounds");
|
|
433
|
-
}
|
|
434
|
-
const reader = new WireReader(payload);
|
|
435
|
-
const value = readWireValue(reader, 0);
|
|
436
|
-
if (!reader.done || !Array.isArray(value)) {
|
|
437
|
-
throw new TypeError("Snapshot metadata page is not canonical");
|
|
346
|
+
reader.entry(depth);
|
|
347
|
+
const tag = reader.byte();
|
|
348
|
+
if (tag === 0)
|
|
349
|
+
return null;
|
|
350
|
+
if (tag === 1)
|
|
351
|
+
return false;
|
|
352
|
+
if (tag === 2)
|
|
353
|
+
return true;
|
|
354
|
+
if (tag === 3) {
|
|
355
|
+
const value = reader.float64();
|
|
356
|
+
if (!Number.isFinite(value))
|
|
357
|
+
throw new TypeError("Snapshot metadata number is not finite");
|
|
358
|
+
return value;
|
|
359
|
+
}
|
|
360
|
+
if (tag === 4)
|
|
361
|
+
return stringFromUtf8(reader.take(reader.uint32()));
|
|
362
|
+
if (tag === 5) {
|
|
363
|
+
const length = reader.uint32();
|
|
364
|
+
if (length < 1 || length > MAX_UINT64_DECIMAL_BYTES || length > reader.remaining) {
|
|
365
|
+
throw new RangeError("Snapshot bigint is outside the unsigned 64-bit range");
|
|
366
|
+
}
|
|
367
|
+
const text = stringFromUtf8(reader.take(length));
|
|
368
|
+
if (!/^(?:0|[1-9][0-9]*)$/.test(text)) {
|
|
369
|
+
throw new TypeError("Snapshot bigint is not canonical");
|
|
370
|
+
}
|
|
371
|
+
const value = BigInt(text);
|
|
372
|
+
if (value >= MAX_ROW_ID_EXCLUSIVE_END) {
|
|
373
|
+
throw new RangeError("Snapshot bigint is outside the unsigned 64-bit range");
|
|
438
374
|
}
|
|
439
|
-
|
|
440
|
-
|
|
375
|
+
return value;
|
|
376
|
+
}
|
|
377
|
+
if (tag === 8) {
|
|
378
|
+
const milliseconds = reader.float64();
|
|
379
|
+
if (!Number.isSafeInteger(milliseconds))
|
|
380
|
+
throw new TypeError("Snapshot date is invalid");
|
|
381
|
+
const value = new Date(milliseconds);
|
|
382
|
+
if (dateMilliseconds(value) !== milliseconds)
|
|
383
|
+
throw new TypeError("Snapshot date is invalid");
|
|
384
|
+
return value;
|
|
385
|
+
}
|
|
386
|
+
if (tag === 9) {
|
|
387
|
+
const length = reader.uint32();
|
|
388
|
+
if (length > MAX_SNAPSHOT_METADATA_FRAME_BYTES || length > reader.remaining) {
|
|
389
|
+
throw new RangeError("Snapshot metadata byte array is too large");
|
|
390
|
+
}
|
|
391
|
+
return reader.take(length);
|
|
392
|
+
}
|
|
393
|
+
if (tag === 6) {
|
|
394
|
+
const length = reader.uint32();
|
|
395
|
+
if (length > MAX_WIRE_ENTRIES || length > reader.bytes.byteLength) {
|
|
396
|
+
throw new RangeError("Snapshot array is too large");
|
|
397
|
+
}
|
|
398
|
+
return Array.from({ length }, () => readWireValue(reader, depth + 1));
|
|
399
|
+
}
|
|
400
|
+
if (tag === 7) {
|
|
401
|
+
const length = reader.uint32();
|
|
402
|
+
if (length > MAX_WIRE_ENTRIES)
|
|
403
|
+
throw new RangeError("Snapshot object is too large");
|
|
404
|
+
const value = {};
|
|
405
|
+
let previous = "";
|
|
406
|
+
for (let index = 0; index < length; index += 1) {
|
|
407
|
+
const key = readWireValue(reader, depth + 1);
|
|
408
|
+
if (typeof key !== "string" || index > 0 && key <= previous) {
|
|
409
|
+
throw new TypeError("Snapshot object keys are not canonical");
|
|
410
|
+
}
|
|
411
|
+
previous = key;
|
|
412
|
+
const item = readWireValue(reader, depth + 1);
|
|
413
|
+
if (key === "__proto__") {
|
|
414
|
+
Object.defineProperty(value, key, { value: item, enumerable: true, writable: true });
|
|
415
|
+
} else
|
|
416
|
+
value[key] = item;
|
|
441
417
|
}
|
|
442
418
|
return value;
|
|
419
|
+
}
|
|
420
|
+
throw new TypeError(`Unknown snapshot metadata tag: ${String(tag)}`);
|
|
421
|
+
}
|
|
422
|
+
function decodeSnapshotMetadataPage(payload) {
|
|
423
|
+
if (payload.byteLength < 1 || payload.byteLength > MAX_SNAPSHOT_METADATA_FRAME_BYTES) {
|
|
424
|
+
throw new RangeError("Snapshot metadata page length is out of bounds");
|
|
425
|
+
}
|
|
426
|
+
const reader = new WireReader(payload);
|
|
427
|
+
const value = readWireValue(reader, 0);
|
|
428
|
+
if (!reader.done || !Array.isArray(value)) {
|
|
429
|
+
throw new TypeError("Snapshot metadata page is not canonical");
|
|
430
|
+
}
|
|
431
|
+
if (value.length < 1 || value.length > MAX_SNAPSHOT_FRAME_ITEMS) {
|
|
432
|
+
throw new RangeError("Snapshot metadata page item count is out of bounds");
|
|
433
|
+
}
|
|
434
|
+
return value;
|
|
443
435
|
}
|
|
444
436
|
function recordValue(value, label) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
437
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
438
|
+
throw new TypeError(`${label} must be an object`);
|
|
439
|
+
}
|
|
440
|
+
return value;
|
|
449
441
|
}
|
|
450
442
|
function exactRecord(value, fields, label) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
443
|
+
const record = recordValue(value, label);
|
|
444
|
+
sameKeys(record, fields, label);
|
|
445
|
+
return record;
|
|
454
446
|
}
|
|
455
447
|
function canonicalOrdinal(value, label) {
|
|
456
|
-
|
|
448
|
+
return safeWhole(value, label);
|
|
457
449
|
}
|
|
458
450
|
function canonicalCounter(value, label) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
451
|
+
if (typeof value !== "bigint" || value < 1n || value > MAX_ROW_ID_EXCLUSIVE_END) {
|
|
452
|
+
throw new RangeError(`${label} must be a positive bounded bigint`);
|
|
453
|
+
}
|
|
454
|
+
return value;
|
|
463
455
|
}
|
|
464
456
|
function prepareCatalogItem(value) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
457
|
+
const item = exactRecord(value, ["kind", "record", "nextRowId", "autoIncrement"], "Snapshot catalog item");
|
|
458
|
+
if (item.kind !== "table")
|
|
459
|
+
throw new TypeError("Snapshot catalog item kind is invalid");
|
|
460
|
+
const table = item.record;
|
|
461
|
+
validateTableRecordBounds(table);
|
|
462
|
+
const nextRowId = canonicalCounter(item.nextRowId, "Snapshot next row ID");
|
|
463
|
+
if (!Array.isArray(item.autoIncrement) || item.autoIncrement.length > table.columns.length) {
|
|
464
|
+
throw new RangeError("Snapshot auto-increment counters are invalid");
|
|
465
|
+
}
|
|
466
|
+
const autoIncrement = item.autoIncrement.map((raw) => {
|
|
467
|
+
const counter = exactRecord(raw, ["columnId", "next"], "Snapshot auto-increment counter");
|
|
468
|
+
const columnId = validateStorageId(counter.columnId, "Snapshot auto-increment column ID");
|
|
469
|
+
const column = table.columns.find((candidate) => candidate.id === columnId);
|
|
470
|
+
if (column?.defaultValue?.kind !== "autoincrement") {
|
|
471
|
+
throw new TypeError("Snapshot auto-increment counter has no matching catalog column");
|
|
472
|
+
}
|
|
473
|
+
const next = canonicalCounter(counter.next, "Snapshot auto-increment value");
|
|
474
|
+
if (next > MAX_AUTO_INCREMENT_EXCLUSIVE_END) {
|
|
475
|
+
throw new RangeError("Snapshot auto-increment value exceeds its safe integer range");
|
|
476
|
+
}
|
|
477
|
+
return { columnId, next };
|
|
478
|
+
});
|
|
479
|
+
const canonicalIds = autoIncrement.map((entry) => entry.columnId);
|
|
480
|
+
if (canonicalIds.some((id, index) => index > 0 && id <= (canonicalIds[index - 1] ?? ""))) {
|
|
481
|
+
throw new TypeError("Snapshot auto-increment counters are not canonical");
|
|
482
|
+
}
|
|
483
|
+
return { kind: "table", record: table, nextRowId, autoIncrement };
|
|
492
484
|
}
|
|
493
485
|
function prepareSegmentItem(value) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
486
|
+
const item = exactRecord(value, ["kind", "record"], "Snapshot segment item");
|
|
487
|
+
if (item.kind !== "segment")
|
|
488
|
+
throw new TypeError("Snapshot segment item kind is invalid");
|
|
489
|
+
return { kind: "segment", record: normalizeSegmentRecord(item.record) };
|
|
498
490
|
}
|
|
499
491
|
function prepareTransactionItem(value) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
(transaction.snapshotVersion !== null &&
|
|
527
|
-
(!Number.isSafeInteger(transaction.snapshotVersion) ||
|
|
528
|
-
transaction.snapshotVersion < 0)) ||
|
|
529
|
-
!Number.isSafeInteger(transaction.revision) ||
|
|
530
|
-
transaction.revision < 0 ||
|
|
531
|
-
!Array.isArray(transaction.pendingBlockIds) ||
|
|
532
|
-
transaction.pendingBlockIds.length !== 0 ||
|
|
533
|
-
!Array.isArray(transaction.pendingSegmentIds) ||
|
|
534
|
-
transaction.pendingSegmentIds.length !== 0) {
|
|
535
|
-
throw new TypeError("Snapshot transaction must be a canonical committed record");
|
|
536
|
-
}
|
|
537
|
-
return { kind: "transaction", record: transaction };
|
|
492
|
+
const item = exactRecord(value, ["kind", "record"], "Snapshot transaction item");
|
|
493
|
+
if (item.kind !== "transaction") {
|
|
494
|
+
throw new TypeError("Snapshot transaction item kind is invalid");
|
|
495
|
+
}
|
|
496
|
+
const transaction = exactRecord(item.record, [
|
|
497
|
+
"id",
|
|
498
|
+
"ownerId",
|
|
499
|
+
"expiresAt",
|
|
500
|
+
"snapshotVersion",
|
|
501
|
+
"pendingBlockIds",
|
|
502
|
+
"pendingSegmentIds",
|
|
503
|
+
"status",
|
|
504
|
+
"revision",
|
|
505
|
+
"startedAt",
|
|
506
|
+
"updatedAt",
|
|
507
|
+
"committedVersion"
|
|
508
|
+
], "Snapshot transaction record");
|
|
509
|
+
validateStorageId(transaction.id, "Snapshot transaction ID");
|
|
510
|
+
validateStorageId(transaction.ownerId, "Snapshot transaction owner ID");
|
|
511
|
+
canonicalTimestamp(transaction.expiresAt, "Snapshot transaction expiry");
|
|
512
|
+
canonicalTimestamp(transaction.startedAt, "Snapshot transaction start time");
|
|
513
|
+
canonicalTimestamp(transaction.updatedAt, "Snapshot transaction update time");
|
|
514
|
+
if (transaction.status !== "committed" || transaction.committedVersion === null || !Number.isSafeInteger(transaction.committedVersion) || transaction.committedVersion < 0 || transaction.snapshotVersion !== null && (!Number.isSafeInteger(transaction.snapshotVersion) || transaction.snapshotVersion < 0) || !Number.isSafeInteger(transaction.revision) || transaction.revision < 0 || !Array.isArray(transaction.pendingBlockIds) || transaction.pendingBlockIds.length !== 0 || !Array.isArray(transaction.pendingSegmentIds) || transaction.pendingSegmentIds.length !== 0) {
|
|
515
|
+
throw new TypeError("Snapshot transaction must be a canonical committed record");
|
|
516
|
+
}
|
|
517
|
+
return { kind: "transaction", record: transaction };
|
|
538
518
|
}
|
|
539
519
|
function prepareUniqueItem(value) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
? null
|
|
545
|
-
: validateStorageId(record.indexId, "Snapshot UNIQUE index ID");
|
|
546
|
-
return {
|
|
547
|
-
kind: "unique-generation",
|
|
548
|
-
tableId: validateStorageId(record.tableId, "Snapshot UNIQUE table ID"),
|
|
549
|
-
indexId,
|
|
550
|
-
namespaceId: validateStorageId(record.namespaceId, "Snapshot UNIQUE namespace ID"),
|
|
551
|
-
generationId: validateStorageId(record.generationId, "Snapshot UNIQUE generation ID"),
|
|
552
|
-
chunkCount: canonicalOrdinal(record.chunkCount, "Snapshot UNIQUE chunk count"),
|
|
553
|
-
tokenCount: canonicalOrdinal(record.tokenCount, "Snapshot UNIQUE token count"),
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
sameKeys(record, ["kind", "namespaceId", "generationId", "ordinal", "keyTokens"], "Snapshot UNIQUE chunk");
|
|
557
|
-
if (record.kind !== "unique-chunk" || !Array.isArray(record.keyTokens)) {
|
|
558
|
-
throw new TypeError("Snapshot UNIQUE chunk is invalid");
|
|
559
|
-
}
|
|
560
|
-
if (record.keyTokens.length < 1 ||
|
|
561
|
-
record.keyTokens.length > MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK) {
|
|
562
|
-
throw new RangeError("Snapshot UNIQUE chunk token count is out of bounds");
|
|
563
|
-
}
|
|
564
|
-
const keyTokens = record.keyTokens.map((token) => {
|
|
565
|
-
if (typeof token !== "string")
|
|
566
|
-
throw new TypeError("Snapshot UNIQUE token must be a string");
|
|
567
|
-
assertWellFormedString(token, "Snapshot UNIQUE token");
|
|
568
|
-
return token;
|
|
569
|
-
});
|
|
570
|
-
if (uniqueKeyBuildChunkRetainedBytes(keyTokens) > MAX_UNIQUE_KEY_BUILD_CHUNK_BYTES) {
|
|
571
|
-
throw new RangeError("Snapshot UNIQUE chunk is too large");
|
|
572
|
-
}
|
|
573
|
-
if (keyTokens.some((token, index) => index > 0 && token <= (keyTokens[index - 1] ?? ""))) {
|
|
574
|
-
throw new TypeError("Snapshot UNIQUE tokens are not strict lexical order");
|
|
575
|
-
}
|
|
520
|
+
const record = recordValue(value, "Snapshot UNIQUE item");
|
|
521
|
+
if (record.kind === "unique-generation") {
|
|
522
|
+
sameKeys(record, ["kind", "tableId", "indexId", "namespaceId", "generationId", "chunkCount", "tokenCount"], "Snapshot UNIQUE generation");
|
|
523
|
+
const indexId = record.indexId === null ? null : validateStorageId(record.indexId, "Snapshot UNIQUE index ID");
|
|
576
524
|
return {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
525
|
+
kind: "unique-generation",
|
|
526
|
+
tableId: validateStorageId(record.tableId, "Snapshot UNIQUE table ID"),
|
|
527
|
+
indexId,
|
|
528
|
+
namespaceId: validateStorageId(record.namespaceId, "Snapshot UNIQUE namespace ID"),
|
|
529
|
+
generationId: validateStorageId(record.generationId, "Snapshot UNIQUE generation ID"),
|
|
530
|
+
chunkCount: canonicalOrdinal(record.chunkCount, "Snapshot UNIQUE chunk count"),
|
|
531
|
+
tokenCount: canonicalOrdinal(record.tokenCount, "Snapshot UNIQUE token count")
|
|
582
532
|
};
|
|
533
|
+
}
|
|
534
|
+
sameKeys(record, ["kind", "namespaceId", "generationId", "ordinal", "keyTokens"], "Snapshot UNIQUE chunk");
|
|
535
|
+
if (record.kind !== "unique-chunk" || !Array.isArray(record.keyTokens)) {
|
|
536
|
+
throw new TypeError("Snapshot UNIQUE chunk is invalid");
|
|
537
|
+
}
|
|
538
|
+
if (record.keyTokens.length < 1 || record.keyTokens.length > MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK) {
|
|
539
|
+
throw new RangeError("Snapshot UNIQUE chunk token count is out of bounds");
|
|
540
|
+
}
|
|
541
|
+
const keyTokens = record.keyTokens.map((token) => {
|
|
542
|
+
if (typeof token !== "string")
|
|
543
|
+
throw new TypeError("Snapshot UNIQUE token must be a string");
|
|
544
|
+
assertWellFormedString(token, "Snapshot UNIQUE token");
|
|
545
|
+
return token;
|
|
546
|
+
});
|
|
547
|
+
if (uniqueKeyBuildChunkRetainedBytes(keyTokens) > MAX_UNIQUE_KEY_BUILD_CHUNK_BYTES) {
|
|
548
|
+
throw new RangeError("Snapshot UNIQUE chunk is too large");
|
|
549
|
+
}
|
|
550
|
+
if (keyTokens.some((token, index) => index > 0 && token <= (keyTokens[index - 1] ?? ""))) {
|
|
551
|
+
throw new TypeError("Snapshot UNIQUE tokens are not strict lexical order");
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
kind: "unique-chunk",
|
|
555
|
+
namespaceId: validateStorageId(record.namespaceId, "Snapshot UNIQUE namespace ID"),
|
|
556
|
+
generationId: validateStorageId(record.generationId, "Snapshot UNIQUE generation ID"),
|
|
557
|
+
ordinal: canonicalOrdinal(record.ordinal, "Snapshot UNIQUE chunk ordinal"),
|
|
558
|
+
keyTokens
|
|
559
|
+
};
|
|
583
560
|
}
|
|
584
561
|
function preparePostingItem(value) {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
return {
|
|
607
|
-
kind: "posting-generation",
|
|
608
|
-
tableId: validateStorageId(record.tableId, "Snapshot posting table ID"),
|
|
609
|
-
ownerKind: record.ownerKind,
|
|
610
|
-
ownerId: validateStorageId(record.ownerId, "Snapshot posting owner ID"),
|
|
611
|
-
storageColumnId: validateStorageId(record.storageColumnId, "Snapshot posting storage column ID"),
|
|
612
|
-
generationId: validateStorageId(record.generationId, "Snapshot posting generation ID"),
|
|
613
|
-
coversVersion: canonicalOrdinal(record.coversVersion, "Snapshot posting covered version"),
|
|
614
|
-
chunkCount,
|
|
615
|
-
totalTokens,
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
sameKeys(record, ["kind", "storageColumnId", "generationId", "ordinal", "postings"], "Snapshot posting chunk");
|
|
619
|
-
if (record.kind !== "posting-chunk" || !Array.isArray(record.postings)) {
|
|
620
|
-
throw new TypeError("Snapshot posting chunk is invalid");
|
|
621
|
-
}
|
|
622
|
-
if (record.postings.length < 1 || record.postings.length > MAX_FTS_POSTINGS_PER_CHUNK) {
|
|
623
|
-
throw new RangeError("Snapshot posting chunk count is out of bounds");
|
|
624
|
-
}
|
|
625
|
-
let rowIds = 0;
|
|
626
|
-
let previousTerm = "";
|
|
627
|
-
const postings = record.postings.map((raw, postingIndex) => {
|
|
628
|
-
const posting = exactRecord(raw, ["term", "rowIds", "tf"], "Snapshot posting");
|
|
629
|
-
if (typeof posting.term !== "string" ||
|
|
630
|
-
posting.term.length > MAX_FTS_POSTING_TERM_CHARACTERS ||
|
|
631
|
-
!Array.isArray(posting.rowIds) ||
|
|
632
|
-
!Array.isArray(posting.tf) ||
|
|
633
|
-
posting.rowIds.length === 0 ||
|
|
634
|
-
posting.rowIds.length !== posting.tf.length ||
|
|
635
|
-
(postingIndex > 0 && posting.term <= previousTerm)) {
|
|
636
|
-
throw new TypeError("Snapshot posting is not canonical");
|
|
637
|
-
}
|
|
638
|
-
assertWellFormedString(posting.term, "Snapshot posting term");
|
|
639
|
-
previousTerm = posting.term;
|
|
640
|
-
rowIds += posting.rowIds.length;
|
|
641
|
-
if (rowIds > MAX_FTS_POSTING_ROW_IDS_PER_CHUNK) {
|
|
642
|
-
throw new RangeError("Snapshot posting chunk has too many row IDs");
|
|
643
|
-
}
|
|
644
|
-
let previousRowId = 0n;
|
|
645
|
-
const normalizedRowIds = posting.rowIds.map((rowId, index) => {
|
|
646
|
-
if (typeof rowId !== "bigint" ||
|
|
647
|
-
rowId < 1n ||
|
|
648
|
-
rowId >= MAX_ROW_ID_EXCLUSIVE_END ||
|
|
649
|
-
(index > 0 && rowId <= previousRowId)) {
|
|
650
|
-
throw new TypeError("Snapshot posting row IDs are not canonical");
|
|
651
|
-
}
|
|
652
|
-
previousRowId = rowId;
|
|
653
|
-
return rowId;
|
|
654
|
-
});
|
|
655
|
-
const tf = posting.tf.map((count) => {
|
|
656
|
-
if (!Number.isSafeInteger(count) ||
|
|
657
|
-
count < 1 ||
|
|
658
|
-
count > MAX_FTS_TOKENS_PER_DOCUMENT) {
|
|
659
|
-
throw new TypeError("Snapshot posting term frequency is invalid");
|
|
660
|
-
}
|
|
661
|
-
return count;
|
|
662
|
-
});
|
|
663
|
-
return { term: posting.term, rowIds: normalizedRowIds, tf };
|
|
664
|
-
});
|
|
562
|
+
const record = recordValue(value, "Snapshot posting item");
|
|
563
|
+
if (record.kind === "posting-generation") {
|
|
564
|
+
sameKeys(record, [
|
|
565
|
+
"kind",
|
|
566
|
+
"tableId",
|
|
567
|
+
"ownerKind",
|
|
568
|
+
"ownerId",
|
|
569
|
+
"storageColumnId",
|
|
570
|
+
"generationId",
|
|
571
|
+
"coversVersion",
|
|
572
|
+
"chunkCount",
|
|
573
|
+
"totalTokens"
|
|
574
|
+
], "Snapshot posting generation");
|
|
575
|
+
if (record.ownerKind !== "fts-column" && record.ownerKind !== "secondary-index") {
|
|
576
|
+
throw new TypeError("Snapshot posting owner kind is invalid");
|
|
577
|
+
}
|
|
578
|
+
const totalTokens = canonicalOrdinal(record.totalTokens, "Snapshot posting token total");
|
|
579
|
+
const chunkCount = canonicalOrdinal(record.chunkCount, "Snapshot posting chunk count");
|
|
580
|
+
if (chunkCount > MAX_FTS_BASE_CHUNKS) {
|
|
581
|
+
throw new RangeError("Snapshot posting generation has too many chunks");
|
|
582
|
+
}
|
|
665
583
|
return {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
584
|
+
kind: "posting-generation",
|
|
585
|
+
tableId: validateStorageId(record.tableId, "Snapshot posting table ID"),
|
|
586
|
+
ownerKind: record.ownerKind,
|
|
587
|
+
ownerId: validateStorageId(record.ownerId, "Snapshot posting owner ID"),
|
|
588
|
+
storageColumnId: validateStorageId(record.storageColumnId, "Snapshot posting storage column ID"),
|
|
589
|
+
generationId: validateStorageId(record.generationId, "Snapshot posting generation ID"),
|
|
590
|
+
coversVersion: canonicalOrdinal(record.coversVersion, "Snapshot posting covered version"),
|
|
591
|
+
chunkCount,
|
|
592
|
+
totalTokens
|
|
671
593
|
};
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
594
|
+
}
|
|
595
|
+
sameKeys(record, ["kind", "storageColumnId", "generationId", "ordinal", "postings"], "Snapshot posting chunk");
|
|
596
|
+
if (record.kind !== "posting-chunk" || !Array.isArray(record.postings)) {
|
|
597
|
+
throw new TypeError("Snapshot posting chunk is invalid");
|
|
598
|
+
}
|
|
599
|
+
if (record.postings.length < 1 || record.postings.length > MAX_FTS_POSTINGS_PER_CHUNK) {
|
|
600
|
+
throw new RangeError("Snapshot posting chunk count is out of bounds");
|
|
601
|
+
}
|
|
602
|
+
let rowIds = 0;
|
|
603
|
+
let previousTerm = "";
|
|
604
|
+
const postings = record.postings.map((raw, postingIndex) => {
|
|
605
|
+
const posting = exactRecord(raw, ["term", "rowIds", "tf"], "Snapshot posting");
|
|
606
|
+
if (typeof posting.term !== "string" || posting.term.length > MAX_FTS_POSTING_TERM_CHARACTERS || !Array.isArray(posting.rowIds) || !Array.isArray(posting.tf) || posting.rowIds.length === 0 || posting.rowIds.length !== posting.tf.length || postingIndex > 0 && posting.term <= previousTerm) {
|
|
607
|
+
throw new TypeError("Snapshot posting is not canonical");
|
|
608
|
+
}
|
|
609
|
+
assertWellFormedString(posting.term, "Snapshot posting term");
|
|
610
|
+
previousTerm = posting.term;
|
|
611
|
+
rowIds += posting.rowIds.length;
|
|
612
|
+
if (rowIds > MAX_FTS_POSTING_ROW_IDS_PER_CHUNK) {
|
|
613
|
+
throw new RangeError("Snapshot posting chunk has too many row IDs");
|
|
614
|
+
}
|
|
615
|
+
let previousRowId = 0n;
|
|
616
|
+
const normalizedRowIds = posting.rowIds.map((rowId, index) => {
|
|
617
|
+
if (typeof rowId !== "bigint" || rowId < 1n || rowId >= MAX_ROW_ID_EXCLUSIVE_END || index > 0 && rowId <= previousRowId) {
|
|
618
|
+
throw new TypeError("Snapshot posting row IDs are not canonical");
|
|
619
|
+
}
|
|
620
|
+
previousRowId = rowId;
|
|
621
|
+
return rowId;
|
|
622
|
+
});
|
|
623
|
+
const tf = posting.tf.map((count) => {
|
|
624
|
+
if (!Number.isSafeInteger(count) || count < 1 || count > MAX_FTS_TOKENS_PER_DOCUMENT) {
|
|
625
|
+
throw new TypeError("Snapshot posting term frequency is invalid");
|
|
626
|
+
}
|
|
627
|
+
return count;
|
|
685
628
|
});
|
|
629
|
+
return { term: posting.term, rowIds: normalizedRowIds, tf };
|
|
630
|
+
});
|
|
631
|
+
return {
|
|
632
|
+
kind: "posting-chunk",
|
|
633
|
+
storageColumnId: validateStorageId(record.storageColumnId, "Snapshot posting storage column ID"),
|
|
634
|
+
generationId: validateStorageId(record.generationId, "Snapshot posting generation ID"),
|
|
635
|
+
ordinal: canonicalOrdinal(record.ordinal, "Snapshot posting chunk ordinal"),
|
|
636
|
+
postings
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function decodeSnapshotMetadataItems(kind, payload) {
|
|
640
|
+
return decodeSnapshotMetadataPage(payload).map((item) => {
|
|
641
|
+
if (kind === "catalog-page")
|
|
642
|
+
return prepareCatalogItem(item);
|
|
643
|
+
if (kind === "segment-page")
|
|
644
|
+
return prepareSegmentItem(item);
|
|
645
|
+
if (kind === "transaction-page")
|
|
646
|
+
return prepareTransactionItem(item);
|
|
647
|
+
if (kind === "unique-page")
|
|
648
|
+
return prepareUniqueItem(item);
|
|
649
|
+
return preparePostingItem(item);
|
|
650
|
+
});
|
|
686
651
|
}
|
|
687
652
|
class ChunkReader {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
653
|
+
#chunk = new Uint8Array();
|
|
654
|
+
#offset = 0;
|
|
655
|
+
#iterator;
|
|
656
|
+
constructor(source) {
|
|
657
|
+
this.#iterator = source[Symbol.asyncIterator]();
|
|
658
|
+
}
|
|
659
|
+
async #nextChunk() {
|
|
660
|
+
const next = await this.#iterator.next();
|
|
661
|
+
if (next.done)
|
|
662
|
+
return false;
|
|
663
|
+
if (!(next.value instanceof Uint8Array) || next.value.byteLength === 0) {
|
|
664
|
+
throw new RangeError("Snapshot source chunks must be non-empty Uint8Array values");
|
|
665
|
+
}
|
|
666
|
+
this.#chunk = next.value;
|
|
667
|
+
this.#offset = 0;
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
async read(length) {
|
|
671
|
+
const result = new Uint8Array(length);
|
|
672
|
+
let written = 0;
|
|
673
|
+
while (written < length) {
|
|
674
|
+
if (this.#offset === this.#chunk.byteLength) {
|
|
675
|
+
if (!await this.#nextChunk())
|
|
676
|
+
throw new Error("Snapshot stream is truncated");
|
|
677
|
+
}
|
|
678
|
+
const count = Math.min(length - written, this.#chunk.byteLength - this.#offset);
|
|
679
|
+
result.set(this.#chunk.subarray(this.#offset, this.#offset + count), written);
|
|
680
|
+
this.#offset += count;
|
|
681
|
+
written += count;
|
|
682
|
+
}
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
685
|
+
async done() {
|
|
686
|
+
if (this.#offset < this.#chunk.byteLength)
|
|
687
|
+
return false;
|
|
688
|
+
return !await this.#nextChunk();
|
|
689
|
+
}
|
|
690
|
+
async close() {
|
|
691
|
+
await this.#iterator.return?.();
|
|
692
|
+
}
|
|
728
693
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
694
|
+
async function* decodeSnapshotFrameStream(source) {
|
|
695
|
+
const reader = new ChunkReader(source);
|
|
696
|
+
let failure;
|
|
697
|
+
try {
|
|
698
|
+
const prefix = await reader.read(HEADER_PREFIX_BYTES);
|
|
699
|
+
if (!MAGIC.every((byte, index) => prefix[index] === byte))
|
|
700
|
+
throw new Error("Not a Minnow snapshot");
|
|
701
|
+
const prefixView = new DataView(prefix.buffer);
|
|
702
|
+
if (prefixView.getUint32(8, true) !== 1)
|
|
703
|
+
throw new Error("Unsupported snapshot format version");
|
|
704
|
+
const headerLength = prefixView.getUint32(12, true);
|
|
705
|
+
if (headerLength < 1 || headerLength > MAX_HEADER_BYTES)
|
|
706
|
+
throw new RangeError("Snapshot header is too large");
|
|
707
|
+
const headerBytes = await reader.read(headerLength);
|
|
708
|
+
if (crc32(headerBytes) !== prefixView.getUint32(16, true))
|
|
709
|
+
throw new Error("Snapshot header checksum mismatch");
|
|
710
|
+
let parsed;
|
|
733
711
|
try {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
if (footerView.getUint32(0, true) !== FOOTER_MARKER)
|
|
847
|
-
throw new Error("Snapshot footer marker mismatch");
|
|
848
|
-
const footerFrameCount = Number(footerView.getBigUint64(4, true));
|
|
849
|
-
const footerItemCount = Number(footerView.getBigUint64(12, true));
|
|
850
|
-
const footerStoredBytes = Number(footerView.getBigUint64(20, true));
|
|
851
|
-
if (!Number.isSafeInteger(footerFrameCount) ||
|
|
852
|
-
!Number.isSafeInteger(footerItemCount) ||
|
|
853
|
-
!Number.isSafeInteger(footerStoredBytes)) {
|
|
854
|
-
throw new RangeError("Snapshot footer totals are too large");
|
|
855
|
-
}
|
|
856
|
-
const footer = {
|
|
857
|
-
frameCount: footerFrameCount,
|
|
858
|
-
itemCount: footerItemCount,
|
|
859
|
-
storedBytes: footerStoredBytes,
|
|
860
|
-
checksum: footerView.getUint32(28, true),
|
|
861
|
-
};
|
|
862
|
-
if (footerBytes.subarray(32).some((byte) => byte !== 0))
|
|
863
|
-
throw new TypeError("Snapshot footer reserved bits are set");
|
|
864
|
-
if (footer.frameCount !== sequence ||
|
|
865
|
-
footer.itemCount !== itemCount ||
|
|
866
|
-
footer.storedBytes !== storedBytes ||
|
|
867
|
-
footer.checksum !== checksum) {
|
|
868
|
-
throw new Error("Snapshot footer does not match its body");
|
|
869
|
-
}
|
|
870
|
-
for (const kind of SNAPSHOT_FRAME_KINDS) {
|
|
871
|
-
const actual = observed[kind];
|
|
872
|
-
const expected = header.kinds[kind];
|
|
873
|
-
if (actual.frameCount !== expected.frameCount ||
|
|
874
|
-
actual.itemCount !== expected.itemCount ||
|
|
875
|
-
actual.storedBytes !== expected.storedBytes) {
|
|
876
|
-
throw new Error(`Snapshot ${kind} summary does not match its frames`);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
if (!(await reader.done()))
|
|
880
|
-
throw new Error("Snapshot has trailing bytes");
|
|
881
|
-
yield { type: "footer", footer };
|
|
882
|
-
}
|
|
883
|
-
catch (error) {
|
|
884
|
-
failure = error;
|
|
885
|
-
throw error;
|
|
712
|
+
parsed = JSON.parse(textDecoder.decode(headerBytes));
|
|
713
|
+
} catch {
|
|
714
|
+
throw new TypeError("Snapshot header is not canonical JSON");
|
|
715
|
+
}
|
|
716
|
+
const header = prepareSnapshotFrameStreamHeader(parsed);
|
|
717
|
+
if (canonicalHeaderJson(header) !== textDecoder.decode(headerBytes)) {
|
|
718
|
+
throw new TypeError("Snapshot header is not canonically encoded");
|
|
719
|
+
}
|
|
720
|
+
yield { type: "header", header };
|
|
721
|
+
const expectedFrames = SNAPSHOT_FRAME_KINDS.reduce((total, kind) => {
|
|
722
|
+
const next = total + header.kinds[kind].frameCount;
|
|
723
|
+
if (!Number.isSafeInteger(next))
|
|
724
|
+
throw new RangeError("Snapshot frame count is too large");
|
|
725
|
+
return next;
|
|
726
|
+
}, 0);
|
|
727
|
+
const observed = Object.fromEntries(SNAPSHOT_FRAME_KINDS.map((kind) => [kind, { frameCount: 0, itemCount: 0, storedBytes: 0 }]));
|
|
728
|
+
let sequence = 0;
|
|
729
|
+
let itemCount = 0;
|
|
730
|
+
let storedBytes = 0;
|
|
731
|
+
let checksum = 0;
|
|
732
|
+
let previousKind = 0;
|
|
733
|
+
while (sequence < expectedFrames) {
|
|
734
|
+
const framePrefix = await reader.read(FRAME_PREFIX_BYTES);
|
|
735
|
+
const view = new DataView(framePrefix.buffer);
|
|
736
|
+
if (view.getUint32(0, true) !== FRAME_MARKER)
|
|
737
|
+
throw new Error("Snapshot frame marker mismatch");
|
|
738
|
+
const kindIndex = view.getUint32(4, true);
|
|
739
|
+
const kind = SNAPSHOT_FRAME_KINDS[kindIndex];
|
|
740
|
+
if (kind === void 0 || kindIndex < previousKind)
|
|
741
|
+
throw new Error("Snapshot frame order is invalid");
|
|
742
|
+
previousKind = kindIndex;
|
|
743
|
+
const actualSequence = Number(view.getBigUint64(8, true));
|
|
744
|
+
if (!Number.isSafeInteger(actualSequence) || actualSequence !== sequence)
|
|
745
|
+
throw new Error("Snapshot frame sequence is invalid");
|
|
746
|
+
const frameItems = view.getUint32(16, true);
|
|
747
|
+
if (frameItems < 1 || frameItems > MAX_SNAPSHOT_FRAME_ITEMS) {
|
|
748
|
+
throw new RangeError("Snapshot frame item count is out of bounds");
|
|
749
|
+
}
|
|
750
|
+
const keyLength = view.getUint32(20, true);
|
|
751
|
+
if (keyLength > MAX_STORAGE_ID_CHARACTERS * 3 || (kind === "block" ? keyLength === 0 || frameItems !== 1 : keyLength !== 0)) {
|
|
752
|
+
throw new RangeError("Snapshot frame key is too large");
|
|
753
|
+
}
|
|
754
|
+
const payloadLength = Number(view.getBigUint64(24, true));
|
|
755
|
+
if (!Number.isSafeInteger(payloadLength))
|
|
756
|
+
throw new RangeError("Snapshot frame is too large");
|
|
757
|
+
const payloadLimit = kind === "block" ? MAX_STORED_BLOCK_BYTE_LENGTH : MAX_SNAPSHOT_METADATA_FRAME_BYTES;
|
|
758
|
+
if (payloadLength < 1 || payloadLength > payloadLimit) {
|
|
759
|
+
throw new RangeError("Snapshot frame payload length is out of bounds");
|
|
760
|
+
}
|
|
761
|
+
const expectedChecksum = view.getUint32(32, true);
|
|
762
|
+
if (view.getUint32(36, true) !== 0)
|
|
763
|
+
throw new TypeError("Snapshot frame reserved bits are set");
|
|
764
|
+
const keyBytes = await reader.read(keyLength);
|
|
765
|
+
const key = keyLength === 0 ? null : validateStorageId(stringFromUtf8(keyBytes));
|
|
766
|
+
const payload = await reader.read(payloadLength);
|
|
767
|
+
const frame = {
|
|
768
|
+
sequence,
|
|
769
|
+
kind,
|
|
770
|
+
itemCount: frameItems,
|
|
771
|
+
key,
|
|
772
|
+
payload,
|
|
773
|
+
checksum: expectedChecksum
|
|
774
|
+
};
|
|
775
|
+
snapshotFrameEnvelopeParts(frame);
|
|
776
|
+
if (crc32(payload) !== expectedChecksum)
|
|
777
|
+
throw new Error("Snapshot frame checksum mismatch");
|
|
778
|
+
if (kind !== "block") {
|
|
779
|
+
if (payload.byteLength < 5 || payload[0] !== 6) {
|
|
780
|
+
throw new TypeError("Snapshot metadata frame must contain one canonical page array");
|
|
781
|
+
}
|
|
782
|
+
const encodedItems = new DataView(payload.buffer, payload.byteOffset + 1, 4).getUint32(0, true);
|
|
783
|
+
if (encodedItems !== frameItems) {
|
|
784
|
+
throw new Error("Snapshot metadata frame item count does not match its payload");
|
|
785
|
+
}
|
|
786
|
+
if (decodeSnapshotMetadataItems(kind, payload).length !== frameItems) {
|
|
787
|
+
throw new Error("Snapshot metadata frame item count is invalid");
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
checksum = extendSnapshotFrameStreamChecksum(checksum, [framePrefix, keyBytes, payload]);
|
|
791
|
+
itemCount = safeWhole(itemCount + frameItems, "Snapshot observed item count");
|
|
792
|
+
storedBytes = safeWhole(storedBytes + payloadLength, "Snapshot observed stored bytes");
|
|
793
|
+
const kindObserved = observed[kind];
|
|
794
|
+
kindObserved.frameCount = safeWhole(kindObserved.frameCount + 1, `Snapshot ${kind} observed frame count`);
|
|
795
|
+
kindObserved.itemCount = safeWhole(kindObserved.itemCount + frameItems, `Snapshot ${kind} observed item count`);
|
|
796
|
+
kindObserved.storedBytes = safeWhole(kindObserved.storedBytes + payloadLength, `Snapshot ${kind} observed stored bytes`);
|
|
797
|
+
const kindExpected = header.kinds[kind];
|
|
798
|
+
if (kindObserved.frameCount > kindExpected.frameCount || kindObserved.itemCount > kindExpected.itemCount || kindObserved.storedBytes > kindExpected.storedBytes) {
|
|
799
|
+
throw new Error(`Snapshot ${kind} frames exceed the declared summary`);
|
|
800
|
+
}
|
|
801
|
+
yield { type: "frame", frame };
|
|
802
|
+
sequence = safeWhole(sequence + 1, "Snapshot frame sequence");
|
|
803
|
+
}
|
|
804
|
+
const footerBytes = await reader.read(FOOTER_BYTES);
|
|
805
|
+
const footerView = new DataView(footerBytes.buffer);
|
|
806
|
+
if (footerView.getUint32(0, true) !== FOOTER_MARKER)
|
|
807
|
+
throw new Error("Snapshot footer marker mismatch");
|
|
808
|
+
const footerFrameCount = Number(footerView.getBigUint64(4, true));
|
|
809
|
+
const footerItemCount = Number(footerView.getBigUint64(12, true));
|
|
810
|
+
const footerStoredBytes = Number(footerView.getBigUint64(20, true));
|
|
811
|
+
if (!Number.isSafeInteger(footerFrameCount) || !Number.isSafeInteger(footerItemCount) || !Number.isSafeInteger(footerStoredBytes)) {
|
|
812
|
+
throw new RangeError("Snapshot footer totals are too large");
|
|
813
|
+
}
|
|
814
|
+
const footer = {
|
|
815
|
+
frameCount: footerFrameCount,
|
|
816
|
+
itemCount: footerItemCount,
|
|
817
|
+
storedBytes: footerStoredBytes,
|
|
818
|
+
checksum: footerView.getUint32(28, true)
|
|
819
|
+
};
|
|
820
|
+
if (footerBytes.subarray(32).some((byte) => byte !== 0))
|
|
821
|
+
throw new TypeError("Snapshot footer reserved bits are set");
|
|
822
|
+
if (footer.frameCount !== sequence || footer.itemCount !== itemCount || footer.storedBytes !== storedBytes || footer.checksum !== checksum) {
|
|
823
|
+
throw new Error("Snapshot footer does not match its body");
|
|
886
824
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
825
|
+
for (const kind of SNAPSHOT_FRAME_KINDS) {
|
|
826
|
+
const actual = observed[kind];
|
|
827
|
+
const expected = header.kinds[kind];
|
|
828
|
+
if (actual.frameCount !== expected.frameCount || actual.itemCount !== expected.itemCount || actual.storedBytes !== expected.storedBytes) {
|
|
829
|
+
throw new Error(`Snapshot ${kind} summary does not match its frames`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (!await reader.done())
|
|
833
|
+
throw new Error("Snapshot has trailing bytes");
|
|
834
|
+
yield { type: "footer", footer };
|
|
835
|
+
} catch (error) {
|
|
836
|
+
failure = error;
|
|
837
|
+
throw error;
|
|
838
|
+
} finally {
|
|
839
|
+
try {
|
|
840
|
+
await reader.close();
|
|
841
|
+
} catch (closeError) {
|
|
842
|
+
if (failure === void 0) {
|
|
843
|
+
await Promise.reject(closeError instanceof Error ? closeError : new Error(String(closeError)));
|
|
844
|
+
}
|
|
896
845
|
}
|
|
846
|
+
}
|
|
897
847
|
}
|
|
848
|
+
export {
|
|
849
|
+
decodeSnapshotFrameStream,
|
|
850
|
+
decodeSnapshotMetadataItems,
|
|
851
|
+
decodeSnapshotMetadataPage,
|
|
852
|
+
encodeSnapshotFrameStreamFooter,
|
|
853
|
+
encodeSnapshotFrameStreamHeader,
|
|
854
|
+
encodeSnapshotMetadataPage,
|
|
855
|
+
extendSnapshotFrameStreamChecksum,
|
|
856
|
+
prepareSnapshotFrameStreamHeader,
|
|
857
|
+
snapshotFrameEnvelopeParts,
|
|
858
|
+
snapshotFrameStreamHeaderIdentity
|
|
859
|
+
};
|