@minnowdb/core 0.2.1 → 0.4.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/README.md +23 -32
- package/dist/block-format/block.d.ts +32 -13
- package/dist/block-format/block.js +179 -55
- package/dist/block-format/checksum.d.ts +2 -1
- package/dist/block-format/checksum.js +5 -2
- package/dist/block-format/codecs.d.ts +7 -2
- package/dist/block-format/codecs.js +53 -12
- package/dist/block-format/column.d.ts +3 -2
- package/dist/block-format/column.js +96 -30
- package/dist/block-format/index.d.ts +2 -2
- package/dist/block-format/index.js +2 -2
- package/dist/block-format/physical.d.ts +8 -2
- package/dist/block-format/physical.js +14 -6
- package/dist/block-format/types.d.ts +7 -3
- package/dist/block-format/types.js +0 -1
- package/dist/block-format/unicode.d.ts +11 -0
- package/dist/block-format/unicode.js +46 -0
- package/dist/date-value.d.ts +15 -0
- package/dist/date-value.js +64 -0
- package/dist/engine/artifact-cache.d.ts +2 -1
- package/dist/engine/artifact-cache.js +5 -1
- package/dist/engine/batch.d.ts +11 -3
- package/dist/engine/batch.js +17 -7
- package/dist/engine/buffered-writer.d.ts +2 -1
- package/dist/engine/buffered-writer.js +34 -9
- package/dist/engine/cache-limits.d.ts +25 -0
- package/dist/engine/cache-limits.js +25 -0
- package/dist/engine/catalog.d.ts +24 -5
- package/dist/engine/catalog.js +34 -7
- package/dist/engine/client.d.ts +29 -7
- package/dist/engine/client.js +216 -40
- package/dist/engine/database.d.ts +224 -39
- package/dist/engine/database.js +7475 -1539
- package/dist/engine/defaults.d.ts +4 -8
- package/dist/engine/defaults.js +46 -22
- package/dist/engine/errors.d.ts +19 -1
- package/dist/engine/errors.js +32 -2
- package/dist/engine/fts.d.ts +0 -6
- package/dist/engine/fts.js +41 -14
- package/dist/engine/group-index.d.ts +0 -1
- package/dist/engine/group-index.js +6 -11
- package/dist/engine/index.d.ts +16 -10
- package/dist/engine/index.js +12 -9
- package/dist/engine/join-index.d.ts +0 -1
- package/dist/engine/join-index.js +2 -2
- package/dist/engine/keyed-live.d.ts +57 -0
- package/dist/engine/keyed-live.js +226 -0
- package/dist/engine/live-api.d.ts +4 -0
- package/dist/engine/live-api.js +4 -0
- package/dist/engine/live.d.ts +43 -26
- package/dist/engine/live.js +504 -126
- package/dist/engine/memory.d.ts +2 -1
- package/dist/engine/memory.js +3 -2
- package/dist/engine/optimizer.d.ts +0 -1
- package/dist/engine/optimizer.js +262 -27
- package/dist/engine/query-api.d.ts +3 -0
- package/dist/engine/query-api.js +3 -0
- package/dist/engine/query-cache.d.ts +1 -2
- package/dist/engine/query-cache.js +7 -5
- package/dist/engine/query.d.ts +134 -29
- package/dist/engine/query.js +1668 -372
- package/dist/engine/result-wire.d.ts +0 -1
- package/dist/engine/result-wire.js +2 -2
- package/dist/engine/schema-wire.d.ts +6 -3
- package/dist/engine/schema-wire.js +40 -34
- package/dist/engine/schema.d.ts +156 -80
- package/dist/engine/schema.js +570 -108
- package/dist/engine/sort-keys.d.ts +4 -4
- package/dist/engine/sort-keys.js +44 -29
- package/dist/engine/sql-domains.d.ts +31 -0
- package/dist/engine/sql-domains.js +585 -0
- package/dist/engine/sql-driver.d.ts +19 -0
- package/dist/engine/sql-driver.js +1 -0
- package/dist/engine/sql-json.d.ts +7 -9
- package/dist/engine/sql-json.js +75 -15
- package/dist/engine/sql-semantics.d.ts +8 -3
- package/dist/engine/sql-semantics.js +458 -30
- package/dist/engine/typed-live.d.ts +67 -0
- package/dist/engine/typed-live.js +349 -0
- package/dist/engine/vector.d.ts +12 -2
- package/dist/engine/vector.js +635 -119
- package/dist/engine/worker-host.d.ts +9 -2
- package/dist/engine/worker-host.js +620 -152
- package/dist/engine/worker.d.ts +0 -1
- package/dist/engine/worker.js +3 -3
- package/dist/engine/write-block-planner.d.ts +18 -0
- package/dist/engine/write-block-planner.js +112 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/plan/index.d.ts +3 -4
- package/dist/plan/index.js +3 -4
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/storage/indexeddb.d.ts +99 -51
- package/dist/storage/indexeddb.js +13448 -2660
- package/dist/storage/memory.d.ts +64 -57
- package/dist/storage/memory.js +1042 -128
- package/dist/storage/opfs/files.d.ts +18 -7
- package/dist/storage/opfs/files.js +115 -28
- package/dist/storage/opfs/index.d.ts +1 -1
- package/dist/storage/opfs/index.js +1 -1
- package/dist/storage/opfs/leader.d.ts +409 -75
- package/dist/storage/opfs/leader.js +4621 -650
- package/dist/storage/opfs/rpc.d.ts +15 -3
- package/dist/storage/opfs/rpc.js +224 -2
- package/dist/storage/opfs/snapshot-ledger.d.ts +40 -0
- package/dist/storage/opfs/snapshot-ledger.js +281 -0
- package/dist/storage/opfs/store.d.ts +33 -99
- package/dist/storage/opfs/store.js +453 -364
- package/dist/storage/persistence.d.ts +37 -0
- package/dist/storage/persistence.js +78 -0
- package/dist/storage/snapshot-stream.d.ts +24 -0
- package/dist/storage/snapshot-stream.js +897 -0
- package/dist/storage/snapshot.d.ts +9 -85
- package/dist/storage/snapshot.js +33 -265
- package/dist/storage/toolkit/extents.d.ts +41 -7
- package/dist/storage/toolkit/extents.js +402 -39
- package/dist/storage/toolkit/index.d.ts +4 -5
- package/dist/storage/toolkit/index.js +28 -4
- package/dist/storage/toolkit/record-core.d.ts +182 -55
- package/dist/storage/toolkit/record-core.js +6158 -1331
- package/dist/storage/toolkit/sync-file.d.ts +10 -1
- package/dist/storage/toolkit/sync-file.js +50 -2
- package/dist/storage/toolkit/wal.d.ts +23 -5
- package/dist/storage/toolkit/wal.js +89 -27
- package/dist/storage/toolkit/wire.d.ts +15 -2
- package/dist/storage/toolkit/wire.js +199 -10
- package/dist/storage/types.d.ts +1439 -204
- package/dist/storage/types.js +1745 -141
- package/dist/testing/block-store-conformance.d.ts +0 -1
- package/dist/testing/block-store-conformance.js +1009 -119
- package/dist/testing/index.d.ts +46 -26
- package/dist/testing/index.js +112 -57
- package/dist/testing/opfs-shim.d.ts +13 -3
- package/dist/testing/opfs-shim.js +40 -6
- package/dist/testing/simulator.d.ts +97 -0
- package/dist/testing/simulator.js +591 -0
- package/dist/testing/sqllogictest.d.ts +89 -0
- package/dist/testing/sqllogictest.js +434 -0
- package/dist/transactions/index.d.ts +92 -16
- package/dist/transactions/index.js +1074 -236
- package/dist/worker-protocol/index.d.ts +3 -2
- package/dist/worker-protocol/index.js +2 -5
- package/package.json +53 -3
- package/postgres-feature-profile.json +223 -0
- package/sql-feature-matrix.json +172 -252
- package/dist/block-format/block.d.ts.map +0 -1
- package/dist/block-format/block.js.map +0 -1
- package/dist/block-format/checksum.d.ts.map +0 -1
- package/dist/block-format/checksum.js.map +0 -1
- package/dist/block-format/codecs.d.ts.map +0 -1
- package/dist/block-format/codecs.js.map +0 -1
- package/dist/block-format/column.d.ts.map +0 -1
- package/dist/block-format/column.js.map +0 -1
- package/dist/block-format/index.d.ts.map +0 -1
- package/dist/block-format/index.js.map +0 -1
- package/dist/block-format/physical.d.ts.map +0 -1
- package/dist/block-format/physical.js.map +0 -1
- package/dist/block-format/types.d.ts.map +0 -1
- package/dist/block-format/types.js.map +0 -1
- package/dist/engine/artifact-cache.d.ts.map +0 -1
- package/dist/engine/artifact-cache.js.map +0 -1
- package/dist/engine/batch.d.ts.map +0 -1
- package/dist/engine/batch.js.map +0 -1
- package/dist/engine/buffered-writer.d.ts.map +0 -1
- package/dist/engine/buffered-writer.js.map +0 -1
- package/dist/engine/catalog.d.ts.map +0 -1
- package/dist/engine/catalog.js.map +0 -1
- package/dist/engine/client.d.ts.map +0 -1
- package/dist/engine/client.js.map +0 -1
- package/dist/engine/coordinator.d.ts +0 -17
- package/dist/engine/coordinator.d.ts.map +0 -1
- package/dist/engine/coordinator.js +0 -60
- package/dist/engine/coordinator.js.map +0 -1
- package/dist/engine/database.d.ts.map +0 -1
- package/dist/engine/database.js.map +0 -1
- package/dist/engine/defaults.d.ts.map +0 -1
- package/dist/engine/defaults.js.map +0 -1
- package/dist/engine/errors.d.ts.map +0 -1
- package/dist/engine/errors.js.map +0 -1
- package/dist/engine/fts.d.ts.map +0 -1
- package/dist/engine/fts.js.map +0 -1
- package/dist/engine/group-index.d.ts.map +0 -1
- package/dist/engine/group-index.js.map +0 -1
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js.map +0 -1
- package/dist/engine/join-index.d.ts.map +0 -1
- package/dist/engine/join-index.js.map +0 -1
- package/dist/engine/live.d.ts.map +0 -1
- package/dist/engine/live.js.map +0 -1
- package/dist/engine/memory.d.ts.map +0 -1
- package/dist/engine/memory.js.map +0 -1
- package/dist/engine/optimizer.d.ts.map +0 -1
- package/dist/engine/optimizer.js.map +0 -1
- package/dist/engine/query-cache.d.ts.map +0 -1
- package/dist/engine/query-cache.js.map +0 -1
- package/dist/engine/query.d.ts.map +0 -1
- package/dist/engine/query.js.map +0 -1
- package/dist/engine/result-wire.d.ts.map +0 -1
- package/dist/engine/result-wire.js.map +0 -1
- package/dist/engine/schema-wire.d.ts.map +0 -1
- package/dist/engine/schema-wire.js.map +0 -1
- package/dist/engine/schema.d.ts.map +0 -1
- package/dist/engine/schema.js.map +0 -1
- package/dist/engine/sort-keys.d.ts.map +0 -1
- package/dist/engine/sort-keys.js.map +0 -1
- package/dist/engine/sql-json.d.ts.map +0 -1
- package/dist/engine/sql-json.js.map +0 -1
- package/dist/engine/sql-semantics.d.ts.map +0 -1
- package/dist/engine/sql-semantics.js.map +0 -1
- package/dist/engine/vector.d.ts.map +0 -1
- package/dist/engine/vector.js.map +0 -1
- package/dist/engine/worker-host.d.ts.map +0 -1
- package/dist/engine/worker-host.js.map +0 -1
- package/dist/engine/worker.d.ts.map +0 -1
- package/dist/engine/worker.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/plan/index.d.ts.map +0 -1
- package/dist/plan/index.js.map +0 -1
- package/dist/storage/fixture-shape.d.ts +0 -42
- package/dist/storage/fixture-shape.d.ts.map +0 -1
- package/dist/storage/fixture-shape.js +0 -146
- package/dist/storage/fixture-shape.js.map +0 -1
- package/dist/storage/index.d.ts.map +0 -1
- package/dist/storage/index.js.map +0 -1
- package/dist/storage/indexeddb.d.ts.map +0 -1
- package/dist/storage/indexeddb.js.map +0 -1
- package/dist/storage/memory.d.ts.map +0 -1
- package/dist/storage/memory.js.map +0 -1
- package/dist/storage/opfs/files.d.ts.map +0 -1
- package/dist/storage/opfs/files.js.map +0 -1
- package/dist/storage/opfs/index.d.ts.map +0 -1
- package/dist/storage/opfs/index.js.map +0 -1
- package/dist/storage/opfs/leader.d.ts.map +0 -1
- package/dist/storage/opfs/leader.js.map +0 -1
- package/dist/storage/opfs/rpc.d.ts.map +0 -1
- package/dist/storage/opfs/rpc.js.map +0 -1
- package/dist/storage/opfs/store.d.ts.map +0 -1
- package/dist/storage/opfs/store.js.map +0 -1
- package/dist/storage/snapshot.d.ts.map +0 -1
- package/dist/storage/snapshot.js.map +0 -1
- package/dist/storage/toolkit/extents.d.ts.map +0 -1
- package/dist/storage/toolkit/extents.js.map +0 -1
- package/dist/storage/toolkit/index.d.ts.map +0 -1
- package/dist/storage/toolkit/index.js.map +0 -1
- package/dist/storage/toolkit/record-core.d.ts.map +0 -1
- package/dist/storage/toolkit/record-core.js.map +0 -1
- package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
- package/dist/storage/toolkit/sync-file.js.map +0 -1
- package/dist/storage/toolkit/wal.d.ts.map +0 -1
- package/dist/storage/toolkit/wal.js.map +0 -1
- package/dist/storage/toolkit/wire.d.ts.map +0 -1
- package/dist/storage/toolkit/wire.js.map +0 -1
- package/dist/storage/types.d.ts.map +0 -1
- package/dist/storage/types.js.map +0 -1
- package/dist/testing/block-store-conformance.d.ts.map +0 -1
- package/dist/testing/block-store-conformance.js.map +0 -1
- package/dist/testing/index.d.ts.map +0 -1
- package/dist/testing/index.js.map +0 -1
- package/dist/testing/opfs-shim.d.ts.map +0 -1
- package/dist/testing/opfs-shim.js.map +0 -1
- package/dist/testing/seeds.d.ts +0 -11
- package/dist/testing/seeds.d.ts.map +0 -1
- package/dist/testing/seeds.js +0 -50
- package/dist/testing/seeds.js.map +0 -1
- package/dist/transactions/index.d.ts.map +0 -1
- package/dist/transactions/index.js.map +0 -1
- package/dist/worker-protocol/index.d.ts.map +0 -1
- package/dist/worker-protocol/index.js.map +0 -1
package/dist/storage/memory.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BlockReadBatchTooLargeError, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_POSTING_BUILD_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, SNAPSHOT_FRAME_KINDS, MAX_TEMP_BYTES_PER_OWNER, MAX_TEMP_BYTES_TOTAL, MAX_TEMP_PAGES_PER_OWNER, MAX_TEMP_PAGES_TOTAL, MAX_TEMP_RUNS_PER_OWNER, MAX_TEMP_RUNS_TOTAL, PostingBuildConflictError, StorageResourceLimitError, MAX_BLOCK_READ_BATCH_BYTES, SnapshotImportConflictError, StorageCorruptionError, activePostingStorageColumnIds, assertStorageBulkReadItems, validateStorageId, assertTempRunPageBatchLimits, } from "./types.js";
|
|
2
|
+
import { verifyStoredBlock } from "../block-format/index.js";
|
|
3
|
+
import { crc32 } from "../block-format/checksum.js";
|
|
4
|
+
import { decodeSnapshotMetadataItems, encodeSnapshotFrameStreamFooter, encodeSnapshotFrameStreamHeader, encodeSnapshotMetadataPage, extendSnapshotFrameStreamChecksum, prepareSnapshotFrameStreamHeader, snapshotFrameEnvelopeParts, snapshotFrameStreamHeaderIdentity, } from "./snapshot-stream.js";
|
|
5
|
+
import { RecordCore, validateFtsPostingChunks, validateId, validateTempRunPage, validateTempRunPageIdentity, } from "./toolkit/record-core.js";
|
|
2
6
|
/**
|
|
3
7
|
* The in-process store: record semantics live in `RecordCore` (shared with the OPFS store),
|
|
4
8
|
* block and temp-page bytes live in Maps here, and atomicity comes from running every mutating
|
|
@@ -7,53 +11,71 @@ import { RecordCore, validateId, validateTempRunPage, validateTempRunPageIdentit
|
|
|
7
11
|
*/
|
|
8
12
|
export class MemoryBlockStore {
|
|
9
13
|
#blocks = new Map();
|
|
14
|
+
#blockChecksums = new Map();
|
|
15
|
+
#physicalBlockBytes = 0;
|
|
10
16
|
#tempRunPages = new Map();
|
|
17
|
+
#tempUsageByOwner = new Map();
|
|
18
|
+
#tempPageKeysByRun = new Map();
|
|
19
|
+
#tempBytesTotal = 0;
|
|
20
|
+
#tempPagesTotal = 0;
|
|
21
|
+
#tempRunsTotal = 0;
|
|
22
|
+
#ftsBaseBuilds = new Map();
|
|
23
|
+
#snapshotFrameExport;
|
|
24
|
+
#snapshotFrameImport;
|
|
25
|
+
#completedSnapshotFrameImport;
|
|
11
26
|
#core = new RecordCore({
|
|
12
27
|
hasBlock: (id) => this.#blocks.has(id),
|
|
13
28
|
blockByteLength: (id) => this.#blocks.get(id)?.byteLength,
|
|
29
|
+
blockChecksum: (id) => this.#blockChecksums.get(id),
|
|
14
30
|
});
|
|
15
31
|
#commitQueue = Promise.resolve();
|
|
16
|
-
async addBlock(id, bytes) {
|
|
17
|
-
return this.addBlocks([{ id, bytes }]);
|
|
18
|
-
}
|
|
19
|
-
async addBlocks(blocks) {
|
|
20
|
-
const ids = new Set();
|
|
21
|
-
for (const block of blocks) {
|
|
22
|
-
validateId(block.id);
|
|
23
|
-
if (ids.has(block.id) || this.#blocks.has(block.id)) {
|
|
24
|
-
throw new Error(`Block already exists: ${block.id}`);
|
|
25
|
-
}
|
|
26
|
-
ids.add(block.id);
|
|
27
|
-
}
|
|
28
|
-
for (const block of blocks)
|
|
29
|
-
this.#blocks.set(block.id, new Uint8Array(block.bytes));
|
|
30
|
-
}
|
|
31
32
|
async getBlock(id) {
|
|
33
|
+
validateStorageId(id, "Block ID");
|
|
32
34
|
const bytes = this.#blocks.get(id);
|
|
33
35
|
return bytes === undefined ? undefined : new Uint8Array(bytes);
|
|
34
36
|
}
|
|
35
37
|
async getBlocks(ids) {
|
|
38
|
+
assertStorageBulkReadItems(ids, "Block read batch");
|
|
39
|
+
for (const id of ids)
|
|
40
|
+
validateStorageId(id, "Block ID");
|
|
41
|
+
let bytes = 0;
|
|
42
|
+
for (const id of ids) {
|
|
43
|
+
bytes += this.#blocks.get(id)?.byteLength ?? 0;
|
|
44
|
+
if (!Number.isSafeInteger(bytes) || bytes > MAX_BLOCK_READ_BATCH_BYTES) {
|
|
45
|
+
throw new BlockReadBatchTooLargeError(bytes);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
36
48
|
return ids.map((id) => {
|
|
37
49
|
const bytes = this.#blocks.get(id);
|
|
38
50
|
return bytes === undefined ? undefined : new Uint8Array(bytes);
|
|
39
51
|
});
|
|
40
52
|
}
|
|
41
|
-
async
|
|
42
|
-
|
|
53
|
+
async readManifestBlock(version, id) {
|
|
54
|
+
validateStorageId(id, "Block ID");
|
|
55
|
+
return this.#runAtomic(() => {
|
|
56
|
+
if (!this.#core.hasManifestBlocks(version, [id])[0])
|
|
57
|
+
return undefined;
|
|
58
|
+
const bytes = this.#blocks.get(id);
|
|
59
|
+
if (bytes === undefined) {
|
|
60
|
+
throw new StorageCorruptionError("memory", id, "manifest member payload is missing");
|
|
61
|
+
}
|
|
62
|
+
return new Uint8Array(bytes);
|
|
63
|
+
});
|
|
43
64
|
}
|
|
44
|
-
async
|
|
45
|
-
|
|
65
|
+
async hasManifestBlocks(version, ids) {
|
|
66
|
+
assertStorageBulkReadItems(ids, "Manifest membership batch");
|
|
67
|
+
return this.#runAtomic(() => this.#core.hasManifestBlocks(version, ids));
|
|
46
68
|
}
|
|
47
69
|
async putTempRunPage(page) {
|
|
70
|
+
assertTempRunPageBatchLimits([page]);
|
|
48
71
|
validateTempRunPage(page);
|
|
49
|
-
this.#
|
|
72
|
+
return this.#runAtomic(() => this.#putTempRunPages([page]));
|
|
50
73
|
}
|
|
51
74
|
async putTempRunPages(pages) {
|
|
75
|
+
assertTempRunPageBatchLimits(pages);
|
|
52
76
|
for (const page of pages)
|
|
53
77
|
validateTempRunPage(page);
|
|
54
|
-
|
|
55
|
-
this.#tempRunPages.set(tempRunPageKey(page.ownerId, page.runId, page.pageIndex), page.bytes.slice());
|
|
56
|
-
}
|
|
78
|
+
return this.#runAtomic(() => this.#putTempRunPages(pages));
|
|
57
79
|
}
|
|
58
80
|
async getTempRunPage(ownerId, runId, pageIndex) {
|
|
59
81
|
validateTempRunPageIdentity(ownerId, runId, pageIndex);
|
|
@@ -61,15 +83,14 @@ export class MemoryBlockStore {
|
|
|
61
83
|
}
|
|
62
84
|
async removeTempRun(ownerId, runId) {
|
|
63
85
|
validateTempRunPageIdentity(ownerId, runId, 0);
|
|
64
|
-
|
|
65
|
-
for (const key of this.#tempRunPages.keys())
|
|
66
|
-
if (key.startsWith(prefix))
|
|
67
|
-
this.#tempRunPages.delete(key);
|
|
86
|
+
return this.#runAtomic(() => this.#removeTempRunPages(ownerId, runId));
|
|
68
87
|
}
|
|
69
88
|
async removeTempOwner(ownerId) {
|
|
70
89
|
validateId(ownerId);
|
|
71
|
-
this.#
|
|
72
|
-
|
|
90
|
+
return this.#runAtomic(() => {
|
|
91
|
+
this.#removeTempOwnerPages(ownerId);
|
|
92
|
+
this.#core.removeTempOwner(ownerId);
|
|
93
|
+
});
|
|
73
94
|
}
|
|
74
95
|
async createTempOwner(record) {
|
|
75
96
|
return this.#runAtomic(() => {
|
|
@@ -79,8 +100,8 @@ export class MemoryBlockStore {
|
|
|
79
100
|
async getTempOwner(ownerId) {
|
|
80
101
|
return this.#core.getTempOwner(ownerId);
|
|
81
102
|
}
|
|
82
|
-
async renewTempOwner(
|
|
83
|
-
return this.#runAtomic(() => this.#core.renewTempOwner(
|
|
103
|
+
async renewTempOwner(input) {
|
|
104
|
+
return this.#runAtomic(() => this.#core.renewTempOwner(input));
|
|
84
105
|
}
|
|
85
106
|
async removeTempOwnerIfExpired(ownerId, expiresAtCutoff) {
|
|
86
107
|
return this.#runAtomic(() => {
|
|
@@ -99,33 +120,297 @@ export class MemoryBlockStore {
|
|
|
99
120
|
}
|
|
100
121
|
return this.#core.listTempOwnerIdsPage(afterOwnerId, limit, pageOwnerIds);
|
|
101
122
|
}
|
|
123
|
+
async listExpiredTempOwnerPage(expiresAtCutoff, afterCursor, limit) {
|
|
124
|
+
return this.#core.listExpiredTempOwnerPage(expiresAtCutoff, afterCursor, limit);
|
|
125
|
+
}
|
|
126
|
+
#putTempRunPages(pages) {
|
|
127
|
+
const seenKeys = new Set();
|
|
128
|
+
const deltas = new Map();
|
|
129
|
+
let totalBytesDelta = 0;
|
|
130
|
+
let totalPagesDelta = 0;
|
|
131
|
+
let totalRunsDelta = 0;
|
|
132
|
+
for (const page of pages) {
|
|
133
|
+
if (this.#core.getTempOwner(page.ownerId) === undefined) {
|
|
134
|
+
throw new Error(`Temp owner does not exist: ${page.ownerId}`);
|
|
135
|
+
}
|
|
136
|
+
const key = tempRunPageKey(page.ownerId, page.runId, page.pageIndex);
|
|
137
|
+
if (seenKeys.has(key))
|
|
138
|
+
throw new TypeError(`Temp run page batch repeats ${key}`);
|
|
139
|
+
seenKeys.add(key);
|
|
140
|
+
const previousBytes = this.#tempRunPages.get(key)?.byteLength ?? 0;
|
|
141
|
+
const isNewPage = !this.#tempRunPages.has(key);
|
|
142
|
+
const delta = deltas.get(page.ownerId) ?? { bytes: 0, pages: 0, newRuns: new Set() };
|
|
143
|
+
delta.bytes += page.bytes.byteLength - previousBytes;
|
|
144
|
+
if (isNewPage)
|
|
145
|
+
delta.pages += 1;
|
|
146
|
+
const usage = this.#tempUsageByOwner.get(page.ownerId);
|
|
147
|
+
if (usage?.runs.has(page.runId) !== true && !delta.newRuns.has(page.runId)) {
|
|
148
|
+
delta.newRuns.add(page.runId);
|
|
149
|
+
}
|
|
150
|
+
deltas.set(page.ownerId, delta);
|
|
151
|
+
totalBytesDelta += page.bytes.byteLength - previousBytes;
|
|
152
|
+
if (isNewPage)
|
|
153
|
+
totalPagesDelta += 1;
|
|
154
|
+
}
|
|
155
|
+
for (const [ownerId, delta] of deltas) {
|
|
156
|
+
const usage = this.#tempUsageByOwner.get(ownerId);
|
|
157
|
+
const nextBytes = (usage?.bytes ?? 0) + delta.bytes;
|
|
158
|
+
const nextPages = (usage?.pages ?? 0) + delta.pages;
|
|
159
|
+
const nextRuns = (usage?.runs.size ?? 0) + delta.newRuns.size;
|
|
160
|
+
assertTempResourceLimit("temp owner byte", nextBytes, MAX_TEMP_BYTES_PER_OWNER);
|
|
161
|
+
assertTempResourceLimit("temp page", nextPages, MAX_TEMP_PAGES_PER_OWNER);
|
|
162
|
+
assertTempResourceLimit("temp run", nextRuns, MAX_TEMP_RUNS_PER_OWNER);
|
|
163
|
+
totalRunsDelta += delta.newRuns.size;
|
|
164
|
+
}
|
|
165
|
+
assertTempResourceLimit("temporary byte", this.#tempBytesTotal + totalBytesDelta, MAX_TEMP_BYTES_TOTAL);
|
|
166
|
+
assertTempResourceLimit("temporary page total", this.#tempPagesTotal + totalPagesDelta, MAX_TEMP_PAGES_TOTAL);
|
|
167
|
+
assertTempResourceLimit("temporary run total", this.#tempRunsTotal + totalRunsDelta, MAX_TEMP_RUNS_TOTAL);
|
|
168
|
+
for (const page of pages) {
|
|
169
|
+
const key = tempRunPageKey(page.ownerId, page.runId, page.pageIndex);
|
|
170
|
+
const previousBytes = this.#tempRunPages.get(key)?.byteLength ?? 0;
|
|
171
|
+
const isNewPage = !this.#tempRunPages.has(key);
|
|
172
|
+
const usage = this.#tempUsageByOwner.get(page.ownerId) ?? {
|
|
173
|
+
bytes: 0,
|
|
174
|
+
pages: 0,
|
|
175
|
+
runs: new Map(),
|
|
176
|
+
};
|
|
177
|
+
const previousRunPages = usage.runs.get(page.runId) ?? 0;
|
|
178
|
+
this.#tempRunPages.set(key, page.bytes.slice());
|
|
179
|
+
const runKey = tempRunPagePrefix(page.ownerId, page.runId);
|
|
180
|
+
const runKeys = this.#tempPageKeysByRun.get(runKey) ?? new Set();
|
|
181
|
+
runKeys.add(key);
|
|
182
|
+
this.#tempPageKeysByRun.set(runKey, runKeys);
|
|
183
|
+
usage.bytes += page.bytes.byteLength - previousBytes;
|
|
184
|
+
this.#tempBytesTotal += page.bytes.byteLength - previousBytes;
|
|
185
|
+
if (isNewPage) {
|
|
186
|
+
usage.pages += 1;
|
|
187
|
+
this.#tempPagesTotal += 1;
|
|
188
|
+
usage.runs.set(page.runId, previousRunPages + 1);
|
|
189
|
+
if (previousRunPages === 0)
|
|
190
|
+
this.#tempRunsTotal += 1;
|
|
191
|
+
}
|
|
192
|
+
this.#tempUsageByOwner.set(page.ownerId, usage);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
#removeTempRunPages(ownerId, runId) {
|
|
196
|
+
const runKey = tempRunPagePrefix(ownerId, runId);
|
|
197
|
+
const keys = this.#tempPageKeysByRun.get(runKey);
|
|
198
|
+
if (keys === undefined)
|
|
199
|
+
return;
|
|
200
|
+
const usage = this.#tempUsageByOwner.get(ownerId);
|
|
201
|
+
let removedBytes = 0;
|
|
202
|
+
for (const key of keys) {
|
|
203
|
+
removedBytes += this.#tempRunPages.get(key)?.byteLength ?? 0;
|
|
204
|
+
this.#tempRunPages.delete(key);
|
|
205
|
+
}
|
|
206
|
+
this.#tempPageKeysByRun.delete(runKey);
|
|
207
|
+
this.#tempBytesTotal -= removedBytes;
|
|
208
|
+
this.#tempPagesTotal -= keys.size;
|
|
209
|
+
this.#tempRunsTotal -= 1;
|
|
210
|
+
if (usage !== undefined) {
|
|
211
|
+
usage.bytes -= removedBytes;
|
|
212
|
+
usage.pages -= keys.size;
|
|
213
|
+
usage.runs.delete(runId);
|
|
214
|
+
if (usage.pages === 0)
|
|
215
|
+
this.#tempUsageByOwner.delete(ownerId);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
102
218
|
#removeTempOwnerPages(ownerId) {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
219
|
+
const usage = this.#tempUsageByOwner.get(ownerId);
|
|
220
|
+
if (usage === undefined)
|
|
221
|
+
return;
|
|
222
|
+
for (const runId of [...usage.runs.keys()])
|
|
223
|
+
this.#removeTempRunPages(ownerId, runId);
|
|
107
224
|
}
|
|
108
|
-
async addTable(record) {
|
|
109
|
-
this.#core.addTable(record);
|
|
225
|
+
async addTable(record, options = {}) {
|
|
226
|
+
this.#core.addTable(record, options);
|
|
110
227
|
}
|
|
111
228
|
async getTable(id) {
|
|
112
229
|
return this.#core.getTable(id);
|
|
113
230
|
}
|
|
114
231
|
async updateTable(id, expectedRevision, update) {
|
|
115
|
-
return this.#runAtomic(() =>
|
|
232
|
+
return this.#runAtomic(() => {
|
|
233
|
+
const before = this.#core.getTable(id);
|
|
234
|
+
const updated = this.#core.updateTable(id, expectedRevision, update);
|
|
235
|
+
if (before !== undefined)
|
|
236
|
+
this.#removeRetiredPostingBuilds(before, updated);
|
|
237
|
+
return updated;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async removeTable(id, expectedRevision, options = {}) {
|
|
241
|
+
return this.#runAtomic(() => {
|
|
242
|
+
this.#core.removeTable(id, expectedRevision, options);
|
|
243
|
+
this.#removeTablePostingBuilds(id);
|
|
244
|
+
});
|
|
116
245
|
}
|
|
117
|
-
async
|
|
246
|
+
async dropTable(input) {
|
|
118
247
|
return this.#runAtomic(() => {
|
|
119
|
-
this.#core.
|
|
248
|
+
const summary = this.#core.dropTable(input);
|
|
249
|
+
this.#removeTablePostingBuilds(input.tableId);
|
|
250
|
+
return summary;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
async dropTableColumn(input) {
|
|
254
|
+
return this.#runAtomic(() => {
|
|
255
|
+
const summary = this.#core.dropTableColumn(input);
|
|
256
|
+
this.#ftsBaseBuilds.delete(`${input.tableId}/${input.columnId}`);
|
|
257
|
+
return summary;
|
|
120
258
|
});
|
|
121
259
|
}
|
|
260
|
+
#removeRetiredPostingBuilds(before, after) {
|
|
261
|
+
const retained = new Set([
|
|
262
|
+
...Object.keys(after.ftsColumns ?? {}),
|
|
263
|
+
...Object.values(after.secondaryIndexes ?? {}).map((index) => index.storageColumnId),
|
|
264
|
+
]);
|
|
265
|
+
for (const columnId of [
|
|
266
|
+
...Object.keys(before.ftsColumns ?? {}),
|
|
267
|
+
...Object.values(before.secondaryIndexes ?? {}).map((index) => index.storageColumnId),
|
|
268
|
+
]) {
|
|
269
|
+
if (!retained.has(columnId))
|
|
270
|
+
this.#ftsBaseBuilds.delete(`${before.id}/${columnId}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
#removeTablePostingBuilds(tableId) {
|
|
274
|
+
const prefix = `${tableId}/`;
|
|
275
|
+
for (const key of [...this.#ftsBaseBuilds.keys()]) {
|
|
276
|
+
if (key.startsWith(prefix))
|
|
277
|
+
this.#ftsBaseBuilds.delete(key);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
122
280
|
async writeFtsBase(tableId, columnId, input) {
|
|
123
281
|
return this.#runAtomic(() => {
|
|
124
282
|
this.#core.writeFtsBase(tableId, columnId, input);
|
|
125
283
|
});
|
|
126
284
|
}
|
|
127
|
-
async
|
|
128
|
-
return this.#
|
|
285
|
+
async removeFtsColumn(tableId, columnId) {
|
|
286
|
+
return this.#runAtomic(() => {
|
|
287
|
+
this.#core.removeFtsColumn(tableId, columnId);
|
|
288
|
+
this.#ftsBaseBuilds.delete(`${tableId}/${columnId}`);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
async beginFtsBaseBuild(input) {
|
|
292
|
+
validatePostingBuildLifetime(input.createdAt, input.expiresAt);
|
|
293
|
+
validateId(input.tableId);
|
|
294
|
+
validateId(input.columnId);
|
|
295
|
+
validateId(input.buildId);
|
|
296
|
+
validateId(input.ownerId);
|
|
297
|
+
return this.#runAtomic(() => {
|
|
298
|
+
const key = `${input.tableId}/${input.columnId}`;
|
|
299
|
+
const current = this.#ftsBaseBuilds.get(key);
|
|
300
|
+
if (current !== undefined && Date.parse(current.expiresAt) > Date.parse(input.createdAt)) {
|
|
301
|
+
if (current.buildId === input.buildId && current.ownerId === input.ownerId)
|
|
302
|
+
return;
|
|
303
|
+
throw new PostingBuildConflictError(current.buildId, current.ownerId, "another live build owns the column");
|
|
304
|
+
}
|
|
305
|
+
const table = this.#core.getTable(input.tableId);
|
|
306
|
+
if (table === undefined || !activePostingStorageColumnIds(table).has(input.columnId)) {
|
|
307
|
+
throw new Error(`Postings index is no longer active: ${key}`);
|
|
308
|
+
}
|
|
309
|
+
this.#ftsBaseBuilds.set(key, {
|
|
310
|
+
buildId: input.buildId,
|
|
311
|
+
ownerId: input.ownerId,
|
|
312
|
+
createdAt: input.createdAt,
|
|
313
|
+
expiresAt: input.expiresAt,
|
|
314
|
+
chunks: new Map(),
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
async renewFtsBaseBuild(input) {
|
|
319
|
+
validatePostingBuildLifetime(input.expiresAtCutoff, input.expiresAt);
|
|
320
|
+
return this.#runAtomic(() => {
|
|
321
|
+
const build = this.#requirePostingBuild(input);
|
|
322
|
+
build.expiresAt = input.expiresAt;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
async writeFtsBaseBuildChunk(input) {
|
|
326
|
+
validatePostingBuildLifetime(input.expiresAtCutoff, input.expiresAt);
|
|
327
|
+
return this.#runAtomic(() => {
|
|
328
|
+
const build = this.#requirePostingBuild(input);
|
|
329
|
+
if (!Number.isSafeInteger(input.ordinal) || input.ordinal < 0) {
|
|
330
|
+
throw new RangeError(`Full-text base chunk ordinal is invalid: ${String(input.ordinal)}`);
|
|
331
|
+
}
|
|
332
|
+
validateFtsPostingChunks([input.chunk], "Full-text base build");
|
|
333
|
+
const replay = build.chunks.get(input.ordinal);
|
|
334
|
+
if (replay !== undefined) {
|
|
335
|
+
if (!samePostingChunk(replay, input.chunk)) {
|
|
336
|
+
throw new PostingBuildConflictError(input.buildId, input.ownerId, "chunk replay changed");
|
|
337
|
+
}
|
|
338
|
+
build.expiresAt = input.expiresAt;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (input.ordinal !== build.chunks.size) {
|
|
342
|
+
throw new Error(`Full-text base chunk is out of order: ${String(input.ordinal)}`);
|
|
343
|
+
}
|
|
344
|
+
// Chunks are row windows, not term partitions. Terms are sorted inside each chunk, but
|
|
345
|
+
// adjacent chunks can legitimately overlap or restart at an earlier term.
|
|
346
|
+
build.chunks.set(input.ordinal, structuredClone(input.chunk));
|
|
347
|
+
build.expiresAt = input.expiresAt;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
async finishFtsBaseBuild(input) {
|
|
351
|
+
return this.#runAtomic(() => {
|
|
352
|
+
const key = `${input.tableId}/${input.columnId}`;
|
|
353
|
+
const build = this.#requirePostingBuild(input);
|
|
354
|
+
if (!Number.isSafeInteger(input.coversVersion) ||
|
|
355
|
+
input.coversVersion < -1 ||
|
|
356
|
+
!Number.isSafeInteger(input.chunkCount) ||
|
|
357
|
+
input.chunkCount < 0 ||
|
|
358
|
+
!Number.isSafeInteger(input.totalTokens) ||
|
|
359
|
+
input.totalTokens < 0) {
|
|
360
|
+
throw new TypeError("Full-text base build metadata is invalid");
|
|
361
|
+
}
|
|
362
|
+
if (input.chunkCount !== build.chunks.size) {
|
|
363
|
+
throw new Error(`Full-text base build is incomplete: ${input.buildId}`);
|
|
364
|
+
}
|
|
365
|
+
const table = this.#core.getTable(input.tableId);
|
|
366
|
+
if (table === undefined || !activePostingStorageColumnIds(table).has(input.columnId)) {
|
|
367
|
+
this.#ftsBaseBuilds.delete(key);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const chunks = Array.from({ length: input.chunkCount }, (_, ordinal) => {
|
|
371
|
+
const chunk = build.chunks.get(ordinal);
|
|
372
|
+
if (chunk === undefined) {
|
|
373
|
+
throw new Error(`Full-text base chunk is missing: ${String(ordinal)}`);
|
|
374
|
+
}
|
|
375
|
+
return chunk;
|
|
376
|
+
});
|
|
377
|
+
this.#core.writeFtsBase(input.tableId, input.columnId, {
|
|
378
|
+
coversVersion: input.coversVersion,
|
|
379
|
+
chunks,
|
|
380
|
+
totalTokens: input.totalTokens,
|
|
381
|
+
});
|
|
382
|
+
this.#ftsBaseBuilds.delete(key);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
async abortFtsBaseBuild(input) {
|
|
386
|
+
return this.#runAtomic(() => {
|
|
387
|
+
const key = `${input.tableId}/${input.columnId}`;
|
|
388
|
+
const build = this.#ftsBaseBuilds.get(key);
|
|
389
|
+
if (build === undefined)
|
|
390
|
+
return;
|
|
391
|
+
if (build.buildId !== input.buildId ||
|
|
392
|
+
(build.ownerId !== input.ownerId &&
|
|
393
|
+
Date.parse(build.expiresAt) > Date.parse(input.expiresAtCutoff))) {
|
|
394
|
+
throw new PostingBuildConflictError(build.buildId, build.ownerId, "abort ownership changed");
|
|
395
|
+
}
|
|
396
|
+
this.#ftsBaseBuilds.delete(key);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
#requirePostingBuild(input) {
|
|
400
|
+
const build = this.#ftsBaseBuilds.get(`${input.tableId}/${input.columnId}`);
|
|
401
|
+
if (build?.buildId !== input.buildId || build.ownerId !== input.ownerId) {
|
|
402
|
+
throw new PostingBuildConflictError(build?.buildId ?? input.buildId, build?.ownerId ?? input.ownerId, "session is missing or owned by another caller");
|
|
403
|
+
}
|
|
404
|
+
if (Date.parse(build.expiresAt) <= Date.parse(input.expiresAtCutoff)) {
|
|
405
|
+
throw new PostingBuildConflictError(build.buildId, build.ownerId, "session is expired");
|
|
406
|
+
}
|
|
407
|
+
return build;
|
|
408
|
+
}
|
|
409
|
+
async readFtsCandidates(tableId, columnId, terms, upToVersion, maxRowIds) {
|
|
410
|
+
return this.#core.readFtsCandidates(tableId, columnId, terms, upToVersion, maxRowIds);
|
|
411
|
+
}
|
|
412
|
+
async readFtsPostings(tableId, columnId, upToVersion, maxRowIds, maxRetainedBytes) {
|
|
413
|
+
return this.#core.readFtsPostings(tableId, columnId, upToVersion, maxRowIds, maxRetainedBytes);
|
|
129
414
|
}
|
|
130
415
|
async getTableByName(name) {
|
|
131
416
|
const id = this.#core.getTableIdByName(name);
|
|
@@ -134,17 +419,20 @@ export class MemoryBlockStore {
|
|
|
134
419
|
async listTables() {
|
|
135
420
|
return this.#core.listTables();
|
|
136
421
|
}
|
|
137
|
-
async addSegment(record) {
|
|
138
|
-
this.#core.addSegment(record);
|
|
139
|
-
}
|
|
140
422
|
async getSegment(id) {
|
|
141
423
|
return this.#core.getSegment(id);
|
|
142
424
|
}
|
|
143
|
-
async
|
|
144
|
-
return this.#core.
|
|
425
|
+
async listSegmentPage(afterId, limit) {
|
|
426
|
+
return this.#core.listSegmentPage(afterId, limit);
|
|
145
427
|
}
|
|
146
|
-
async
|
|
147
|
-
this.#core.
|
|
428
|
+
async listTableSegmentPage(tableId, afterId, limit) {
|
|
429
|
+
return this.#core.listTableSegmentPage(tableId, afterId, limit);
|
|
430
|
+
}
|
|
431
|
+
async removeAbortedSegment(id, expectedTransactionId) {
|
|
432
|
+
return this.#runAtomic(() => this.#core.removeAbortedSegment(id, expectedTransactionId));
|
|
433
|
+
}
|
|
434
|
+
async adoptAbortedSegment(input) {
|
|
435
|
+
return this.#runAtomic(() => this.#core.adoptAbortedSegment(input));
|
|
148
436
|
}
|
|
149
437
|
async reserveRowIds(tableId, count) {
|
|
150
438
|
return this.#core.reserveRowIds(tableId, count);
|
|
@@ -153,33 +441,69 @@ export class MemoryBlockStore {
|
|
|
153
441
|
return this.#runAtomic(() => this.#core.reserveAutoIncrement(tableId, columnId, count, atLeast));
|
|
154
442
|
}
|
|
155
443
|
async getExistingUniqueKeys(tableId, keyTokens) {
|
|
444
|
+
assertStorageBulkReadItems(keyTokens, "Unique-key lookup batch");
|
|
156
445
|
return this.#core.getExistingUniqueKeys(tableId, keyTokens);
|
|
157
446
|
}
|
|
447
|
+
async beginUniqueKeyBuild(input) {
|
|
448
|
+
return this.#runAtomic(() => this.#core.beginUniqueKeyBuild(input));
|
|
449
|
+
}
|
|
450
|
+
async getUniqueKeyBuild(buildId) {
|
|
451
|
+
return this.#core.getUniqueKeyBuild(buildId);
|
|
452
|
+
}
|
|
453
|
+
async renewUniqueKeyBuild(input) {
|
|
454
|
+
return this.#runAtomic(() => this.#core.renewUniqueKeyBuild(input));
|
|
455
|
+
}
|
|
456
|
+
async appendUniqueKeyBuildChunk(input) {
|
|
457
|
+
return this.#runAtomic(() => this.#core.appendUniqueKeyBuildChunk(input));
|
|
458
|
+
}
|
|
459
|
+
async finishUniqueKeyBuild(input) {
|
|
460
|
+
return this.#runAtomic(() => this.#core.finishUniqueKeyBuild(input));
|
|
461
|
+
}
|
|
462
|
+
async abortUniqueKeyBuild(input) {
|
|
463
|
+
return this.#runAtomic(() => this.#core.abortUniqueKeyBuild(input));
|
|
464
|
+
}
|
|
158
465
|
async getCurrentManifestVersion() {
|
|
159
466
|
return this.#core.getCurrentManifestVersion();
|
|
160
467
|
}
|
|
161
468
|
async getCatalogProbe() {
|
|
162
469
|
return this.#core.getCatalogProbe();
|
|
163
470
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
471
|
+
async getQueryCatalogState(names) {
|
|
472
|
+
assertStorageBulkReadItems(names, "Query catalog table batch");
|
|
473
|
+
return this.#runAtomic(() => {
|
|
474
|
+
const probe = this.#core.getCatalogProbe();
|
|
475
|
+
const tables = names.map((name) => {
|
|
476
|
+
const id = this.#core.getTableIdByName(name);
|
|
477
|
+
return id === undefined ? undefined : this.#core.getTable(id);
|
|
478
|
+
});
|
|
479
|
+
const segments = [];
|
|
480
|
+
for (const table of tables) {
|
|
481
|
+
if (table === undefined)
|
|
482
|
+
continue;
|
|
483
|
+
let cursor = null;
|
|
484
|
+
do {
|
|
485
|
+
const page = this.#core.listTableSegmentPage(table.id, cursor, 1_024);
|
|
486
|
+
segments.push(...page.records);
|
|
487
|
+
cursor = page.nextCursor;
|
|
488
|
+
} while (cursor !== null);
|
|
489
|
+
}
|
|
490
|
+
segments.sort((left, right) => left.id.localeCompare(right.id));
|
|
491
|
+
const transactionIds = [...new Set(segments.map((segment) => segment.transactionId))];
|
|
492
|
+
const transactions = [];
|
|
493
|
+
for (let start = 0; start < transactionIds.length; start += 64) {
|
|
494
|
+
for (const record of this.#core.getTransactions(transactionIds.slice(start, start + 64))) {
|
|
495
|
+
if (record !== undefined)
|
|
496
|
+
transactions.push(record);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
manifestVersion: probe.manifestVersion,
|
|
501
|
+
tables,
|
|
502
|
+
segments,
|
|
503
|
+
transactions,
|
|
504
|
+
catalogEpoch: probe.catalogEpoch,
|
|
505
|
+
};
|
|
506
|
+
});
|
|
183
507
|
}
|
|
184
508
|
async getCurrentManifest() {
|
|
185
509
|
return this.#core.getCurrentManifest();
|
|
@@ -187,15 +511,15 @@ export class MemoryBlockStore {
|
|
|
187
511
|
async getManifest(version) {
|
|
188
512
|
return this.#core.getManifest(version);
|
|
189
513
|
}
|
|
190
|
-
async
|
|
191
|
-
return this.#core.
|
|
514
|
+
async listManifestBlockPage(input) {
|
|
515
|
+
return this.#core.listManifestBlockPage(input);
|
|
516
|
+
}
|
|
517
|
+
async listRetiredManifestBlockPage(input) {
|
|
518
|
+
return this.#core.listRetiredManifestBlockPage(input);
|
|
192
519
|
}
|
|
193
520
|
async listManifestPage(afterVersion, limit) {
|
|
194
521
|
return this.#core.listManifestPage(afterVersion, limit);
|
|
195
522
|
}
|
|
196
|
-
async publishManifest(input) {
|
|
197
|
-
return this.#runAtomic(() => this.#core.publishManifest(input));
|
|
198
|
-
}
|
|
199
523
|
async beginTransaction(input) {
|
|
200
524
|
return this.#runAtomic(() => this.#core.beginTransaction(input));
|
|
201
525
|
}
|
|
@@ -208,23 +532,35 @@ export class MemoryBlockStore {
|
|
|
208
532
|
return this.#core.getTransaction(id);
|
|
209
533
|
}
|
|
210
534
|
async getTransactions(ids) {
|
|
535
|
+
assertStorageBulkReadItems(ids, "Transaction read batch");
|
|
211
536
|
return this.#core.getTransactions(ids);
|
|
212
537
|
}
|
|
213
|
-
async listTransactions() {
|
|
214
|
-
return this.#core.listTransactions();
|
|
215
|
-
}
|
|
216
538
|
async listTransactionPage(afterId, limit) {
|
|
217
539
|
return this.#core.listTransactionPage(afterId, limit);
|
|
218
540
|
}
|
|
219
541
|
async updateTransaction(id, expectedRevision, update) {
|
|
220
542
|
return this.#runAtomic(() => this.#core.updateTransaction(id, expectedRevision, update));
|
|
221
543
|
}
|
|
544
|
+
async renewTransaction(input) {
|
|
545
|
+
return this.#runAtomic(() => this.#core.renewTransaction(input));
|
|
546
|
+
}
|
|
547
|
+
async abortTransactionIfExpired(input) {
|
|
548
|
+
return this.#runAtomic(() => this.#core.abortTransactionIfExpired(input));
|
|
549
|
+
}
|
|
222
550
|
async stageTransactionArtifacts(input) {
|
|
223
551
|
return this.#runAtomic(() => {
|
|
224
552
|
const updated = this.#core.stageTransactionArtifacts(input);
|
|
225
553
|
// The record half validated everything; the bytes land in this same atomic step.
|
|
226
554
|
for (const block of input.blocks)
|
|
227
|
-
this.#
|
|
555
|
+
this.#putBlock(block.id, block.bytes);
|
|
556
|
+
return updated;
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
async rollbackTransactionArtifacts(input) {
|
|
560
|
+
return this.#runAtomic(() => {
|
|
561
|
+
const updated = this.#core.rollbackTransactionArtifacts(input);
|
|
562
|
+
for (const id of input.removeBlockIds)
|
|
563
|
+
this.#deleteBlock(id);
|
|
228
564
|
return updated;
|
|
229
565
|
});
|
|
230
566
|
}
|
|
@@ -236,7 +572,7 @@ export class MemoryBlockStore {
|
|
|
236
572
|
const summary = this.#core.writeTransaction(input);
|
|
237
573
|
// The record half validated everything; the bytes land in this same atomic step.
|
|
238
574
|
for (const block of input.blocks)
|
|
239
|
-
this.#
|
|
575
|
+
this.#putBlock(block.id, block.bytes);
|
|
240
576
|
return summary;
|
|
241
577
|
});
|
|
242
578
|
}
|
|
@@ -251,19 +587,20 @@ export class MemoryBlockStore {
|
|
|
251
587
|
async listLeases() {
|
|
252
588
|
return this.#core.listLeases();
|
|
253
589
|
}
|
|
254
|
-
async
|
|
255
|
-
return this.#
|
|
590
|
+
async listExpiredLeasePage(expiresAtCutoff, afterCursor, limit) {
|
|
591
|
+
return this.#core.listExpiredLeasePage(expiresAtCutoff, afterCursor, limit);
|
|
592
|
+
}
|
|
593
|
+
async renewLease(input) {
|
|
594
|
+
return this.#runAtomic(() => this.#core.renewLease(input));
|
|
256
595
|
}
|
|
257
|
-
async moveLease(
|
|
258
|
-
return this.#runAtomic(() => this.#core.moveLease(
|
|
596
|
+
async moveLease(input) {
|
|
597
|
+
return this.#runAtomic(() => this.#core.moveLease(input));
|
|
259
598
|
}
|
|
260
599
|
async removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff) {
|
|
261
600
|
return this.#runAtomic(() => this.#core.removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff));
|
|
262
601
|
}
|
|
263
|
-
async removeLease(
|
|
264
|
-
return this.#runAtomic(() =>
|
|
265
|
-
this.#core.removeLease(id);
|
|
266
|
-
});
|
|
602
|
+
async removeLease(input) {
|
|
603
|
+
return this.#runAtomic(() => this.#core.removeLease(input));
|
|
267
604
|
}
|
|
268
605
|
async createCompactionJob(record) {
|
|
269
606
|
this.#core.createCompactionJob(record);
|
|
@@ -284,71 +621,551 @@ export class MemoryBlockStore {
|
|
|
284
621
|
return this.#runAtomic(() => this.#core.cancelCompactionJob(id, expectedRevision, cancelledAt));
|
|
285
622
|
}
|
|
286
623
|
async removeCompactionJob(id) {
|
|
287
|
-
this.#core.removeCompactionJob(id);
|
|
624
|
+
return this.#core.removeCompactionJob(id);
|
|
288
625
|
}
|
|
289
626
|
async createGarbageCollectionJob(input) {
|
|
290
627
|
return this.#runAtomic(() => this.#core.createGarbageCollectionJob(input));
|
|
291
628
|
}
|
|
629
|
+
async updateGarbageCollectionPlanning(input) {
|
|
630
|
+
return this.#runAtomic(() => this.#core.updateGarbageCollectionPlanning(input));
|
|
631
|
+
}
|
|
292
632
|
async getGarbageCollectionJob(id) {
|
|
293
633
|
return this.#core.getGarbageCollectionJob(id);
|
|
294
634
|
}
|
|
295
635
|
async listGarbageCollectionJobs() {
|
|
296
636
|
return this.#core.listGarbageCollectionJobs();
|
|
297
637
|
}
|
|
638
|
+
async listGarbageCollectionJobPage(afterId, limit) {
|
|
639
|
+
return this.#core.listGarbageCollectionJobPage(afterId, limit);
|
|
640
|
+
}
|
|
298
641
|
async runGarbageCollectionStep(input) {
|
|
299
642
|
return this.#runAtomic(() => {
|
|
300
643
|
const result = this.#core.runGarbageCollectionStep(input);
|
|
301
|
-
result.reclaimedBlockIds.forEach((id) => this.#
|
|
644
|
+
result.reclaimedBlockIds.forEach((id) => this.#deleteBlock(id));
|
|
302
645
|
return result;
|
|
303
646
|
});
|
|
304
647
|
}
|
|
305
|
-
async removePrunedManifestRecords() {
|
|
306
|
-
return this.#runAtomic(() => this.#core.removePrunedManifestRecords()
|
|
648
|
+
async removePrunedManifestRecords(maxItems) {
|
|
649
|
+
return this.#runAtomic(() => this.#core.removePrunedManifestRecords(maxItems));
|
|
307
650
|
}
|
|
308
651
|
async removeGarbageCollectionJob(id) {
|
|
309
652
|
return this.#runAtomic(() => {
|
|
310
653
|
this.#core.removeGarbageCollectionJob(id);
|
|
311
654
|
});
|
|
312
655
|
}
|
|
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
|
-
|
|
656
|
+
async beginSnapshotFrameExport(input) {
|
|
657
|
+
validateSnapshotSessionLifetime(input.createdAt, input.expiresAt);
|
|
658
|
+
validateId(input.ownerId);
|
|
659
|
+
return this.#runAtomic(() => {
|
|
660
|
+
const current = this.#snapshotFrameExport;
|
|
661
|
+
if (current !== undefined) {
|
|
662
|
+
if (Date.parse(current.session.expiresAt) > Date.parse(input.createdAt)) {
|
|
663
|
+
throw new SnapshotImportConflictError(current.session.sessionId, current.session.ownerId, "another snapshot export is still live");
|
|
664
|
+
}
|
|
665
|
+
this.#core.removeLease({
|
|
666
|
+
id: current.session.sessionId,
|
|
667
|
+
ownerId: current.session.ownerId,
|
|
668
|
+
});
|
|
669
|
+
this.#snapshotFrameExport = undefined;
|
|
670
|
+
}
|
|
671
|
+
const manifest = this.#core.snapshotFrameManifest();
|
|
672
|
+
const metadataFrames = [];
|
|
673
|
+
const summaries = emptySnapshotObservedSummaries();
|
|
674
|
+
const append = (item) => {
|
|
675
|
+
const payload = encodeSnapshotMetadataPage([item]);
|
|
676
|
+
const kind = snapshotMetadataFrameKind(item);
|
|
677
|
+
const frame = {
|
|
678
|
+
sequence: metadataFrames.length,
|
|
679
|
+
kind,
|
|
680
|
+
itemCount: 1,
|
|
681
|
+
key: null,
|
|
682
|
+
payload,
|
|
683
|
+
checksum: crc32(payload),
|
|
684
|
+
};
|
|
685
|
+
metadataFrames.push(frame);
|
|
686
|
+
const summary = summaries[kind];
|
|
687
|
+
summary.frameCount += 1;
|
|
688
|
+
summary.itemCount += 1;
|
|
689
|
+
summary.storedBytes = safeSnapshotByteSum(summary.storedBytes, payload.byteLength);
|
|
690
|
+
};
|
|
691
|
+
for (const item of this.#core.snapshotFrameMetadataItems())
|
|
692
|
+
append(item);
|
|
693
|
+
for (const item of this.#core.snapshotFramePostingItems())
|
|
694
|
+
append(item);
|
|
695
|
+
summaries.block = {
|
|
696
|
+
frameCount: manifest.liveBlockCount,
|
|
697
|
+
itemCount: manifest.liveBlockCount,
|
|
698
|
+
storedBytes: manifest.liveBlockBytes,
|
|
699
|
+
};
|
|
700
|
+
const header = prepareSnapshotFrameStreamHeader({
|
|
701
|
+
formatVersion: 1,
|
|
702
|
+
databaseVersion: manifest.version,
|
|
703
|
+
createdAt: input.createdAt,
|
|
704
|
+
kinds: summaries,
|
|
705
|
+
});
|
|
706
|
+
const sessionId = `snapshot-export/${crypto.randomUUID()}`;
|
|
707
|
+
this.#core.createLease({
|
|
708
|
+
id: sessionId,
|
|
709
|
+
kind: "backup",
|
|
710
|
+
manifestVersion: manifest.version,
|
|
711
|
+
ownerId: input.ownerId,
|
|
712
|
+
createdAt: input.createdAt,
|
|
713
|
+
expiresAt: input.expiresAt,
|
|
714
|
+
revision: 0,
|
|
715
|
+
});
|
|
716
|
+
const session = { sessionId, ownerId: input.ownerId, expiresAt: input.expiresAt, header };
|
|
717
|
+
this.#snapshotFrameExport = {
|
|
718
|
+
session,
|
|
719
|
+
manifestVersion: manifest.version,
|
|
720
|
+
metadataFrames,
|
|
721
|
+
nextSequence: 0,
|
|
722
|
+
lastBlockId: null,
|
|
723
|
+
};
|
|
724
|
+
return structuredClone(session);
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
async readSnapshotExportFrame(input) {
|
|
728
|
+
validateSnapshotSessionLifetime(input.expiresAtCutoff, input.expiresAt);
|
|
729
|
+
if (!Number.isSafeInteger(input.sequence) || input.sequence < 0) {
|
|
730
|
+
throw new RangeError("Snapshot export sequence is invalid");
|
|
731
|
+
}
|
|
732
|
+
return this.#runAtomic(() => {
|
|
733
|
+
const state = this.#snapshotFrameExport;
|
|
734
|
+
if (state?.session.sessionId !== input.sessionId || state.session.ownerId !== input.ownerId) {
|
|
735
|
+
throw new SnapshotImportConflictError(input.sessionId, input.ownerId, "export session is missing or owned by another caller");
|
|
736
|
+
}
|
|
737
|
+
if (input.sequence === state.nextSequence - 1 && state.lastFrame !== undefined) {
|
|
738
|
+
return copySnapshotFrame(state.lastFrame);
|
|
739
|
+
}
|
|
740
|
+
if (input.sequence !== state.nextSequence) {
|
|
741
|
+
throw new RangeError("Snapshot export frames must be pulled in contiguous order");
|
|
742
|
+
}
|
|
743
|
+
const lease = this.#core.getLease(input.sessionId);
|
|
744
|
+
if (lease === undefined) {
|
|
745
|
+
throw new SnapshotImportConflictError(input.sessionId, input.ownerId, "lease is missing");
|
|
746
|
+
}
|
|
747
|
+
this.#core.renewLease({
|
|
748
|
+
id: lease.id,
|
|
749
|
+
expectedRevision: lease.revision,
|
|
750
|
+
expiresAtCutoff: input.expiresAtCutoff,
|
|
751
|
+
expiresAt: input.expiresAt,
|
|
752
|
+
});
|
|
753
|
+
state.session = { ...state.session, expiresAt: input.expiresAt };
|
|
754
|
+
const metadata = state.metadataFrames[input.sequence];
|
|
755
|
+
let frame;
|
|
756
|
+
if (metadata !== undefined) {
|
|
757
|
+
frame = metadata;
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
const blockPage = this.#core.listManifestBlockPage({
|
|
761
|
+
version: state.manifestVersion,
|
|
762
|
+
afterBlockId: state.lastBlockId,
|
|
763
|
+
limit: 1,
|
|
764
|
+
});
|
|
765
|
+
const record = blockPage.records[0];
|
|
766
|
+
if (record === undefined)
|
|
767
|
+
return undefined;
|
|
768
|
+
const bytes = this.#blocks.get(record.blockId);
|
|
769
|
+
if (bytes?.byteLength !== record.byteLength || crc32(bytes) !== record.checksum) {
|
|
770
|
+
throw new StorageCorruptionError("memory", record.blockId, "snapshot block disagrees with its manifest provenance");
|
|
771
|
+
}
|
|
772
|
+
frame = {
|
|
773
|
+
sequence: input.sequence,
|
|
774
|
+
kind: "block",
|
|
775
|
+
itemCount: 1,
|
|
776
|
+
key: record.blockId,
|
|
777
|
+
payload: bytes.slice(),
|
|
778
|
+
checksum: record.checksum,
|
|
779
|
+
};
|
|
780
|
+
state.lastBlockId = record.blockId;
|
|
781
|
+
}
|
|
782
|
+
state.lastFrame = copySnapshotFrame(frame);
|
|
783
|
+
state.nextSequence += 1;
|
|
784
|
+
return copySnapshotFrame(frame);
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
async closeSnapshotFrameExport(input) {
|
|
788
|
+
return this.#runAtomic(() => {
|
|
789
|
+
const state = this.#snapshotFrameExport;
|
|
790
|
+
if (state === undefined)
|
|
791
|
+
return false;
|
|
792
|
+
if (state.session.sessionId !== input.sessionId || state.session.ownerId !== input.ownerId) {
|
|
793
|
+
throw new SnapshotImportConflictError(input.sessionId, input.ownerId, "export session belongs to another caller");
|
|
794
|
+
}
|
|
795
|
+
this.#core.removeLease({ id: input.sessionId, ownerId: input.ownerId });
|
|
796
|
+
this.#snapshotFrameExport = undefined;
|
|
797
|
+
return true;
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
async beginSnapshotFrameImport(input) {
|
|
801
|
+
validateSnapshotSessionLifetime(input.createdAt, input.expiresAt);
|
|
802
|
+
validateId(input.ownerId);
|
|
803
|
+
const header = prepareSnapshotFrameStreamHeader(input.header);
|
|
804
|
+
if (snapshotFrameStreamHeaderIdentity(header) !== input.identity) {
|
|
805
|
+
throw new TypeError("Snapshot import identity does not match its canonical header");
|
|
806
|
+
}
|
|
807
|
+
return this.#runAtomic(() => {
|
|
808
|
+
const current = this.#snapshotFrameImport;
|
|
809
|
+
if (current !== undefined) {
|
|
810
|
+
const expired = Date.parse(current.session.expiresAt) <= Date.parse(input.createdAt);
|
|
811
|
+
const sameIdentity = current.session.identity === input.identity;
|
|
812
|
+
const sameHeader = sameSnapshotFrameHeader(current.header, header);
|
|
813
|
+
if (expired && (!sameIdentity || !sameHeader)) {
|
|
814
|
+
if (!current.completedReplay)
|
|
815
|
+
this.#clearPhysicalBlocks();
|
|
816
|
+
this.#snapshotFrameImport = undefined;
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
if (!expired && current.session.identity !== input.identity) {
|
|
820
|
+
throw new SnapshotImportConflictError(current.session.identity, current.session.ownerId, "another snapshot import is still live");
|
|
821
|
+
}
|
|
822
|
+
if (!expired && current.session.ownerId !== input.ownerId) {
|
|
823
|
+
throw new SnapshotImportConflictError(current.session.identity, current.session.ownerId, "snapshot import is owned by another caller");
|
|
824
|
+
}
|
|
825
|
+
if (!sameHeader) {
|
|
826
|
+
throw new SnapshotImportConflictError(current.session.identity, current.session.ownerId, "snapshot import header changed");
|
|
827
|
+
}
|
|
828
|
+
current.session = {
|
|
829
|
+
...current.session,
|
|
830
|
+
ownerId: input.ownerId,
|
|
831
|
+
expiresAt: input.expiresAt,
|
|
832
|
+
};
|
|
833
|
+
return structuredClone(current.session);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const completed = this.#completedSnapshotFrameImport;
|
|
837
|
+
const completedReplay = completed?.identity === input.identity &&
|
|
838
|
+
completed.version === this.#core.getCurrentManifestVersion();
|
|
839
|
+
if (completedReplay && !sameSnapshotFrameHeader(completed.header, header)) {
|
|
840
|
+
throw new SnapshotImportConflictError(input.identity, input.ownerId, "completed snapshot header changed");
|
|
841
|
+
}
|
|
842
|
+
if (!completedReplay && this.#core.getCurrentManifestVersion() !== null) {
|
|
843
|
+
throw new Error("Snapshot store already holds a database");
|
|
844
|
+
}
|
|
845
|
+
if (!completedReplay && this.#blocks.size > 0) {
|
|
846
|
+
throw new StorageCorruptionError("memory", "snapshot-import", "empty target contains unowned payloads");
|
|
847
|
+
}
|
|
848
|
+
const session = {
|
|
849
|
+
identity: input.identity,
|
|
850
|
+
ownerId: input.ownerId,
|
|
851
|
+
version: header.databaseVersion,
|
|
852
|
+
createdAt: input.createdAt,
|
|
853
|
+
expiresAt: input.expiresAt,
|
|
854
|
+
nextSequence: 0,
|
|
855
|
+
stagedBytes: 0,
|
|
856
|
+
};
|
|
857
|
+
this.#snapshotFrameImport = {
|
|
858
|
+
session,
|
|
859
|
+
header,
|
|
860
|
+
metadataFrames: completedReplay ? this.#captureSnapshotMetadataFrames() : [],
|
|
861
|
+
observed: emptySnapshotObservedSummaries(),
|
|
862
|
+
checksum: 0,
|
|
863
|
+
itemCount: 0,
|
|
864
|
+
previousKindIndex: 0,
|
|
865
|
+
lastBlockId: null,
|
|
866
|
+
lastBatchFrames: [],
|
|
867
|
+
completedReplay,
|
|
868
|
+
};
|
|
869
|
+
return structuredClone(session);
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
async renewSnapshotFrameImport(input) {
|
|
873
|
+
validateSnapshotSessionLifetime(input.expiresAtCutoff, input.expiresAt);
|
|
874
|
+
return this.#runAtomic(() => {
|
|
875
|
+
const state = this.#requireLiveSnapshotFrameImport(input);
|
|
876
|
+
state.session = { ...state.session, expiresAt: input.expiresAt };
|
|
877
|
+
return structuredClone(state.session);
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
async appendSnapshotImportFrames(input) {
|
|
881
|
+
validateSnapshotSessionLifetime(input.expiresAtCutoff, input.expiresAt);
|
|
882
|
+
if (input.frames.length < 1 || input.frames.length > MAX_SNAPSHOT_FRAME_BATCH_ITEMS) {
|
|
883
|
+
throw new RangeError("Snapshot import frame batch item count is out of bounds");
|
|
884
|
+
}
|
|
885
|
+
let batchBytes = 0;
|
|
886
|
+
let metadataBatchBytes = 0;
|
|
887
|
+
for (const frame of input.frames) {
|
|
888
|
+
batchBytes = safeSnapshotByteSum(batchBytes, frame.payload.byteLength);
|
|
889
|
+
if (batchBytes > MAX_SNAPSHOT_FRAME_BATCH_BYTES) {
|
|
890
|
+
throw new RangeError("Snapshot import frame batch exceeds its byte limit");
|
|
891
|
+
}
|
|
892
|
+
if (frame.kind !== "block") {
|
|
893
|
+
metadataBatchBytes = safeSnapshotByteSum(metadataBatchBytes, frame.payload.byteLength);
|
|
894
|
+
if (metadataBatchBytes > MAX_SNAPSHOT_METADATA_BATCH_BYTES) {
|
|
895
|
+
throw new RangeError("Snapshot metadata frame batch exceeds its byte limit");
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return this.#runAtomic(() => {
|
|
900
|
+
const state = this.#requireLiveSnapshotFrameImport(input);
|
|
901
|
+
if (state.lastBatchFrames.length === input.frames.length &&
|
|
902
|
+
input.frames[0]?.sequence === state.session.nextSequence - state.lastBatchFrames.length) {
|
|
903
|
+
if (!state.lastBatchFrames.every((frame, index) => sameSnapshotFrame(frame, input.frames[index]))) {
|
|
904
|
+
throw new SnapshotImportConflictError(input.identity, input.ownerId, "replayed snapshot frame bytes changed");
|
|
905
|
+
}
|
|
906
|
+
state.session = { ...state.session, expiresAt: input.expiresAt };
|
|
907
|
+
return structuredClone(state.session);
|
|
908
|
+
}
|
|
909
|
+
if (input.frames[0]?.sequence !== state.session.nextSequence) {
|
|
910
|
+
throw new RangeError("Snapshot import frame batch is not the next contiguous sequence");
|
|
911
|
+
}
|
|
912
|
+
const observed = structuredClone(state.observed);
|
|
913
|
+
let checksum = state.checksum;
|
|
914
|
+
let itemCount = state.itemCount;
|
|
915
|
+
let previousKindIndex = state.previousKindIndex;
|
|
916
|
+
let lastBlockId = state.lastBlockId;
|
|
917
|
+
let nextSequence = state.session.nextSequence;
|
|
918
|
+
let stagedBytes = state.session.stagedBytes;
|
|
919
|
+
const metadataFrames = [];
|
|
920
|
+
const blockFrames = [];
|
|
921
|
+
for (const frame of input.frames) {
|
|
922
|
+
if (frame.sequence !== nextSequence || crc32(frame.payload) !== frame.checksum) {
|
|
923
|
+
throw new Error("Snapshot import frame sequence or checksum is invalid");
|
|
924
|
+
}
|
|
925
|
+
const envelope = snapshotFrameEnvelopeParts(frame);
|
|
926
|
+
const kindIndex = SNAPSHOT_FRAME_KINDS.indexOf(frame.kind);
|
|
927
|
+
if (kindIndex < previousKindIndex)
|
|
928
|
+
throw new Error("Snapshot frame kinds are out of order");
|
|
929
|
+
previousKindIndex = kindIndex;
|
|
930
|
+
if (frame.kind === "block") {
|
|
931
|
+
if (frame.key === null || (lastBlockId !== null && frame.key <= lastBlockId)) {
|
|
932
|
+
throw new Error("Snapshot block frames are not in canonical ID order");
|
|
933
|
+
}
|
|
934
|
+
verifyStoredBlock(frame.payload);
|
|
935
|
+
if (state.completedReplay) {
|
|
936
|
+
const current = this.#blocks.get(frame.key);
|
|
937
|
+
if (current === undefined || !sameSnapshotBytes(current, frame.payload)) {
|
|
938
|
+
throw new SnapshotImportConflictError(input.identity, input.ownerId, `completed snapshot block changed: ${frame.key}`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
else
|
|
942
|
+
blockFrames.push(copySnapshotFrame(frame));
|
|
943
|
+
lastBlockId = frame.key;
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
if (frame.itemCount !== 1) {
|
|
947
|
+
throw new TypeError("Snapshot v1 metadata frames contain exactly one item");
|
|
948
|
+
}
|
|
949
|
+
const items = decodeSnapshotMetadataItems(frame.kind, frame.payload);
|
|
950
|
+
if (items.length !== frame.itemCount) {
|
|
951
|
+
throw new Error("Snapshot metadata item count disagrees with its frame");
|
|
952
|
+
}
|
|
953
|
+
if (state.completedReplay) {
|
|
954
|
+
const expected = state.metadataFrames[frame.sequence];
|
|
955
|
+
if (!sameSnapshotFrame(expected, frame)) {
|
|
956
|
+
throw new SnapshotImportConflictError(input.identity, input.ownerId, "completed snapshot metadata changed");
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
else
|
|
960
|
+
metadataFrames.push(copySnapshotFrame(frame));
|
|
961
|
+
}
|
|
962
|
+
checksum = extendSnapshotFrameStreamChecksum(checksum, envelope);
|
|
963
|
+
itemCount = safeSnapshotByteSum(itemCount, frame.itemCount);
|
|
964
|
+
stagedBytes = safeSnapshotByteSum(stagedBytes, frame.payload.byteLength);
|
|
965
|
+
const summary = observed[frame.kind];
|
|
966
|
+
summary.frameCount += 1;
|
|
967
|
+
summary.itemCount = safeSnapshotByteSum(summary.itemCount, frame.itemCount);
|
|
968
|
+
summary.storedBytes = safeSnapshotByteSum(summary.storedBytes, frame.payload.byteLength);
|
|
969
|
+
const expected = state.header.kinds[frame.kind];
|
|
970
|
+
if (summary.frameCount > expected.frameCount ||
|
|
971
|
+
summary.itemCount > expected.itemCount ||
|
|
972
|
+
summary.storedBytes > expected.storedBytes) {
|
|
973
|
+
throw new Error(`Snapshot ${frame.kind} frames exceed their header summary`);
|
|
974
|
+
}
|
|
975
|
+
nextSequence += 1;
|
|
976
|
+
}
|
|
977
|
+
for (const frame of metadataFrames)
|
|
978
|
+
state.metadataFrames.push(frame);
|
|
979
|
+
for (const frame of blockFrames) {
|
|
980
|
+
if (frame.key === null)
|
|
981
|
+
throw new Error("Snapshot block frame lost its ID");
|
|
982
|
+
this.#putBlock(frame.key, frame.payload);
|
|
983
|
+
}
|
|
984
|
+
state.observed = observed;
|
|
985
|
+
state.checksum = checksum;
|
|
986
|
+
state.itemCount = itemCount;
|
|
987
|
+
state.previousKindIndex = previousKindIndex;
|
|
988
|
+
state.lastBlockId = lastBlockId;
|
|
989
|
+
state.lastBatchFrames = input.frames.map(copySnapshotFrame);
|
|
990
|
+
state.session = {
|
|
991
|
+
...state.session,
|
|
992
|
+
expiresAt: input.expiresAt,
|
|
993
|
+
nextSequence,
|
|
994
|
+
stagedBytes,
|
|
995
|
+
};
|
|
996
|
+
return structuredClone(state.session);
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
async finishSnapshotFrameImport(input) {
|
|
1000
|
+
encodeSnapshotFrameStreamFooter(input.footer);
|
|
1001
|
+
return this.#runAtomic(() => {
|
|
1002
|
+
const state = this.#requireLiveSnapshotFrameImport(input);
|
|
1003
|
+
const expectedFrameCount = snapshotFrameCount(state.header);
|
|
1004
|
+
if (input.footer.frameCount !== expectedFrameCount ||
|
|
1005
|
+
input.footer.frameCount !== state.session.nextSequence ||
|
|
1006
|
+
input.footer.itemCount !== state.itemCount ||
|
|
1007
|
+
input.footer.storedBytes !== state.session.stagedBytes ||
|
|
1008
|
+
input.footer.checksum !== state.checksum) {
|
|
1009
|
+
throw new Error("Snapshot import footer does not match its staged frames");
|
|
1010
|
+
}
|
|
1011
|
+
for (const kind of SNAPSHOT_FRAME_KINDS) {
|
|
1012
|
+
const actual = state.observed[kind];
|
|
1013
|
+
const expected = state.header.kinds[kind];
|
|
1014
|
+
if (actual.frameCount !== expected.frameCount ||
|
|
1015
|
+
actual.itemCount !== expected.itemCount ||
|
|
1016
|
+
actual.storedBytes !== expected.storedBytes) {
|
|
1017
|
+
throw new Error(`Snapshot import ${kind} generation is incomplete`);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (!state.completedReplay) {
|
|
1021
|
+
const items = function* () {
|
|
1022
|
+
for (const frame of state.metadataFrames) {
|
|
1023
|
+
if (frame.kind === "block")
|
|
1024
|
+
continue;
|
|
1025
|
+
yield* decodeSnapshotMetadataItems(frame.kind, frame.payload);
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
const blocks = function* (store) {
|
|
1029
|
+
for (const [blockId, bytes] of store.#blocks) {
|
|
1030
|
+
yield { blockId, byteLength: bytes.byteLength, checksum: crc32(bytes) };
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
this.#core.loadSnapshotFrameItems(state.header, items(), blocks(this));
|
|
1034
|
+
}
|
|
1035
|
+
this.#completedSnapshotFrameImport = {
|
|
1036
|
+
identity: state.session.identity,
|
|
1037
|
+
version: state.header.databaseVersion,
|
|
1038
|
+
header: structuredClone(state.header),
|
|
1039
|
+
};
|
|
1040
|
+
this.#snapshotFrameImport = undefined;
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
async cancelSnapshotFrameImport(input) {
|
|
1044
|
+
return this.#runAtomic(() => {
|
|
1045
|
+
const state = this.#snapshotFrameImport;
|
|
1046
|
+
if (state?.session.identity !== input.identity || state.session.ownerId !== input.ownerId) {
|
|
1047
|
+
throw new SnapshotImportConflictError(input.identity, input.ownerId, "session is missing or owned by another caller");
|
|
1048
|
+
}
|
|
1049
|
+
const result = {
|
|
1050
|
+
identity: input.identity,
|
|
1051
|
+
removedBlockCount: state.completedReplay ? 0 : state.observed.block.itemCount,
|
|
1052
|
+
removedBytes: state.completedReplay ? 0 : state.observed.block.storedBytes,
|
|
1053
|
+
};
|
|
1054
|
+
if (!state.completedReplay)
|
|
1055
|
+
this.#clearPhysicalBlocks();
|
|
1056
|
+
this.#snapshotFrameImport = undefined;
|
|
1057
|
+
return result;
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
#requireLiveSnapshotFrameImport(input) {
|
|
1061
|
+
const state = this.#snapshotFrameImport;
|
|
1062
|
+
if (state?.session.identity !== input.identity || state.session.ownerId !== input.ownerId) {
|
|
1063
|
+
throw new SnapshotImportConflictError(input.identity, input.ownerId, "session is missing or owned by another caller");
|
|
1064
|
+
}
|
|
1065
|
+
if (Date.parse(state.session.expiresAt) <= Date.parse(input.expiresAtCutoff)) {
|
|
1066
|
+
throw new SnapshotImportConflictError(input.identity, input.ownerId, "session is expired");
|
|
1067
|
+
}
|
|
1068
|
+
return state;
|
|
1069
|
+
}
|
|
1070
|
+
#captureSnapshotMetadataFrames() {
|
|
1071
|
+
const frames = [];
|
|
1072
|
+
const append = (item) => {
|
|
1073
|
+
const payload = encodeSnapshotMetadataPage([item]);
|
|
1074
|
+
frames.push({
|
|
1075
|
+
sequence: frames.length,
|
|
1076
|
+
kind: snapshotMetadataFrameKind(item),
|
|
1077
|
+
itemCount: 1,
|
|
1078
|
+
key: null,
|
|
1079
|
+
payload,
|
|
1080
|
+
checksum: crc32(payload),
|
|
1081
|
+
});
|
|
1082
|
+
};
|
|
1083
|
+
for (const item of this.#core.snapshotFrameMetadataItems())
|
|
1084
|
+
append(item);
|
|
1085
|
+
for (const item of this.#core.snapshotFramePostingItems())
|
|
1086
|
+
append(item);
|
|
1087
|
+
return frames;
|
|
1088
|
+
}
|
|
1089
|
+
async inspectInterruptedImport() {
|
|
1090
|
+
const framed = this.#snapshotFrameImport;
|
|
1091
|
+
if (framed !== undefined) {
|
|
1092
|
+
return {
|
|
1093
|
+
identity: framed.session.identity,
|
|
1094
|
+
version: framed.session.version,
|
|
1095
|
+
createdAt: framed.session.createdAt,
|
|
1096
|
+
stagedBlockCount: framed.observed.block.itemCount,
|
|
1097
|
+
stagedBytes: framed.session.stagedBytes,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
async abortInterruptedImport(identity) {
|
|
1103
|
+
return this.#runAtomic(() => {
|
|
1104
|
+
const framed = this.#snapshotFrameImport;
|
|
1105
|
+
if (framed?.session.identity === identity) {
|
|
1106
|
+
const result = {
|
|
1107
|
+
identity,
|
|
1108
|
+
removedBlockCount: framed.completedReplay ? 0 : framed.observed.block.itemCount,
|
|
1109
|
+
removedBytes: framed.completedReplay ? 0 : framed.observed.block.storedBytes,
|
|
1110
|
+
};
|
|
1111
|
+
if (!framed.completedReplay)
|
|
1112
|
+
this.#clearPhysicalBlocks();
|
|
1113
|
+
this.#snapshotFrameImport = undefined;
|
|
1114
|
+
return result;
|
|
1115
|
+
}
|
|
1116
|
+
throw new Error(`Interrupted snapshot import not found: ${identity}`);
|
|
1117
|
+
});
|
|
343
1118
|
}
|
|
344
1119
|
/** Block and spill bytes held; the record maps are negligible beside them. */
|
|
345
1120
|
async getLogicalStorageBytes() {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
1121
|
+
return this.#physicalBlockBytes + this.#tempBytesTotal;
|
|
1122
|
+
}
|
|
1123
|
+
async getStorageStats() {
|
|
1124
|
+
return this.#runAtomic(() => {
|
|
1125
|
+
const current = this.#core.getCurrentManifest();
|
|
1126
|
+
const temporaryBytes = this.#tempBytesTotal;
|
|
1127
|
+
const manifestCount = countRecordPages((cursor) => this.#core.listManifestPage(cursor, 1_024));
|
|
1128
|
+
const transactionCount = countRecordPages((cursor) => this.#core.listTransactionPage(cursor, 1_024));
|
|
1129
|
+
const segmentCount = countRecordPages((cursor) => this.#core.listSegmentPage(cursor, 1_024));
|
|
1130
|
+
const liveBlockCount = current?.liveBlockCount ?? 0;
|
|
1131
|
+
const liveBlockBytes = current?.liveBlockBytes ?? 0;
|
|
1132
|
+
if (liveBlockCount > this.#blocks.size || liveBlockBytes > this.#physicalBlockBytes) {
|
|
1133
|
+
throw new StorageCorruptionError("memory", "manifest", "live block summary exceeds physical payloads");
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
backend: "memory",
|
|
1137
|
+
logicalBytes: this.#physicalBlockBytes + temporaryBytes,
|
|
1138
|
+
physicalBytes: this.#physicalBlockBytes + temporaryBytes,
|
|
1139
|
+
liveBlockCount,
|
|
1140
|
+
obsoleteBlockCount: this.#blocks.size - liveBlockCount,
|
|
1141
|
+
liveBlockBytes,
|
|
1142
|
+
obsoleteBlockBytes: this.#physicalBlockBytes - liveBlockBytes,
|
|
1143
|
+
temporaryBytes,
|
|
1144
|
+
walBytes: null,
|
|
1145
|
+
checkpointBytes: null,
|
|
1146
|
+
orphanBytes: null,
|
|
1147
|
+
manifestCount,
|
|
1148
|
+
transactionCount,
|
|
1149
|
+
segmentCount,
|
|
1150
|
+
};
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
#putBlock(id, bytes) {
|
|
1154
|
+
const stored = new Uint8Array(bytes);
|
|
1155
|
+
const previous = this.#blocks.get(id);
|
|
1156
|
+
this.#physicalBlockBytes += stored.byteLength - (previous?.byteLength ?? 0);
|
|
1157
|
+
this.#blocks.set(id, stored);
|
|
1158
|
+
this.#blockChecksums.set(id, crc32(stored));
|
|
1159
|
+
}
|
|
1160
|
+
#deleteBlock(id) {
|
|
1161
|
+
this.#physicalBlockBytes -= this.#blocks.get(id)?.byteLength ?? 0;
|
|
1162
|
+
this.#blocks.delete(id);
|
|
1163
|
+
this.#blockChecksums.delete(id);
|
|
1164
|
+
}
|
|
1165
|
+
#clearPhysicalBlocks() {
|
|
1166
|
+
this.#blocks.clear();
|
|
1167
|
+
this.#blockChecksums.clear();
|
|
1168
|
+
this.#physicalBlockBytes = 0;
|
|
352
1169
|
}
|
|
353
1170
|
close() {
|
|
354
1171
|
// The in-memory implementation owns no external resources.
|
|
@@ -362,6 +1179,10 @@ export class MemoryBlockStore {
|
|
|
362
1179
|
});
|
|
363
1180
|
this.#commitQueue = this.#commitQueue.then(() => {
|
|
364
1181
|
try {
|
|
1182
|
+
if (this.#completedSnapshotFrameImport !== undefined &&
|
|
1183
|
+
this.#core.getCurrentManifestVersion() !== this.#completedSnapshotFrameImport.version) {
|
|
1184
|
+
this.#completedSnapshotFrameImport = undefined;
|
|
1185
|
+
}
|
|
365
1186
|
resolveResult(operation());
|
|
366
1187
|
}
|
|
367
1188
|
catch (error) {
|
|
@@ -377,4 +1198,97 @@ function tempRunPageKey(ownerId, runId, pageIndex) {
|
|
|
377
1198
|
function tempRunPagePrefix(ownerId, runId) {
|
|
378
1199
|
return `${String(ownerId.length)}:${ownerId}:${String(runId.length)}:${runId}:`;
|
|
379
1200
|
}
|
|
380
|
-
|
|
1201
|
+
function countRecordPages(read) {
|
|
1202
|
+
let count = 0;
|
|
1203
|
+
let cursor = null;
|
|
1204
|
+
do {
|
|
1205
|
+
const page = read(cursor);
|
|
1206
|
+
count += page.records.length;
|
|
1207
|
+
if (!Number.isSafeInteger(count))
|
|
1208
|
+
throw new RangeError("Storage record count overflow");
|
|
1209
|
+
cursor = page.nextCursor;
|
|
1210
|
+
} while (cursor !== null);
|
|
1211
|
+
return count;
|
|
1212
|
+
}
|
|
1213
|
+
function assertTempResourceLimit(resource, actual, limit) {
|
|
1214
|
+
if (!Number.isSafeInteger(actual) || actual < 0 || actual > limit) {
|
|
1215
|
+
throw new StorageResourceLimitError(resource, actual, limit);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
function validateSnapshotSessionLifetime(cutoff, expiresAt) {
|
|
1219
|
+
const cutoffMs = Date.parse(cutoff);
|
|
1220
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
1221
|
+
if (!Number.isFinite(cutoffMs) || !Number.isFinite(expiresAtMs)) {
|
|
1222
|
+
throw new TypeError("Snapshot session timestamps are invalid");
|
|
1223
|
+
}
|
|
1224
|
+
if (expiresAtMs <= cutoffMs) {
|
|
1225
|
+
throw new RangeError("Snapshot session expiration must be after its cutoff");
|
|
1226
|
+
}
|
|
1227
|
+
if (expiresAtMs - cutoffMs > MAX_SNAPSHOT_SESSION_TTL_MS) {
|
|
1228
|
+
throw new RangeError("Snapshot session exceeds the maximum lifetime");
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
function validatePostingBuildLifetime(cutoff, expiresAt) {
|
|
1232
|
+
const cutoffMs = Date.parse(cutoff);
|
|
1233
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
1234
|
+
if (!Number.isFinite(cutoffMs) || !Number.isFinite(expiresAtMs)) {
|
|
1235
|
+
throw new TypeError("Posting build timestamps are invalid");
|
|
1236
|
+
}
|
|
1237
|
+
if (expiresAtMs <= cutoffMs || expiresAtMs - cutoffMs > MAX_POSTING_BUILD_TTL_MS) {
|
|
1238
|
+
throw new RangeError("Posting build expiration is outside its bounded lifetime");
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
function samePostingChunk(left, right) {
|
|
1242
|
+
return (left.length === right.length &&
|
|
1243
|
+
left.every((posting, postingIndex) => {
|
|
1244
|
+
const other = right[postingIndex];
|
|
1245
|
+
return (other?.term === posting.term &&
|
|
1246
|
+
posting.rowIds.length === other.rowIds.length &&
|
|
1247
|
+
posting.rowIds.every((rowId, index) => rowId === other.rowIds[index]) &&
|
|
1248
|
+
posting.tf.length === other.tf.length &&
|
|
1249
|
+
posting.tf.every((frequency, index) => frequency === other.tf[index]));
|
|
1250
|
+
}));
|
|
1251
|
+
}
|
|
1252
|
+
function safeSnapshotByteSum(total, byteLength) {
|
|
1253
|
+
const result = total + byteLength;
|
|
1254
|
+
if (!Number.isSafeInteger(result) || result < 0) {
|
|
1255
|
+
throw new RangeError("Snapshot payload is too large");
|
|
1256
|
+
}
|
|
1257
|
+
return result;
|
|
1258
|
+
}
|
|
1259
|
+
function emptySnapshotObservedSummaries() {
|
|
1260
|
+
return Object.fromEntries(SNAPSHOT_FRAME_KINDS.map((kind) => [kind, { frameCount: 0, itemCount: 0, storedBytes: 0 }]));
|
|
1261
|
+
}
|
|
1262
|
+
function snapshotMetadataFrameKind(item) {
|
|
1263
|
+
if (item.kind === "table")
|
|
1264
|
+
return "catalog-page";
|
|
1265
|
+
if (item.kind === "segment")
|
|
1266
|
+
return "segment-page";
|
|
1267
|
+
if (item.kind === "transaction")
|
|
1268
|
+
return "transaction-page";
|
|
1269
|
+
if (item.kind === "unique-generation" || item.kind === "unique-chunk")
|
|
1270
|
+
return "unique-page";
|
|
1271
|
+
return "posting-page";
|
|
1272
|
+
}
|
|
1273
|
+
function copySnapshotFrame(frame) {
|
|
1274
|
+
return { ...frame, payload: frame.payload.slice() };
|
|
1275
|
+
}
|
|
1276
|
+
function sameSnapshotFrameHeader(left, right) {
|
|
1277
|
+
return sameSnapshotBytes(encodeSnapshotFrameStreamHeader(left), encodeSnapshotFrameStreamHeader(right));
|
|
1278
|
+
}
|
|
1279
|
+
function sameSnapshotFrame(left, right) {
|
|
1280
|
+
if (left?.sequence === undefined || right?.sequence === undefined)
|
|
1281
|
+
return false;
|
|
1282
|
+
return (left.sequence === right.sequence &&
|
|
1283
|
+
left.kind === right.kind &&
|
|
1284
|
+
left.itemCount === right.itemCount &&
|
|
1285
|
+
left.key === right.key &&
|
|
1286
|
+
left.checksum === right.checksum &&
|
|
1287
|
+
sameSnapshotBytes(left.payload, right.payload));
|
|
1288
|
+
}
|
|
1289
|
+
function snapshotFrameCount(header) {
|
|
1290
|
+
return SNAPSHOT_FRAME_KINDS.reduce((total, kind) => safeSnapshotByteSum(total, header.kinds[kind].frameCount), 0);
|
|
1291
|
+
}
|
|
1292
|
+
function sameSnapshotBytes(left, right) {
|
|
1293
|
+
return left.byteLength === right.byteLength && left.every((byte, index) => right[index] === byte);
|
|
1294
|
+
}
|