@minnowdb/core 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -32
- package/dist/block-format/block.d.ts +32 -13
- package/dist/block-format/block.js +179 -55
- package/dist/block-format/checksum.d.ts +2 -1
- package/dist/block-format/checksum.js +5 -2
- package/dist/block-format/codecs.d.ts +7 -2
- package/dist/block-format/codecs.js +53 -12
- package/dist/block-format/column.d.ts +3 -2
- package/dist/block-format/column.js +96 -30
- package/dist/block-format/index.d.ts +2 -2
- package/dist/block-format/index.js +2 -2
- package/dist/block-format/physical.d.ts +8 -2
- package/dist/block-format/physical.js +14 -6
- package/dist/block-format/types.d.ts +7 -3
- package/dist/block-format/types.js +0 -1
- package/dist/block-format/unicode.d.ts +11 -0
- package/dist/block-format/unicode.js +46 -0
- package/dist/date-value.d.ts +15 -0
- package/dist/date-value.js +64 -0
- package/dist/engine/artifact-cache.d.ts +2 -1
- package/dist/engine/artifact-cache.js +5 -1
- package/dist/engine/batch.d.ts +11 -3
- package/dist/engine/batch.js +17 -7
- package/dist/engine/buffered-writer.d.ts +2 -1
- package/dist/engine/buffered-writer.js +34 -9
- package/dist/engine/cache-limits.d.ts +25 -0
- package/dist/engine/cache-limits.js +25 -0
- package/dist/engine/catalog.d.ts +24 -5
- package/dist/engine/catalog.js +34 -7
- package/dist/engine/client.d.ts +29 -7
- package/dist/engine/client.js +216 -40
- package/dist/engine/database.d.ts +224 -39
- package/dist/engine/database.js +7475 -1539
- package/dist/engine/defaults.d.ts +4 -8
- package/dist/engine/defaults.js +46 -22
- package/dist/engine/errors.d.ts +19 -1
- package/dist/engine/errors.js +32 -2
- package/dist/engine/fts.d.ts +0 -6
- package/dist/engine/fts.js +41 -14
- package/dist/engine/group-index.d.ts +0 -1
- package/dist/engine/group-index.js +6 -11
- package/dist/engine/index.d.ts +16 -10
- package/dist/engine/index.js +12 -9
- package/dist/engine/join-index.d.ts +0 -1
- package/dist/engine/join-index.js +2 -2
- package/dist/engine/keyed-live.d.ts +57 -0
- package/dist/engine/keyed-live.js +226 -0
- package/dist/engine/live-api.d.ts +4 -0
- package/dist/engine/live-api.js +4 -0
- package/dist/engine/live.d.ts +43 -26
- package/dist/engine/live.js +504 -126
- package/dist/engine/memory.d.ts +2 -1
- package/dist/engine/memory.js +3 -2
- package/dist/engine/optimizer.d.ts +0 -1
- package/dist/engine/optimizer.js +262 -27
- package/dist/engine/query-api.d.ts +3 -0
- package/dist/engine/query-api.js +3 -0
- package/dist/engine/query-cache.d.ts +1 -2
- package/dist/engine/query-cache.js +7 -5
- package/dist/engine/query.d.ts +134 -29
- package/dist/engine/query.js +1668 -372
- package/dist/engine/result-wire.d.ts +0 -1
- package/dist/engine/result-wire.js +2 -2
- package/dist/engine/schema-wire.d.ts +6 -3
- package/dist/engine/schema-wire.js +40 -34
- package/dist/engine/schema.d.ts +156 -80
- package/dist/engine/schema.js +570 -108
- package/dist/engine/sort-keys.d.ts +4 -4
- package/dist/engine/sort-keys.js +44 -29
- package/dist/engine/sql-domains.d.ts +31 -0
- package/dist/engine/sql-domains.js +585 -0
- package/dist/engine/sql-driver.d.ts +19 -0
- package/dist/engine/sql-driver.js +1 -0
- package/dist/engine/sql-json.d.ts +7 -9
- package/dist/engine/sql-json.js +75 -15
- package/dist/engine/sql-semantics.d.ts +8 -3
- package/dist/engine/sql-semantics.js +458 -30
- package/dist/engine/typed-live.d.ts +67 -0
- package/dist/engine/typed-live.js +349 -0
- package/dist/engine/vector.d.ts +12 -2
- package/dist/engine/vector.js +635 -119
- package/dist/engine/worker-host.d.ts +9 -2
- package/dist/engine/worker-host.js +620 -152
- package/dist/engine/worker.d.ts +0 -1
- package/dist/engine/worker.js +3 -3
- package/dist/engine/write-block-planner.d.ts +18 -0
- package/dist/engine/write-block-planner.js +112 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/plan/index.d.ts +3 -4
- package/dist/plan/index.js +3 -4
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/storage/indexeddb.d.ts +99 -51
- package/dist/storage/indexeddb.js +13448 -2660
- package/dist/storage/memory.d.ts +64 -57
- package/dist/storage/memory.js +1042 -128
- package/dist/storage/opfs/files.d.ts +18 -7
- package/dist/storage/opfs/files.js +115 -28
- package/dist/storage/opfs/index.d.ts +1 -1
- package/dist/storage/opfs/index.js +1 -1
- package/dist/storage/opfs/leader.d.ts +409 -75
- package/dist/storage/opfs/leader.js +4621 -650
- package/dist/storage/opfs/rpc.d.ts +15 -3
- package/dist/storage/opfs/rpc.js +224 -2
- package/dist/storage/opfs/snapshot-ledger.d.ts +40 -0
- package/dist/storage/opfs/snapshot-ledger.js +281 -0
- package/dist/storage/opfs/store.d.ts +33 -99
- package/dist/storage/opfs/store.js +453 -364
- package/dist/storage/persistence.d.ts +37 -0
- package/dist/storage/persistence.js +78 -0
- package/dist/storage/snapshot-stream.d.ts +24 -0
- package/dist/storage/snapshot-stream.js +897 -0
- package/dist/storage/snapshot.d.ts +9 -85
- package/dist/storage/snapshot.js +33 -265
- package/dist/storage/toolkit/extents.d.ts +41 -7
- package/dist/storage/toolkit/extents.js +402 -39
- package/dist/storage/toolkit/index.d.ts +4 -5
- package/dist/storage/toolkit/index.js +28 -4
- package/dist/storage/toolkit/record-core.d.ts +182 -55
- package/dist/storage/toolkit/record-core.js +6158 -1331
- package/dist/storage/toolkit/sync-file.d.ts +10 -1
- package/dist/storage/toolkit/sync-file.js +50 -2
- package/dist/storage/toolkit/wal.d.ts +23 -5
- package/dist/storage/toolkit/wal.js +89 -27
- package/dist/storage/toolkit/wire.d.ts +15 -2
- package/dist/storage/toolkit/wire.js +199 -10
- package/dist/storage/types.d.ts +1439 -204
- package/dist/storage/types.js +1745 -141
- package/dist/testing/block-store-conformance.d.ts +0 -1
- package/dist/testing/block-store-conformance.js +1009 -119
- package/dist/testing/index.d.ts +46 -26
- package/dist/testing/index.js +112 -57
- package/dist/testing/opfs-shim.d.ts +13 -3
- package/dist/testing/opfs-shim.js +40 -6
- package/dist/testing/simulator.d.ts +97 -0
- package/dist/testing/simulator.js +591 -0
- package/dist/testing/sqllogictest.d.ts +89 -0
- package/dist/testing/sqllogictest.js +434 -0
- package/dist/transactions/index.d.ts +92 -16
- package/dist/transactions/index.js +1074 -236
- package/dist/worker-protocol/index.d.ts +3 -2
- package/dist/worker-protocol/index.js +2 -5
- package/package.json +53 -3
- package/postgres-feature-profile.json +223 -0
- package/sql-feature-matrix.json +172 -252
- package/dist/block-format/block.d.ts.map +0 -1
- package/dist/block-format/block.js.map +0 -1
- package/dist/block-format/checksum.d.ts.map +0 -1
- package/dist/block-format/checksum.js.map +0 -1
- package/dist/block-format/codecs.d.ts.map +0 -1
- package/dist/block-format/codecs.js.map +0 -1
- package/dist/block-format/column.d.ts.map +0 -1
- package/dist/block-format/column.js.map +0 -1
- package/dist/block-format/index.d.ts.map +0 -1
- package/dist/block-format/index.js.map +0 -1
- package/dist/block-format/physical.d.ts.map +0 -1
- package/dist/block-format/physical.js.map +0 -1
- package/dist/block-format/types.d.ts.map +0 -1
- package/dist/block-format/types.js.map +0 -1
- package/dist/engine/artifact-cache.d.ts.map +0 -1
- package/dist/engine/artifact-cache.js.map +0 -1
- package/dist/engine/batch.d.ts.map +0 -1
- package/dist/engine/batch.js.map +0 -1
- package/dist/engine/buffered-writer.d.ts.map +0 -1
- package/dist/engine/buffered-writer.js.map +0 -1
- package/dist/engine/catalog.d.ts.map +0 -1
- package/dist/engine/catalog.js.map +0 -1
- package/dist/engine/client.d.ts.map +0 -1
- package/dist/engine/client.js.map +0 -1
- package/dist/engine/coordinator.d.ts +0 -17
- package/dist/engine/coordinator.d.ts.map +0 -1
- package/dist/engine/coordinator.js +0 -60
- package/dist/engine/coordinator.js.map +0 -1
- package/dist/engine/database.d.ts.map +0 -1
- package/dist/engine/database.js.map +0 -1
- package/dist/engine/defaults.d.ts.map +0 -1
- package/dist/engine/defaults.js.map +0 -1
- package/dist/engine/errors.d.ts.map +0 -1
- package/dist/engine/errors.js.map +0 -1
- package/dist/engine/fts.d.ts.map +0 -1
- package/dist/engine/fts.js.map +0 -1
- package/dist/engine/group-index.d.ts.map +0 -1
- package/dist/engine/group-index.js.map +0 -1
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js.map +0 -1
- package/dist/engine/join-index.d.ts.map +0 -1
- package/dist/engine/join-index.js.map +0 -1
- package/dist/engine/live.d.ts.map +0 -1
- package/dist/engine/live.js.map +0 -1
- package/dist/engine/memory.d.ts.map +0 -1
- package/dist/engine/memory.js.map +0 -1
- package/dist/engine/optimizer.d.ts.map +0 -1
- package/dist/engine/optimizer.js.map +0 -1
- package/dist/engine/query-cache.d.ts.map +0 -1
- package/dist/engine/query-cache.js.map +0 -1
- package/dist/engine/query.d.ts.map +0 -1
- package/dist/engine/query.js.map +0 -1
- package/dist/engine/result-wire.d.ts.map +0 -1
- package/dist/engine/result-wire.js.map +0 -1
- package/dist/engine/schema-wire.d.ts.map +0 -1
- package/dist/engine/schema-wire.js.map +0 -1
- package/dist/engine/schema.d.ts.map +0 -1
- package/dist/engine/schema.js.map +0 -1
- package/dist/engine/sort-keys.d.ts.map +0 -1
- package/dist/engine/sort-keys.js.map +0 -1
- package/dist/engine/sql-json.d.ts.map +0 -1
- package/dist/engine/sql-json.js.map +0 -1
- package/dist/engine/sql-semantics.d.ts.map +0 -1
- package/dist/engine/sql-semantics.js.map +0 -1
- package/dist/engine/vector.d.ts.map +0 -1
- package/dist/engine/vector.js.map +0 -1
- package/dist/engine/worker-host.d.ts.map +0 -1
- package/dist/engine/worker-host.js.map +0 -1
- package/dist/engine/worker.d.ts.map +0 -1
- package/dist/engine/worker.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/plan/index.d.ts.map +0 -1
- package/dist/plan/index.js.map +0 -1
- package/dist/storage/fixture-shape.d.ts +0 -42
- package/dist/storage/fixture-shape.d.ts.map +0 -1
- package/dist/storage/fixture-shape.js +0 -146
- package/dist/storage/fixture-shape.js.map +0 -1
- package/dist/storage/index.d.ts.map +0 -1
- package/dist/storage/index.js.map +0 -1
- package/dist/storage/indexeddb.d.ts.map +0 -1
- package/dist/storage/indexeddb.js.map +0 -1
- package/dist/storage/memory.d.ts.map +0 -1
- package/dist/storage/memory.js.map +0 -1
- package/dist/storage/opfs/files.d.ts.map +0 -1
- package/dist/storage/opfs/files.js.map +0 -1
- package/dist/storage/opfs/index.d.ts.map +0 -1
- package/dist/storage/opfs/index.js.map +0 -1
- package/dist/storage/opfs/leader.d.ts.map +0 -1
- package/dist/storage/opfs/leader.js.map +0 -1
- package/dist/storage/opfs/rpc.d.ts.map +0 -1
- package/dist/storage/opfs/rpc.js.map +0 -1
- package/dist/storage/opfs/store.d.ts.map +0 -1
- package/dist/storage/opfs/store.js.map +0 -1
- package/dist/storage/snapshot.d.ts.map +0 -1
- package/dist/storage/snapshot.js.map +0 -1
- package/dist/storage/toolkit/extents.d.ts.map +0 -1
- package/dist/storage/toolkit/extents.js.map +0 -1
- package/dist/storage/toolkit/index.d.ts.map +0 -1
- package/dist/storage/toolkit/index.js.map +0 -1
- package/dist/storage/toolkit/record-core.d.ts.map +0 -1
- package/dist/storage/toolkit/record-core.js.map +0 -1
- package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
- package/dist/storage/toolkit/sync-file.js.map +0 -1
- package/dist/storage/toolkit/wal.d.ts.map +0 -1
- package/dist/storage/toolkit/wal.js.map +0 -1
- package/dist/storage/toolkit/wire.d.ts.map +0 -1
- package/dist/storage/toolkit/wire.js.map +0 -1
- package/dist/storage/types.d.ts.map +0 -1
- package/dist/storage/types.js.map +0 -1
- package/dist/testing/block-store-conformance.d.ts.map +0 -1
- package/dist/testing/block-store-conformance.js.map +0 -1
- package/dist/testing/index.d.ts.map +0 -1
- package/dist/testing/index.js.map +0 -1
- package/dist/testing/opfs-shim.d.ts.map +0 -1
- package/dist/testing/opfs-shim.js.map +0 -1
- package/dist/testing/seeds.d.ts +0 -11
- package/dist/testing/seeds.d.ts.map +0 -1
- package/dist/testing/seeds.js +0 -50
- package/dist/testing/seeds.js.map +0 -1
- package/dist/transactions/index.d.ts.map +0 -1
- package/dist/transactions/index.js.map +0 -1
- package/dist/worker-protocol/index.d.ts.map +0 -1
- package/dist/worker-protocol/index.js.map +0 -1
|
@@ -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,52 +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",
|
|
48
|
+
"removeFtsColumn",
|
|
32
49
|
"writeFtsBase",
|
|
50
|
+
"beginFtsBaseBuild",
|
|
51
|
+
"renewFtsBaseBuild",
|
|
52
|
+
"writeFtsBaseBuildChunk",
|
|
53
|
+
"finishFtsBaseBuild",
|
|
54
|
+
"abortFtsBaseBuild",
|
|
33
55
|
"readFtsCandidates",
|
|
34
|
-
"
|
|
56
|
+
"readFtsPostings",
|
|
35
57
|
"getSegment",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
58
|
+
"listSegmentPage",
|
|
59
|
+
"listTableSegmentPage",
|
|
60
|
+
"removeAbortedSegment",
|
|
61
|
+
"adoptAbortedSegment",
|
|
38
62
|
"reserveRowIds",
|
|
39
63
|
"reserveAutoIncrement",
|
|
40
64
|
"getExistingUniqueKeys",
|
|
65
|
+
"beginUniqueKeyBuild",
|
|
66
|
+
"getUniqueKeyBuild",
|
|
67
|
+
"renewUniqueKeyBuild",
|
|
68
|
+
"appendUniqueKeyBuildChunk",
|
|
69
|
+
"finishUniqueKeyBuild",
|
|
70
|
+
"abortUniqueKeyBuild",
|
|
41
71
|
"getCurrentManifest",
|
|
42
72
|
"getCurrentManifestVersion",
|
|
43
73
|
"getCatalogProbe",
|
|
44
|
-
"getQueryCatalogState",
|
|
45
74
|
"getManifest",
|
|
46
|
-
"listManifests",
|
|
47
75
|
"listManifestPage",
|
|
48
|
-
"publishManifest",
|
|
49
76
|
"createTransaction",
|
|
77
|
+
"renewTransaction",
|
|
78
|
+
"abortTransactionIfExpired",
|
|
50
79
|
"beginTransaction",
|
|
51
80
|
"getTransaction",
|
|
52
81
|
"getTransactions",
|
|
53
|
-
"listTransactions",
|
|
54
82
|
"listTransactionPage",
|
|
55
83
|
"updateTransaction",
|
|
56
84
|
"stageTransactionArtifacts",
|
|
85
|
+
"rollbackTransactionArtifacts",
|
|
57
86
|
"commitTransaction",
|
|
58
87
|
"writeTransaction",
|
|
59
88
|
"createLease",
|
|
60
89
|
"getLease",
|
|
61
90
|
"listLeases",
|
|
91
|
+
"listExpiredLeasePage",
|
|
62
92
|
"renewLease",
|
|
63
93
|
"moveLease",
|
|
64
94
|
"removeLeaseIfExpired",
|
|
@@ -71,20 +101,35 @@ const RPC_METHODS = new Set([
|
|
|
71
101
|
"cancelCompactionJob",
|
|
72
102
|
"removeCompactionJob",
|
|
73
103
|
"createGarbageCollectionJob",
|
|
104
|
+
"updateGarbageCollectionPlanning",
|
|
74
105
|
"getGarbageCollectionJob",
|
|
75
106
|
"listGarbageCollectionJobs",
|
|
107
|
+
"listGarbageCollectionJobPage",
|
|
76
108
|
"runGarbageCollectionStep",
|
|
77
109
|
"removePrunedManifestRecords",
|
|
78
110
|
"removeGarbageCollectionJob",
|
|
79
111
|
"createTempOwner",
|
|
80
112
|
"getTempOwner",
|
|
113
|
+
"putTempRunPage",
|
|
114
|
+
"putTempRunPages",
|
|
115
|
+
"getTempRunPage",
|
|
116
|
+
"removeTempRun",
|
|
81
117
|
"renewTempOwner",
|
|
82
118
|
"removeTempOwnerIfExpired",
|
|
83
119
|
"removeTempOwner",
|
|
84
120
|
"listTempOwnerIdsPage",
|
|
121
|
+
"listExpiredTempOwnerPage",
|
|
85
122
|
"getLogicalStorageBytes",
|
|
86
|
-
"
|
|
87
|
-
"
|
|
123
|
+
"getStorageStats",
|
|
124
|
+
"checkIntegrity",
|
|
125
|
+
"beginSnapshotFrameExport",
|
|
126
|
+
"readSnapshotExportFrame",
|
|
127
|
+
"closeSnapshotFrameExport",
|
|
128
|
+
"beginSnapshotFrameImport",
|
|
129
|
+
"renewSnapshotFrameImport",
|
|
130
|
+
"appendSnapshotImportFrames",
|
|
131
|
+
"finishSnapshotFrameImport",
|
|
132
|
+
"cancelSnapshotFrameImport",
|
|
88
133
|
]);
|
|
89
134
|
/**
|
|
90
135
|
* Reads are idempotent — a lost-acknowledgement retry can simply run again — so only
|
|
@@ -94,68 +139,56 @@ const RPC_METHODS = new Set([
|
|
|
94
139
|
const READ_METHODS = new Set([
|
|
95
140
|
"getBlock",
|
|
96
141
|
"getBlocks",
|
|
97
|
-
"
|
|
142
|
+
"readManifestBlock",
|
|
143
|
+
"hasManifestBlocks",
|
|
144
|
+
"listManifestBlockPage",
|
|
145
|
+
"listRetiredManifestBlockPage",
|
|
98
146
|
"getTable",
|
|
99
147
|
"getTableByName",
|
|
100
148
|
"listTables",
|
|
101
149
|
"readFtsCandidates",
|
|
150
|
+
"readFtsPostings",
|
|
102
151
|
"getSegment",
|
|
103
|
-
"
|
|
152
|
+
"listSegmentPage",
|
|
153
|
+
"listTableSegmentPage",
|
|
104
154
|
"getExistingUniqueKeys",
|
|
155
|
+
"getUniqueKeyBuild",
|
|
105
156
|
"getCurrentManifest",
|
|
106
157
|
"getCurrentManifestVersion",
|
|
107
158
|
"getCatalogProbe",
|
|
108
|
-
"getQueryCatalogState",
|
|
109
159
|
"getManifest",
|
|
110
|
-
"listManifests",
|
|
111
160
|
"listManifestPage",
|
|
112
161
|
"getTransaction",
|
|
113
162
|
"getTransactions",
|
|
114
|
-
"listTransactions",
|
|
115
163
|
"listTransactionPage",
|
|
116
164
|
"getLease",
|
|
117
165
|
"listLeases",
|
|
166
|
+
"listExpiredLeasePage",
|
|
118
167
|
"getCompactionJob",
|
|
119
168
|
"listCompactionJobs",
|
|
120
169
|
"listCompactionJobPage",
|
|
121
170
|
"getGarbageCollectionJob",
|
|
122
171
|
"listGarbageCollectionJobs",
|
|
172
|
+
"listGarbageCollectionJobPage",
|
|
123
173
|
"getTempOwner",
|
|
174
|
+
"getTempRunPage",
|
|
124
175
|
"listTempOwnerIdsPage",
|
|
176
|
+
"listExpiredTempOwnerPage",
|
|
125
177
|
"getLogicalStorageBytes",
|
|
126
|
-
"
|
|
178
|
+
"getStorageStats",
|
|
179
|
+
"checkIntegrity",
|
|
180
|
+
"readSnapshotExportFrame",
|
|
127
181
|
]);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
*
|
|
133
|
-
* Two kinds of channel carry the protocol. One shared channel per database carries what every
|
|
134
|
-
* connection must hear: leader announcements, pings, bids, yields, releases. Every connection
|
|
135
|
-
* also owns an inbox — a channel named for its instance id — where the messages meant for it
|
|
136
|
-
* alone arrive: operations at the leader, results and busy notices at the requester. A block
|
|
137
|
-
* read's bytes are therefore structured-cloned once, into the tab that asked, rather than into
|
|
138
|
-
* every tab of the origin.
|
|
139
|
-
*
|
|
140
|
-
* Leadership is the write-ahead log's own exclusive sync-access handle — enforced by the
|
|
141
|
-
* browser against the actual resource, released the instant its holder dies. Elections are
|
|
142
|
-
* simply attempts to open it. Correctness never rides on a message: an operation is
|
|
143
|
-
* acknowledged only after the leader's WAL holds it, a lost message costs a retry (request
|
|
144
|
-
* ids deduplicate retries while the same leader serves), and a dead leader costs a failover
|
|
145
|
-
* in which the next acquirer replays checkpoint-plus-WAL. A retry that crosses a failover is
|
|
146
|
-
* the one case a duplicate can execute — the same "committed but the response was lost"
|
|
147
|
-
* class the IndexedDB store has — and it resolves the same way: compare-and-swap operations
|
|
148
|
-
* surface a conflict the engine's recovery already reconciles. The channel affects how fast
|
|
149
|
-
* multi-tab work moves, never whether it is right.
|
|
150
|
-
*
|
|
151
|
-
* `setForeground(true)` marks this connection as the one the user is looking at; a background
|
|
152
|
-
* leader yields to a foreground bidder, so the tab doing the work is normally the tab holding
|
|
153
|
-
* the microsecond-fast path.
|
|
154
|
-
*/
|
|
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
|
|
155
186
|
export class OpfsBlockStore {
|
|
187
|
+
liveQueryChannelName;
|
|
156
188
|
#tree;
|
|
157
189
|
#durability;
|
|
158
190
|
#checkpointEntries;
|
|
191
|
+
#cleanupLimitBytes;
|
|
159
192
|
#rpcTimeoutMs;
|
|
160
193
|
#instanceId = crypto.randomUUID();
|
|
161
194
|
#channelName;
|
|
@@ -172,14 +205,26 @@ export class OpfsBlockStore {
|
|
|
172
205
|
#lastYieldAt = 0;
|
|
173
206
|
#electing;
|
|
174
207
|
#pending = new Map();
|
|
175
|
-
#
|
|
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;
|
|
176
219
|
#reacquireTimer;
|
|
177
220
|
constructor(tree, options) {
|
|
178
221
|
this.#tree = tree;
|
|
179
|
-
this.#durability = options.durability ?? "
|
|
222
|
+
this.#durability = options.durability ?? "strict";
|
|
180
223
|
this.#checkpointEntries = options.checkpointEntries;
|
|
224
|
+
this.#cleanupLimitBytes = options.cleanupLimitBytes;
|
|
181
225
|
this.#rpcTimeoutMs = options.rpcTimeoutMs ?? RPC_TIMEOUT_MS;
|
|
182
226
|
this.#channelName = `minnowdb-store:${options.name}`;
|
|
227
|
+
this.liveQueryChannelName = `minnowdb-live:opfs:${options.name}`;
|
|
183
228
|
}
|
|
184
229
|
static async open(options) {
|
|
185
230
|
const tree = new OpfsTree(await resolveDatabaseRoot(options));
|
|
@@ -187,7 +232,9 @@ export class OpfsBlockStore {
|
|
|
187
232
|
await store.#ensureFormatMarker();
|
|
188
233
|
if (typeof BroadcastChannel === "function") {
|
|
189
234
|
const onMessage = (event) => {
|
|
190
|
-
|
|
235
|
+
const message = parseStoreRpcMessage(event.data, RPC_METHODS);
|
|
236
|
+
if (message !== undefined)
|
|
237
|
+
store.#onMessage(message);
|
|
191
238
|
};
|
|
192
239
|
const channel = new BroadcastChannel(store.#channelName);
|
|
193
240
|
channel.onmessage = onMessage;
|
|
@@ -235,7 +282,7 @@ export class OpfsBlockStore {
|
|
|
235
282
|
// them in sequence; the brief retry covers the gap.
|
|
236
283
|
slotA = await this.#openWithRetry(["checkpoint-a"]);
|
|
237
284
|
slotB = await this.#openWithRetry(["checkpoint-b"]);
|
|
238
|
-
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);
|
|
239
286
|
}
|
|
240
287
|
catch (error) {
|
|
241
288
|
wal.close();
|
|
@@ -311,15 +358,31 @@ export class OpfsBlockStore {
|
|
|
311
358
|
return;
|
|
312
359
|
switch (message.kind) {
|
|
313
360
|
case "op": {
|
|
314
|
-
if (this.#leader !== undefined)
|
|
315
|
-
|
|
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
|
+
}
|
|
316
380
|
return;
|
|
317
381
|
}
|
|
318
382
|
case "result": {
|
|
319
|
-
const pending = this.#
|
|
383
|
+
const pending = this.#takePending(message.requestId);
|
|
320
384
|
if (pending === undefined)
|
|
321
385
|
return;
|
|
322
|
-
this.#pending.delete(message.requestId);
|
|
323
386
|
clearTimeout(pending.timer);
|
|
324
387
|
if (message.ok)
|
|
325
388
|
pending.resolve(message.value);
|
|
@@ -333,8 +396,8 @@ export class OpfsBlockStore {
|
|
|
333
396
|
return;
|
|
334
397
|
clearTimeout(pending.timer);
|
|
335
398
|
pending.timer = setTimeout(() => {
|
|
336
|
-
this.#
|
|
337
|
-
|
|
399
|
+
const expired = this.#takePending(message.requestId);
|
|
400
|
+
expired?.reject(RPC_TIMED_OUT);
|
|
338
401
|
}, this.#rpcTimeoutMs);
|
|
339
402
|
return;
|
|
340
403
|
}
|
|
@@ -344,12 +407,17 @@ export class OpfsBlockStore {
|
|
|
344
407
|
clearTimeout(this.#reacquireTimer);
|
|
345
408
|
this.#reacquireTimer = undefined;
|
|
346
409
|
}
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
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) {
|
|
351
413
|
if (pending.sentTo === message.leaderId)
|
|
352
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
|
+
}
|
|
353
421
|
pending.sentTo = message.leaderId;
|
|
354
422
|
this.#send(message.leaderId, pending.message);
|
|
355
423
|
}
|
|
@@ -387,31 +455,133 @@ export class OpfsBlockStore {
|
|
|
387
455
|
}
|
|
388
456
|
}
|
|
389
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
|
+
}
|
|
390
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) {
|
|
391
494
|
const leader = this.#leader;
|
|
392
495
|
if (leader === undefined || !RPC_METHODS.has(message.method))
|
|
393
496
|
return;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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;
|
|
406
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;
|
|
407
516
|
}
|
|
408
|
-
else {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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.
|
|
412
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;
|
|
413
584
|
}
|
|
414
|
-
const settled = await outcome;
|
|
415
585
|
this.#answer(message.from, settled.ok
|
|
416
586
|
? { kind: "result", requestId: message.requestId, ok: true, value: settled.value }
|
|
417
587
|
: {
|
|
@@ -421,6 +591,45 @@ export class OpfsBlockStore {
|
|
|
421
591
|
error: settled.error ?? { name: "Error", message: "unknown" },
|
|
422
592
|
});
|
|
423
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
|
+
}
|
|
424
633
|
async #executeServedOp(leader, message) {
|
|
425
634
|
try {
|
|
426
635
|
const method = leader[message.method];
|
|
@@ -542,6 +751,8 @@ export class OpfsBlockStore {
|
|
|
542
751
|
catch (error) {
|
|
543
752
|
if (error === RPC_TIMED_OUT) {
|
|
544
753
|
this.#knownLeader = undefined;
|
|
754
|
+
if (!READ_METHODS.has(method))
|
|
755
|
+
throw new OpfsUncertainOutcomeError(method);
|
|
545
756
|
continue;
|
|
546
757
|
}
|
|
547
758
|
throw error;
|
|
@@ -554,14 +765,33 @@ export class OpfsBlockStore {
|
|
|
554
765
|
return this.#knownLeader !== undefined;
|
|
555
766
|
}
|
|
556
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
|
+
}
|
|
557
779
|
return new Promise((resolve, reject) => {
|
|
558
780
|
const message = { kind: "op", requestId, from: this.#instanceId, method, args };
|
|
559
781
|
const timer = setTimeout(() => {
|
|
560
|
-
this.#
|
|
561
|
-
reject(RPC_TIMED_OUT);
|
|
782
|
+
const expired = this.#takePending(requestId);
|
|
783
|
+
expired?.reject(RPC_TIMED_OUT);
|
|
562
784
|
}, this.#rpcTimeoutMs);
|
|
563
785
|
const leaderId = this.#knownLeader;
|
|
564
|
-
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;
|
|
565
795
|
// The dispatch loop only gets here with a leader known; should it have slipped away in
|
|
566
796
|
// between, the timeout (or the next leader's announcement, which re-sends) takes over.
|
|
567
797
|
if (leaderId !== undefined)
|
|
@@ -571,286 +801,35 @@ export class OpfsBlockStore {
|
|
|
571
801
|
// ---------------------------------------------------------------------------------------
|
|
572
802
|
// The BlockStore surface.
|
|
573
803
|
// ---------------------------------------------------------------------------------------
|
|
574
|
-
async addBlock(id, bytes) {
|
|
575
|
-
await this.#dispatch("addBlock", [id, bytes]);
|
|
576
|
-
}
|
|
577
|
-
async addBlocks(blocks) {
|
|
578
|
-
await this.#dispatch("addBlocks", [blocks]);
|
|
579
|
-
}
|
|
580
|
-
async getBlock(id) {
|
|
581
|
-
return (await this.#dispatch("getBlock", [id]));
|
|
582
|
-
}
|
|
583
|
-
async getBlocks(ids) {
|
|
584
|
-
return (await this.#dispatch("getBlocks", [ids]));
|
|
585
|
-
}
|
|
586
|
-
async removeBlock(id) {
|
|
587
|
-
await this.#dispatch("removeBlock", [id]);
|
|
588
|
-
}
|
|
589
|
-
async listBlockIds() {
|
|
590
|
-
return (await this.#dispatch("listBlockIds", []));
|
|
591
|
-
}
|
|
592
804
|
async putTempRunPage(page) {
|
|
593
805
|
validateTempRunPage(page);
|
|
594
|
-
await this.#
|
|
806
|
+
await this.#dispatch("putTempRunPage", [page]);
|
|
595
807
|
}
|
|
596
808
|
async putTempRunPages(pages) {
|
|
597
809
|
// Spill pages are instance-local files; the batch is a convenience, not a round-trip win.
|
|
810
|
+
assertTempRunPageBatchLimits(pages);
|
|
598
811
|
for (const page of pages)
|
|
599
|
-
|
|
812
|
+
validateTempRunPage(page);
|
|
813
|
+
await this.#dispatch("putTempRunPages", [pages]);
|
|
600
814
|
}
|
|
601
815
|
async getTempRunPage(ownerId, runId, pageIndex) {
|
|
602
816
|
validateTempRunPageIdentity(ownerId, runId, pageIndex);
|
|
603
|
-
return this.#
|
|
604
|
-
"temp",
|
|
605
|
-
encodeSegment(ownerId),
|
|
606
|
-
encodeSegment(runId),
|
|
607
|
-
String(pageIndex),
|
|
608
|
-
]);
|
|
817
|
+
return (await this.#dispatch("getTempRunPage", [ownerId, runId, pageIndex]));
|
|
609
818
|
}
|
|
610
819
|
async removeTempRun(ownerId, runId) {
|
|
611
820
|
validateTempRunPageIdentity(ownerId, runId, 0);
|
|
612
|
-
await this.#
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
async getTempOwner(ownerId) {
|
|
623
|
-
return (await this.#dispatch("getTempOwner", [ownerId]));
|
|
624
|
-
}
|
|
625
|
-
async renewTempOwner(ownerId, expectedRevision, expiresAt) {
|
|
626
|
-
return (await this.#dispatch("renewTempOwner", [
|
|
627
|
-
ownerId,
|
|
628
|
-
expectedRevision,
|
|
629
|
-
expiresAt,
|
|
630
|
-
]));
|
|
631
|
-
}
|
|
632
|
-
async removeTempOwnerIfExpired(ownerId, expiresAtCutoff) {
|
|
633
|
-
const removed = (await this.#dispatch("removeTempOwnerIfExpired", [
|
|
634
|
-
ownerId,
|
|
635
|
-
expiresAtCutoff,
|
|
636
|
-
]));
|
|
637
|
-
if (removed)
|
|
638
|
-
await this.#tree.deleteTree(["temp", encodeSegment(ownerId)]);
|
|
639
|
-
return removed;
|
|
640
|
-
}
|
|
641
|
-
async listTempOwnerIdsPage(afterOwnerId, limit) {
|
|
642
|
-
return (await this.#dispatch("listTempOwnerIdsPage", [afterOwnerId, limit]));
|
|
643
|
-
}
|
|
644
|
-
async addTable(record) {
|
|
645
|
-
await this.#dispatch("addTable", [record]);
|
|
646
|
-
}
|
|
647
|
-
async getTable(id) {
|
|
648
|
-
return (await this.#dispatch("getTable", [id]));
|
|
649
|
-
}
|
|
650
|
-
async getTableByName(name) {
|
|
651
|
-
return (await this.#dispatch("getTableByName", [name]));
|
|
652
|
-
}
|
|
653
|
-
async listTables() {
|
|
654
|
-
return (await this.#dispatch("listTables", []));
|
|
655
|
-
}
|
|
656
|
-
async updateTable(id, expectedRevision, update) {
|
|
657
|
-
return (await this.#dispatch("updateTable", [id, expectedRevision, update]));
|
|
658
|
-
}
|
|
659
|
-
async removeTable(id, expectedRevision) {
|
|
660
|
-
await this.#dispatch("removeTable", [id, expectedRevision]);
|
|
661
|
-
}
|
|
662
|
-
async writeFtsBase(tableId, columnId, input) {
|
|
663
|
-
await this.#dispatch("writeFtsBase", [tableId, columnId, input]);
|
|
664
|
-
}
|
|
665
|
-
async readFtsCandidates(tableId, columnId, terms, upToVersion) {
|
|
666
|
-
return (await this.#dispatch("readFtsCandidates", [
|
|
667
|
-
tableId,
|
|
668
|
-
columnId,
|
|
669
|
-
terms,
|
|
670
|
-
upToVersion,
|
|
671
|
-
]));
|
|
672
|
-
}
|
|
673
|
-
async addSegment(record) {
|
|
674
|
-
await this.#dispatch("addSegment", [record]);
|
|
675
|
-
}
|
|
676
|
-
async getSegment(id) {
|
|
677
|
-
return (await this.#dispatch("getSegment", [id]));
|
|
678
|
-
}
|
|
679
|
-
async listSegments(tableId) {
|
|
680
|
-
return (await this.#dispatch("listSegments", [tableId]));
|
|
681
|
-
}
|
|
682
|
-
async removeSegment(id) {
|
|
683
|
-
await this.#dispatch("removeSegment", [id]);
|
|
684
|
-
}
|
|
685
|
-
async reserveRowIds(tableId, count) {
|
|
686
|
-
return (await this.#dispatch("reserveRowIds", [tableId, count]));
|
|
687
|
-
}
|
|
688
|
-
async reserveAutoIncrement(tableId, columnId, count, atLeast) {
|
|
689
|
-
return (await this.#dispatch("reserveAutoIncrement", [
|
|
690
|
-
tableId,
|
|
691
|
-
columnId,
|
|
692
|
-
count,
|
|
693
|
-
atLeast,
|
|
694
|
-
]));
|
|
695
|
-
}
|
|
696
|
-
async getExistingUniqueKeys(tableId, keyTokens) {
|
|
697
|
-
return (await this.#dispatch("getExistingUniqueKeys", [tableId, keyTokens]));
|
|
698
|
-
}
|
|
699
|
-
async getCurrentManifest() {
|
|
700
|
-
return (await this.#dispatch("getCurrentManifest", []));
|
|
701
|
-
}
|
|
702
|
-
async getCurrentManifestVersion() {
|
|
703
|
-
return (await this.#dispatch("getCurrentManifestVersion", []));
|
|
704
|
-
}
|
|
705
|
-
async getCatalogProbe() {
|
|
706
|
-
return (await this.#dispatch("getCatalogProbe", []));
|
|
707
|
-
}
|
|
708
|
-
async getQueryCatalogState(tableNames) {
|
|
709
|
-
return (await this.#dispatch("getQueryCatalogState", [tableNames]));
|
|
710
|
-
}
|
|
711
|
-
async getManifest(version) {
|
|
712
|
-
return (await this.#dispatch("getManifest", [version]));
|
|
713
|
-
}
|
|
714
|
-
async listManifests() {
|
|
715
|
-
return (await this.#dispatch("listManifests", []));
|
|
716
|
-
}
|
|
717
|
-
async listManifestPage(afterVersion, limit) {
|
|
718
|
-
return (await this.#dispatch("listManifestPage", [afterVersion, limit]));
|
|
719
|
-
}
|
|
720
|
-
async publishManifest(input) {
|
|
721
|
-
return (await this.#dispatch("publishManifest", [input]));
|
|
722
|
-
}
|
|
723
|
-
async createTransaction(record) {
|
|
724
|
-
await this.#dispatch("createTransaction", [record]);
|
|
725
|
-
}
|
|
726
|
-
async beginTransaction(input) {
|
|
727
|
-
return (await this.#dispatch("beginTransaction", [input]));
|
|
728
|
-
}
|
|
729
|
-
async getTransaction(id) {
|
|
730
|
-
return (await this.#dispatch("getTransaction", [id]));
|
|
731
|
-
}
|
|
732
|
-
async getTransactions(ids) {
|
|
733
|
-
return (await this.#dispatch("getTransactions", [ids]));
|
|
734
|
-
}
|
|
735
|
-
async listTransactions() {
|
|
736
|
-
return (await this.#dispatch("listTransactions", []));
|
|
737
|
-
}
|
|
738
|
-
async listTransactionPage(afterId, limit) {
|
|
739
|
-
return (await this.#dispatch("listTransactionPage", [afterId, limit]));
|
|
740
|
-
}
|
|
741
|
-
async updateTransaction(id, expectedRevision, update) {
|
|
742
|
-
return (await this.#dispatch("updateTransaction", [
|
|
743
|
-
id,
|
|
744
|
-
expectedRevision,
|
|
745
|
-
update,
|
|
746
|
-
]));
|
|
747
|
-
}
|
|
748
|
-
async stageTransactionArtifacts(input) {
|
|
749
|
-
return (await this.#dispatch("stageTransactionArtifacts", [input]));
|
|
750
|
-
}
|
|
751
|
-
async commitTransaction(input) {
|
|
752
|
-
return (await this.#dispatch("commitTransaction", [input]));
|
|
753
|
-
}
|
|
754
|
-
async writeTransaction(input) {
|
|
755
|
-
return (await this.#dispatch("writeTransaction", [input]));
|
|
756
|
-
}
|
|
757
|
-
async createLease(record) {
|
|
758
|
-
await this.#dispatch("createLease", [record]);
|
|
759
|
-
}
|
|
760
|
-
async getLease(id) {
|
|
761
|
-
return (await this.#dispatch("getLease", [id]));
|
|
762
|
-
}
|
|
763
|
-
async listLeases() {
|
|
764
|
-
return (await this.#dispatch("listLeases", []));
|
|
765
|
-
}
|
|
766
|
-
async renewLease(id, expectedRevision, expiresAt) {
|
|
767
|
-
return (await this.#dispatch("renewLease", [id, expectedRevision, expiresAt]));
|
|
768
|
-
}
|
|
769
|
-
async moveLease(id, expectedRevision, manifestVersion, expiresAt) {
|
|
770
|
-
return (await this.#dispatch("moveLease", [
|
|
771
|
-
id,
|
|
772
|
-
expectedRevision,
|
|
773
|
-
manifestVersion,
|
|
774
|
-
expiresAt,
|
|
775
|
-
]));
|
|
776
|
-
}
|
|
777
|
-
async removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff) {
|
|
778
|
-
return (await this.#dispatch("removeLeaseIfExpired", [
|
|
779
|
-
id,
|
|
780
|
-
expectedRevision,
|
|
781
|
-
expiresAtCutoff,
|
|
782
|
-
]));
|
|
783
|
-
}
|
|
784
|
-
async removeLease(id) {
|
|
785
|
-
await this.#dispatch("removeLease", [id]);
|
|
786
|
-
}
|
|
787
|
-
async createCompactionJob(record) {
|
|
788
|
-
await this.#dispatch("createCompactionJob", [record]);
|
|
789
|
-
}
|
|
790
|
-
async getCompactionJob(id) {
|
|
791
|
-
return (await this.#dispatch("getCompactionJob", [id]));
|
|
792
|
-
}
|
|
793
|
-
async listCompactionJobs(tableId) {
|
|
794
|
-
return (await this.#dispatch("listCompactionJobs", [tableId]));
|
|
795
|
-
}
|
|
796
|
-
async listCompactionJobPage(afterId, limit) {
|
|
797
|
-
return (await this.#dispatch("listCompactionJobPage", [afterId, limit]));
|
|
798
|
-
}
|
|
799
|
-
async updateCompactionJob(id, expectedRevision, update) {
|
|
800
|
-
return (await this.#dispatch("updateCompactionJob", [
|
|
801
|
-
id,
|
|
802
|
-
expectedRevision,
|
|
803
|
-
update,
|
|
804
|
-
]));
|
|
805
|
-
}
|
|
806
|
-
async cancelCompactionJob(id, expectedRevision, cancelledAt) {
|
|
807
|
-
return (await this.#dispatch("cancelCompactionJob", [
|
|
808
|
-
id,
|
|
809
|
-
expectedRevision,
|
|
810
|
-
cancelledAt,
|
|
811
|
-
]));
|
|
812
|
-
}
|
|
813
|
-
async removeCompactionJob(id) {
|
|
814
|
-
await this.#dispatch("removeCompactionJob", [id]);
|
|
815
|
-
}
|
|
816
|
-
async createGarbageCollectionJob(input) {
|
|
817
|
-
return (await this.#dispatch("createGarbageCollectionJob", [
|
|
818
|
-
input,
|
|
819
|
-
]));
|
|
820
|
-
}
|
|
821
|
-
async getGarbageCollectionJob(id) {
|
|
822
|
-
return (await this.#dispatch("getGarbageCollectionJob", [id]));
|
|
823
|
-
}
|
|
824
|
-
async listGarbageCollectionJobs() {
|
|
825
|
-
return (await this.#dispatch("listGarbageCollectionJobs", []));
|
|
826
|
-
}
|
|
827
|
-
async runGarbageCollectionStep(input) {
|
|
828
|
-
return (await this.#dispatch("runGarbageCollectionStep", [
|
|
829
|
-
input,
|
|
830
|
-
]));
|
|
831
|
-
}
|
|
832
|
-
async removeGarbageCollectionJob(id) {
|
|
833
|
-
await this.#dispatch("removeGarbageCollectionJob", [id]);
|
|
834
|
-
}
|
|
835
|
-
async removePrunedManifestRecords() {
|
|
836
|
-
return (await this.#dispatch("removePrunedManifestRecords", []));
|
|
837
|
-
}
|
|
838
|
-
async getLogicalStorageBytes() {
|
|
839
|
-
return (await this.#dispatch("getLogicalStorageBytes", []));
|
|
840
|
-
}
|
|
841
|
-
async exportSnapshot() {
|
|
842
|
-
return (await this.#dispatch("exportSnapshot", []));
|
|
843
|
-
}
|
|
844
|
-
async importSnapshot(snapshot, options = {}) {
|
|
845
|
-
const leader = this.#leader;
|
|
846
|
-
if (leader !== undefined) {
|
|
847
|
-
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`);
|
|
848
831
|
}
|
|
849
|
-
|
|
850
|
-
const totalBytes = snapshot.blocks.reduce((total, block) => total + block.bytes.byteLength, 0);
|
|
851
|
-
options.onProgress?.({ phase: "blocks", writtenBytes: 0, totalBytes });
|
|
852
|
-
await this.#dispatch("importSnapshot", [snapshot, {}]);
|
|
853
|
-
options.onProgress?.({ phase: "done", writtenBytes: totalBytes, totalBytes });
|
|
832
|
+
return this.#dispatch(method, args);
|
|
854
833
|
}
|
|
855
834
|
close() {
|
|
856
835
|
if (this.#closed)
|
|
@@ -863,6 +842,15 @@ export class OpfsBlockStore {
|
|
|
863
842
|
pending.reject(new Error("This OPFS store connection is closed"));
|
|
864
843
|
}
|
|
865
844
|
this.#pending.clear();
|
|
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;
|
|
866
854
|
const leader = this.#leader;
|
|
867
855
|
this.#leader = undefined;
|
|
868
856
|
if (leader !== undefined) {
|
|
@@ -887,6 +875,45 @@ export class OpfsBlockStore {
|
|
|
887
875
|
_instanceIdForTests() {
|
|
888
876
|
return this.#instanceId;
|
|
889
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
|
+
}
|
|
902
|
+
/** Test-only counters that pin the connection's bounded RPC state and close-time cleanup. */
|
|
903
|
+
_residentStateForTests() {
|
|
904
|
+
return {
|
|
905
|
+
answerChannels: this.#answerChannels.size,
|
|
906
|
+
dedupeEntries: this.#settledMutations.size,
|
|
907
|
+
inFlightMutations: this.#inFlightMutations.size,
|
|
908
|
+
pendingRequests: this.#pending.size,
|
|
909
|
+
retainedRpcBytes: this.#pendingRpcBytes +
|
|
910
|
+
this.#inFlightMutationBytes +
|
|
911
|
+
this.#inFlightReadBytes +
|
|
912
|
+
this.#settledMutationBytes +
|
|
913
|
+
this.#servedRequestBytes,
|
|
914
|
+
closed: this.#closed,
|
|
915
|
+
};
|
|
916
|
+
}
|
|
890
917
|
/** Test-only: what tab death looks like — locks release, nothing flushes, no goodbyes. */
|
|
891
918
|
_crashForTests() {
|
|
892
919
|
this.#closed = true;
|
|
@@ -895,46 +922,110 @@ export class OpfsBlockStore {
|
|
|
895
922
|
for (const pending of this.#pending.values())
|
|
896
923
|
clearTimeout(pending.timer);
|
|
897
924
|
this.#pending.clear();
|
|
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;
|
|
898
934
|
this.#leader?.crash();
|
|
899
935
|
this.#leader = undefined;
|
|
900
936
|
this.#closeChannels();
|
|
901
937
|
}
|
|
902
938
|
async #ensureFormatMarker() {
|
|
903
|
-
const existing = await this.#tree.readFile(["format.json"], {
|
|
939
|
+
const existing = await this.#tree.readFile(["format.json"], {
|
|
940
|
+
lockedMeansAbsent: true,
|
|
941
|
+
maxBytes: 1024,
|
|
942
|
+
});
|
|
904
943
|
if (existing !== undefined) {
|
|
905
944
|
try {
|
|
906
|
-
|
|
907
|
-
if (typeof parsed.formatVersion === "number" &&
|
|
908
|
-
parsed.formatVersion !== LOG_FORMAT_VERSION) {
|
|
909
|
-
throw new Error(`This database uses OPFS layout version ${String(parsed.formatVersion)}; this build ` +
|
|
910
|
-
`reads version ${String(LOG_FORMAT_VERSION)}. Delete it with deleteOpfsDatabase() — ` +
|
|
911
|
-
`earlier layouts were experimental and carry no compatibility promise.`);
|
|
912
|
-
}
|
|
945
|
+
this.#validateFormatMarker(existing);
|
|
913
946
|
return;
|
|
914
947
|
}
|
|
915
948
|
catch (error) {
|
|
916
949
|
if (!(error instanceof SyntaxError))
|
|
917
950
|
throw error;
|
|
918
|
-
// 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.
|
|
919
953
|
}
|
|
920
954
|
}
|
|
955
|
+
let artifact;
|
|
956
|
+
for await (const name of this.#tree.iterateNames([])) {
|
|
957
|
+
if (name !== "format.json") {
|
|
958
|
+
artifact = name;
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
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
|
+
}
|
|
921
967
|
const bytes = new TextEncoder().encode(JSON.stringify({ formatVersion: LOG_FORMAT_VERSION }));
|
|
922
968
|
try {
|
|
923
969
|
await this.#tree.writeFile(["format.json"], bytes, { flush: true });
|
|
924
970
|
}
|
|
925
971
|
catch (error) {
|
|
926
|
-
// A concurrent open won the marker; theirs says the same thing.
|
|
927
972
|
if (!isLockContention(error))
|
|
928
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
|
+
});
|
|
990
|
+
}
|
|
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");
|
|
929
1009
|
}
|
|
930
1010
|
}
|
|
931
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
|
+
});
|
|
1021
|
+
}
|
|
932
1022
|
/** Removes every file of a database created by `OpfsBlockStore.open` under this name. */
|
|
933
1023
|
export async function deleteOpfsDatabase(options) {
|
|
1024
|
+
const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
|
|
934
1025
|
const root = options.root ?? (await navigator.storage.getDirectory());
|
|
935
1026
|
try {
|
|
936
1027
|
const namespace = await root.getDirectoryHandle("minnowdb");
|
|
937
|
-
await namespace.removeEntry(
|
|
1028
|
+
await namespace.removeEntry(encodedName, { recursive: true });
|
|
938
1029
|
}
|
|
939
1030
|
catch (error) {
|
|
940
1031
|
if (!isDomError(error, "NotFoundError"))
|
|
@@ -943,11 +1034,10 @@ export async function deleteOpfsDatabase(options) {
|
|
|
943
1034
|
}
|
|
944
1035
|
const RPC_TIMED_OUT = new Error("The leader did not answer in time");
|
|
945
1036
|
async function resolveDatabaseRoot(options) {
|
|
946
|
-
|
|
947
|
-
throw new TypeError("Database name cannot be empty");
|
|
1037
|
+
const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
|
|
948
1038
|
const root = options.root ?? (await navigator.storage.getDirectory());
|
|
949
1039
|
const namespace = await root.getDirectoryHandle("minnowdb", { create: true });
|
|
950
|
-
return namespace.getDirectoryHandle(
|
|
1040
|
+
return namespace.getDirectoryHandle(encodedName, { create: true });
|
|
951
1041
|
}
|
|
952
1042
|
function isLockContention(error) {
|
|
953
1043
|
return isDomError(error, "NoModificationAllowedError") || isDomError(error, "InvalidStateError");
|
|
@@ -955,4 +1045,3 @@ function isLockContention(error) {
|
|
|
955
1045
|
function sleep(ms) {
|
|
956
1046
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
957
1047
|
}
|
|
958
|
-
//# sourceMappingURL=store.js.map
|