@minnowdb/core 0.3.0 → 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 +4 -2
- 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 +0 -1
- package/dist/engine/artifact-cache.js +0 -1
- package/dist/engine/batch.d.ts +5 -2
- package/dist/engine/batch.js +0 -1
- 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 +5 -6
- package/dist/engine/catalog.js +5 -5
- package/dist/engine/client.d.ts +27 -7
- package/dist/engine/client.js +197 -31
- package/dist/engine/database.d.ts +161 -38
- package/dist/engine/database.js +4320 -1523
- package/dist/engine/defaults.d.ts +0 -1
- package/dist/engine/defaults.js +2 -4
- 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 +39 -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 -11
- package/dist/engine/index.js +12 -10
- 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 +499 -129
- 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 +4 -4
- package/dist/engine/query-api.d.ts +3 -0
- package/dist/engine/query-api.js +3 -0
- package/dist/engine/query-cache.d.ts +0 -1
- package/dist/engine/query-cache.js +4 -4
- package/dist/engine/query.d.ts +4 -7
- package/dist/engine/query.js +348 -155
- package/dist/engine/result-wire.d.ts +0 -1
- package/dist/engine/result-wire.js +2 -2
- package/dist/engine/schema-wire.d.ts +1 -2
- package/dist/engine/schema-wire.js +0 -1
- package/dist/engine/schema.d.ts +3 -8
- package/dist/engine/schema.js +41 -24
- package/dist/engine/sort-keys.d.ts +0 -1
- package/dist/engine/sort-keys.js +2 -2
- package/dist/engine/sql-domains.d.ts +6 -1
- package/dist/engine/sql-domains.js +209 -45
- package/dist/engine/sql-driver.d.ts +2 -2
- package/dist/engine/sql-driver.js +0 -1
- package/dist/engine/sql-json.d.ts +0 -1
- package/dist/engine/sql-json.js +53 -7
- package/dist/engine/sql-semantics.d.ts +8 -5
- package/dist/engine/sql-semantics.js +427 -65
- package/dist/engine/typed-live.d.ts +67 -0
- package/dist/engine/typed-live.js +349 -0
- package/dist/engine/vector.d.ts +7 -1
- package/dist/engine/vector.js +202 -58
- package/dist/engine/worker-host.d.ts +8 -2
- package/dist/engine/worker-host.js +616 -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 +0 -1
- package/dist/plan/index.js +0 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/storage/indexeddb.d.ts +74 -65
- package/dist/storage/indexeddb.js +12301 -2076
- package/dist/storage/memory.d.ts +56 -66
- package/dist/storage/memory.js +1004 -139
- 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 +403 -91
- package/dist/storage/opfs/leader.js +4306 -602
- package/dist/storage/opfs/rpc.d.ts +15 -3
- package/dist/storage/opfs/rpc.js +223 -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 +26 -121
- package/dist/storage/opfs/store.js +436 -388
- 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 -99
- package/dist/storage/snapshot.js +33 -309
- package/dist/storage/toolkit/extents.d.ts +41 -7
- package/dist/storage/toolkit/extents.js +402 -39
- package/dist/storage/toolkit/index.d.ts +3 -4
- package/dist/storage/toolkit/index.js +27 -3
- package/dist/storage/toolkit/record-core.d.ts +174 -61
- package/dist/storage/toolkit/record-core.js +5401 -849
- 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 +4 -6
- package/dist/storage/toolkit/wire.js +80 -25
- package/dist/storage/types.d.ts +1271 -218
- package/dist/storage/types.js +1471 -138
- package/dist/testing/block-store-conformance.d.ts +0 -1
- package/dist/testing/block-store-conformance.js +877 -138
- package/dist/testing/index.d.ts +42 -31
- package/dist/testing/index.js +100 -68
- package/dist/testing/opfs-shim.d.ts +13 -3
- package/dist/testing/opfs-shim.js +40 -6
- package/dist/testing/simulator.d.ts +0 -1
- package/dist/testing/simulator.js +43 -12
- package/dist/testing/sqllogictest.d.ts +0 -1
- package/dist/testing/sqllogictest.js +0 -1
- package/dist/transactions/index.d.ts +81 -20
- package/dist/transactions/index.js +1050 -268
- package/dist/worker-protocol/index.d.ts +3 -2
- package/dist/worker-protocol/index.js +2 -5
- package/package.json +49 -1
- 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-domains.d.ts.map +0 -1
- package/dist/engine/sql-domains.js.map +0 -1
- package/dist/engine/sql-driver.d.ts.map +0 -1
- package/dist/engine/sql-driver.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/testing/simulator.d.ts.map +0 -1
- package/dist/testing/simulator.js.map +0 -1
- package/dist/testing/sqllogictest.d.ts.map +0 -1
- package/dist/testing/sqllogictest.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,5 +1,8 @@
|
|
|
1
|
-
import { activePostingStorageColumnIds, } from "./types.js";
|
|
2
|
-
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";
|
|
3
6
|
/**
|
|
4
7
|
* The in-process store: record semantics live in `RecordCore` (shared with the OPFS store),
|
|
5
8
|
* block and temp-page bytes live in Maps here, and atomicity comes from running every mutating
|
|
@@ -8,54 +11,71 @@ import { RecordCore, validateId, validateTempRunPage, validateTempRunPageIdentit
|
|
|
8
11
|
*/
|
|
9
12
|
export class MemoryBlockStore {
|
|
10
13
|
#blocks = new Map();
|
|
14
|
+
#blockChecksums = new Map();
|
|
15
|
+
#physicalBlockBytes = 0;
|
|
11
16
|
#tempRunPages = new Map();
|
|
17
|
+
#tempUsageByOwner = new Map();
|
|
18
|
+
#tempPageKeysByRun = new Map();
|
|
19
|
+
#tempBytesTotal = 0;
|
|
20
|
+
#tempPagesTotal = 0;
|
|
21
|
+
#tempRunsTotal = 0;
|
|
12
22
|
#ftsBaseBuilds = new Map();
|
|
23
|
+
#snapshotFrameExport;
|
|
24
|
+
#snapshotFrameImport;
|
|
25
|
+
#completedSnapshotFrameImport;
|
|
13
26
|
#core = new RecordCore({
|
|
14
27
|
hasBlock: (id) => this.#blocks.has(id),
|
|
15
28
|
blockByteLength: (id) => this.#blocks.get(id)?.byteLength,
|
|
29
|
+
blockChecksum: (id) => this.#blockChecksums.get(id),
|
|
16
30
|
});
|
|
17
31
|
#commitQueue = Promise.resolve();
|
|
18
|
-
async addBlock(id, bytes) {
|
|
19
|
-
return this.addBlocks([{ id, bytes }]);
|
|
20
|
-
}
|
|
21
|
-
async addBlocks(blocks) {
|
|
22
|
-
const ids = new Set();
|
|
23
|
-
for (const block of blocks) {
|
|
24
|
-
validateId(block.id);
|
|
25
|
-
if (ids.has(block.id) || this.#blocks.has(block.id)) {
|
|
26
|
-
throw new Error(`Block already exists: ${block.id}`);
|
|
27
|
-
}
|
|
28
|
-
ids.add(block.id);
|
|
29
|
-
}
|
|
30
|
-
for (const block of blocks)
|
|
31
|
-
this.#blocks.set(block.id, new Uint8Array(block.bytes));
|
|
32
|
-
}
|
|
33
32
|
async getBlock(id) {
|
|
33
|
+
validateStorageId(id, "Block ID");
|
|
34
34
|
const bytes = this.#blocks.get(id);
|
|
35
35
|
return bytes === undefined ? undefined : new Uint8Array(bytes);
|
|
36
36
|
}
|
|
37
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
|
+
}
|
|
38
48
|
return ids.map((id) => {
|
|
39
49
|
const bytes = this.#blocks.get(id);
|
|
40
50
|
return bytes === undefined ? undefined : new Uint8Array(bytes);
|
|
41
51
|
});
|
|
42
52
|
}
|
|
43
|
-
async
|
|
44
|
-
|
|
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
|
+
});
|
|
45
64
|
}
|
|
46
|
-
async
|
|
47
|
-
|
|
65
|
+
async hasManifestBlocks(version, ids) {
|
|
66
|
+
assertStorageBulkReadItems(ids, "Manifest membership batch");
|
|
67
|
+
return this.#runAtomic(() => this.#core.hasManifestBlocks(version, ids));
|
|
48
68
|
}
|
|
49
69
|
async putTempRunPage(page) {
|
|
70
|
+
assertTempRunPageBatchLimits([page]);
|
|
50
71
|
validateTempRunPage(page);
|
|
51
|
-
this.#
|
|
72
|
+
return this.#runAtomic(() => this.#putTempRunPages([page]));
|
|
52
73
|
}
|
|
53
74
|
async putTempRunPages(pages) {
|
|
75
|
+
assertTempRunPageBatchLimits(pages);
|
|
54
76
|
for (const page of pages)
|
|
55
77
|
validateTempRunPage(page);
|
|
56
|
-
|
|
57
|
-
this.#tempRunPages.set(tempRunPageKey(page.ownerId, page.runId, page.pageIndex), page.bytes.slice());
|
|
58
|
-
}
|
|
78
|
+
return this.#runAtomic(() => this.#putTempRunPages(pages));
|
|
59
79
|
}
|
|
60
80
|
async getTempRunPage(ownerId, runId, pageIndex) {
|
|
61
81
|
validateTempRunPageIdentity(ownerId, runId, pageIndex);
|
|
@@ -63,15 +83,14 @@ export class MemoryBlockStore {
|
|
|
63
83
|
}
|
|
64
84
|
async removeTempRun(ownerId, runId) {
|
|
65
85
|
validateTempRunPageIdentity(ownerId, runId, 0);
|
|
66
|
-
|
|
67
|
-
for (const key of this.#tempRunPages.keys())
|
|
68
|
-
if (key.startsWith(prefix))
|
|
69
|
-
this.#tempRunPages.delete(key);
|
|
86
|
+
return this.#runAtomic(() => this.#removeTempRunPages(ownerId, runId));
|
|
70
87
|
}
|
|
71
88
|
async removeTempOwner(ownerId) {
|
|
72
89
|
validateId(ownerId);
|
|
73
|
-
this.#
|
|
74
|
-
|
|
90
|
+
return this.#runAtomic(() => {
|
|
91
|
+
this.#removeTempOwnerPages(ownerId);
|
|
92
|
+
this.#core.removeTempOwner(ownerId);
|
|
93
|
+
});
|
|
75
94
|
}
|
|
76
95
|
async createTempOwner(record) {
|
|
77
96
|
return this.#runAtomic(() => {
|
|
@@ -81,8 +100,8 @@ export class MemoryBlockStore {
|
|
|
81
100
|
async getTempOwner(ownerId) {
|
|
82
101
|
return this.#core.getTempOwner(ownerId);
|
|
83
102
|
}
|
|
84
|
-
async renewTempOwner(
|
|
85
|
-
return this.#runAtomic(() => this.#core.renewTempOwner(
|
|
103
|
+
async renewTempOwner(input) {
|
|
104
|
+
return this.#runAtomic(() => this.#core.renewTempOwner(input));
|
|
86
105
|
}
|
|
87
106
|
async removeTempOwnerIfExpired(ownerId, expiresAtCutoff) {
|
|
88
107
|
return this.#runAtomic(() => {
|
|
@@ -101,26 +120,163 @@ export class MemoryBlockStore {
|
|
|
101
120
|
}
|
|
102
121
|
return this.#core.listTempOwnerIdsPage(afterOwnerId, limit, pageOwnerIds);
|
|
103
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
|
+
}
|
|
104
218
|
#removeTempOwnerPages(ownerId) {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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);
|
|
109
224
|
}
|
|
110
|
-
async addTable(record) {
|
|
111
|
-
this.#core.addTable(record);
|
|
225
|
+
async addTable(record, options = {}) {
|
|
226
|
+
this.#core.addTable(record, options);
|
|
112
227
|
}
|
|
113
228
|
async getTable(id) {
|
|
114
229
|
return this.#core.getTable(id);
|
|
115
230
|
}
|
|
116
231
|
async updateTable(id, expectedRevision, update) {
|
|
117
|
-
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
|
+
});
|
|
118
245
|
}
|
|
119
|
-
async
|
|
246
|
+
async dropTable(input) {
|
|
120
247
|
return this.#runAtomic(() => {
|
|
121
|
-
this.#core.
|
|
248
|
+
const summary = this.#core.dropTable(input);
|
|
249
|
+
this.#removeTablePostingBuilds(input.tableId);
|
|
250
|
+
return summary;
|
|
122
251
|
});
|
|
123
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;
|
|
258
|
+
});
|
|
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
|
+
}
|
|
124
280
|
async writeFtsBase(tableId, columnId, input) {
|
|
125
281
|
return this.#runAtomic(() => {
|
|
126
282
|
this.#core.writeFtsBase(tableId, columnId, input);
|
|
@@ -132,31 +288,82 @@ export class MemoryBlockStore {
|
|
|
132
288
|
this.#ftsBaseBuilds.delete(`${tableId}/${columnId}`);
|
|
133
289
|
});
|
|
134
290
|
}
|
|
135
|
-
async beginFtsBaseBuild(
|
|
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);
|
|
136
297
|
return this.#runAtomic(() => {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
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");
|
|
140
304
|
}
|
|
141
|
-
this.#
|
|
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
|
+
});
|
|
142
316
|
});
|
|
143
317
|
}
|
|
144
|
-
async
|
|
318
|
+
async renewFtsBaseBuild(input) {
|
|
319
|
+
validatePostingBuildLifetime(input.expiresAtCutoff, input.expiresAt);
|
|
145
320
|
return this.#runAtomic(() => {
|
|
146
|
-
const build = this.#
|
|
147
|
-
|
|
148
|
-
throw new Error(`Full-text base build changed: ${buildId}`);
|
|
149
|
-
build.chunks.set(ordinal, structuredClone(chunk));
|
|
321
|
+
const build = this.#requirePostingBuild(input);
|
|
322
|
+
build.expiresAt = input.expiresAt;
|
|
150
323
|
});
|
|
151
324
|
}
|
|
152
|
-
async
|
|
325
|
+
async writeFtsBaseBuildChunk(input) {
|
|
326
|
+
validatePostingBuildLifetime(input.expiresAtCutoff, input.expiresAt);
|
|
153
327
|
return this.#runAtomic(() => {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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)) {
|
|
160
367
|
this.#ftsBaseBuilds.delete(key);
|
|
161
368
|
return;
|
|
162
369
|
}
|
|
@@ -167,7 +374,7 @@ export class MemoryBlockStore {
|
|
|
167
374
|
}
|
|
168
375
|
return chunk;
|
|
169
376
|
});
|
|
170
|
-
this.#core.writeFtsBase(tableId, columnId, {
|
|
377
|
+
this.#core.writeFtsBase(input.tableId, input.columnId, {
|
|
171
378
|
coversVersion: input.coversVersion,
|
|
172
379
|
chunks,
|
|
173
380
|
totalTokens: input.totalTokens,
|
|
@@ -175,18 +382,35 @@ export class MemoryBlockStore {
|
|
|
175
382
|
this.#ftsBaseBuilds.delete(key);
|
|
176
383
|
});
|
|
177
384
|
}
|
|
178
|
-
async abortFtsBaseBuild(
|
|
385
|
+
async abortFtsBaseBuild(input) {
|
|
179
386
|
return this.#runAtomic(() => {
|
|
180
|
-
const key = `${tableId}/${columnId}`;
|
|
181
|
-
|
|
182
|
-
|
|
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);
|
|
183
397
|
});
|
|
184
398
|
}
|
|
185
|
-
|
|
186
|
-
|
|
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);
|
|
187
411
|
}
|
|
188
|
-
async readFtsPostings(tableId, columnId, upToVersion) {
|
|
189
|
-
return this.#core.readFtsPostings(tableId, columnId, upToVersion);
|
|
412
|
+
async readFtsPostings(tableId, columnId, upToVersion, maxRowIds, maxRetainedBytes) {
|
|
413
|
+
return this.#core.readFtsPostings(tableId, columnId, upToVersion, maxRowIds, maxRetainedBytes);
|
|
190
414
|
}
|
|
191
415
|
async getTableByName(name) {
|
|
192
416
|
const id = this.#core.getTableIdByName(name);
|
|
@@ -195,20 +419,20 @@ export class MemoryBlockStore {
|
|
|
195
419
|
async listTables() {
|
|
196
420
|
return this.#core.listTables();
|
|
197
421
|
}
|
|
198
|
-
async addSegment(record) {
|
|
199
|
-
this.#core.addSegment(record);
|
|
200
|
-
}
|
|
201
422
|
async getSegment(id) {
|
|
202
423
|
return this.#core.getSegment(id);
|
|
203
424
|
}
|
|
204
|
-
async listSegments(tableId) {
|
|
205
|
-
return this.#core.listSegments(tableId);
|
|
206
|
-
}
|
|
207
425
|
async listSegmentPage(afterId, limit) {
|
|
208
426
|
return this.#core.listSegmentPage(afterId, limit);
|
|
209
427
|
}
|
|
210
|
-
async
|
|
211
|
-
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));
|
|
212
436
|
}
|
|
213
437
|
async reserveRowIds(tableId, count) {
|
|
214
438
|
return this.#core.reserveRowIds(tableId, count);
|
|
@@ -217,18 +441,69 @@ export class MemoryBlockStore {
|
|
|
217
441
|
return this.#runAtomic(() => this.#core.reserveAutoIncrement(tableId, columnId, count, atLeast));
|
|
218
442
|
}
|
|
219
443
|
async getExistingUniqueKeys(tableId, keyTokens) {
|
|
444
|
+
assertStorageBulkReadItems(keyTokens, "Unique-key lookup batch");
|
|
220
445
|
return this.#core.getExistingUniqueKeys(tableId, keyTokens);
|
|
221
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
|
+
}
|
|
222
465
|
async getCurrentManifestVersion() {
|
|
223
466
|
return this.#core.getCurrentManifestVersion();
|
|
224
467
|
}
|
|
225
468
|
async getCatalogProbe() {
|
|
226
469
|
return this.#core.getCatalogProbe();
|
|
227
470
|
}
|
|
228
|
-
async getQueryCatalogState(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
});
|
|
232
507
|
}
|
|
233
508
|
async getCurrentManifest() {
|
|
234
509
|
return this.#core.getCurrentManifest();
|
|
@@ -236,15 +511,15 @@ export class MemoryBlockStore {
|
|
|
236
511
|
async getManifest(version) {
|
|
237
512
|
return this.#core.getManifest(version);
|
|
238
513
|
}
|
|
239
|
-
async
|
|
240
|
-
return this.#core.
|
|
514
|
+
async listManifestBlockPage(input) {
|
|
515
|
+
return this.#core.listManifestBlockPage(input);
|
|
516
|
+
}
|
|
517
|
+
async listRetiredManifestBlockPage(input) {
|
|
518
|
+
return this.#core.listRetiredManifestBlockPage(input);
|
|
241
519
|
}
|
|
242
520
|
async listManifestPage(afterVersion, limit) {
|
|
243
521
|
return this.#core.listManifestPage(afterVersion, limit);
|
|
244
522
|
}
|
|
245
|
-
async publishManifest(input) {
|
|
246
|
-
return this.#runAtomic(() => this.#core.publishManifest(input));
|
|
247
|
-
}
|
|
248
523
|
async beginTransaction(input) {
|
|
249
524
|
return this.#runAtomic(() => this.#core.beginTransaction(input));
|
|
250
525
|
}
|
|
@@ -257,23 +532,35 @@ export class MemoryBlockStore {
|
|
|
257
532
|
return this.#core.getTransaction(id);
|
|
258
533
|
}
|
|
259
534
|
async getTransactions(ids) {
|
|
535
|
+
assertStorageBulkReadItems(ids, "Transaction read batch");
|
|
260
536
|
return this.#core.getTransactions(ids);
|
|
261
537
|
}
|
|
262
|
-
async listTransactions() {
|
|
263
|
-
return this.#core.listTransactions();
|
|
264
|
-
}
|
|
265
538
|
async listTransactionPage(afterId, limit) {
|
|
266
539
|
return this.#core.listTransactionPage(afterId, limit);
|
|
267
540
|
}
|
|
268
541
|
async updateTransaction(id, expectedRevision, update) {
|
|
269
542
|
return this.#runAtomic(() => this.#core.updateTransaction(id, expectedRevision, update));
|
|
270
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
|
+
}
|
|
271
550
|
async stageTransactionArtifacts(input) {
|
|
272
551
|
return this.#runAtomic(() => {
|
|
273
552
|
const updated = this.#core.stageTransactionArtifacts(input);
|
|
274
553
|
// The record half validated everything; the bytes land in this same atomic step.
|
|
275
554
|
for (const block of input.blocks)
|
|
276
|
-
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);
|
|
277
564
|
return updated;
|
|
278
565
|
});
|
|
279
566
|
}
|
|
@@ -285,7 +572,7 @@ export class MemoryBlockStore {
|
|
|
285
572
|
const summary = this.#core.writeTransaction(input);
|
|
286
573
|
// The record half validated everything; the bytes land in this same atomic step.
|
|
287
574
|
for (const block of input.blocks)
|
|
288
|
-
this.#
|
|
575
|
+
this.#putBlock(block.id, block.bytes);
|
|
289
576
|
return summary;
|
|
290
577
|
});
|
|
291
578
|
}
|
|
@@ -300,19 +587,20 @@ export class MemoryBlockStore {
|
|
|
300
587
|
async listLeases() {
|
|
301
588
|
return this.#core.listLeases();
|
|
302
589
|
}
|
|
303
|
-
async
|
|
304
|
-
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));
|
|
305
595
|
}
|
|
306
|
-
async moveLease(
|
|
307
|
-
return this.#runAtomic(() => this.#core.moveLease(
|
|
596
|
+
async moveLease(input) {
|
|
597
|
+
return this.#runAtomic(() => this.#core.moveLease(input));
|
|
308
598
|
}
|
|
309
599
|
async removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff) {
|
|
310
600
|
return this.#runAtomic(() => this.#core.removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff));
|
|
311
601
|
}
|
|
312
|
-
async removeLease(
|
|
313
|
-
return this.#runAtomic(() =>
|
|
314
|
-
this.#core.removeLease(id);
|
|
315
|
-
});
|
|
602
|
+
async removeLease(input) {
|
|
603
|
+
return this.#runAtomic(() => this.#core.removeLease(input));
|
|
316
604
|
}
|
|
317
605
|
async createCompactionJob(record) {
|
|
318
606
|
this.#core.createCompactionJob(record);
|
|
@@ -333,71 +621,551 @@ export class MemoryBlockStore {
|
|
|
333
621
|
return this.#runAtomic(() => this.#core.cancelCompactionJob(id, expectedRevision, cancelledAt));
|
|
334
622
|
}
|
|
335
623
|
async removeCompactionJob(id) {
|
|
336
|
-
this.#core.removeCompactionJob(id);
|
|
624
|
+
return this.#core.removeCompactionJob(id);
|
|
337
625
|
}
|
|
338
626
|
async createGarbageCollectionJob(input) {
|
|
339
627
|
return this.#runAtomic(() => this.#core.createGarbageCollectionJob(input));
|
|
340
628
|
}
|
|
629
|
+
async updateGarbageCollectionPlanning(input) {
|
|
630
|
+
return this.#runAtomic(() => this.#core.updateGarbageCollectionPlanning(input));
|
|
631
|
+
}
|
|
341
632
|
async getGarbageCollectionJob(id) {
|
|
342
633
|
return this.#core.getGarbageCollectionJob(id);
|
|
343
634
|
}
|
|
344
635
|
async listGarbageCollectionJobs() {
|
|
345
636
|
return this.#core.listGarbageCollectionJobs();
|
|
346
637
|
}
|
|
638
|
+
async listGarbageCollectionJobPage(afterId, limit) {
|
|
639
|
+
return this.#core.listGarbageCollectionJobPage(afterId, limit);
|
|
640
|
+
}
|
|
347
641
|
async runGarbageCollectionStep(input) {
|
|
348
642
|
return this.#runAtomic(() => {
|
|
349
643
|
const result = this.#core.runGarbageCollectionStep(input);
|
|
350
|
-
result.reclaimedBlockIds.forEach((id) => this.#
|
|
644
|
+
result.reclaimedBlockIds.forEach((id) => this.#deleteBlock(id));
|
|
351
645
|
return result;
|
|
352
646
|
});
|
|
353
647
|
}
|
|
354
|
-
async removePrunedManifestRecords() {
|
|
355
|
-
return this.#runAtomic(() => this.#core.removePrunedManifestRecords()
|
|
648
|
+
async removePrunedManifestRecords(maxItems) {
|
|
649
|
+
return this.#runAtomic(() => this.#core.removePrunedManifestRecords(maxItems));
|
|
356
650
|
}
|
|
357
651
|
async removeGarbageCollectionJob(id) {
|
|
358
652
|
return this.#runAtomic(() => {
|
|
359
653
|
this.#core.removeGarbageCollectionJob(id);
|
|
360
654
|
});
|
|
361
655
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
+
});
|
|
392
1118
|
}
|
|
393
1119
|
/** Block and spill bytes held; the record maps are negligible beside them. */
|
|
394
1120
|
async getLogicalStorageBytes() {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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;
|
|
401
1169
|
}
|
|
402
1170
|
close() {
|
|
403
1171
|
// The in-memory implementation owns no external resources.
|
|
@@ -411,6 +1179,10 @@ export class MemoryBlockStore {
|
|
|
411
1179
|
});
|
|
412
1180
|
this.#commitQueue = this.#commitQueue.then(() => {
|
|
413
1181
|
try {
|
|
1182
|
+
if (this.#completedSnapshotFrameImport !== undefined &&
|
|
1183
|
+
this.#core.getCurrentManifestVersion() !== this.#completedSnapshotFrameImport.version) {
|
|
1184
|
+
this.#completedSnapshotFrameImport = undefined;
|
|
1185
|
+
}
|
|
414
1186
|
resolveResult(operation());
|
|
415
1187
|
}
|
|
416
1188
|
catch (error) {
|
|
@@ -426,4 +1198,97 @@ function tempRunPageKey(ownerId, runId, pageIndex) {
|
|
|
426
1198
|
function tempRunPagePrefix(ownerId, runId) {
|
|
427
1199
|
return `${String(ownerId.length)}:${ownerId}:${String(runId.length)}:${runId}:`;
|
|
428
1200
|
}
|
|
429
|
-
|
|
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
|
+
}
|