@minnowdb/core 0.6.9 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/block-format/block.js +254 -325
- package/dist/block-format/checksum.js +30 -52
- package/dist/block-format/codecs.js +112 -125
- package/dist/block-format/column.js +187 -205
- package/dist/block-format/index.js +5 -1
- package/dist/block-format/physical.js +371 -387
- package/dist/block-format/types.js +4 -1
- package/dist/block-format/unicode.js +30 -39
- package/dist/date-value.d.ts +5 -0
- package/dist/date-value.js +71 -41
- package/dist/engine/artifact-cache.js +106 -116
- package/dist/engine/batch.js +36 -44
- package/dist/engine/buffered-writer.js +156 -158
- package/dist/engine/byte-estimates.js +20 -22
- package/dist/engine/cache-limits.js +24 -25
- package/dist/engine/cancellation.js +5 -3
- package/dist/engine/catalog.js +64 -73
- package/dist/engine/client.js +715 -780
- package/dist/engine/database.js +15952 -18465
- package/dist/engine/defaults.js +104 -111
- package/dist/engine/errors.js +113 -128
- package/dist/engine/fts.js +233 -280
- package/dist/engine/group-index.js +287 -321
- package/dist/engine/index.js +23 -12
- package/dist/engine/join-index.js +177 -186
- package/dist/engine/keyed-live.js +168 -173
- package/dist/engine/live-api.js +0 -1
- package/dist/engine/live-equal.js +32 -37
- package/dist/engine/live.js +603 -647
- package/dist/engine/memory.js +109 -111
- package/dist/engine/optimizer.d.ts +19 -1
- package/dist/engine/optimizer.js +2642 -2515
- package/dist/engine/point-read.d.ts +4 -2
- package/dist/engine/point-read.js +121 -171
- package/dist/engine/query-api.js +12 -3
- package/dist/engine/query-cache.js +61 -69
- package/dist/engine/query.d.ts +45 -0
- package/dist/engine/query.js +8506 -9375
- package/dist/engine/result-wire.js +221 -252
- package/dist/engine/schema-wire.js +97 -112
- package/dist/engine/schema.js +1099 -1254
- package/dist/engine/sort-keys.js +315 -389
- package/dist/engine/sql-domains.d.ts +7 -1
- package/dist/engine/sql-domains.js +648 -734
- package/dist/engine/sql-driver.js +0 -1
- package/dist/engine/sql-functions.d.ts +11 -0
- package/dist/engine/sql-functions.js +1092 -0
- package/dist/engine/sql-json.js +199 -218
- package/dist/engine/sql-semantics.d.ts +25 -4
- package/dist/engine/sql-semantics.js +579 -513
- package/dist/engine/typed-live.js +271 -293
- package/dist/engine/vector.d.ts +7 -1
- package/dist/engine/vector.js +4513 -4580
- package/dist/engine/worker-host.d.ts +8 -63
- package/dist/engine/worker-host.js +28 -1130
- package/dist/engine/worker-indexeddb.d.ts +1 -0
- package/dist/engine/worker-indexeddb.js +3 -0
- package/dist/engine/worker-memory.d.ts +1 -0
- package/dist/engine/worker-memory.js +3 -0
- package/dist/engine/worker-opfs.d.ts +1 -0
- package/dist/engine/worker-opfs.js +3 -0
- package/dist/engine/worker-server.d.ts +93 -0
- package/dist/engine/worker-server.js +999 -0
- package/dist/engine/worker-store-indexeddb.d.ts +2 -0
- package/dist/engine/worker-store-indexeddb.js +11 -0
- package/dist/engine/worker-store-memory.d.ts +2 -0
- package/dist/engine/worker-store-memory.js +6 -0
- package/dist/engine/worker-store-opfs.d.ts +2 -0
- package/dist/engine/worker-store-opfs.js +9 -0
- package/dist/engine/worker.js +0 -12
- package/dist/engine/write-block-planner.js +93 -100
- package/dist/plan/index.js +15 -16
- package/dist/plan/model.d.ts +9 -1
- package/dist/plan/model.js +20 -27
- package/dist/storage/index.js +0 -12
- package/dist/storage/indexeddb.js +11776 -13171
- package/dist/storage/memory.js +1198 -1241
- package/dist/storage/opfs/files.js +238 -295
- package/dist/storage/opfs/index.js +9 -3
- package/dist/storage/opfs/leader.js +4386 -4856
- package/dist/storage/opfs/rpc.js +223 -259
- package/dist/storage/opfs/snapshot-ledger.js +219 -260
- package/dist/storage/opfs/store.js +884 -1003
- package/dist/storage/persistence.js +59 -74
- package/dist/storage/snapshot-stream.js +788 -826
- package/dist/storage/snapshot.js +36 -37
- package/dist/storage/toolkit/extents.js +444 -520
- package/dist/storage/toolkit/index.js +28 -29
- package/dist/storage/toolkit/record-core.js +5792 -6377
- package/dist/storage/toolkit/sync-file.js +34 -42
- package/dist/storage/toolkit/wal.js +92 -127
- package/dist/storage/toolkit/wire.js +232 -256
- package/dist/storage/types.js +2952 -3228
- package/dist/testing/block-store-conformance.js +1556 -1629
- package/dist/testing/index.js +310 -291
- package/dist/testing/opfs-shim.js +280 -312
- package/dist/testing/simulator.js +496 -534
- package/dist/testing/sqllogictest.js +425 -365
- package/dist/transactions/index.js +1464 -1712
- package/dist/worker-protocol/index.js +70 -69
- package/package.json +20 -2
- package/postgres-feature-profile.json +15 -5
- package/sql-feature-matrix.json +252 -2
|
@@ -1,1047 +1,928 @@
|
|
|
1
|
-
import { assertStorageBulkReadItems, assertTempRunPageBatchLimits, OpfsUncertainOutcomeError, StorageCorruptionError, StorageFormatVersionError, validateStorageDatabaseName
|
|
1
|
+
import { assertStorageBulkReadItems, assertTempRunPageBatchLimits, OpfsUncertainOutcomeError, StorageCorruptionError, StorageFormatVersionError, validateStorageDatabaseName } from "../types.js";
|
|
2
2
|
import { validateTempRunPage, validateTempRunPageIdentity } from "../toolkit/record-core.js";
|
|
3
3
|
import { OpfsTree, encodeSegment, isDomError } from "./files.js";
|
|
4
4
|
import { LOG_FORMAT_VERSION } from "../toolkit/wire.js";
|
|
5
5
|
import { OpfsLeader } from "./leader.js";
|
|
6
|
-
import { rehydrateStoreError, estimateRpcValueBytes, fingerprintStoreRequest, MAX_OPFS_RPC_MESSAGE_BYTES, parseStoreRpcMessage, serializeStoreError
|
|
7
|
-
|
|
8
|
-
const RPC_TIMEOUT_MS = 1_000;
|
|
9
|
-
/** How long a ping waits for a leadership announcement. */
|
|
6
|
+
import { rehydrateStoreError, estimateRpcValueBytes, fingerprintStoreRequest, MAX_OPFS_RPC_MESSAGE_BYTES, parseStoreRpcMessage, serializeStoreError } from "./rpc.js";
|
|
7
|
+
const RPC_TIMEOUT_MS = 1e3;
|
|
10
8
|
const DISCOVERY_WAIT_MS = 150;
|
|
11
|
-
/** Attempts across the discover → call → elect loop before an operation gives up. */
|
|
12
9
|
const DISPATCH_ATTEMPTS = 10;
|
|
13
|
-
|
|
14
|
-
const YIELD_COOLDOWN_MS = 3_000;
|
|
15
|
-
/** Results remembered for retried requests whose acknowledgement was lost. */
|
|
10
|
+
const YIELD_COOLDOWN_MS = 3e3;
|
|
16
11
|
const DEDUPE_CACHE_SIZE = 512;
|
|
17
|
-
/** Requests admitted concurrently by either side of the follower protocol. */
|
|
18
12
|
const RPC_IN_FLIGHT_LIMIT = 512;
|
|
19
13
|
const RPC_IN_FLIGHT_MUTATION_BYTES = 128 * 1024 * 1024;
|
|
20
14
|
const RPC_IN_FLIGHT_READ_BYTES = 256 * 1024 * 1024;
|
|
21
15
|
const RPC_SETTLED_OUTCOME_BYTES = 128 * 1024 * 1024;
|
|
22
16
|
const RPC_SERVER_ADMISSION_LIMIT = 1024;
|
|
23
17
|
const RPC_SERVER_ADMISSION_BYTES = 256 * 1024 * 1024;
|
|
24
|
-
/** Channels into requesters' inboxes a leader keeps open between answers. */
|
|
25
18
|
const ANSWER_CHANNEL_CACHE_SIZE = 64;
|
|
26
19
|
const BULK_READ_ARGUMENT_INDEX = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
getBlocks: 0,
|
|
21
|
+
getTransactions: 0,
|
|
22
|
+
getExistingUniqueKeys: 1,
|
|
23
|
+
hasManifestBlocks: 1
|
|
31
24
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"cancelSnapshotFrameImport",
|
|
25
|
+
const RPC_METHODS = /* @__PURE__ */ new Set([
|
|
26
|
+
"getBlock",
|
|
27
|
+
"getBlocks",
|
|
28
|
+
"readManifestBlock",
|
|
29
|
+
"hasManifestBlocks",
|
|
30
|
+
"listManifestBlockPage",
|
|
31
|
+
"listRetiredManifestBlockPage",
|
|
32
|
+
"addTable",
|
|
33
|
+
"getTable",
|
|
34
|
+
"getTableByName",
|
|
35
|
+
"listTables",
|
|
36
|
+
"updateTable",
|
|
37
|
+
"removeTable",
|
|
38
|
+
"dropTable",
|
|
39
|
+
"dropTableColumn",
|
|
40
|
+
"removeFtsColumn",
|
|
41
|
+
"writeFtsBase",
|
|
42
|
+
"beginFtsBaseBuild",
|
|
43
|
+
"renewFtsBaseBuild",
|
|
44
|
+
"writeFtsBaseBuildChunk",
|
|
45
|
+
"finishFtsBaseBuild",
|
|
46
|
+
"abortFtsBaseBuild",
|
|
47
|
+
"readFtsCandidates",
|
|
48
|
+
"readFtsPostings",
|
|
49
|
+
"getSegment",
|
|
50
|
+
"listSegmentPage",
|
|
51
|
+
"listTableSegmentPage",
|
|
52
|
+
"removeAbortedSegment",
|
|
53
|
+
"adoptAbortedSegment",
|
|
54
|
+
"reserveRowIds",
|
|
55
|
+
"reserveAutoIncrement",
|
|
56
|
+
"getExistingUniqueKeys",
|
|
57
|
+
"beginUniqueKeyBuild",
|
|
58
|
+
"getUniqueKeyBuild",
|
|
59
|
+
"renewUniqueKeyBuild",
|
|
60
|
+
"appendUniqueKeyBuildChunk",
|
|
61
|
+
"finishUniqueKeyBuild",
|
|
62
|
+
"abortUniqueKeyBuild",
|
|
63
|
+
"getCurrentManifest",
|
|
64
|
+
"getCurrentManifestVersion",
|
|
65
|
+
"getCatalogProbe",
|
|
66
|
+
"getManifest",
|
|
67
|
+
"listManifestPage",
|
|
68
|
+
"createTransaction",
|
|
69
|
+
"renewTransaction",
|
|
70
|
+
"abortTransactionIfExpired",
|
|
71
|
+
"beginTransaction",
|
|
72
|
+
"getTransaction",
|
|
73
|
+
"getTransactions",
|
|
74
|
+
"listTransactionPage",
|
|
75
|
+
"updateTransaction",
|
|
76
|
+
"stageTransactionArtifacts",
|
|
77
|
+
"rollbackTransactionArtifacts",
|
|
78
|
+
"commitTransaction",
|
|
79
|
+
"writeTransaction",
|
|
80
|
+
"createLease",
|
|
81
|
+
"getLease",
|
|
82
|
+
"listLeases",
|
|
83
|
+
"listExpiredLeasePage",
|
|
84
|
+
"renewLease",
|
|
85
|
+
"moveLease",
|
|
86
|
+
"removeLeaseIfExpired",
|
|
87
|
+
"removeLease",
|
|
88
|
+
"createCompactionJob",
|
|
89
|
+
"getCompactionJob",
|
|
90
|
+
"listCompactionJobs",
|
|
91
|
+
"listCompactionJobPage",
|
|
92
|
+
"updateCompactionJob",
|
|
93
|
+
"cancelCompactionJob",
|
|
94
|
+
"removeCompactionJob",
|
|
95
|
+
"createGarbageCollectionJob",
|
|
96
|
+
"updateGarbageCollectionPlanning",
|
|
97
|
+
"getGarbageCollectionJob",
|
|
98
|
+
"listGarbageCollectionJobs",
|
|
99
|
+
"listGarbageCollectionJobPage",
|
|
100
|
+
"runGarbageCollectionStep",
|
|
101
|
+
"removePrunedManifestRecords",
|
|
102
|
+
"removeGarbageCollectionJob",
|
|
103
|
+
"createTempOwner",
|
|
104
|
+
"getTempOwner",
|
|
105
|
+
"putTempRunPage",
|
|
106
|
+
"putTempRunPages",
|
|
107
|
+
"getTempRunPage",
|
|
108
|
+
"removeTempRun",
|
|
109
|
+
"renewTempOwner",
|
|
110
|
+
"removeTempOwnerIfExpired",
|
|
111
|
+
"removeTempOwner",
|
|
112
|
+
"listTempOwnerIdsPage",
|
|
113
|
+
"listExpiredTempOwnerPage",
|
|
114
|
+
"getLogicalStorageBytes",
|
|
115
|
+
"getStorageStats",
|
|
116
|
+
"checkIntegrity",
|
|
117
|
+
"beginSnapshotFrameExport",
|
|
118
|
+
"readSnapshotExportFrame",
|
|
119
|
+
"closeSnapshotFrameExport",
|
|
120
|
+
"beginSnapshotFrameImport",
|
|
121
|
+
"renewSnapshotFrameImport",
|
|
122
|
+
"appendSnapshotImportFrames",
|
|
123
|
+
"finishSnapshotFrameImport",
|
|
124
|
+
"cancelSnapshotFrameImport"
|
|
133
125
|
]);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
"listExpiredTempOwnerPage",
|
|
177
|
-
"getLogicalStorageBytes",
|
|
178
|
-
"getStorageStats",
|
|
179
|
-
"checkIntegrity",
|
|
180
|
-
"readSnapshotExportFrame",
|
|
126
|
+
const READ_METHODS = /* @__PURE__ */ new Set([
|
|
127
|
+
"getBlock",
|
|
128
|
+
"getBlocks",
|
|
129
|
+
"readManifestBlock",
|
|
130
|
+
"hasManifestBlocks",
|
|
131
|
+
"listManifestBlockPage",
|
|
132
|
+
"listRetiredManifestBlockPage",
|
|
133
|
+
"getTable",
|
|
134
|
+
"getTableByName",
|
|
135
|
+
"listTables",
|
|
136
|
+
"readFtsCandidates",
|
|
137
|
+
"readFtsPostings",
|
|
138
|
+
"getSegment",
|
|
139
|
+
"listSegmentPage",
|
|
140
|
+
"listTableSegmentPage",
|
|
141
|
+
"getExistingUniqueKeys",
|
|
142
|
+
"getUniqueKeyBuild",
|
|
143
|
+
"getCurrentManifest",
|
|
144
|
+
"getCurrentManifestVersion",
|
|
145
|
+
"getCatalogProbe",
|
|
146
|
+
"getManifest",
|
|
147
|
+
"listManifestPage",
|
|
148
|
+
"getTransaction",
|
|
149
|
+
"getTransactions",
|
|
150
|
+
"listTransactionPage",
|
|
151
|
+
"getLease",
|
|
152
|
+
"listLeases",
|
|
153
|
+
"listExpiredLeasePage",
|
|
154
|
+
"getCompactionJob",
|
|
155
|
+
"listCompactionJobs",
|
|
156
|
+
"listCompactionJobPage",
|
|
157
|
+
"getGarbageCollectionJob",
|
|
158
|
+
"listGarbageCollectionJobs",
|
|
159
|
+
"listGarbageCollectionJobPage",
|
|
160
|
+
"getTempOwner",
|
|
161
|
+
"getTempRunPage",
|
|
162
|
+
"listTempOwnerIdsPage",
|
|
163
|
+
"listExpiredTempOwnerPage",
|
|
164
|
+
"getLogicalStorageBytes",
|
|
165
|
+
"getStorageStats",
|
|
166
|
+
"checkIntegrity",
|
|
167
|
+
"readSnapshotExportFrame"
|
|
181
168
|
]);
|
|
182
169
|
function sameServedRequest(remembered, message, signature) {
|
|
183
|
-
|
|
170
|
+
return remembered.method === message.method && remembered.signature === signature;
|
|
184
171
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return this.#electing;
|
|
259
|
-
const election = this.#elect();
|
|
260
|
-
this.#electing = election;
|
|
261
|
-
try {
|
|
262
|
-
return await election;
|
|
263
|
-
}
|
|
264
|
-
finally {
|
|
265
|
-
this.#electing = undefined;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
async #elect() {
|
|
269
|
-
let wal;
|
|
270
|
-
try {
|
|
271
|
-
wal = await this.#tree.openHandle(["wal"], { create: true });
|
|
272
|
-
}
|
|
273
|
-
catch (error) {
|
|
274
|
-
if (isLockContention(error))
|
|
275
|
-
return false;
|
|
276
|
-
throw error;
|
|
277
|
-
}
|
|
278
|
-
let slotA;
|
|
279
|
-
let slotB;
|
|
280
|
-
try {
|
|
281
|
-
// A dying ex-leader releases all its handles at once, but a graceful demotion closes
|
|
282
|
-
// them in sequence; the brief retry covers the gap.
|
|
283
|
-
slotA = await this.#openWithRetry(["checkpoint-a"]);
|
|
284
|
-
slotB = await this.#openWithRetry(["checkpoint-b"]);
|
|
285
|
-
this.#leader = await OpfsLeader.recover(this.#tree, this.#durability === "strict", { wal, slotA, slotB }, this.#checkpointEntries, this.#cleanupLimitBytes);
|
|
286
|
-
}
|
|
287
|
-
catch (error) {
|
|
288
|
-
wal.close();
|
|
289
|
-
slotA?.close();
|
|
290
|
-
slotB?.close();
|
|
291
|
-
if (isLockContention(error))
|
|
292
|
-
return false;
|
|
293
|
-
throw error;
|
|
294
|
-
}
|
|
295
|
-
if (this.#closed) {
|
|
296
|
-
// close() ran while this election was in flight; a leader installed now would hold the
|
|
297
|
-
// browser's file lock with no owner to ever release it.
|
|
298
|
-
const leader = this.#leader;
|
|
299
|
-
this.#leader = undefined;
|
|
300
|
-
await leader.shutdown().catch(() => {
|
|
301
|
-
leader.crash();
|
|
302
|
-
});
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
this.#knownLeader = this.#instanceId;
|
|
306
|
-
this.#post({ kind: "leader", leaderId: this.#instanceId });
|
|
307
|
-
return true;
|
|
308
|
-
}
|
|
309
|
-
async #openWithRetry(path) {
|
|
310
|
-
for (let attempt = 0;; attempt += 1) {
|
|
311
|
-
try {
|
|
312
|
-
return await this.#tree.openHandle(path, { create: true });
|
|
313
|
-
}
|
|
314
|
-
catch (error) {
|
|
315
|
-
if (isLockContention(error) && attempt < 40) {
|
|
316
|
-
await sleep(5 + Math.random() * 10);
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
throw error;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
/** Marks this connection as the one the user is looking at — a leadership preference. */
|
|
324
|
-
setForeground(foreground) {
|
|
325
|
-
this.#foreground = foreground;
|
|
326
|
-
if (foreground && this.#leader === undefined && !this.#closed) {
|
|
327
|
-
this.#post({ kind: "bid", bidderId: this.#instanceId, foreground: true });
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
async #yieldLeadership(to) {
|
|
331
|
-
const leader = this.#leader;
|
|
332
|
-
if (leader === undefined)
|
|
333
|
-
return;
|
|
334
|
-
this.#leader = undefined;
|
|
335
|
-
this.#knownLeader = undefined;
|
|
336
|
-
this.#lastYieldAt = Date.now();
|
|
337
|
-
try {
|
|
338
|
-
await leader.shutdown();
|
|
339
|
-
}
|
|
340
|
-
catch {
|
|
341
|
-
// Whatever failed, the handles must not outlive the leadership; crash-close is
|
|
342
|
-
// idempotent and releases them.
|
|
343
|
-
leader.crash();
|
|
344
|
-
}
|
|
345
|
-
// An open channel into a follower's inbox would hear the next leader's answers to it.
|
|
346
|
-
this.#closeAnswerChannels();
|
|
347
|
-
this.#post({ kind: "yield", to });
|
|
348
|
-
// If the bidder loses the race or vanishes, someone must still hold the database.
|
|
349
|
-
if (this.#reacquireTimer !== undefined)
|
|
350
|
-
clearTimeout(this.#reacquireTimer);
|
|
351
|
-
this.#reacquireTimer = setTimeout(() => {
|
|
352
|
-
if (this.#knownLeader === undefined && !this.#closed)
|
|
353
|
-
void this.#tryBecomeLeader();
|
|
354
|
-
}, 1_500);
|
|
355
|
-
}
|
|
356
|
-
#onMessage(message) {
|
|
357
|
-
if (this.#closed)
|
|
358
|
-
return;
|
|
359
|
-
switch (message.kind) {
|
|
360
|
-
case "op": {
|
|
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
|
-
}
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
case "result": {
|
|
383
|
-
const pending = this.#takePending(message.requestId);
|
|
384
|
-
if (pending === undefined)
|
|
385
|
-
return;
|
|
386
|
-
clearTimeout(pending.timer);
|
|
387
|
-
if (message.ok)
|
|
388
|
-
pending.resolve(message.value);
|
|
389
|
-
else
|
|
390
|
-
pending.reject(rehydrateStoreError(message.error));
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
case "busy": {
|
|
394
|
-
const pending = this.#pending.get(message.requestId);
|
|
395
|
-
if (pending === undefined)
|
|
396
|
-
return;
|
|
397
|
-
clearTimeout(pending.timer);
|
|
398
|
-
pending.timer = setTimeout(() => {
|
|
399
|
-
const expired = this.#takePending(message.requestId);
|
|
400
|
-
expired?.reject(RPC_TIMED_OUT);
|
|
401
|
-
}, this.#rpcTimeoutMs);
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
case "leader": {
|
|
405
|
-
this.#knownLeader = message.leaderId;
|
|
406
|
-
if (this.#reacquireTimer !== undefined) {
|
|
407
|
-
clearTimeout(this.#reacquireTimer);
|
|
408
|
-
this.#reacquireTimer = undefined;
|
|
409
|
-
}
|
|
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) {
|
|
413
|
-
if (pending.sentTo === message.leaderId)
|
|
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
|
-
}
|
|
421
|
-
pending.sentTo = message.leaderId;
|
|
422
|
-
this.#send(message.leaderId, pending.message);
|
|
423
|
-
}
|
|
424
|
-
// The freshly announced leader may be background while we are what the user sees.
|
|
425
|
-
if (this.#foreground && this.#leader === undefined) {
|
|
426
|
-
this.#post({ kind: "bid", bidderId: this.#instanceId, foreground: true });
|
|
427
|
-
}
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
case "ping": {
|
|
431
|
-
if (this.#leader !== undefined) {
|
|
432
|
-
this.#post({ kind: "leader", leaderId: this.#instanceId });
|
|
433
|
-
}
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
case "bid": {
|
|
437
|
-
if (this.#leader !== undefined &&
|
|
438
|
-
!this.#foreground &&
|
|
439
|
-
message.foreground &&
|
|
440
|
-
message.bidderId !== this.#instanceId &&
|
|
441
|
-
Date.now() - this.#lastYieldAt > YIELD_COOLDOWN_MS) {
|
|
442
|
-
void this.#yieldLeadership(message.bidderId);
|
|
443
|
-
}
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
case "yield": {
|
|
447
|
-
if (message.to === this.#instanceId)
|
|
448
|
-
void this.#tryBecomeLeader();
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
case "released": {
|
|
452
|
-
if (this.#knownLeader === message.leaderId)
|
|
453
|
-
this.#knownLeader = undefined;
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
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
|
-
}
|
|
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
|
-
}
|
|
172
|
+
class OpfsBlockStore {
|
|
173
|
+
liveQueryChannelName;
|
|
174
|
+
#tree;
|
|
175
|
+
#durability;
|
|
176
|
+
#checkpointEntries;
|
|
177
|
+
#cleanupLimitBytes;
|
|
178
|
+
#rpcTimeoutMs;
|
|
179
|
+
#instanceId = crypto.randomUUID();
|
|
180
|
+
#channelName;
|
|
181
|
+
#channel;
|
|
182
|
+
#inbox;
|
|
183
|
+
#answerChannels = /* @__PURE__ */ new Map();
|
|
184
|
+
#leader;
|
|
185
|
+
#knownLeader;
|
|
186
|
+
#foreground = false;
|
|
187
|
+
#closed = false;
|
|
188
|
+
#lastYieldAt = 0;
|
|
189
|
+
#electing;
|
|
190
|
+
#pending = /* @__PURE__ */ new Map();
|
|
191
|
+
#inFlightMutations = /* @__PURE__ */ new Map();
|
|
192
|
+
#settledMutations = /* @__PURE__ */ new Map();
|
|
193
|
+
#servedRequestLocks = /* @__PURE__ */ new Map();
|
|
194
|
+
#inFlightMutationBytes = 0;
|
|
195
|
+
#inFlightReadBytes = 0;
|
|
196
|
+
#settledMutationBytes = 0;
|
|
197
|
+
#pendingRpcBytes = 0;
|
|
198
|
+
#servedRequestCount = 0;
|
|
199
|
+
#servedRequestBytes = 0;
|
|
200
|
+
#servedMutationGateForTests;
|
|
201
|
+
#dropNextRpcResultForTests = false;
|
|
202
|
+
#reacquireTimer;
|
|
203
|
+
constructor(tree, options) {
|
|
204
|
+
this.#tree = tree;
|
|
205
|
+
this.#durability = options.durability ?? "strict";
|
|
206
|
+
this.#checkpointEntries = options.checkpointEntries;
|
|
207
|
+
this.#cleanupLimitBytes = options.cleanupLimitBytes;
|
|
208
|
+
this.#rpcTimeoutMs = options.rpcTimeoutMs ?? RPC_TIMEOUT_MS;
|
|
209
|
+
this.#channelName = `minnowdb-store:${options.name}`;
|
|
210
|
+
this.liveQueryChannelName = `minnowdb-live:opfs:${options.name}`;
|
|
211
|
+
}
|
|
212
|
+
static async open(options) {
|
|
213
|
+
const tree = new OpfsTree(await resolveDatabaseRoot(options));
|
|
214
|
+
const store = new OpfsBlockStore(tree, options);
|
|
215
|
+
await store.#ensureFormatMarker();
|
|
216
|
+
if (typeof BroadcastChannel === "function") {
|
|
217
|
+
const onMessage = (event) => {
|
|
218
|
+
const message = parseStoreRpcMessage(event.data, RPC_METHODS);
|
|
219
|
+
if (message !== void 0)
|
|
220
|
+
store.#onMessage(message);
|
|
221
|
+
};
|
|
222
|
+
const channel = new BroadcastChannel(store.#channelName);
|
|
223
|
+
channel.onmessage = onMessage;
|
|
224
|
+
const inbox = new BroadcastChannel(store.#inboxName(store.#instanceId));
|
|
225
|
+
inbox.onmessage = onMessage;
|
|
226
|
+
store.#channel = channel;
|
|
227
|
+
store.#inbox = inbox;
|
|
228
|
+
}
|
|
229
|
+
await store.#tryBecomeLeader();
|
|
230
|
+
return store;
|
|
231
|
+
}
|
|
232
|
+
async #tryBecomeLeader() {
|
|
233
|
+
if (this.#closed)
|
|
234
|
+
return false;
|
|
235
|
+
if (this.#leader !== void 0)
|
|
236
|
+
return true;
|
|
237
|
+
if (this.#electing !== void 0)
|
|
238
|
+
return this.#electing;
|
|
239
|
+
const election = this.#elect();
|
|
240
|
+
this.#electing = election;
|
|
241
|
+
try {
|
|
242
|
+
return await election;
|
|
243
|
+
} finally {
|
|
244
|
+
this.#electing = void 0;
|
|
492
245
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
this.#dropNextRpcResultForTests = false;
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
this.#answer(message.from, settled.ok
|
|
586
|
-
? { kind: "result", requestId: message.requestId, ok: true, value: settled.value }
|
|
587
|
-
: {
|
|
588
|
-
kind: "result",
|
|
589
|
-
requestId: message.requestId,
|
|
590
|
-
ok: false,
|
|
591
|
-
error: settled.error ?? { name: "Error", message: "unknown" },
|
|
246
|
+
}
|
|
247
|
+
async #elect() {
|
|
248
|
+
let wal;
|
|
249
|
+
try {
|
|
250
|
+
wal = await this.#tree.openHandle(["wal"], { create: true });
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (isLockContention(error))
|
|
253
|
+
return false;
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
let slotA;
|
|
257
|
+
let slotB;
|
|
258
|
+
try {
|
|
259
|
+
slotA = await this.#openWithRetry(["checkpoint-a"]);
|
|
260
|
+
slotB = await this.#openWithRetry(["checkpoint-b"]);
|
|
261
|
+
this.#leader = await OpfsLeader.recover(this.#tree, this.#durability === "strict", { wal, slotA, slotB }, this.#checkpointEntries, this.#cleanupLimitBytes);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
wal.close();
|
|
264
|
+
slotA?.close();
|
|
265
|
+
slotB?.close();
|
|
266
|
+
if (isLockContention(error))
|
|
267
|
+
return false;
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
if (this.#closed) {
|
|
271
|
+
const leader = this.#leader;
|
|
272
|
+
this.#leader = void 0;
|
|
273
|
+
await leader.shutdown().catch(() => {
|
|
274
|
+
leader.crash();
|
|
275
|
+
});
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
this.#knownLeader = this.#instanceId;
|
|
279
|
+
this.#post({ kind: "leader", leaderId: this.#instanceId });
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
async #openWithRetry(path) {
|
|
283
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
284
|
+
try {
|
|
285
|
+
return await this.#tree.openHandle(path, { create: true });
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (isLockContention(error) && attempt < 40) {
|
|
288
|
+
await sleep(5 + Math.random() * 10);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
setForeground(foreground) {
|
|
296
|
+
this.#foreground = foreground;
|
|
297
|
+
if (foreground && this.#leader === void 0 && !this.#closed) {
|
|
298
|
+
this.#post({ kind: "bid", bidderId: this.#instanceId, foreground: true });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async #yieldLeadership(to) {
|
|
302
|
+
const leader = this.#leader;
|
|
303
|
+
if (leader === void 0)
|
|
304
|
+
return;
|
|
305
|
+
this.#leader = void 0;
|
|
306
|
+
this.#knownLeader = void 0;
|
|
307
|
+
this.#lastYieldAt = Date.now();
|
|
308
|
+
try {
|
|
309
|
+
await leader.shutdown();
|
|
310
|
+
} catch {
|
|
311
|
+
leader.crash();
|
|
312
|
+
}
|
|
313
|
+
this.#closeAnswerChannels();
|
|
314
|
+
this.#post({ kind: "yield", to });
|
|
315
|
+
if (this.#reacquireTimer !== void 0)
|
|
316
|
+
clearTimeout(this.#reacquireTimer);
|
|
317
|
+
this.#reacquireTimer = setTimeout(() => {
|
|
318
|
+
if (this.#knownLeader === void 0 && !this.#closed)
|
|
319
|
+
void this.#tryBecomeLeader();
|
|
320
|
+
}, 1500);
|
|
321
|
+
}
|
|
322
|
+
#onMessage(message) {
|
|
323
|
+
if (this.#closed)
|
|
324
|
+
return;
|
|
325
|
+
switch (message.kind) {
|
|
326
|
+
case "op": {
|
|
327
|
+
if (this.#leader !== void 0) {
|
|
328
|
+
const requestBytes = estimateRpcValueBytes(message.args);
|
|
329
|
+
if (this.#servedRequestCount >= RPC_SERVER_ADMISSION_LIMIT || this.#servedRequestBytes + requestBytes > RPC_SERVER_ADMISSION_BYTES) {
|
|
330
|
+
this.#answer(message.from, {
|
|
331
|
+
kind: "result",
|
|
332
|
+
requestId: message.requestId,
|
|
333
|
+
ok: false,
|
|
334
|
+
error: { name: "Error", message: "The OPFS leader RPC queue is full" }
|
|
592
335
|
});
|
|
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
336
|
return;
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
337
|
+
}
|
|
338
|
+
this.#servedRequestCount += 1;
|
|
339
|
+
this.#servedRequestBytes += requestBytes;
|
|
340
|
+
void this.#serveOp(message).finally(() => {
|
|
341
|
+
this.#servedRequestCount = Math.max(0, this.#servedRequestCount - 1);
|
|
342
|
+
this.#servedRequestBytes = Math.max(0, this.#servedRequestBytes - requestBytes);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
case "result": {
|
|
348
|
+
const pending = this.#takePending(message.requestId);
|
|
349
|
+
if (pending === void 0)
|
|
350
|
+
return;
|
|
351
|
+
clearTimeout(pending.timer);
|
|
352
|
+
if (message.ok)
|
|
353
|
+
pending.resolve(message.value);
|
|
354
|
+
else
|
|
355
|
+
pending.reject(rehydrateStoreError(message.error));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
case "busy": {
|
|
359
|
+
const pending = this.#pending.get(message.requestId);
|
|
360
|
+
if (pending === void 0)
|
|
361
|
+
return;
|
|
362
|
+
clearTimeout(pending.timer);
|
|
363
|
+
pending.timer = setTimeout(() => {
|
|
364
|
+
const expired = this.#takePending(message.requestId);
|
|
365
|
+
expired?.reject(RPC_TIMED_OUT);
|
|
366
|
+
}, this.#rpcTimeoutMs);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
case "leader": {
|
|
370
|
+
this.#knownLeader = message.leaderId;
|
|
371
|
+
if (this.#reacquireTimer !== void 0) {
|
|
372
|
+
clearTimeout(this.#reacquireTimer);
|
|
373
|
+
this.#reacquireTimer = void 0;
|
|
374
|
+
}
|
|
375
|
+
for (const [requestId, pending] of this.#pending) {
|
|
376
|
+
if (pending.sentTo === message.leaderId)
|
|
377
|
+
continue;
|
|
378
|
+
if (!READ_METHODS.has(pending.message.method)) {
|
|
379
|
+
this.#takePending(requestId);
|
|
380
|
+
clearTimeout(pending.timer);
|
|
381
|
+
pending.reject(new OpfsUncertainOutcomeError(pending.message.method));
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
pending.sentTo = message.leaderId;
|
|
385
|
+
this.#send(message.leaderId, pending.message);
|
|
386
|
+
}
|
|
387
|
+
if (this.#foreground && this.#leader === void 0) {
|
|
388
|
+
this.#post({ kind: "bid", bidderId: this.#instanceId, foreground: true });
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
case "ping": {
|
|
393
|
+
if (this.#leader !== void 0) {
|
|
394
|
+
this.#post({ kind: "leader", leaderId: this.#instanceId });
|
|
395
|
+
}
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
case "bid": {
|
|
399
|
+
if (this.#leader !== void 0 && !this.#foreground && message.foreground && message.bidderId !== this.#instanceId && Date.now() - this.#lastYieldAt > YIELD_COOLDOWN_MS) {
|
|
400
|
+
void this.#yieldLeadership(message.bidderId);
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
case "yield": {
|
|
405
|
+
if (message.to === this.#instanceId)
|
|
406
|
+
void this.#tryBecomeLeader();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
case "released": {
|
|
410
|
+
if (this.#knownLeader === message.leaderId)
|
|
411
|
+
this.#knownLeader = void 0;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
#takePending(requestId) {
|
|
417
|
+
const pending = this.#pending.get(requestId);
|
|
418
|
+
if (pending === void 0)
|
|
419
|
+
return void 0;
|
|
420
|
+
this.#pending.delete(requestId);
|
|
421
|
+
this.#pendingRpcBytes -= pending.retainedBytes;
|
|
422
|
+
return pending;
|
|
423
|
+
}
|
|
424
|
+
async #serveOp(message) {
|
|
425
|
+
const requestKey = `${String(message.from.length)}:${message.from}${message.requestId}`;
|
|
426
|
+
if (this.#inFlightMutations.has(requestKey) || this.#settledMutations.has(requestKey)) {
|
|
427
|
+
await this.#serveOpLocked(message, requestKey);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const previous = this.#servedRequestLocks.get(requestKey) ?? Promise.resolve();
|
|
431
|
+
let release;
|
|
432
|
+
const hold = new Promise((resolve) => {
|
|
433
|
+
release = resolve;
|
|
434
|
+
});
|
|
435
|
+
const tail = previous.then(() => hold);
|
|
436
|
+
this.#servedRequestLocks.set(requestKey, tail);
|
|
437
|
+
await previous;
|
|
438
|
+
try {
|
|
439
|
+
await this.#serveOpLocked(message, requestKey);
|
|
440
|
+
} finally {
|
|
441
|
+
release();
|
|
442
|
+
if (this.#servedRequestLocks.get(requestKey) === tail) {
|
|
443
|
+
this.#servedRequestLocks.delete(requestKey);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async #serveOpLocked(message, requestKey) {
|
|
448
|
+
const leader = this.#leader;
|
|
449
|
+
if (leader === void 0 || !RPC_METHODS.has(message.method))
|
|
450
|
+
return;
|
|
451
|
+
const isRead = READ_METHODS.has(message.method);
|
|
452
|
+
let fingerprint;
|
|
453
|
+
try {
|
|
454
|
+
fingerprint = await fingerprintStoreRequest(message.method, message.args);
|
|
455
|
+
} catch {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const remembered = isRead ? void 0 : this.#settledMutations.get(requestKey);
|
|
459
|
+
let settled;
|
|
460
|
+
if (remembered !== void 0) {
|
|
461
|
+
if (!sameServedRequest(remembered, message, fingerprint.signature)) {
|
|
462
|
+
this.#rejectReusedRequestIdentity(message);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.#settledMutations.delete(requestKey);
|
|
466
|
+
this.#settledMutations.set(requestKey, remembered);
|
|
467
|
+
settled = remembered.outcome;
|
|
468
|
+
} else if (!isRead && this.#inFlightMutations.has(requestKey)) {
|
|
469
|
+
const inFlight = this.#inFlightMutations.get(requestKey);
|
|
470
|
+
if (inFlight === void 0)
|
|
471
|
+
throw new Error("In-flight RPC identity disappeared");
|
|
472
|
+
if (!sameServedRequest(inFlight, message, fingerprint.signature)) {
|
|
473
|
+
this.#rejectReusedRequestIdentity(message);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
this.#answer(message.from, { kind: "busy", requestId: message.requestId });
|
|
477
|
+
settled = await inFlight.outcome;
|
|
478
|
+
} else {
|
|
479
|
+
if (!isRead && (this.#inFlightMutations.size >= RPC_IN_FLIGHT_LIMIT || this.#inFlightMutationBytes + fingerprint.retainedBytes > RPC_IN_FLIGHT_MUTATION_BYTES)) {
|
|
617
480
|
this.#answer(message.from, {
|
|
481
|
+
kind: "result",
|
|
482
|
+
requestId: message.requestId,
|
|
483
|
+
ok: false,
|
|
484
|
+
error: { name: "Error", message: "The OPFS leader mutation queue is full" }
|
|
485
|
+
});
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (isRead) {
|
|
489
|
+
const reservation = Math.max(fingerprint.retainedBytes, MAX_OPFS_RPC_MESSAGE_BYTES);
|
|
490
|
+
if (this.#inFlightReadBytes + reservation > RPC_IN_FLIGHT_READ_BYTES) {
|
|
491
|
+
this.#answer(message.from, {
|
|
618
492
|
kind: "result",
|
|
619
493
|
requestId: message.requestId,
|
|
620
494
|
ok: false,
|
|
621
|
-
error: {
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
}
|
|
633
|
-
async #executeServedOp(leader, message) {
|
|
634
|
-
try {
|
|
635
|
-
const method = leader[message.method];
|
|
636
|
-
if (method === undefined) {
|
|
637
|
-
return { ok: false, error: { name: "Error", message: "Unknown store operation" } };
|
|
638
|
-
}
|
|
639
|
-
return { ok: true, value: await method.apply(leader, message.args) };
|
|
640
|
-
}
|
|
641
|
-
catch (error) {
|
|
642
|
-
return { ok: false, error: serializeStoreError(error) };
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
/** Posts on the shared channel: leadership traffic, heard by every connection. */
|
|
646
|
-
#post(message) {
|
|
647
|
-
this.#channel?.postMessage(message);
|
|
648
|
-
}
|
|
649
|
-
/**
|
|
650
|
-
* Posts an operation into the leader's inbox through a channel opened for this one message.
|
|
651
|
-
* Every follower posts into the same inbox, and a `BroadcastChannel` hears whatever any other
|
|
652
|
-
* object of its name posts — an outbound channel kept open would deliver every other
|
|
653
|
-
* follower's operation (block bytes included) to this tab. Opened and closed around a single
|
|
654
|
-
* message, nothing but the leader's inbox is ever listening on that name.
|
|
655
|
-
*/
|
|
656
|
-
#send(leaderId, message) {
|
|
657
|
-
// Once the channels are torn down nothing may be posted.
|
|
658
|
-
if (this.#channel === undefined)
|
|
659
|
-
return;
|
|
660
|
-
this.#postOnce(leaderId, message);
|
|
661
|
-
}
|
|
662
|
-
/**
|
|
663
|
-
* Posts an answer into the requester's inbox. Only the leader posts there, so the leader
|
|
664
|
-
* keeps these channels open across requests and hears nothing through them. They close on
|
|
665
|
-
* demotion: an ex-leader's open channel would hear the next leader's answers to that tab.
|
|
666
|
-
*/
|
|
667
|
-
#answer(requesterId, message) {
|
|
668
|
-
if (this.#channel === undefined)
|
|
669
|
-
return;
|
|
670
|
-
if (this.#leader === undefined) {
|
|
671
|
-
// An answer finishing after demotion; do not reopen a channel that demotion just closed.
|
|
672
|
-
this.#postOnce(requesterId, message);
|
|
673
|
-
return;
|
|
495
|
+
error: { name: "Error", message: "The OPFS leader read queue is full" }
|
|
496
|
+
});
|
|
497
|
+
return;
|
|
674
498
|
}
|
|
675
|
-
|
|
676
|
-
if (outbound === undefined) {
|
|
677
|
-
outbound = new BroadcastChannel(this.#inboxName(requesterId));
|
|
678
|
-
this.#answerChannels.set(requesterId, outbound);
|
|
679
|
-
if (this.#answerChannels.size > ANSWER_CHANNEL_CACHE_SIZE) {
|
|
680
|
-
// Followers leave without a goodbye; the oldest entry is the likeliest to be gone.
|
|
681
|
-
const [oldest] = this.#answerChannels;
|
|
682
|
-
if (oldest !== undefined) {
|
|
683
|
-
this.#answerChannels.delete(oldest[0]);
|
|
684
|
-
oldest[1].close();
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
outbound.postMessage(message);
|
|
689
|
-
}
|
|
690
|
-
#closeAnswerChannels() {
|
|
691
|
-
for (const outbound of this.#answerChannels.values())
|
|
692
|
-
outbound.close();
|
|
693
|
-
this.#answerChannels.clear();
|
|
694
|
-
}
|
|
695
|
-
/** Posts one message into an inbox through a channel that lives only for that message. */
|
|
696
|
-
#postOnce(instanceId, message) {
|
|
697
|
-
const outbound = new BroadcastChannel(this.#inboxName(instanceId));
|
|
698
|
-
outbound.postMessage(message);
|
|
699
|
-
outbound.close();
|
|
700
|
-
}
|
|
701
|
-
#inboxName(instanceId) {
|
|
702
|
-
return `${this.#channelName}:${instanceId}`;
|
|
703
|
-
}
|
|
704
|
-
/** Closes every channel; nothing is posted or delivered after this. */
|
|
705
|
-
#closeChannels() {
|
|
706
|
-
this.#channel?.close();
|
|
707
|
-
this.#channel = undefined;
|
|
708
|
-
this.#inbox?.close();
|
|
709
|
-
this.#inbox = undefined;
|
|
710
|
-
this.#closeAnswerChannels();
|
|
711
|
-
}
|
|
712
|
-
// ---------------------------------------------------------------------------------------
|
|
713
|
-
// Dispatch: local when leading, RPC when following, elect when leaderless.
|
|
714
|
-
// ---------------------------------------------------------------------------------------
|
|
715
|
-
#assertOpen() {
|
|
716
|
-
if (this.#closed)
|
|
717
|
-
throw new Error("This OPFS store connection is closed");
|
|
718
|
-
}
|
|
719
|
-
async #dispatch(method, args) {
|
|
720
|
-
this.#assertOpen();
|
|
721
|
-
const requestId = crypto.randomUUID();
|
|
722
|
-
for (let attempt = 0; attempt < DISPATCH_ATTEMPTS; attempt += 1) {
|
|
723
|
-
// Re-checked each attempt: the awaits below (elections, RPC round trips) give close()
|
|
724
|
-
// every opportunity to run.
|
|
725
|
-
this.#assertOpen();
|
|
726
|
-
const leader = this.#leader;
|
|
727
|
-
if (leader !== undefined) {
|
|
728
|
-
const bound = leader[method];
|
|
729
|
-
if (bound === undefined)
|
|
730
|
-
throw new Error(`Unknown store operation: ${method}`);
|
|
731
|
-
return bound.apply(leader, args);
|
|
732
|
-
}
|
|
733
|
-
if (this.#channel === undefined) {
|
|
734
|
-
if (await this.#tryBecomeLeader())
|
|
735
|
-
continue;
|
|
736
|
-
throw new Error("Another connection holds this OPFS database, and BroadcastChannel is unavailable to reach it");
|
|
737
|
-
}
|
|
738
|
-
if (!this.#leaderKnown()) {
|
|
739
|
-
this.#post({ kind: "ping" });
|
|
740
|
-
await sleep(DISCOVERY_WAIT_MS);
|
|
741
|
-
if (!this.#leaderKnown()) {
|
|
742
|
-
if (await this.#tryBecomeLeader())
|
|
743
|
-
continue;
|
|
744
|
-
await sleep(20 + Math.random() * 50);
|
|
745
|
-
continue;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
try {
|
|
749
|
-
return await this.#rpc(requestId, method, args);
|
|
750
|
-
}
|
|
751
|
-
catch (error) {
|
|
752
|
-
if (error === RPC_TIMED_OUT) {
|
|
753
|
-
this.#knownLeader = undefined;
|
|
754
|
-
if (!READ_METHODS.has(method))
|
|
755
|
-
throw new OpfsUncertainOutcomeError(method);
|
|
756
|
-
continue;
|
|
757
|
-
}
|
|
758
|
-
throw error;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
throw new Error("The OPFS store could not reach or become a leader");
|
|
762
|
-
}
|
|
763
|
-
/** The channel can change #knownLeader between any two awaits; a method defeats narrowing. */
|
|
764
|
-
#leaderKnown() {
|
|
765
|
-
return this.#knownLeader !== undefined;
|
|
766
|
-
}
|
|
767
|
-
#rpc(requestId, method, args) {
|
|
768
|
-
let retainedBytes;
|
|
499
|
+
this.#inFlightReadBytes += reservation;
|
|
769
500
|
try {
|
|
770
|
-
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
}, this.#rpcTimeoutMs);
|
|
785
|
-
const leaderId = this.#knownLeader;
|
|
786
|
-
this.#pending.set(requestId, {
|
|
787
|
-
message,
|
|
788
|
-
retainedBytes,
|
|
789
|
-
sentTo: leaderId,
|
|
790
|
-
resolve,
|
|
791
|
-
reject,
|
|
792
|
-
timer,
|
|
793
|
-
});
|
|
794
|
-
this.#pendingRpcBytes += retainedBytes;
|
|
795
|
-
// The dispatch loop only gets here with a leader known; should it have slipped away in
|
|
796
|
-
// between, the timeout (or the next leader's announcement, which re-sends) takes over.
|
|
797
|
-
if (leaderId !== undefined)
|
|
798
|
-
this.#send(leaderId, message);
|
|
501
|
+
settled = await this.#executeServedOpAfterGate(leader, message, true);
|
|
502
|
+
} finally {
|
|
503
|
+
this.#inFlightReadBytes = Math.max(0, this.#inFlightReadBytes - reservation);
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
this.#inFlightMutationBytes += fingerprint.retainedBytes;
|
|
507
|
+
const execution = this.#executeServedOpAfterGate(leader, message, false);
|
|
508
|
+
const outcome = execution.then((result) => {
|
|
509
|
+
this.#inFlightMutations.delete(requestKey);
|
|
510
|
+
this.#inFlightMutationBytes = Math.max(0, this.#inFlightMutationBytes - fingerprint.retainedBytes);
|
|
511
|
+
if (!this.#closed) {
|
|
512
|
+
this.#rememberSettledMutation(requestKey, message.method, fingerprint, result);
|
|
513
|
+
}
|
|
514
|
+
return result;
|
|
799
515
|
});
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
validateTempRunPage(page);
|
|
806
|
-
await this.#dispatch("putTempRunPage", [page]);
|
|
807
|
-
}
|
|
808
|
-
async putTempRunPages(pages) {
|
|
809
|
-
// Spill pages are instance-local files; the batch is a convenience, not a round-trip win.
|
|
810
|
-
assertTempRunPageBatchLimits(pages);
|
|
811
|
-
for (const page of pages)
|
|
812
|
-
validateTempRunPage(page);
|
|
813
|
-
await this.#dispatch("putTempRunPages", [pages]);
|
|
814
|
-
}
|
|
815
|
-
async getTempRunPage(ownerId, runId, pageIndex) {
|
|
816
|
-
validateTempRunPageIdentity(ownerId, runId, pageIndex);
|
|
817
|
-
return (await this.#dispatch("getTempRunPage", [ownerId, runId, pageIndex]));
|
|
818
|
-
}
|
|
819
|
-
async removeTempRun(ownerId, runId) {
|
|
820
|
-
validateTempRunPageIdentity(ownerId, runId, 0);
|
|
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`);
|
|
831
|
-
}
|
|
832
|
-
return this.#dispatch(method, args);
|
|
833
|
-
}
|
|
834
|
-
close() {
|
|
835
|
-
if (this.#closed)
|
|
836
|
-
return;
|
|
837
|
-
this.#closed = true;
|
|
838
|
-
if (this.#reacquireTimer !== undefined)
|
|
839
|
-
clearTimeout(this.#reacquireTimer);
|
|
840
|
-
for (const pending of this.#pending.values()) {
|
|
841
|
-
clearTimeout(pending.timer);
|
|
842
|
-
pending.reject(new Error("This OPFS store connection is closed"));
|
|
843
|
-
}
|
|
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;
|
|
854
|
-
const leader = this.#leader;
|
|
855
|
-
this.#leader = undefined;
|
|
856
|
-
if (leader !== undefined) {
|
|
857
|
-
void leader
|
|
858
|
-
.shutdown()
|
|
859
|
-
.catch(() => {
|
|
860
|
-
leader.crash();
|
|
861
|
-
})
|
|
862
|
-
.then(() => {
|
|
863
|
-
this.#post({ kind: "released", leaderId: this.#instanceId });
|
|
864
|
-
this.#closeChannels();
|
|
865
|
-
});
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
this.#closeChannels();
|
|
869
|
-
}
|
|
870
|
-
/** Test-only: whether this connection currently holds the database's handles. */
|
|
871
|
-
_isLeaderForTests() {
|
|
872
|
-
return this.#leader !== undefined;
|
|
873
|
-
}
|
|
874
|
-
/** Test-only: the id that names this connection's inbox channel. */
|
|
875
|
-
_instanceIdForTests() {
|
|
876
|
-
return this.#instanceId;
|
|
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;
|
|
516
|
+
this.#inFlightMutations.set(requestKey, {
|
|
517
|
+
method: message.method,
|
|
518
|
+
signature: fingerprint.signature,
|
|
519
|
+
requestBytes: fingerprint.retainedBytes,
|
|
520
|
+
outcome
|
|
890
521
|
});
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
522
|
+
settled = await outcome;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (this.#dropNextRpcResultForTests) {
|
|
526
|
+
this.#dropNextRpcResultForTests = false;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
this.#answer(message.from, settled.ok ? { kind: "result", requestId: message.requestId, ok: true, value: settled.value } : {
|
|
530
|
+
kind: "result",
|
|
531
|
+
requestId: message.requestId,
|
|
532
|
+
ok: false,
|
|
533
|
+
error: settled.error ?? { name: "Error", message: "unknown" }
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
#rememberSettledMutation(requestKey, method, fingerprint, outcome) {
|
|
537
|
+
const outcomeBytes = estimateRpcValueBytes(outcome);
|
|
538
|
+
while (this.#settledMutations.size >= DEDUPE_CACHE_SIZE || this.#settledMutationBytes + outcomeBytes > RPC_SETTLED_OUTCOME_BYTES) {
|
|
539
|
+
const oldest = this.#settledMutations.keys().next().value;
|
|
540
|
+
if (oldest === void 0)
|
|
541
|
+
break;
|
|
542
|
+
const removed = this.#settledMutations.get(oldest);
|
|
543
|
+
this.#settledMutations.delete(oldest);
|
|
544
|
+
this.#settledMutationBytes -= removed?.outcomeBytes ?? 0;
|
|
545
|
+
}
|
|
546
|
+
if (outcomeBytes > RPC_SETTLED_OUTCOME_BYTES)
|
|
547
|
+
return;
|
|
548
|
+
this.#settledMutations.set(requestKey, {
|
|
549
|
+
method,
|
|
550
|
+
signature: fingerprint.signature,
|
|
551
|
+
requestBytes: fingerprint.retainedBytes,
|
|
552
|
+
outcome,
|
|
553
|
+
outcomeBytes
|
|
554
|
+
});
|
|
555
|
+
this.#settledMutationBytes += outcomeBytes;
|
|
556
|
+
}
|
|
557
|
+
#rejectReusedRequestIdentity(message) {
|
|
558
|
+
this.#answer(message.from, {
|
|
559
|
+
kind: "result",
|
|
560
|
+
requestId: message.requestId,
|
|
561
|
+
ok: false,
|
|
562
|
+
error: {
|
|
563
|
+
name: "Error",
|
|
564
|
+
message: "The OPFS RPC request identity was reused with different contents"
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
async #executeServedOpAfterGate(leader, message, isRead) {
|
|
569
|
+
const gate = this.#servedMutationGateForTests;
|
|
570
|
+
if (!isRead && gate !== void 0)
|
|
571
|
+
await gate;
|
|
572
|
+
return this.#executeServedOp(leader, message);
|
|
573
|
+
}
|
|
574
|
+
async #executeServedOp(leader, message) {
|
|
575
|
+
try {
|
|
576
|
+
const method = leader[message.method];
|
|
577
|
+
if (method === void 0) {
|
|
578
|
+
return { ok: false, error: { name: "Error", message: "Unknown store operation" } };
|
|
579
|
+
}
|
|
580
|
+
return { ok: true, value: await method.apply(leader, message.args) };
|
|
581
|
+
} catch (error) {
|
|
582
|
+
return { ok: false, error: serializeStoreError(error) };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
#post(message) {
|
|
586
|
+
this.#channel?.postMessage(message);
|
|
587
|
+
}
|
|
588
|
+
#send(leaderId, message) {
|
|
589
|
+
if (this.#channel === void 0)
|
|
590
|
+
return;
|
|
591
|
+
this.#postOnce(leaderId, message);
|
|
592
|
+
}
|
|
593
|
+
#answer(requesterId, message) {
|
|
594
|
+
if (this.#channel === void 0)
|
|
595
|
+
return;
|
|
596
|
+
if (this.#leader === void 0) {
|
|
597
|
+
this.#postOnce(requesterId, message);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
let outbound = this.#answerChannels.get(requesterId);
|
|
601
|
+
if (outbound === void 0) {
|
|
602
|
+
outbound = new BroadcastChannel(this.#inboxName(requesterId));
|
|
603
|
+
this.#answerChannels.set(requesterId, outbound);
|
|
604
|
+
if (this.#answerChannels.size > ANSWER_CHANNEL_CACHE_SIZE) {
|
|
605
|
+
const [oldest] = this.#answerChannels;
|
|
606
|
+
if (oldest !== void 0) {
|
|
607
|
+
this.#answerChannels.delete(oldest[0]);
|
|
608
|
+
oldest[1].close();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
outbound.postMessage(message);
|
|
613
|
+
}
|
|
614
|
+
#closeAnswerChannels() {
|
|
615
|
+
for (const outbound of this.#answerChannels.values())
|
|
616
|
+
outbound.close();
|
|
617
|
+
this.#answerChannels.clear();
|
|
618
|
+
}
|
|
619
|
+
#postOnce(instanceId, message) {
|
|
620
|
+
const outbound = new BroadcastChannel(this.#inboxName(instanceId));
|
|
621
|
+
outbound.postMessage(message);
|
|
622
|
+
outbound.close();
|
|
623
|
+
}
|
|
624
|
+
#inboxName(instanceId) {
|
|
625
|
+
return `${this.#channelName}:${instanceId}`;
|
|
626
|
+
}
|
|
627
|
+
#closeChannels() {
|
|
628
|
+
this.#channel?.close();
|
|
629
|
+
this.#channel = void 0;
|
|
630
|
+
this.#inbox?.close();
|
|
631
|
+
this.#inbox = void 0;
|
|
632
|
+
this.#closeAnswerChannels();
|
|
633
|
+
}
|
|
634
|
+
#assertOpen() {
|
|
635
|
+
if (this.#closed)
|
|
636
|
+
throw new Error("This OPFS store connection is closed");
|
|
637
|
+
}
|
|
638
|
+
async #dispatch(method, args) {
|
|
639
|
+
this.#assertOpen();
|
|
640
|
+
const requestId = crypto.randomUUID();
|
|
641
|
+
for (let attempt = 0; attempt < DISPATCH_ATTEMPTS; attempt += 1) {
|
|
642
|
+
this.#assertOpen();
|
|
643
|
+
const leader = this.#leader;
|
|
644
|
+
if (leader !== void 0) {
|
|
645
|
+
const bound = leader[method];
|
|
646
|
+
if (bound === void 0)
|
|
647
|
+
throw new Error(`Unknown store operation: ${method}`);
|
|
648
|
+
return bound.apply(leader, args);
|
|
649
|
+
}
|
|
650
|
+
if (this.#channel === void 0) {
|
|
651
|
+
if (await this.#tryBecomeLeader())
|
|
652
|
+
continue;
|
|
653
|
+
throw new Error("Another connection holds this OPFS database, and BroadcastChannel is unavailable to reach it");
|
|
654
|
+
}
|
|
655
|
+
if (!this.#leaderKnown()) {
|
|
656
|
+
this.#post({ kind: "ping" });
|
|
657
|
+
await sleep(DISCOVERY_WAIT_MS);
|
|
658
|
+
if (!this.#leaderKnown()) {
|
|
659
|
+
if (await this.#tryBecomeLeader())
|
|
660
|
+
continue;
|
|
661
|
+
await sleep(20 + Math.random() * 50);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
return await this.#rpc(requestId, method, args);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
if (error === RPC_TIMED_OUT) {
|
|
669
|
+
this.#knownLeader = void 0;
|
|
670
|
+
if (!READ_METHODS.has(method))
|
|
671
|
+
throw new OpfsUncertainOutcomeError(method);
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
throw new Error("The OPFS store could not reach or become a leader");
|
|
678
|
+
}
|
|
679
|
+
#leaderKnown() {
|
|
680
|
+
return this.#knownLeader !== void 0;
|
|
681
|
+
}
|
|
682
|
+
#rpc(requestId, method, args) {
|
|
683
|
+
let retainedBytes;
|
|
684
|
+
try {
|
|
685
|
+
retainedBytes = estimateRpcValueBytes(args);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
|
688
|
+
}
|
|
689
|
+
if (this.#pending.size >= RPC_IN_FLIGHT_LIMIT || this.#pendingRpcBytes + retainedBytes > RPC_IN_FLIGHT_MUTATION_BYTES) {
|
|
690
|
+
return Promise.reject(new Error("The OPFS follower request queue is full"));
|
|
691
|
+
}
|
|
692
|
+
return new Promise((resolve, reject) => {
|
|
693
|
+
const message = { kind: "op", requestId, from: this.#instanceId, method, args };
|
|
694
|
+
const timer = setTimeout(() => {
|
|
695
|
+
const expired = this.#takePending(requestId);
|
|
696
|
+
expired?.reject(RPC_TIMED_OUT);
|
|
697
|
+
}, this.#rpcTimeoutMs);
|
|
698
|
+
const leaderId = this.#knownLeader;
|
|
699
|
+
this.#pending.set(requestId, {
|
|
700
|
+
message,
|
|
701
|
+
retainedBytes,
|
|
702
|
+
sentTo: leaderId,
|
|
703
|
+
resolve,
|
|
704
|
+
reject,
|
|
705
|
+
timer
|
|
706
|
+
});
|
|
707
|
+
this.#pendingRpcBytes += retainedBytes;
|
|
708
|
+
if (leaderId !== void 0)
|
|
709
|
+
this.#send(leaderId, message);
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
async putTempRunPage(page) {
|
|
713
|
+
validateTempRunPage(page);
|
|
714
|
+
await this.#dispatch("putTempRunPage", [page]);
|
|
715
|
+
}
|
|
716
|
+
async putTempRunPages(pages) {
|
|
717
|
+
assertTempRunPageBatchLimits(pages);
|
|
718
|
+
for (const page of pages)
|
|
719
|
+
validateTempRunPage(page);
|
|
720
|
+
await this.#dispatch("putTempRunPages", [pages]);
|
|
721
|
+
}
|
|
722
|
+
async getTempRunPage(ownerId, runId, pageIndex) {
|
|
723
|
+
validateTempRunPageIdentity(ownerId, runId, pageIndex);
|
|
724
|
+
return await this.#dispatch("getTempRunPage", [ownerId, runId, pageIndex]);
|
|
725
|
+
}
|
|
726
|
+
async removeTempRun(ownerId, runId) {
|
|
727
|
+
validateTempRunPageIdentity(ownerId, runId, 0);
|
|
728
|
+
await this.#dispatch("removeTempRun", [ownerId, runId]);
|
|
729
|
+
}
|
|
730
|
+
async _dispatchGenerated(method, args) {
|
|
731
|
+
const bulkIndex = BULK_READ_ARGUMENT_INDEX[method];
|
|
732
|
+
if (bulkIndex !== void 0) {
|
|
733
|
+
const bulkItems = args[bulkIndex];
|
|
734
|
+
if (!Array.isArray(bulkItems))
|
|
735
|
+
throw new TypeError(`${method} items must be an array`);
|
|
736
|
+
assertStorageBulkReadItems(bulkItems, `${method} request`);
|
|
737
|
+
}
|
|
738
|
+
return this.#dispatch(method, args);
|
|
739
|
+
}
|
|
740
|
+
close() {
|
|
741
|
+
if (this.#closed)
|
|
742
|
+
return;
|
|
743
|
+
this.#closed = true;
|
|
744
|
+
if (this.#reacquireTimer !== void 0)
|
|
745
|
+
clearTimeout(this.#reacquireTimer);
|
|
746
|
+
for (const pending of this.#pending.values()) {
|
|
747
|
+
clearTimeout(pending.timer);
|
|
748
|
+
pending.reject(new Error("This OPFS store connection is closed"));
|
|
749
|
+
}
|
|
750
|
+
this.#pending.clear();
|
|
751
|
+
this.#inFlightMutations.clear();
|
|
752
|
+
this.#settledMutations.clear();
|
|
753
|
+
this.#servedRequestLocks.clear();
|
|
754
|
+
this.#pendingRpcBytes = 0;
|
|
755
|
+
this.#inFlightMutationBytes = 0;
|
|
756
|
+
this.#inFlightReadBytes = 0;
|
|
757
|
+
this.#settledMutationBytes = 0;
|
|
758
|
+
this.#servedRequestCount = 0;
|
|
759
|
+
this.#servedRequestBytes = 0;
|
|
760
|
+
const leader = this.#leader;
|
|
761
|
+
this.#leader = void 0;
|
|
762
|
+
if (leader !== void 0) {
|
|
763
|
+
void leader.shutdown().catch(() => {
|
|
764
|
+
leader.crash();
|
|
765
|
+
}).then(() => {
|
|
766
|
+
this.#post({ kind: "released", leaderId: this.#instanceId });
|
|
936
767
|
this.#closeChannels();
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
768
|
+
});
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
this.#closeChannels();
|
|
772
|
+
}
|
|
773
|
+
_isLeaderForTests() {
|
|
774
|
+
return this.#leader !== void 0;
|
|
775
|
+
}
|
|
776
|
+
_instanceIdForTests() {
|
|
777
|
+
return this.#instanceId;
|
|
778
|
+
}
|
|
779
|
+
_dropNextRpcResultForTests() {
|
|
780
|
+
this.#dropNextRpcResultForTests = true;
|
|
781
|
+
}
|
|
782
|
+
_holdServedMutationsForTests() {
|
|
783
|
+
if (this.#servedMutationGateForTests !== void 0) {
|
|
784
|
+
throw new Error("Served mutations are already held");
|
|
785
|
+
}
|
|
786
|
+
let release;
|
|
787
|
+
this.#servedMutationGateForTests = new Promise((resolve) => {
|
|
788
|
+
release = resolve;
|
|
789
|
+
});
|
|
790
|
+
return () => {
|
|
791
|
+
this.#servedMutationGateForTests = void 0;
|
|
792
|
+
release();
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
_resendOldestPendingForTests() {
|
|
796
|
+
const pending = this.#pending.values().next().value;
|
|
797
|
+
if (pending?.sentTo !== void 0)
|
|
798
|
+
this.#send(pending.sentTo, pending.message);
|
|
799
|
+
}
|
|
800
|
+
_residentStateForTests() {
|
|
801
|
+
return {
|
|
802
|
+
answerChannels: this.#answerChannels.size,
|
|
803
|
+
dedupeEntries: this.#settledMutations.size,
|
|
804
|
+
inFlightMutations: this.#inFlightMutations.size,
|
|
805
|
+
pendingRequests: this.#pending.size,
|
|
806
|
+
retainedRpcBytes: this.#pendingRpcBytes + this.#inFlightMutationBytes + this.#inFlightReadBytes + this.#settledMutationBytes + this.#servedRequestBytes,
|
|
807
|
+
closed: this.#closed
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
_crashForTests() {
|
|
811
|
+
this.#closed = true;
|
|
812
|
+
if (this.#reacquireTimer !== void 0)
|
|
813
|
+
clearTimeout(this.#reacquireTimer);
|
|
814
|
+
for (const pending of this.#pending.values())
|
|
815
|
+
clearTimeout(pending.timer);
|
|
816
|
+
this.#pending.clear();
|
|
817
|
+
this.#inFlightMutations.clear();
|
|
818
|
+
this.#settledMutations.clear();
|
|
819
|
+
this.#servedRequestLocks.clear();
|
|
820
|
+
this.#pendingRpcBytes = 0;
|
|
821
|
+
this.#inFlightMutationBytes = 0;
|
|
822
|
+
this.#inFlightReadBytes = 0;
|
|
823
|
+
this.#settledMutationBytes = 0;
|
|
824
|
+
this.#servedRequestCount = 0;
|
|
825
|
+
this.#servedRequestBytes = 0;
|
|
826
|
+
this.#leader?.crash();
|
|
827
|
+
this.#leader = void 0;
|
|
828
|
+
this.#closeChannels();
|
|
829
|
+
}
|
|
830
|
+
async #ensureFormatMarker() {
|
|
831
|
+
const existing = await this.#tree.readFile(["format.json"], {
|
|
832
|
+
lockedMeansAbsent: true,
|
|
833
|
+
maxBytes: 1024
|
|
834
|
+
});
|
|
835
|
+
if (existing !== void 0) {
|
|
836
|
+
try {
|
|
837
|
+
this.#validateFormatMarker(existing);
|
|
838
|
+
return;
|
|
839
|
+
} catch (error) {
|
|
840
|
+
if (!(error instanceof SyntaxError))
|
|
841
|
+
throw error;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
let artifact;
|
|
845
|
+
for await (const name of this.#tree.iterateNames([])) {
|
|
846
|
+
if (name !== "format.json") {
|
|
847
|
+
artifact = name;
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (artifact !== void 0) {
|
|
852
|
+
throw new StorageCorruptionError("opfs", "format.json", `The OPFS format marker is ${existing === void 0 ? "missing" : "torn"}, but the database directory already contains storage artifacts (${artifact}). Refusing to guess their layout version.`);
|
|
853
|
+
}
|
|
854
|
+
const bytes = new TextEncoder().encode(JSON.stringify({ formatVersion: LOG_FORMAT_VERSION }));
|
|
855
|
+
try {
|
|
856
|
+
await this.#tree.writeFile(["format.json"], bytes, { flush: true });
|
|
857
|
+
} catch (error) {
|
|
858
|
+
if (!isLockContention(error))
|
|
859
|
+
throw error;
|
|
860
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
861
|
+
await sleep(5 + Math.random() * 10);
|
|
862
|
+
const published = await this.#tree.readFile(["format.json"], {
|
|
863
|
+
lockedMeansAbsent: true,
|
|
864
|
+
maxBytes: 1024
|
|
942
865
|
});
|
|
943
|
-
if (
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
const bytes = new TextEncoder().encode(JSON.stringify({ formatVersion: LOG_FORMAT_VERSION }));
|
|
968
|
-
try {
|
|
969
|
-
await this.#tree.writeFile(["format.json"], bytes, { flush: true });
|
|
970
|
-
}
|
|
971
|
-
catch (error) {
|
|
972
|
-
if (!isLockContention(error))
|
|
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");
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
866
|
+
if (published === void 0)
|
|
867
|
+
continue;
|
|
868
|
+
this.#validateFormatMarker(published);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
throw new Error("A concurrent opener did not publish a readable OPFS format marker", {
|
|
872
|
+
cause: error
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
#validateFormatMarker(bytes) {
|
|
877
|
+
const text = new TextDecoder().decode(bytes);
|
|
878
|
+
const parsed = JSON.parse(text);
|
|
879
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || !Number.isSafeInteger(parsed.formatVersion)) {
|
|
880
|
+
throw new StorageCorruptionError("opfs", "format.json", "The OPFS format marker is invalid: formatVersion must be a safe integer");
|
|
881
|
+
}
|
|
882
|
+
const formatVersion = parsed.formatVersion;
|
|
883
|
+
if (formatVersion !== LOG_FORMAT_VERSION) {
|
|
884
|
+
throw new StorageFormatVersionError("opfs", "format.json", formatVersion, LOG_FORMAT_VERSION, formatVersion < LOG_FORMAT_VERSION ? "older" : "newer");
|
|
885
|
+
}
|
|
886
|
+
if (Object.keys(parsed).length !== 1 || !Object.hasOwn(parsed, "formatVersion") || text !== JSON.stringify({ formatVersion })) {
|
|
887
|
+
throw new StorageCorruptionError("opfs", "format.json", "The OPFS format marker is not the canonical layout marker");
|
|
888
|
+
}
|
|
889
|
+
}
|
|
1011
890
|
}
|
|
1012
891
|
for (const method of RPC_METHODS) {
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
892
|
+
if (method in OpfsBlockStore.prototype)
|
|
893
|
+
continue;
|
|
894
|
+
Object.defineProperty(OpfsBlockStore.prototype, method, {
|
|
895
|
+
configurable: true,
|
|
896
|
+
value(...args) {
|
|
897
|
+
return this._dispatchGenerated(method, args);
|
|
898
|
+
}
|
|
899
|
+
});
|
|
1021
900
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
throw error;
|
|
1033
|
-
}
|
|
901
|
+
async function deleteOpfsDatabase(options) {
|
|
902
|
+
const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
|
|
903
|
+
const root = options.root ?? await navigator.storage.getDirectory();
|
|
904
|
+
try {
|
|
905
|
+
const namespace = await root.getDirectoryHandle("minnowdb");
|
|
906
|
+
await namespace.removeEntry(encodedName, { recursive: true });
|
|
907
|
+
} catch (error) {
|
|
908
|
+
if (!isDomError(error, "NotFoundError"))
|
|
909
|
+
throw error;
|
|
910
|
+
}
|
|
1034
911
|
}
|
|
1035
912
|
const RPC_TIMED_OUT = new Error("The leader did not answer in time");
|
|
1036
913
|
async function resolveDatabaseRoot(options) {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
914
|
+
const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
|
|
915
|
+
const root = options.root ?? await navigator.storage.getDirectory();
|
|
916
|
+
const namespace = await root.getDirectoryHandle("minnowdb", { create: true });
|
|
917
|
+
return namespace.getDirectoryHandle(encodedName, { create: true });
|
|
1041
918
|
}
|
|
1042
919
|
function isLockContention(error) {
|
|
1043
|
-
|
|
920
|
+
return isDomError(error, "NoModificationAllowedError") || isDomError(error, "InvalidStateError");
|
|
1044
921
|
}
|
|
1045
922
|
function sleep(ms) {
|
|
1046
|
-
|
|
923
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1047
924
|
}
|
|
925
|
+
export {
|
|
926
|
+
OpfsBlockStore,
|
|
927
|
+
deleteOpfsDatabase
|
|
928
|
+
};
|