@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
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertStorageBulkReadItems, assertTempRunPageBatchLimits, OpfsUncertainOutcomeError, StorageCorruptionError, StorageFormatVersionError, validateStorageDatabaseName, } from "../types.js";
|
|
2
|
+
import { validateTempRunPage, validateTempRunPageIdentity } from "../toolkit/record-core.js";
|
|
2
3
|
import { OpfsTree, encodeSegment, isDomError } from "./files.js";
|
|
3
4
|
import { LOG_FORMAT_VERSION } from "../toolkit/wire.js";
|
|
4
5
|
import { OpfsLeader } from "./leader.js";
|
|
5
|
-
import { rehydrateStoreError, serializeStoreError, } from "./rpc.js";
|
|
6
|
+
import { rehydrateStoreError, estimateRpcValueBytes, fingerprintStoreRequest, MAX_OPFS_RPC_MESSAGE_BYTES, parseStoreRpcMessage, serializeStoreError, } from "./rpc.js";
|
|
6
7
|
/** How long a follower waits for a leader's answer before assuming it is gone. */
|
|
7
8
|
const RPC_TIMEOUT_MS = 1_000;
|
|
8
9
|
/** How long a ping waits for a leadership announcement. */
|
|
@@ -13,59 +14,81 @@ const DISPATCH_ATTEMPTS = 10;
|
|
|
13
14
|
const YIELD_COOLDOWN_MS = 3_000;
|
|
14
15
|
/** Results remembered for retried requests whose acknowledgement was lost. */
|
|
15
16
|
const DEDUPE_CACHE_SIZE = 512;
|
|
17
|
+
/** Requests admitted concurrently by either side of the follower protocol. */
|
|
18
|
+
const RPC_IN_FLIGHT_LIMIT = 512;
|
|
19
|
+
const RPC_IN_FLIGHT_MUTATION_BYTES = 128 * 1024 * 1024;
|
|
20
|
+
const RPC_IN_FLIGHT_READ_BYTES = 256 * 1024 * 1024;
|
|
21
|
+
const RPC_SETTLED_OUTCOME_BYTES = 128 * 1024 * 1024;
|
|
22
|
+
const RPC_SERVER_ADMISSION_LIMIT = 1024;
|
|
23
|
+
const RPC_SERVER_ADMISSION_BYTES = 256 * 1024 * 1024;
|
|
16
24
|
/** Channels into requesters' inboxes a leader keeps open between answers. */
|
|
17
25
|
const ANSWER_CHANNEL_CACHE_SIZE = 64;
|
|
26
|
+
const BULK_READ_ARGUMENT_INDEX = {
|
|
27
|
+
getBlocks: 0,
|
|
28
|
+
getTransactions: 0,
|
|
29
|
+
getExistingUniqueKeys: 1,
|
|
30
|
+
hasManifestBlocks: 1,
|
|
31
|
+
};
|
|
18
32
|
/** Methods a follower may invoke on the leader. Temp-page IO is instance-local by design. */
|
|
19
33
|
const RPC_METHODS = new Set([
|
|
20
|
-
"addBlock",
|
|
21
|
-
"addBlocks",
|
|
22
34
|
"getBlock",
|
|
23
35
|
"getBlocks",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
36
|
+
"readManifestBlock",
|
|
37
|
+
"hasManifestBlocks",
|
|
38
|
+
"listManifestBlockPage",
|
|
39
|
+
"listRetiredManifestBlockPage",
|
|
26
40
|
"addTable",
|
|
27
41
|
"getTable",
|
|
28
42
|
"getTableByName",
|
|
29
43
|
"listTables",
|
|
30
44
|
"updateTable",
|
|
31
45
|
"removeTable",
|
|
46
|
+
"dropTable",
|
|
47
|
+
"dropTableColumn",
|
|
32
48
|
"removeFtsColumn",
|
|
33
49
|
"writeFtsBase",
|
|
34
50
|
"beginFtsBaseBuild",
|
|
51
|
+
"renewFtsBaseBuild",
|
|
35
52
|
"writeFtsBaseBuildChunk",
|
|
36
53
|
"finishFtsBaseBuild",
|
|
37
54
|
"abortFtsBaseBuild",
|
|
38
55
|
"readFtsCandidates",
|
|
39
56
|
"readFtsPostings",
|
|
40
|
-
"addSegment",
|
|
41
57
|
"getSegment",
|
|
42
|
-
"listSegments",
|
|
43
58
|
"listSegmentPage",
|
|
44
|
-
"
|
|
59
|
+
"listTableSegmentPage",
|
|
60
|
+
"removeAbortedSegment",
|
|
61
|
+
"adoptAbortedSegment",
|
|
45
62
|
"reserveRowIds",
|
|
46
63
|
"reserveAutoIncrement",
|
|
47
64
|
"getExistingUniqueKeys",
|
|
65
|
+
"beginUniqueKeyBuild",
|
|
66
|
+
"getUniqueKeyBuild",
|
|
67
|
+
"renewUniqueKeyBuild",
|
|
68
|
+
"appendUniqueKeyBuildChunk",
|
|
69
|
+
"finishUniqueKeyBuild",
|
|
70
|
+
"abortUniqueKeyBuild",
|
|
48
71
|
"getCurrentManifest",
|
|
49
72
|
"getCurrentManifestVersion",
|
|
50
73
|
"getCatalogProbe",
|
|
51
|
-
"getQueryCatalogState",
|
|
52
74
|
"getManifest",
|
|
53
|
-
"listManifests",
|
|
54
75
|
"listManifestPage",
|
|
55
|
-
"publishManifest",
|
|
56
76
|
"createTransaction",
|
|
77
|
+
"renewTransaction",
|
|
78
|
+
"abortTransactionIfExpired",
|
|
57
79
|
"beginTransaction",
|
|
58
80
|
"getTransaction",
|
|
59
81
|
"getTransactions",
|
|
60
|
-
"listTransactions",
|
|
61
82
|
"listTransactionPage",
|
|
62
83
|
"updateTransaction",
|
|
63
84
|
"stageTransactionArtifacts",
|
|
85
|
+
"rollbackTransactionArtifacts",
|
|
64
86
|
"commitTransaction",
|
|
65
87
|
"writeTransaction",
|
|
66
88
|
"createLease",
|
|
67
89
|
"getLease",
|
|
68
90
|
"listLeases",
|
|
91
|
+
"listExpiredLeasePage",
|
|
69
92
|
"renewLease",
|
|
70
93
|
"moveLease",
|
|
71
94
|
"removeLeaseIfExpired",
|
|
@@ -78,20 +101,35 @@ const RPC_METHODS = new Set([
|
|
|
78
101
|
"cancelCompactionJob",
|
|
79
102
|
"removeCompactionJob",
|
|
80
103
|
"createGarbageCollectionJob",
|
|
104
|
+
"updateGarbageCollectionPlanning",
|
|
81
105
|
"getGarbageCollectionJob",
|
|
82
106
|
"listGarbageCollectionJobs",
|
|
107
|
+
"listGarbageCollectionJobPage",
|
|
83
108
|
"runGarbageCollectionStep",
|
|
84
109
|
"removePrunedManifestRecords",
|
|
85
110
|
"removeGarbageCollectionJob",
|
|
86
111
|
"createTempOwner",
|
|
87
112
|
"getTempOwner",
|
|
113
|
+
"putTempRunPage",
|
|
114
|
+
"putTempRunPages",
|
|
115
|
+
"getTempRunPage",
|
|
116
|
+
"removeTempRun",
|
|
88
117
|
"renewTempOwner",
|
|
89
118
|
"removeTempOwnerIfExpired",
|
|
90
119
|
"removeTempOwner",
|
|
91
120
|
"listTempOwnerIdsPage",
|
|
121
|
+
"listExpiredTempOwnerPage",
|
|
92
122
|
"getLogicalStorageBytes",
|
|
93
|
-
"
|
|
94
|
-
"
|
|
123
|
+
"getStorageStats",
|
|
124
|
+
"checkIntegrity",
|
|
125
|
+
"beginSnapshotFrameExport",
|
|
126
|
+
"readSnapshotExportFrame",
|
|
127
|
+
"closeSnapshotFrameExport",
|
|
128
|
+
"beginSnapshotFrameImport",
|
|
129
|
+
"renewSnapshotFrameImport",
|
|
130
|
+
"appendSnapshotImportFrames",
|
|
131
|
+
"finishSnapshotFrameImport",
|
|
132
|
+
"cancelSnapshotFrameImport",
|
|
95
133
|
]);
|
|
96
134
|
/**
|
|
97
135
|
* Reads are idempotent — a lost-acknowledgement retry can simply run again — so only
|
|
@@ -101,70 +139,56 @@ const RPC_METHODS = new Set([
|
|
|
101
139
|
const READ_METHODS = new Set([
|
|
102
140
|
"getBlock",
|
|
103
141
|
"getBlocks",
|
|
104
|
-
"
|
|
142
|
+
"readManifestBlock",
|
|
143
|
+
"hasManifestBlocks",
|
|
144
|
+
"listManifestBlockPage",
|
|
145
|
+
"listRetiredManifestBlockPage",
|
|
105
146
|
"getTable",
|
|
106
147
|
"getTableByName",
|
|
107
148
|
"listTables",
|
|
108
149
|
"readFtsCandidates",
|
|
109
150
|
"readFtsPostings",
|
|
110
151
|
"getSegment",
|
|
111
|
-
"listSegments",
|
|
112
152
|
"listSegmentPage",
|
|
153
|
+
"listTableSegmentPage",
|
|
113
154
|
"getExistingUniqueKeys",
|
|
155
|
+
"getUniqueKeyBuild",
|
|
114
156
|
"getCurrentManifest",
|
|
115
157
|
"getCurrentManifestVersion",
|
|
116
158
|
"getCatalogProbe",
|
|
117
|
-
"getQueryCatalogState",
|
|
118
159
|
"getManifest",
|
|
119
|
-
"listManifests",
|
|
120
160
|
"listManifestPage",
|
|
121
161
|
"getTransaction",
|
|
122
162
|
"getTransactions",
|
|
123
|
-
"listTransactions",
|
|
124
163
|
"listTransactionPage",
|
|
125
164
|
"getLease",
|
|
126
165
|
"listLeases",
|
|
166
|
+
"listExpiredLeasePage",
|
|
127
167
|
"getCompactionJob",
|
|
128
168
|
"listCompactionJobs",
|
|
129
169
|
"listCompactionJobPage",
|
|
130
170
|
"getGarbageCollectionJob",
|
|
131
171
|
"listGarbageCollectionJobs",
|
|
172
|
+
"listGarbageCollectionJobPage",
|
|
132
173
|
"getTempOwner",
|
|
174
|
+
"getTempRunPage",
|
|
133
175
|
"listTempOwnerIdsPage",
|
|
176
|
+
"listExpiredTempOwnerPage",
|
|
134
177
|
"getLogicalStorageBytes",
|
|
135
|
-
"
|
|
178
|
+
"getStorageStats",
|
|
179
|
+
"checkIntegrity",
|
|
180
|
+
"readSnapshotExportFrame",
|
|
136
181
|
]);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
*
|
|
142
|
-
* Two kinds of channel carry the protocol. One shared channel per database carries what every
|
|
143
|
-
* connection must hear: leader announcements, pings, bids, yields, releases. Every connection
|
|
144
|
-
* also owns an inbox — a channel named for its instance id — where the messages meant for it
|
|
145
|
-
* alone arrive: operations at the leader, results and busy notices at the requester. A block
|
|
146
|
-
* read's bytes are therefore structured-cloned once, into the tab that asked, rather than into
|
|
147
|
-
* every tab of the origin.
|
|
148
|
-
*
|
|
149
|
-
* Leadership is the write-ahead log's own exclusive sync-access handle — enforced by the
|
|
150
|
-
* browser against the actual resource, released the instant its holder dies. Elections are
|
|
151
|
-
* simply attempts to open it. Correctness never rides on a message: an operation is
|
|
152
|
-
* acknowledged only after the leader's WAL holds it, a lost message costs a retry (request
|
|
153
|
-
* ids deduplicate retries while the same leader serves), and a dead leader costs a failover
|
|
154
|
-
* in which the next acquirer replays checkpoint-plus-WAL. A retry that crosses a failover is
|
|
155
|
-
* the one case a duplicate can execute — the same "committed but the response was lost"
|
|
156
|
-
* class the IndexedDB store has — and it resolves the same way: compare-and-swap operations
|
|
157
|
-
* surface a conflict the engine's recovery already reconciles. The channel affects how fast
|
|
158
|
-
* multi-tab work moves, never whether it is right.
|
|
159
|
-
*
|
|
160
|
-
* `setForeground(true)` marks this connection as the one the user is looking at; a background
|
|
161
|
-
* leader yields to a foreground bidder, so the tab doing the work is normally the tab holding
|
|
162
|
-
* the microsecond-fast path.
|
|
163
|
-
*/
|
|
182
|
+
function sameServedRequest(remembered, message, signature) {
|
|
183
|
+
return remembered.method === message.method && remembered.signature === signature;
|
|
184
|
+
}
|
|
185
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
|
164
186
|
export class OpfsBlockStore {
|
|
187
|
+
liveQueryChannelName;
|
|
165
188
|
#tree;
|
|
166
189
|
#durability;
|
|
167
190
|
#checkpointEntries;
|
|
191
|
+
#cleanupLimitBytes;
|
|
168
192
|
#rpcTimeoutMs;
|
|
169
193
|
#instanceId = crypto.randomUUID();
|
|
170
194
|
#channelName;
|
|
@@ -181,14 +205,26 @@ export class OpfsBlockStore {
|
|
|
181
205
|
#lastYieldAt = 0;
|
|
182
206
|
#electing;
|
|
183
207
|
#pending = new Map();
|
|
184
|
-
#
|
|
208
|
+
#inFlightMutations = new Map();
|
|
209
|
+
#settledMutations = new Map();
|
|
210
|
+
#servedRequestLocks = new Map();
|
|
211
|
+
#inFlightMutationBytes = 0;
|
|
212
|
+
#inFlightReadBytes = 0;
|
|
213
|
+
#settledMutationBytes = 0;
|
|
214
|
+
#pendingRpcBytes = 0;
|
|
215
|
+
#servedRequestCount = 0;
|
|
216
|
+
#servedRequestBytes = 0;
|
|
217
|
+
#servedMutationGateForTests;
|
|
218
|
+
#dropNextRpcResultForTests = false;
|
|
185
219
|
#reacquireTimer;
|
|
186
220
|
constructor(tree, options) {
|
|
187
221
|
this.#tree = tree;
|
|
188
|
-
this.#durability = options.durability ?? "
|
|
222
|
+
this.#durability = options.durability ?? "strict";
|
|
189
223
|
this.#checkpointEntries = options.checkpointEntries;
|
|
224
|
+
this.#cleanupLimitBytes = options.cleanupLimitBytes;
|
|
190
225
|
this.#rpcTimeoutMs = options.rpcTimeoutMs ?? RPC_TIMEOUT_MS;
|
|
191
226
|
this.#channelName = `minnowdb-store:${options.name}`;
|
|
227
|
+
this.liveQueryChannelName = `minnowdb-live:opfs:${options.name}`;
|
|
192
228
|
}
|
|
193
229
|
static async open(options) {
|
|
194
230
|
const tree = new OpfsTree(await resolveDatabaseRoot(options));
|
|
@@ -196,7 +232,9 @@ export class OpfsBlockStore {
|
|
|
196
232
|
await store.#ensureFormatMarker();
|
|
197
233
|
if (typeof BroadcastChannel === "function") {
|
|
198
234
|
const onMessage = (event) => {
|
|
199
|
-
|
|
235
|
+
const message = parseStoreRpcMessage(event.data, RPC_METHODS);
|
|
236
|
+
if (message !== undefined)
|
|
237
|
+
store.#onMessage(message);
|
|
200
238
|
};
|
|
201
239
|
const channel = new BroadcastChannel(store.#channelName);
|
|
202
240
|
channel.onmessage = onMessage;
|
|
@@ -244,7 +282,7 @@ export class OpfsBlockStore {
|
|
|
244
282
|
// them in sequence; the brief retry covers the gap.
|
|
245
283
|
slotA = await this.#openWithRetry(["checkpoint-a"]);
|
|
246
284
|
slotB = await this.#openWithRetry(["checkpoint-b"]);
|
|
247
|
-
this.#leader = await OpfsLeader.recover(this.#tree, this.#durability === "strict", { wal, slotA, slotB }, this.#checkpointEntries);
|
|
285
|
+
this.#leader = await OpfsLeader.recover(this.#tree, this.#durability === "strict", { wal, slotA, slotB }, this.#checkpointEntries, this.#cleanupLimitBytes);
|
|
248
286
|
}
|
|
249
287
|
catch (error) {
|
|
250
288
|
wal.close();
|
|
@@ -320,15 +358,31 @@ export class OpfsBlockStore {
|
|
|
320
358
|
return;
|
|
321
359
|
switch (message.kind) {
|
|
322
360
|
case "op": {
|
|
323
|
-
if (this.#leader !== undefined)
|
|
324
|
-
|
|
361
|
+
if (this.#leader !== undefined) {
|
|
362
|
+
const requestBytes = estimateRpcValueBytes(message.args);
|
|
363
|
+
if (this.#servedRequestCount >= RPC_SERVER_ADMISSION_LIMIT ||
|
|
364
|
+
this.#servedRequestBytes + requestBytes > RPC_SERVER_ADMISSION_BYTES) {
|
|
365
|
+
this.#answer(message.from, {
|
|
366
|
+
kind: "result",
|
|
367
|
+
requestId: message.requestId,
|
|
368
|
+
ok: false,
|
|
369
|
+
error: { name: "Error", message: "The OPFS leader RPC queue is full" },
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
this.#servedRequestCount += 1;
|
|
374
|
+
this.#servedRequestBytes += requestBytes;
|
|
375
|
+
void this.#serveOp(message).finally(() => {
|
|
376
|
+
this.#servedRequestCount = Math.max(0, this.#servedRequestCount - 1);
|
|
377
|
+
this.#servedRequestBytes = Math.max(0, this.#servedRequestBytes - requestBytes);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
325
380
|
return;
|
|
326
381
|
}
|
|
327
382
|
case "result": {
|
|
328
|
-
const pending = this.#
|
|
383
|
+
const pending = this.#takePending(message.requestId);
|
|
329
384
|
if (pending === undefined)
|
|
330
385
|
return;
|
|
331
|
-
this.#pending.delete(message.requestId);
|
|
332
386
|
clearTimeout(pending.timer);
|
|
333
387
|
if (message.ok)
|
|
334
388
|
pending.resolve(message.value);
|
|
@@ -342,8 +396,8 @@ export class OpfsBlockStore {
|
|
|
342
396
|
return;
|
|
343
397
|
clearTimeout(pending.timer);
|
|
344
398
|
pending.timer = setTimeout(() => {
|
|
345
|
-
this.#
|
|
346
|
-
|
|
399
|
+
const expired = this.#takePending(message.requestId);
|
|
400
|
+
expired?.reject(RPC_TIMED_OUT);
|
|
347
401
|
}, this.#rpcTimeoutMs);
|
|
348
402
|
return;
|
|
349
403
|
}
|
|
@@ -353,12 +407,17 @@ export class OpfsBlockStore {
|
|
|
353
407
|
clearTimeout(this.#reacquireTimer);
|
|
354
408
|
this.#reacquireTimer = undefined;
|
|
355
409
|
}
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
for (const pending of this.#pending.values()) {
|
|
410
|
+
// Reads can move to the new leader. A mutation may already be durable on the old one;
|
|
411
|
+
// never execute it twice merely because its acknowledgement was lost.
|
|
412
|
+
for (const [requestId, pending] of this.#pending) {
|
|
360
413
|
if (pending.sentTo === message.leaderId)
|
|
361
414
|
continue;
|
|
415
|
+
if (!READ_METHODS.has(pending.message.method)) {
|
|
416
|
+
this.#takePending(requestId);
|
|
417
|
+
clearTimeout(pending.timer);
|
|
418
|
+
pending.reject(new OpfsUncertainOutcomeError(pending.message.method));
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
362
421
|
pending.sentTo = message.leaderId;
|
|
363
422
|
this.#send(message.leaderId, pending.message);
|
|
364
423
|
}
|
|
@@ -396,31 +455,133 @@ export class OpfsBlockStore {
|
|
|
396
455
|
}
|
|
397
456
|
}
|
|
398
457
|
}
|
|
458
|
+
#takePending(requestId) {
|
|
459
|
+
const pending = this.#pending.get(requestId);
|
|
460
|
+
if (pending === undefined)
|
|
461
|
+
return undefined;
|
|
462
|
+
this.#pending.delete(requestId);
|
|
463
|
+
this.#pendingRpcBytes -= pending.retainedBytes;
|
|
464
|
+
return pending;
|
|
465
|
+
}
|
|
399
466
|
async #serveOp(message) {
|
|
467
|
+
const requestKey = `${String(message.from.length)}:${message.from}${message.requestId}`;
|
|
468
|
+
// Once an identity is admitted, duplicates can compare their fingerprint immediately and
|
|
469
|
+
// either attach to the exact execution or fail closed. The short lock below exists only for
|
|
470
|
+
// the pre-admission fingerprint race between two first deliveries.
|
|
471
|
+
if (this.#inFlightMutations.has(requestKey) || this.#settledMutations.has(requestKey)) {
|
|
472
|
+
await this.#serveOpLocked(message, requestKey);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const previous = this.#servedRequestLocks.get(requestKey) ?? Promise.resolve();
|
|
476
|
+
let release;
|
|
477
|
+
const hold = new Promise((resolve) => {
|
|
478
|
+
release = resolve;
|
|
479
|
+
});
|
|
480
|
+
const tail = previous.then(() => hold);
|
|
481
|
+
this.#servedRequestLocks.set(requestKey, tail);
|
|
482
|
+
await previous;
|
|
483
|
+
try {
|
|
484
|
+
await this.#serveOpLocked(message, requestKey);
|
|
485
|
+
}
|
|
486
|
+
finally {
|
|
487
|
+
release();
|
|
488
|
+
if (this.#servedRequestLocks.get(requestKey) === tail) {
|
|
489
|
+
this.#servedRequestLocks.delete(requestKey);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
async #serveOpLocked(message, requestKey) {
|
|
400
494
|
const leader = this.#leader;
|
|
401
495
|
if (leader === undefined || !RPC_METHODS.has(message.method))
|
|
402
496
|
return;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
497
|
+
const isRead = READ_METHODS.has(message.method);
|
|
498
|
+
let fingerprint;
|
|
499
|
+
try {
|
|
500
|
+
fingerprint = await fingerprintStoreRequest(message.method, message.args);
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const remembered = isRead ? undefined : this.#settledMutations.get(requestKey);
|
|
506
|
+
let settled;
|
|
507
|
+
if (remembered !== undefined) {
|
|
508
|
+
if (!sameServedRequest(remembered, message, fingerprint.signature)) {
|
|
509
|
+
this.#rejectReusedRequestIdentity(message);
|
|
510
|
+
return;
|
|
415
511
|
}
|
|
512
|
+
// Refresh bounded settled outcomes as an LRU. This is the lost-ack retry path.
|
|
513
|
+
this.#settledMutations.delete(requestKey);
|
|
514
|
+
this.#settledMutations.set(requestKey, remembered);
|
|
515
|
+
settled = remembered.outcome;
|
|
416
516
|
}
|
|
417
|
-
else {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
517
|
+
else if (!isRead && this.#inFlightMutations.has(requestKey)) {
|
|
518
|
+
const inFlight = this.#inFlightMutations.get(requestKey);
|
|
519
|
+
if (inFlight === undefined)
|
|
520
|
+
throw new Error("In-flight RPC identity disappeared");
|
|
521
|
+
if (!sameServedRequest(inFlight, message, fingerprint.signature)) {
|
|
522
|
+
this.#rejectReusedRequestIdentity(message);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
// Never evict an in-flight mutation. A duplicate attaches to the one execution and resets
|
|
526
|
+
// the requester's patience while it remains queued or running.
|
|
421
527
|
this.#answer(message.from, { kind: "busy", requestId: message.requestId });
|
|
528
|
+
settled = await inFlight.outcome;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
if (!isRead &&
|
|
532
|
+
(this.#inFlightMutations.size >= RPC_IN_FLIGHT_LIMIT ||
|
|
533
|
+
this.#inFlightMutationBytes + fingerprint.retainedBytes > RPC_IN_FLIGHT_MUTATION_BYTES)) {
|
|
534
|
+
this.#answer(message.from, {
|
|
535
|
+
kind: "result",
|
|
536
|
+
requestId: message.requestId,
|
|
537
|
+
ok: false,
|
|
538
|
+
error: { name: "Error", message: "The OPFS leader mutation queue is full" },
|
|
539
|
+
});
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (isRead) {
|
|
543
|
+
const reservation = Math.max(fingerprint.retainedBytes, MAX_OPFS_RPC_MESSAGE_BYTES);
|
|
544
|
+
if (this.#inFlightReadBytes + reservation > RPC_IN_FLIGHT_READ_BYTES) {
|
|
545
|
+
this.#answer(message.from, {
|
|
546
|
+
kind: "result",
|
|
547
|
+
requestId: message.requestId,
|
|
548
|
+
ok: false,
|
|
549
|
+
error: { name: "Error", message: "The OPFS leader read queue is full" },
|
|
550
|
+
});
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
this.#inFlightReadBytes += reservation;
|
|
554
|
+
try {
|
|
555
|
+
settled = await this.#executeServedOpAfterGate(leader, message, true);
|
|
556
|
+
}
|
|
557
|
+
finally {
|
|
558
|
+
this.#inFlightReadBytes = Math.max(0, this.#inFlightReadBytes - reservation);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
this.#inFlightMutationBytes += fingerprint.retainedBytes;
|
|
563
|
+
const execution = this.#executeServedOpAfterGate(leader, message, false);
|
|
564
|
+
const outcome = execution.then((result) => {
|
|
565
|
+
this.#inFlightMutations.delete(requestKey);
|
|
566
|
+
this.#inFlightMutationBytes = Math.max(0, this.#inFlightMutationBytes - fingerprint.retainedBytes);
|
|
567
|
+
if (!this.#closed) {
|
|
568
|
+
this.#rememberSettledMutation(requestKey, message.method, fingerprint, result);
|
|
569
|
+
}
|
|
570
|
+
return result;
|
|
571
|
+
});
|
|
572
|
+
this.#inFlightMutations.set(requestKey, {
|
|
573
|
+
method: message.method,
|
|
574
|
+
signature: fingerprint.signature,
|
|
575
|
+
requestBytes: fingerprint.retainedBytes,
|
|
576
|
+
outcome,
|
|
577
|
+
});
|
|
578
|
+
settled = await outcome;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (this.#dropNextRpcResultForTests) {
|
|
582
|
+
this.#dropNextRpcResultForTests = false;
|
|
583
|
+
return;
|
|
422
584
|
}
|
|
423
|
-
const settled = await outcome;
|
|
424
585
|
this.#answer(message.from, settled.ok
|
|
425
586
|
? { kind: "result", requestId: message.requestId, ok: true, value: settled.value }
|
|
426
587
|
: {
|
|
@@ -430,6 +591,45 @@ export class OpfsBlockStore {
|
|
|
430
591
|
error: settled.error ?? { name: "Error", message: "unknown" },
|
|
431
592
|
});
|
|
432
593
|
}
|
|
594
|
+
#rememberSettledMutation(requestKey, method, fingerprint, outcome) {
|
|
595
|
+
const outcomeBytes = estimateRpcValueBytes(outcome);
|
|
596
|
+
while (this.#settledMutations.size >= DEDUPE_CACHE_SIZE ||
|
|
597
|
+
this.#settledMutationBytes + outcomeBytes > RPC_SETTLED_OUTCOME_BYTES) {
|
|
598
|
+
const oldest = this.#settledMutations.keys().next().value;
|
|
599
|
+
if (oldest === undefined)
|
|
600
|
+
break;
|
|
601
|
+
const removed = this.#settledMutations.get(oldest);
|
|
602
|
+
this.#settledMutations.delete(oldest);
|
|
603
|
+
this.#settledMutationBytes -= removed?.outcomeBytes ?? 0;
|
|
604
|
+
}
|
|
605
|
+
if (outcomeBytes > RPC_SETTLED_OUTCOME_BYTES)
|
|
606
|
+
return;
|
|
607
|
+
this.#settledMutations.set(requestKey, {
|
|
608
|
+
method,
|
|
609
|
+
signature: fingerprint.signature,
|
|
610
|
+
requestBytes: fingerprint.retainedBytes,
|
|
611
|
+
outcome,
|
|
612
|
+
outcomeBytes,
|
|
613
|
+
});
|
|
614
|
+
this.#settledMutationBytes += outcomeBytes;
|
|
615
|
+
}
|
|
616
|
+
#rejectReusedRequestIdentity(message) {
|
|
617
|
+
this.#answer(message.from, {
|
|
618
|
+
kind: "result",
|
|
619
|
+
requestId: message.requestId,
|
|
620
|
+
ok: false,
|
|
621
|
+
error: {
|
|
622
|
+
name: "Error",
|
|
623
|
+
message: "The OPFS RPC request identity was reused with different contents",
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
async #executeServedOpAfterGate(leader, message, isRead) {
|
|
628
|
+
const gate = this.#servedMutationGateForTests;
|
|
629
|
+
if (!isRead && gate !== undefined)
|
|
630
|
+
await gate;
|
|
631
|
+
return this.#executeServedOp(leader, message);
|
|
632
|
+
}
|
|
433
633
|
async #executeServedOp(leader, message) {
|
|
434
634
|
try {
|
|
435
635
|
const method = leader[message.method];
|
|
@@ -551,6 +751,8 @@ export class OpfsBlockStore {
|
|
|
551
751
|
catch (error) {
|
|
552
752
|
if (error === RPC_TIMED_OUT) {
|
|
553
753
|
this.#knownLeader = undefined;
|
|
754
|
+
if (!READ_METHODS.has(method))
|
|
755
|
+
throw new OpfsUncertainOutcomeError(method);
|
|
554
756
|
continue;
|
|
555
757
|
}
|
|
556
758
|
throw error;
|
|
@@ -563,14 +765,33 @@ export class OpfsBlockStore {
|
|
|
563
765
|
return this.#knownLeader !== undefined;
|
|
564
766
|
}
|
|
565
767
|
#rpc(requestId, method, args) {
|
|
768
|
+
let retainedBytes;
|
|
769
|
+
try {
|
|
770
|
+
retainedBytes = estimateRpcValueBytes(args);
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
|
774
|
+
}
|
|
775
|
+
if (this.#pending.size >= RPC_IN_FLIGHT_LIMIT ||
|
|
776
|
+
this.#pendingRpcBytes + retainedBytes > RPC_IN_FLIGHT_MUTATION_BYTES) {
|
|
777
|
+
return Promise.reject(new Error("The OPFS follower request queue is full"));
|
|
778
|
+
}
|
|
566
779
|
return new Promise((resolve, reject) => {
|
|
567
780
|
const message = { kind: "op", requestId, from: this.#instanceId, method, args };
|
|
568
781
|
const timer = setTimeout(() => {
|
|
569
|
-
this.#
|
|
570
|
-
reject(RPC_TIMED_OUT);
|
|
782
|
+
const expired = this.#takePending(requestId);
|
|
783
|
+
expired?.reject(RPC_TIMED_OUT);
|
|
571
784
|
}, this.#rpcTimeoutMs);
|
|
572
785
|
const leaderId = this.#knownLeader;
|
|
573
|
-
this.#pending.set(requestId, {
|
|
786
|
+
this.#pending.set(requestId, {
|
|
787
|
+
message,
|
|
788
|
+
retainedBytes,
|
|
789
|
+
sentTo: leaderId,
|
|
790
|
+
resolve,
|
|
791
|
+
reject,
|
|
792
|
+
timer,
|
|
793
|
+
});
|
|
794
|
+
this.#pendingRpcBytes += retainedBytes;
|
|
574
795
|
// The dispatch loop only gets here with a leader known; should it have slipped away in
|
|
575
796
|
// between, the timeout (or the next leader's announcement, which re-sends) takes over.
|
|
576
797
|
if (leaderId !== undefined)
|
|
@@ -580,307 +801,35 @@ export class OpfsBlockStore {
|
|
|
580
801
|
// ---------------------------------------------------------------------------------------
|
|
581
802
|
// The BlockStore surface.
|
|
582
803
|
// ---------------------------------------------------------------------------------------
|
|
583
|
-
async addBlock(id, bytes) {
|
|
584
|
-
await this.#dispatch("addBlock", [id, bytes]);
|
|
585
|
-
}
|
|
586
|
-
async addBlocks(blocks) {
|
|
587
|
-
await this.#dispatch("addBlocks", [blocks]);
|
|
588
|
-
}
|
|
589
|
-
async getBlock(id) {
|
|
590
|
-
return (await this.#dispatch("getBlock", [id]));
|
|
591
|
-
}
|
|
592
|
-
async getBlocks(ids) {
|
|
593
|
-
return (await this.#dispatch("getBlocks", [ids]));
|
|
594
|
-
}
|
|
595
|
-
async removeBlock(id) {
|
|
596
|
-
await this.#dispatch("removeBlock", [id]);
|
|
597
|
-
}
|
|
598
|
-
async listBlockIds() {
|
|
599
|
-
return (await this.#dispatch("listBlockIds", []));
|
|
600
|
-
}
|
|
601
804
|
async putTempRunPage(page) {
|
|
602
805
|
validateTempRunPage(page);
|
|
603
|
-
await this.#
|
|
806
|
+
await this.#dispatch("putTempRunPage", [page]);
|
|
604
807
|
}
|
|
605
808
|
async putTempRunPages(pages) {
|
|
606
809
|
// Spill pages are instance-local files; the batch is a convenience, not a round-trip win.
|
|
810
|
+
assertTempRunPageBatchLimits(pages);
|
|
607
811
|
for (const page of pages)
|
|
608
|
-
|
|
812
|
+
validateTempRunPage(page);
|
|
813
|
+
await this.#dispatch("putTempRunPages", [pages]);
|
|
609
814
|
}
|
|
610
815
|
async getTempRunPage(ownerId, runId, pageIndex) {
|
|
611
816
|
validateTempRunPageIdentity(ownerId, runId, pageIndex);
|
|
612
|
-
return this.#
|
|
613
|
-
"temp",
|
|
614
|
-
encodeSegment(ownerId),
|
|
615
|
-
encodeSegment(runId),
|
|
616
|
-
String(pageIndex),
|
|
617
|
-
]);
|
|
817
|
+
return (await this.#dispatch("getTempRunPage", [ownerId, runId, pageIndex]));
|
|
618
818
|
}
|
|
619
819
|
async removeTempRun(ownerId, runId) {
|
|
620
820
|
validateTempRunPageIdentity(ownerId, runId, 0);
|
|
621
|
-
await this.#
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
async getTempOwner(ownerId) {
|
|
632
|
-
return (await this.#dispatch("getTempOwner", [ownerId]));
|
|
633
|
-
}
|
|
634
|
-
async renewTempOwner(ownerId, expectedRevision, expiresAt) {
|
|
635
|
-
return (await this.#dispatch("renewTempOwner", [
|
|
636
|
-
ownerId,
|
|
637
|
-
expectedRevision,
|
|
638
|
-
expiresAt,
|
|
639
|
-
]));
|
|
640
|
-
}
|
|
641
|
-
async removeTempOwnerIfExpired(ownerId, expiresAtCutoff) {
|
|
642
|
-
const removed = (await this.#dispatch("removeTempOwnerIfExpired", [
|
|
643
|
-
ownerId,
|
|
644
|
-
expiresAtCutoff,
|
|
645
|
-
]));
|
|
646
|
-
if (removed)
|
|
647
|
-
await this.#tree.deleteTree(["temp", encodeSegment(ownerId)]);
|
|
648
|
-
return removed;
|
|
649
|
-
}
|
|
650
|
-
async listTempOwnerIdsPage(afterOwnerId, limit) {
|
|
651
|
-
return (await this.#dispatch("listTempOwnerIdsPage", [afterOwnerId, limit]));
|
|
652
|
-
}
|
|
653
|
-
async addTable(record) {
|
|
654
|
-
await this.#dispatch("addTable", [record]);
|
|
655
|
-
}
|
|
656
|
-
async getTable(id) {
|
|
657
|
-
return (await this.#dispatch("getTable", [id]));
|
|
658
|
-
}
|
|
659
|
-
async getTableByName(name) {
|
|
660
|
-
return (await this.#dispatch("getTableByName", [name]));
|
|
661
|
-
}
|
|
662
|
-
async listTables() {
|
|
663
|
-
return (await this.#dispatch("listTables", []));
|
|
664
|
-
}
|
|
665
|
-
async updateTable(id, expectedRevision, update) {
|
|
666
|
-
return (await this.#dispatch("updateTable", [id, expectedRevision, update]));
|
|
667
|
-
}
|
|
668
|
-
async removeTable(id, expectedRevision) {
|
|
669
|
-
await this.#dispatch("removeTable", [id, expectedRevision]);
|
|
670
|
-
}
|
|
671
|
-
async writeFtsBase(tableId, columnId, input) {
|
|
672
|
-
await this.#dispatch("writeFtsBase", [tableId, columnId, input]);
|
|
673
|
-
}
|
|
674
|
-
async removeFtsColumn(tableId, columnId) {
|
|
675
|
-
await this.#dispatch("removeFtsColumn", [tableId, columnId]);
|
|
676
|
-
}
|
|
677
|
-
async beginFtsBaseBuild(tableId, columnId, buildId) {
|
|
678
|
-
await this.#dispatch("beginFtsBaseBuild", [tableId, columnId, buildId]);
|
|
679
|
-
}
|
|
680
|
-
async writeFtsBaseBuildChunk(tableId, columnId, buildId, ordinal, chunk) {
|
|
681
|
-
await this.#dispatch("writeFtsBaseBuildChunk", [tableId, columnId, buildId, ordinal, chunk]);
|
|
682
|
-
}
|
|
683
|
-
async finishFtsBaseBuild(tableId, columnId, buildId, input) {
|
|
684
|
-
await this.#dispatch("finishFtsBaseBuild", [tableId, columnId, buildId, input]);
|
|
685
|
-
}
|
|
686
|
-
async abortFtsBaseBuild(tableId, columnId, buildId) {
|
|
687
|
-
await this.#dispatch("abortFtsBaseBuild", [tableId, columnId, buildId]);
|
|
688
|
-
}
|
|
689
|
-
async readFtsCandidates(tableId, columnId, terms, upToVersion) {
|
|
690
|
-
return (await this.#dispatch("readFtsCandidates", [
|
|
691
|
-
tableId,
|
|
692
|
-
columnId,
|
|
693
|
-
terms,
|
|
694
|
-
upToVersion,
|
|
695
|
-
]));
|
|
696
|
-
}
|
|
697
|
-
async readFtsPostings(tableId, columnId, upToVersion) {
|
|
698
|
-
return this.#dispatch("readFtsPostings", [tableId, columnId, upToVersion]);
|
|
699
|
-
}
|
|
700
|
-
async addSegment(record) {
|
|
701
|
-
await this.#dispatch("addSegment", [record]);
|
|
702
|
-
}
|
|
703
|
-
async getSegment(id) {
|
|
704
|
-
return (await this.#dispatch("getSegment", [id]));
|
|
705
|
-
}
|
|
706
|
-
async listSegments(tableId) {
|
|
707
|
-
return (await this.#dispatch("listSegments", [tableId]));
|
|
708
|
-
}
|
|
709
|
-
async listSegmentPage(afterId, limit) {
|
|
710
|
-
return (await this.#dispatch("listSegmentPage", [afterId, limit]));
|
|
711
|
-
}
|
|
712
|
-
async removeSegment(id) {
|
|
713
|
-
await this.#dispatch("removeSegment", [id]);
|
|
714
|
-
}
|
|
715
|
-
async reserveRowIds(tableId, count) {
|
|
716
|
-
return (await this.#dispatch("reserveRowIds", [tableId, count]));
|
|
717
|
-
}
|
|
718
|
-
async reserveAutoIncrement(tableId, columnId, count, atLeast) {
|
|
719
|
-
return (await this.#dispatch("reserveAutoIncrement", [
|
|
720
|
-
tableId,
|
|
721
|
-
columnId,
|
|
722
|
-
count,
|
|
723
|
-
atLeast,
|
|
724
|
-
]));
|
|
725
|
-
}
|
|
726
|
-
async getExistingUniqueKeys(tableId, keyTokens) {
|
|
727
|
-
return (await this.#dispatch("getExistingUniqueKeys", [tableId, keyTokens]));
|
|
728
|
-
}
|
|
729
|
-
async getCurrentManifest() {
|
|
730
|
-
return (await this.#dispatch("getCurrentManifest", []));
|
|
731
|
-
}
|
|
732
|
-
async getCurrentManifestVersion() {
|
|
733
|
-
return (await this.#dispatch("getCurrentManifestVersion", []));
|
|
734
|
-
}
|
|
735
|
-
async getCatalogProbe() {
|
|
736
|
-
return (await this.#dispatch("getCatalogProbe", []));
|
|
737
|
-
}
|
|
738
|
-
async getQueryCatalogState(tableNames) {
|
|
739
|
-
return (await this.#dispatch("getQueryCatalogState", [tableNames]));
|
|
740
|
-
}
|
|
741
|
-
async getManifest(version) {
|
|
742
|
-
return (await this.#dispatch("getManifest", [version]));
|
|
743
|
-
}
|
|
744
|
-
async listManifests() {
|
|
745
|
-
return (await this.#dispatch("listManifests", []));
|
|
746
|
-
}
|
|
747
|
-
async listManifestPage(afterVersion, limit) {
|
|
748
|
-
return (await this.#dispatch("listManifestPage", [afterVersion, limit]));
|
|
749
|
-
}
|
|
750
|
-
async publishManifest(input) {
|
|
751
|
-
return (await this.#dispatch("publishManifest", [input]));
|
|
752
|
-
}
|
|
753
|
-
async createTransaction(record) {
|
|
754
|
-
await this.#dispatch("createTransaction", [record]);
|
|
755
|
-
}
|
|
756
|
-
async beginTransaction(input) {
|
|
757
|
-
return (await this.#dispatch("beginTransaction", [input]));
|
|
758
|
-
}
|
|
759
|
-
async getTransaction(id) {
|
|
760
|
-
return (await this.#dispatch("getTransaction", [id]));
|
|
761
|
-
}
|
|
762
|
-
async getTransactions(ids) {
|
|
763
|
-
return (await this.#dispatch("getTransactions", [ids]));
|
|
764
|
-
}
|
|
765
|
-
async listTransactions() {
|
|
766
|
-
return (await this.#dispatch("listTransactions", []));
|
|
767
|
-
}
|
|
768
|
-
async listTransactionPage(afterId, limit) {
|
|
769
|
-
return (await this.#dispatch("listTransactionPage", [afterId, limit]));
|
|
770
|
-
}
|
|
771
|
-
async updateTransaction(id, expectedRevision, update) {
|
|
772
|
-
return (await this.#dispatch("updateTransaction", [
|
|
773
|
-
id,
|
|
774
|
-
expectedRevision,
|
|
775
|
-
update,
|
|
776
|
-
]));
|
|
777
|
-
}
|
|
778
|
-
async stageTransactionArtifacts(input) {
|
|
779
|
-
return (await this.#dispatch("stageTransactionArtifacts", [input]));
|
|
780
|
-
}
|
|
781
|
-
async commitTransaction(input) {
|
|
782
|
-
return (await this.#dispatch("commitTransaction", [input]));
|
|
783
|
-
}
|
|
784
|
-
async writeTransaction(input) {
|
|
785
|
-
return (await this.#dispatch("writeTransaction", [input]));
|
|
786
|
-
}
|
|
787
|
-
async createLease(record) {
|
|
788
|
-
await this.#dispatch("createLease", [record]);
|
|
789
|
-
}
|
|
790
|
-
async getLease(id) {
|
|
791
|
-
return (await this.#dispatch("getLease", [id]));
|
|
792
|
-
}
|
|
793
|
-
async listLeases() {
|
|
794
|
-
return (await this.#dispatch("listLeases", []));
|
|
795
|
-
}
|
|
796
|
-
async renewLease(id, expectedRevision, expiresAt) {
|
|
797
|
-
return (await this.#dispatch("renewLease", [id, expectedRevision, expiresAt]));
|
|
798
|
-
}
|
|
799
|
-
async moveLease(id, expectedRevision, manifestVersion, expiresAt) {
|
|
800
|
-
return (await this.#dispatch("moveLease", [
|
|
801
|
-
id,
|
|
802
|
-
expectedRevision,
|
|
803
|
-
manifestVersion,
|
|
804
|
-
expiresAt,
|
|
805
|
-
]));
|
|
806
|
-
}
|
|
807
|
-
async removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff) {
|
|
808
|
-
return (await this.#dispatch("removeLeaseIfExpired", [
|
|
809
|
-
id,
|
|
810
|
-
expectedRevision,
|
|
811
|
-
expiresAtCutoff,
|
|
812
|
-
]));
|
|
813
|
-
}
|
|
814
|
-
async removeLease(id) {
|
|
815
|
-
await this.#dispatch("removeLease", [id]);
|
|
816
|
-
}
|
|
817
|
-
async createCompactionJob(record) {
|
|
818
|
-
await this.#dispatch("createCompactionJob", [record]);
|
|
819
|
-
}
|
|
820
|
-
async getCompactionJob(id) {
|
|
821
|
-
return (await this.#dispatch("getCompactionJob", [id]));
|
|
822
|
-
}
|
|
823
|
-
async listCompactionJobs(tableId) {
|
|
824
|
-
return (await this.#dispatch("listCompactionJobs", [tableId]));
|
|
825
|
-
}
|
|
826
|
-
async listCompactionJobPage(afterId, limit) {
|
|
827
|
-
return (await this.#dispatch("listCompactionJobPage", [afterId, limit]));
|
|
828
|
-
}
|
|
829
|
-
async updateCompactionJob(id, expectedRevision, update) {
|
|
830
|
-
return (await this.#dispatch("updateCompactionJob", [
|
|
831
|
-
id,
|
|
832
|
-
expectedRevision,
|
|
833
|
-
update,
|
|
834
|
-
]));
|
|
835
|
-
}
|
|
836
|
-
async cancelCompactionJob(id, expectedRevision, cancelledAt) {
|
|
837
|
-
return (await this.#dispatch("cancelCompactionJob", [
|
|
838
|
-
id,
|
|
839
|
-
expectedRevision,
|
|
840
|
-
cancelledAt,
|
|
841
|
-
]));
|
|
842
|
-
}
|
|
843
|
-
async removeCompactionJob(id) {
|
|
844
|
-
await this.#dispatch("removeCompactionJob", [id]);
|
|
845
|
-
}
|
|
846
|
-
async createGarbageCollectionJob(input) {
|
|
847
|
-
return (await this.#dispatch("createGarbageCollectionJob", [
|
|
848
|
-
input,
|
|
849
|
-
]));
|
|
850
|
-
}
|
|
851
|
-
async getGarbageCollectionJob(id) {
|
|
852
|
-
return (await this.#dispatch("getGarbageCollectionJob", [id]));
|
|
853
|
-
}
|
|
854
|
-
async listGarbageCollectionJobs() {
|
|
855
|
-
return (await this.#dispatch("listGarbageCollectionJobs", []));
|
|
856
|
-
}
|
|
857
|
-
async runGarbageCollectionStep(input) {
|
|
858
|
-
return (await this.#dispatch("runGarbageCollectionStep", [
|
|
859
|
-
input,
|
|
860
|
-
]));
|
|
861
|
-
}
|
|
862
|
-
async removeGarbageCollectionJob(id) {
|
|
863
|
-
await this.#dispatch("removeGarbageCollectionJob", [id]);
|
|
864
|
-
}
|
|
865
|
-
async removePrunedManifestRecords() {
|
|
866
|
-
return (await this.#dispatch("removePrunedManifestRecords", []));
|
|
867
|
-
}
|
|
868
|
-
async getLogicalStorageBytes() {
|
|
869
|
-
return (await this.#dispatch("getLogicalStorageBytes", []));
|
|
870
|
-
}
|
|
871
|
-
async exportSnapshot() {
|
|
872
|
-
return (await this.#dispatch("exportSnapshot", []));
|
|
873
|
-
}
|
|
874
|
-
async importSnapshot(snapshot, options = {}) {
|
|
875
|
-
const leader = this.#leader;
|
|
876
|
-
if (leader !== undefined) {
|
|
877
|
-
return leader.importSnapshot(snapshot, options);
|
|
821
|
+
await this.#dispatch("removeTempRun", [ownerId, runId]);
|
|
822
|
+
}
|
|
823
|
+
/** @internal Shared implementation installed for every ordinary RPC method below. */
|
|
824
|
+
async _dispatchGenerated(method, args) {
|
|
825
|
+
const bulkIndex = BULK_READ_ARGUMENT_INDEX[method];
|
|
826
|
+
if (bulkIndex !== undefined) {
|
|
827
|
+
const bulkItems = args[bulkIndex];
|
|
828
|
+
if (!Array.isArray(bulkItems))
|
|
829
|
+
throw new TypeError(`${method} items must be an array`);
|
|
830
|
+
assertStorageBulkReadItems(bulkItems, `${method} request`);
|
|
878
831
|
}
|
|
879
|
-
|
|
880
|
-
const totalBytes = snapshot.blocks.reduce((total, block) => total + block.bytes.byteLength, 0);
|
|
881
|
-
options.onProgress?.({ phase: "blocks", writtenBytes: 0, totalBytes });
|
|
882
|
-
await this.#dispatch("importSnapshot", [snapshot, {}]);
|
|
883
|
-
options.onProgress?.({ phase: "done", writtenBytes: totalBytes, totalBytes });
|
|
832
|
+
return this.#dispatch(method, args);
|
|
884
833
|
}
|
|
885
834
|
close() {
|
|
886
835
|
if (this.#closed)
|
|
@@ -893,7 +842,15 @@ export class OpfsBlockStore {
|
|
|
893
842
|
pending.reject(new Error("This OPFS store connection is closed"));
|
|
894
843
|
}
|
|
895
844
|
this.#pending.clear();
|
|
896
|
-
this.#
|
|
845
|
+
this.#inFlightMutations.clear();
|
|
846
|
+
this.#settledMutations.clear();
|
|
847
|
+
this.#servedRequestLocks.clear();
|
|
848
|
+
this.#pendingRpcBytes = 0;
|
|
849
|
+
this.#inFlightMutationBytes = 0;
|
|
850
|
+
this.#inFlightReadBytes = 0;
|
|
851
|
+
this.#settledMutationBytes = 0;
|
|
852
|
+
this.#servedRequestCount = 0;
|
|
853
|
+
this.#servedRequestBytes = 0;
|
|
897
854
|
const leader = this.#leader;
|
|
898
855
|
this.#leader = undefined;
|
|
899
856
|
if (leader !== undefined) {
|
|
@@ -918,12 +875,42 @@ export class OpfsBlockStore {
|
|
|
918
875
|
_instanceIdForTests() {
|
|
919
876
|
return this.#instanceId;
|
|
920
877
|
}
|
|
878
|
+
/** Test-only: simulates an acknowledgement lost after the served operation settles. */
|
|
879
|
+
_dropNextRpcResultForTests() {
|
|
880
|
+
this.#dropNextRpcResultForTests = true;
|
|
881
|
+
}
|
|
882
|
+
/** Test-only: holds newly admitted served mutations until the returned release is called. */
|
|
883
|
+
_holdServedMutationsForTests() {
|
|
884
|
+
if (this.#servedMutationGateForTests !== undefined) {
|
|
885
|
+
throw new Error("Served mutations are already held");
|
|
886
|
+
}
|
|
887
|
+
let release;
|
|
888
|
+
this.#servedMutationGateForTests = new Promise((resolve) => {
|
|
889
|
+
release = resolve;
|
|
890
|
+
});
|
|
891
|
+
return () => {
|
|
892
|
+
this.#servedMutationGateForTests = undefined;
|
|
893
|
+
release();
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
/** Test-only: retransmits the oldest request with its stable deduplication identity. */
|
|
897
|
+
_resendOldestPendingForTests() {
|
|
898
|
+
const pending = this.#pending.values().next().value;
|
|
899
|
+
if (pending?.sentTo !== undefined)
|
|
900
|
+
this.#send(pending.sentTo, pending.message);
|
|
901
|
+
}
|
|
921
902
|
/** Test-only counters that pin the connection's bounded RPC state and close-time cleanup. */
|
|
922
903
|
_residentStateForTests() {
|
|
923
904
|
return {
|
|
924
905
|
answerChannels: this.#answerChannels.size,
|
|
925
|
-
dedupeEntries: this.#
|
|
906
|
+
dedupeEntries: this.#settledMutations.size,
|
|
907
|
+
inFlightMutations: this.#inFlightMutations.size,
|
|
926
908
|
pendingRequests: this.#pending.size,
|
|
909
|
+
retainedRpcBytes: this.#pendingRpcBytes +
|
|
910
|
+
this.#inFlightMutationBytes +
|
|
911
|
+
this.#inFlightReadBytes +
|
|
912
|
+
this.#settledMutationBytes +
|
|
913
|
+
this.#servedRequestBytes,
|
|
927
914
|
closed: this.#closed,
|
|
928
915
|
};
|
|
929
916
|
}
|
|
@@ -935,47 +922,110 @@ export class OpfsBlockStore {
|
|
|
935
922
|
for (const pending of this.#pending.values())
|
|
936
923
|
clearTimeout(pending.timer);
|
|
937
924
|
this.#pending.clear();
|
|
938
|
-
this.#
|
|
925
|
+
this.#inFlightMutations.clear();
|
|
926
|
+
this.#settledMutations.clear();
|
|
927
|
+
this.#servedRequestLocks.clear();
|
|
928
|
+
this.#pendingRpcBytes = 0;
|
|
929
|
+
this.#inFlightMutationBytes = 0;
|
|
930
|
+
this.#inFlightReadBytes = 0;
|
|
931
|
+
this.#settledMutationBytes = 0;
|
|
932
|
+
this.#servedRequestCount = 0;
|
|
933
|
+
this.#servedRequestBytes = 0;
|
|
939
934
|
this.#leader?.crash();
|
|
940
935
|
this.#leader = undefined;
|
|
941
936
|
this.#closeChannels();
|
|
942
937
|
}
|
|
943
938
|
async #ensureFormatMarker() {
|
|
944
|
-
const existing = await this.#tree.readFile(["format.json"], {
|
|
939
|
+
const existing = await this.#tree.readFile(["format.json"], {
|
|
940
|
+
lockedMeansAbsent: true,
|
|
941
|
+
maxBytes: 1024,
|
|
942
|
+
});
|
|
945
943
|
if (existing !== undefined) {
|
|
946
944
|
try {
|
|
947
|
-
|
|
948
|
-
if (typeof parsed.formatVersion === "number" &&
|
|
949
|
-
parsed.formatVersion !== LOG_FORMAT_VERSION) {
|
|
950
|
-
throw new Error(`This database uses OPFS layout version ${String(parsed.formatVersion)}; this build ` +
|
|
951
|
-
`reads version ${String(LOG_FORMAT_VERSION)}. Delete it with deleteOpfsDatabase() — ` +
|
|
952
|
-
`earlier layouts were experimental and carry no compatibility promise.`);
|
|
953
|
-
}
|
|
945
|
+
this.#validateFormatMarker(existing);
|
|
954
946
|
return;
|
|
955
947
|
}
|
|
956
948
|
catch (error) {
|
|
957
949
|
if (!(error instanceof SyntaxError))
|
|
958
950
|
throw error;
|
|
959
|
-
// A torn marker from a crashed first open
|
|
951
|
+
// A torn marker from a crashed first open is repairable only while no database
|
|
952
|
+
// artifacts exist. Never stamp the current version over an unversioned WAL.
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
let artifact;
|
|
956
|
+
for await (const name of this.#tree.iterateNames([])) {
|
|
957
|
+
if (name !== "format.json") {
|
|
958
|
+
artifact = name;
|
|
959
|
+
break;
|
|
960
960
|
}
|
|
961
961
|
}
|
|
962
|
+
if (artifact !== undefined) {
|
|
963
|
+
throw new StorageCorruptionError("opfs", "format.json", `The OPFS format marker is ${existing === undefined ? "missing" : "torn"}, but the ` +
|
|
964
|
+
`database directory already contains storage artifacts (${artifact}). ` +
|
|
965
|
+
`Refusing to guess their layout version.`);
|
|
966
|
+
}
|
|
962
967
|
const bytes = new TextEncoder().encode(JSON.stringify({ formatVersion: LOG_FORMAT_VERSION }));
|
|
963
968
|
try {
|
|
964
969
|
await this.#tree.writeFile(["format.json"], bytes, { flush: true });
|
|
965
970
|
}
|
|
966
971
|
catch (error) {
|
|
967
|
-
// A concurrent open won the marker; theirs says the same thing.
|
|
968
972
|
if (!isLockContention(error))
|
|
969
973
|
throw error;
|
|
974
|
+
// Another opener owns the marker write. Do not assume it writes this build's version:
|
|
975
|
+
// wait until its handle closes, then validate the bytes it actually published.
|
|
976
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
977
|
+
await sleep(5 + Math.random() * 10);
|
|
978
|
+
const published = await this.#tree.readFile(["format.json"], {
|
|
979
|
+
lockedMeansAbsent: true,
|
|
980
|
+
maxBytes: 1024,
|
|
981
|
+
});
|
|
982
|
+
if (published === undefined)
|
|
983
|
+
continue;
|
|
984
|
+
this.#validateFormatMarker(published);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
throw new Error("A concurrent opener did not publish a readable OPFS format marker", {
|
|
988
|
+
cause: error,
|
|
989
|
+
});
|
|
970
990
|
}
|
|
971
991
|
}
|
|
992
|
+
#validateFormatMarker(bytes) {
|
|
993
|
+
const text = new TextDecoder().decode(bytes);
|
|
994
|
+
const parsed = JSON.parse(text);
|
|
995
|
+
if (typeof parsed !== "object" ||
|
|
996
|
+
parsed === null ||
|
|
997
|
+
Array.isArray(parsed) ||
|
|
998
|
+
!Number.isSafeInteger(parsed.formatVersion)) {
|
|
999
|
+
throw new StorageCorruptionError("opfs", "format.json", "The OPFS format marker is invalid: formatVersion must be a safe integer");
|
|
1000
|
+
}
|
|
1001
|
+
const formatVersion = parsed.formatVersion;
|
|
1002
|
+
if (formatVersion !== LOG_FORMAT_VERSION) {
|
|
1003
|
+
throw new StorageFormatVersionError("opfs", "format.json", formatVersion, LOG_FORMAT_VERSION, formatVersion < LOG_FORMAT_VERSION ? "older" : "newer");
|
|
1004
|
+
}
|
|
1005
|
+
if (Object.keys(parsed).length !== 1 ||
|
|
1006
|
+
!Object.hasOwn(parsed, "formatVersion") ||
|
|
1007
|
+
text !== JSON.stringify({ formatVersion })) {
|
|
1008
|
+
throw new StorageCorruptionError("opfs", "format.json", "The OPFS format marker is not the canonical layout marker");
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
for (const method of RPC_METHODS) {
|
|
1013
|
+
if (method in OpfsBlockStore.prototype)
|
|
1014
|
+
continue;
|
|
1015
|
+
Object.defineProperty(OpfsBlockStore.prototype, method, {
|
|
1016
|
+
configurable: true,
|
|
1017
|
+
value(...args) {
|
|
1018
|
+
return this._dispatchGenerated(method, args);
|
|
1019
|
+
},
|
|
1020
|
+
});
|
|
972
1021
|
}
|
|
973
1022
|
/** Removes every file of a database created by `OpfsBlockStore.open` under this name. */
|
|
974
1023
|
export async function deleteOpfsDatabase(options) {
|
|
1024
|
+
const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
|
|
975
1025
|
const root = options.root ?? (await navigator.storage.getDirectory());
|
|
976
1026
|
try {
|
|
977
1027
|
const namespace = await root.getDirectoryHandle("minnowdb");
|
|
978
|
-
await namespace.removeEntry(
|
|
1028
|
+
await namespace.removeEntry(encodedName, { recursive: true });
|
|
979
1029
|
}
|
|
980
1030
|
catch (error) {
|
|
981
1031
|
if (!isDomError(error, "NotFoundError"))
|
|
@@ -984,11 +1034,10 @@ export async function deleteOpfsDatabase(options) {
|
|
|
984
1034
|
}
|
|
985
1035
|
const RPC_TIMED_OUT = new Error("The leader did not answer in time");
|
|
986
1036
|
async function resolveDatabaseRoot(options) {
|
|
987
|
-
|
|
988
|
-
throw new TypeError("Database name cannot be empty");
|
|
1037
|
+
const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
|
|
989
1038
|
const root = options.root ?? (await navigator.storage.getDirectory());
|
|
990
1039
|
const namespace = await root.getDirectoryHandle("minnowdb", { create: true });
|
|
991
|
-
return namespace.getDirectoryHandle(
|
|
1040
|
+
return namespace.getDirectoryHandle(encodedName, { create: true });
|
|
992
1041
|
}
|
|
993
1042
|
function isLockContention(error) {
|
|
994
1043
|
return isDomError(error, "NoModificationAllowedError") || isDomError(error, "InvalidStateError");
|
|
@@ -996,4 +1045,3 @@ function isLockContention(error) {
|
|
|
996
1045
|
function sleep(ms) {
|
|
997
1046
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
998
1047
|
}
|
|
999
|
-
//# sourceMappingURL=store.js.map
|