@minnowdb/core 0.9.0 → 0.10.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/dist/engine/auto-store.d.ts +40 -0
- package/dist/engine/auto-store.js +115 -0
- package/dist/engine/buffered-writer.d.ts +2 -0
- package/dist/engine/buffered-writer.js +15 -2
- package/dist/engine/client.d.ts +55 -6
- package/dist/engine/client.js +155 -40
- package/dist/engine/database.d.ts +15 -1
- package/dist/engine/database.js +1085 -227
- package/dist/engine/errors.d.ts +61 -2
- package/dist/engine/errors.js +116 -3
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +2 -0
- package/dist/engine/live.d.ts +24 -1
- package/dist/engine/live.js +33 -9
- package/dist/engine/scope-write-set.js +36 -0
- package/dist/engine/worker-auto.d.ts +1 -0
- package/dist/engine/worker-auto.js +3 -0
- package/dist/engine/worker-host.d.ts +2 -1
- package/dist/engine/worker-host.js +17 -1
- package/dist/engine/worker-server.d.ts +53 -1
- package/dist/engine/worker-server.js +118 -14
- package/dist/engine/worker-store-auto.js +36 -0
- package/dist/engine/worker-store-opfs.js +3 -2
- package/dist/engine/write-coordinator.js +24 -2
- package/dist/storage/indexeddb.js +494 -207
- package/dist/storage/opfs/leader.js +201 -14
- package/dist/storage/opfs/rpc.js +23 -43
- package/dist/storage/opfs/store.d.ts +20 -0
- package/dist/storage/opfs/store.js +501 -70
- package/dist/storage/toolkit/record-core.js +67 -38
- package/dist/storage/toolkit/wire.d.ts +1 -1
- package/dist/storage/toolkit/wire.js +1 -1
- package/dist/storage/types.d.ts +29 -8
- package/dist/storage/types.js +27 -16
- package/dist/testing/opfs-shim.js +14 -6
- package/dist/transactions/index.d.ts +12 -0
- package/dist/transactions/index.js +83 -23
- package/dist/worker-protocol/index.d.ts +50 -2
- package/dist/worker-protocol/index.js +106 -4
- package/package.json +7 -2
package/dist/engine/errors.d.ts
CHANGED
|
@@ -9,14 +9,35 @@ export declare class TransactionExpiredError extends Error {
|
|
|
9
9
|
constructor();
|
|
10
10
|
}
|
|
11
11
|
/** The worker stopped responding within the configured request deadline. */
|
|
12
|
-
export declare class DatabaseWorkerTimeoutError extends
|
|
12
|
+
export declare class DatabaseWorkerTimeoutError extends ConnectionLostError {
|
|
13
13
|
readonly method: string;
|
|
14
14
|
readonly timeoutMs: number;
|
|
15
15
|
readonly name = "DatabaseWorkerTimeoutError";
|
|
16
16
|
constructor(method: string, timeoutMs: number);
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* The worker's channel failed underneath every pending call: the worker raised a script error it
|
|
20
|
+
* did not handle, or sent a frame that could not be read. `reason` says which; `cause` carries
|
|
21
|
+
* the worker's own error when the browser handed it over.
|
|
22
|
+
*/
|
|
23
|
+
export declare class DatabaseWorkerFailedError extends ConnectionLostError {
|
|
24
|
+
readonly reason: "error" | "messageerror" | "reopened";
|
|
25
|
+
readonly name = "DatabaseWorkerFailedError";
|
|
26
|
+
constructor(reason: "error" | "messageerror" | "reopened", message: string, options?: ErrorOptions);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The store an `auto` descriptor remembered for this database cannot be opened here — OPFS in a
|
|
30
|
+
* context without synchronous access handles, typically — and the database is not reopened,
|
|
31
|
+
* empty, on the other store.
|
|
32
|
+
*/
|
|
33
|
+
export declare class DatabaseStoreUnavailableError extends Error {
|
|
34
|
+
readonly store: "opfs" | "indexeddb";
|
|
35
|
+
readonly databaseName: string;
|
|
36
|
+
readonly name = "DatabaseStoreUnavailableError";
|
|
37
|
+
constructor(store: "opfs" | "indexeddb", databaseName: string, message: string);
|
|
38
|
+
}
|
|
18
39
|
/** A transport failure cannot prove whether this operation published before its reply was lost. */
|
|
19
|
-
export declare class DatabaseWorkerOutcomeUnknownError extends
|
|
40
|
+
export declare class DatabaseWorkerOutcomeUnknownError extends UnknownOutcomeError {
|
|
20
41
|
readonly method: string;
|
|
21
42
|
readonly requestId: string;
|
|
22
43
|
readonly name = "DatabaseWorkerOutcomeUnknownError";
|
|
@@ -108,4 +129,42 @@ export declare class SqlCompileError extends TypeError {
|
|
|
108
129
|
readonly name = "SqlCompileError";
|
|
109
130
|
constructor(message: string, offset: number, length: number);
|
|
110
131
|
}
|
|
132
|
+
import { ConnectionLostError, UnknownOutcomeError } from "../storage/types.js";
|
|
133
|
+
/**
|
|
134
|
+
* What an error means for the caller, in the three terms that decide what to do next. Every
|
|
135
|
+
* error Minnow throws maps to exactly one kind; an unrecognized error is `"other"` with the
|
|
136
|
+
* cautious answers.
|
|
137
|
+
*/
|
|
138
|
+
export type ErrorKind =
|
|
139
|
+
/** The operation may have happened. Reconcile a stable id or revision before retrying. */
|
|
140
|
+
"unknown-outcome"
|
|
141
|
+
/** This connection is finished; open a new one. Pending mutations were reported separately. */
|
|
142
|
+
| "connection-lost"
|
|
143
|
+
/** Lost a race with another writer or a schema change; nothing happened. Retry as is. */
|
|
144
|
+
| "conflict"
|
|
145
|
+
/** The request itself is wrong (a duplicate key, a bad table, a compile error). Fix it. */
|
|
146
|
+
| "rejected"
|
|
147
|
+
/** A momentary condition: backpressure, coordination, an expired scope. Retry with backoff. */
|
|
148
|
+
| "transient"
|
|
149
|
+
/** A limit was reached: quota, memory, a resource ceiling. Free something, then retry. */
|
|
150
|
+
| "resource"
|
|
151
|
+
/** The stored data is unreadable by this build. Do not retry, and do not delete. */
|
|
152
|
+
| "corruption"
|
|
153
|
+
/** The caller cancelled it; nothing happened. */
|
|
154
|
+
| "cancelled" | "other";
|
|
155
|
+
export interface ErrorClassification {
|
|
156
|
+
kind: ErrorKind;
|
|
157
|
+
/** True when the operation may have published despite the error. Never replay when true. */
|
|
158
|
+
mayHavePublished: boolean;
|
|
159
|
+
/** Whether repeating the same call is sound: as is, only after reconciling, or not at all. */
|
|
160
|
+
retry: "safe" | "after-reconcile" | "never";
|
|
161
|
+
/** False when every later call on the same connection will fail the same way. */
|
|
162
|
+
connectionUsable: boolean;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Classifies any error into what it means for the caller: may the operation have published, is
|
|
166
|
+
* a retry sound, and is the connection still usable. Works on errors rehydrated from the worker
|
|
167
|
+
* or a follower hop, and on the platform's own `QuotaExceededError` and `AbortError`.
|
|
168
|
+
*/
|
|
169
|
+
export declare function classifyError(error: unknown): ErrorClassification;
|
|
111
170
|
export {};
|
package/dist/engine/errors.js
CHANGED
|
@@ -4,7 +4,7 @@ class TransactionExpiredError extends Error {
|
|
|
4
4
|
super("The SQL transaction expired and rolled back; issue ROLLBACK or BEGIN before continuing");
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
|
-
class DatabaseWorkerTimeoutError extends
|
|
7
|
+
class DatabaseWorkerTimeoutError extends ConnectionLostError {
|
|
8
8
|
method;
|
|
9
9
|
timeoutMs;
|
|
10
10
|
name = "DatabaseWorkerTimeoutError";
|
|
@@ -14,7 +14,25 @@ class DatabaseWorkerTimeoutError extends Error {
|
|
|
14
14
|
this.timeoutMs = timeoutMs;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
class
|
|
17
|
+
class DatabaseWorkerFailedError extends ConnectionLostError {
|
|
18
|
+
reason;
|
|
19
|
+
name = "DatabaseWorkerFailedError";
|
|
20
|
+
constructor(reason, message, options) {
|
|
21
|
+
super(message, options);
|
|
22
|
+
this.reason = reason;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
class DatabaseStoreUnavailableError extends Error {
|
|
26
|
+
store;
|
|
27
|
+
databaseName;
|
|
28
|
+
name = "DatabaseStoreUnavailableError";
|
|
29
|
+
constructor(store, databaseName, message) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.store = store;
|
|
32
|
+
this.databaseName = databaseName;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
class DatabaseWorkerOutcomeUnknownError extends UnknownOutcomeError {
|
|
18
36
|
method;
|
|
19
37
|
requestId;
|
|
20
38
|
name = "DatabaseWorkerOutcomeUnknownError";
|
|
@@ -138,11 +156,105 @@ class SqlCompileError extends TypeError {
|
|
|
138
156
|
}
|
|
139
157
|
}
|
|
140
158
|
import { dateIsoString } from "../date-value.js";
|
|
159
|
+
import { BlockReadBatchTooLargeError, CompactionBacklogError, CompactionJobConflictError, ConnectionLostError, GarbageCollectionJobConflictError, IndexedDbSchemaUpgradeBlockedError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, OpfsCoordinationError, OpfsDatabaseInUseError, PostingBuildConflictError, SchemaConflictError, SnapshotImportConflictError, SnapshotManifestMissingError, StorageCorruptionError, StorageFormatVersionError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueIndexCoverageError, UniqueKeyBuildConflictError, UniqueKeyConflictError, UnknownOutcomeError, WriteConflictError } from "../storage/types.js";
|
|
160
|
+
const CONFLICT_ERRORS = [
|
|
161
|
+
WriteConflictError,
|
|
162
|
+
SchemaConflictError,
|
|
163
|
+
TableRecordConflictError,
|
|
164
|
+
TransactionRecordConflictError,
|
|
165
|
+
LeaseConflictError,
|
|
166
|
+
LeaseOwnerConflictError,
|
|
167
|
+
CompactionJobConflictError,
|
|
168
|
+
GarbageCollectionJobConflictError,
|
|
169
|
+
TempOwnerConflictError,
|
|
170
|
+
UniqueKeyBuildConflictError,
|
|
171
|
+
PostingBuildConflictError,
|
|
172
|
+
SnapshotImportConflictError,
|
|
173
|
+
SnapshotManifestMissingError,
|
|
174
|
+
TableInUseError,
|
|
175
|
+
OpfsDatabaseInUseError
|
|
176
|
+
];
|
|
177
|
+
const TRANSIENT_ERRORS = [
|
|
178
|
+
OpfsCoordinationError,
|
|
179
|
+
DatabaseReadBacklogError,
|
|
180
|
+
MaintenanceBacklogError,
|
|
181
|
+
CompactionBacklogError,
|
|
182
|
+
LiveQueryLimitError,
|
|
183
|
+
LeaseExpiredError,
|
|
184
|
+
TransactionExpiredError,
|
|
185
|
+
IndexedDbSchemaUpgradeBlockedError,
|
|
186
|
+
VisibleSegmentCursorStaleError
|
|
187
|
+
];
|
|
188
|
+
const RESOURCE_ERRORS = [
|
|
189
|
+
StorageResourceLimitError,
|
|
190
|
+
BlockReadBatchTooLargeError,
|
|
191
|
+
CompactionMemoryBudgetError,
|
|
192
|
+
CompactionWriteAmplificationError
|
|
193
|
+
];
|
|
194
|
+
const REJECTED_ERRORS = [
|
|
195
|
+
UniqueConstraintError,
|
|
196
|
+
UniqueKeyConflictError,
|
|
197
|
+
UniqueIndexCoverageError,
|
|
198
|
+
MissingKeyError,
|
|
199
|
+
UnknownTableError,
|
|
200
|
+
SqlCompileError,
|
|
201
|
+
CompactionJobCancelledError
|
|
202
|
+
];
|
|
203
|
+
function isAnyOf(error, classes) {
|
|
204
|
+
return classes.some((constructor) => error instanceof constructor);
|
|
205
|
+
}
|
|
206
|
+
function hasErrorName(error, name) {
|
|
207
|
+
return typeof error === "object" && error !== null && error.name === name;
|
|
208
|
+
}
|
|
209
|
+
function classifyError(error) {
|
|
210
|
+
if (error instanceof UnknownOutcomeError) {
|
|
211
|
+
return {
|
|
212
|
+
kind: "unknown-outcome",
|
|
213
|
+
mayHavePublished: true,
|
|
214
|
+
retry: "after-reconcile",
|
|
215
|
+
connectionUsable: !(error.cause instanceof ConnectionLostError)
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (error instanceof ConnectionLostError) {
|
|
219
|
+
return {
|
|
220
|
+
kind: "connection-lost",
|
|
221
|
+
mayHavePublished: false,
|
|
222
|
+
retry: "never",
|
|
223
|
+
connectionUsable: false
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (error instanceof StorageCorruptionError || error instanceof StorageFormatVersionError) {
|
|
227
|
+
return { kind: "corruption", mayHavePublished: false, retry: "never", connectionUsable: true };
|
|
228
|
+
}
|
|
229
|
+
if (isAnyOf(error, CONFLICT_ERRORS)) {
|
|
230
|
+
return { kind: "conflict", mayHavePublished: false, retry: "safe", connectionUsable: true };
|
|
231
|
+
}
|
|
232
|
+
if (isAnyOf(error, TRANSIENT_ERRORS)) {
|
|
233
|
+
return { kind: "transient", mayHavePublished: false, retry: "safe", connectionUsable: true };
|
|
234
|
+
}
|
|
235
|
+
if (isAnyOf(error, RESOURCE_ERRORS) || hasErrorName(error, "QuotaExceededError")) {
|
|
236
|
+
return {
|
|
237
|
+
kind: "resource",
|
|
238
|
+
mayHavePublished: false,
|
|
239
|
+
retry: "after-reconcile",
|
|
240
|
+
connectionUsable: true
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (isAnyOf(error, REJECTED_ERRORS) || error instanceof TypeError || error instanceof RangeError || error instanceof SyntaxError) {
|
|
244
|
+
return { kind: "rejected", mayHavePublished: false, retry: "never", connectionUsable: true };
|
|
245
|
+
}
|
|
246
|
+
if (hasErrorName(error, "AbortError")) {
|
|
247
|
+
return { kind: "cancelled", mayHavePublished: false, retry: "safe", connectionUsable: true };
|
|
248
|
+
}
|
|
249
|
+
return { kind: "other", mayHavePublished: false, retry: "never", connectionUsable: true };
|
|
250
|
+
}
|
|
141
251
|
export {
|
|
142
252
|
CompactionJobCancelledError,
|
|
143
253
|
CompactionMemoryBudgetError,
|
|
144
254
|
CompactionWriteAmplificationError,
|
|
145
255
|
DatabaseReadBacklogError,
|
|
256
|
+
DatabaseStoreUnavailableError,
|
|
257
|
+
DatabaseWorkerFailedError,
|
|
146
258
|
DatabaseWorkerOutcomeUnknownError,
|
|
147
259
|
DatabaseWorkerTimeoutError,
|
|
148
260
|
LiveQueryLimitError,
|
|
@@ -152,5 +264,6 @@ export {
|
|
|
152
264
|
TransactionExpiredError,
|
|
153
265
|
UniqueConstraintError,
|
|
154
266
|
UnknownTableError,
|
|
155
|
-
VisibleSegmentCursorStaleError
|
|
267
|
+
VisibleSegmentCursorStaleError,
|
|
268
|
+
classifyError
|
|
156
269
|
};
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
export * from "./catalog.js";
|
|
6
6
|
export * from "./database.js";
|
|
7
7
|
export * from "./errors.js";
|
|
8
|
+
export { forgetStoreChoice } from "./auto-store.js";
|
|
8
9
|
export { QueryMemoryBudgetError, type QueryMemoryUsage } from "./memory.js";
|
|
9
10
|
export { MAX_SQL_NESTING_DEPTH, MAX_SQL_NUMERIC_DIGITS, MAX_SQL_PARAMETERS, MAX_SQL_PATTERN_CHARACTERS, MAX_SQL_PATTERN_MATCH_STEPS, MAX_SQL_SCALAR_RESULT_CHARACTERS, MAX_SQL_STRUCTURED_VALUE_DEPTH, MAX_SQL_STRUCTURED_VALUE_ITEMS, MAX_SQL_TEXT_CHARACTERS, MAX_SQL_TOKENS, } from "./cache-limits.js";
|
|
10
11
|
/**
|
package/dist/engine/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./catalog.js";
|
|
2
2
|
export * from "./database.js";
|
|
3
3
|
export * from "./errors.js";
|
|
4
|
+
import { forgetStoreChoice } from "./auto-store.js";
|
|
4
5
|
import { QueryMemoryBudgetError } from "./memory.js";
|
|
5
6
|
import { MAX_SQL_NESTING_DEPTH, MAX_SQL_NUMERIC_DIGITS, MAX_SQL_PARAMETERS, MAX_SQL_PATTERN_CHARACTERS, MAX_SQL_PATTERN_MATCH_STEPS, MAX_SQL_SCALAR_RESULT_CHARACTERS, MAX_SQL_STRUCTURED_VALUE_DEPTH, MAX_SQL_STRUCTURED_VALUE_ITEMS, MAX_SQL_TEXT_CHARACTERS, MAX_SQL_TOKENS } from "./cache-limits.js";
|
|
6
7
|
import { column, foreignKeyName, isDestructiveStep, planMigration, schema, table, view } from "./schema.js";
|
|
@@ -22,6 +23,7 @@ export {
|
|
|
22
23
|
QueryMemoryBudgetError,
|
|
23
24
|
column,
|
|
24
25
|
foreignKeyName,
|
|
26
|
+
forgetStoreChoice,
|
|
25
27
|
isDestructiveStep,
|
|
26
28
|
planMigration,
|
|
27
29
|
schema,
|
package/dist/engine/live.d.ts
CHANGED
|
@@ -125,6 +125,25 @@ export interface LiveQueryStats {
|
|
|
125
125
|
/** Work avoided because equal statements shared one query group or in-flight execution. */
|
|
126
126
|
sharedExecutions: number;
|
|
127
127
|
lastSweepMs: number;
|
|
128
|
+
/** One entry per distinct statement, with why it re-executes when it does. */
|
|
129
|
+
groups: readonly LiveQueryGroupStats[];
|
|
130
|
+
}
|
|
131
|
+
/** What one distinct live statement costs, and why. */
|
|
132
|
+
export interface LiveQueryGroupStats {
|
|
133
|
+
/** The statement's SQL; undefined for a compiled-plan envelope. */
|
|
134
|
+
readonly sql: string | undefined;
|
|
135
|
+
readonly subscribers: number;
|
|
136
|
+
/** Whether the engine maintains this statement incrementally; false before its first run too. */
|
|
137
|
+
readonly maintainable: boolean;
|
|
138
|
+
/** Why it re-executes on every relevant commit; empty when it is maintained. */
|
|
139
|
+
readonly reasons: readonly string[];
|
|
140
|
+
/** Full executions after the first. */
|
|
141
|
+
readonly reruns: number;
|
|
142
|
+
/** Deliveries answered by patching the retained result. */
|
|
143
|
+
readonly maintained: number;
|
|
144
|
+
/** Full executions of a maintainable statement that a particular commit could not be patched for. */
|
|
145
|
+
readonly fallbacks: number;
|
|
146
|
+
readonly retainedRows: number;
|
|
128
147
|
}
|
|
129
148
|
/** A live query: plain SQL, parameterized SQL, or a compiled-plan envelope. */
|
|
130
149
|
export type LiveQueryInput = string | {
|
|
@@ -157,6 +176,10 @@ export interface LiveMaintainedExecution {
|
|
|
157
176
|
/** Modeled total bytes retained by the result and opaque maintenance state. */
|
|
158
177
|
readonly retainedBytes?: number;
|
|
159
178
|
}
|
|
179
|
+
/** The host will not maintain this statement, and says why, in the terms a developer fixes. */
|
|
180
|
+
export interface LiveMaintenanceDeclined {
|
|
181
|
+
readonly declined: readonly string[];
|
|
182
|
+
}
|
|
160
183
|
export interface LiveMaintainedChange extends LiveMaintainedExecution {
|
|
161
184
|
readonly changed: boolean;
|
|
162
185
|
/** For each row of `result`, its index in the previous result, or -1; see `LiveQueryDelivery`. */
|
|
@@ -175,7 +198,7 @@ export interface LiveQueryHost {
|
|
|
175
198
|
* Executes a statement the host can later maintain incrementally, or returns undefined when
|
|
176
199
|
* the statement's shape rules that out; the set then executes it in full from then on.
|
|
177
200
|
*/
|
|
178
|
-
executeMaintainable?(query: LiveQueryInput, context?: LiveQueryExecuteContext): Promise<LiveMaintainedExecution | undefined>;
|
|
201
|
+
executeMaintainable?(query: LiveQueryInput, context?: LiveQueryExecuteContext): Promise<LiveMaintainedExecution | LiveMaintenanceDeclined | undefined>;
|
|
179
202
|
/**
|
|
180
203
|
* Patches a retained result with the row changes the commits in (after, until] made to
|
|
181
204
|
* `tableIds`, or returns undefined when only a full execution can answer.
|
package/dist/engine/live.js
CHANGED
|
@@ -111,7 +111,8 @@ class LiveQuerySet {
|
|
|
111
111
|
retainedRows: 0,
|
|
112
112
|
retainedBytes: 0,
|
|
113
113
|
sharedExecutions: 0,
|
|
114
|
-
lastSweepMs: 0
|
|
114
|
+
lastSweepMs: 0,
|
|
115
|
+
groups: []
|
|
115
116
|
};
|
|
116
117
|
#lastProbe;
|
|
117
118
|
#pendingProbe;
|
|
@@ -159,9 +160,22 @@ class LiveQuerySet {
|
|
|
159
160
|
}
|
|
160
161
|
get stats() {
|
|
161
162
|
let retainedRows = 0;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
const groups = [];
|
|
164
|
+
for (const group of this.#groups.values()) {
|
|
165
|
+
const rows = group.result?.rows.length ?? 0;
|
|
166
|
+
retainedRows += rows;
|
|
167
|
+
groups.push({
|
|
168
|
+
sql: typeof group.query === "string" ? group.query : group.query.kind === "sql-query" ? group.query.sql : void 0,
|
|
169
|
+
subscribers: group.subscribers.size,
|
|
170
|
+
maintainable: group.maintenance !== void 0,
|
|
171
|
+
reasons: group.reasons,
|
|
172
|
+
reruns: group.reruns,
|
|
173
|
+
maintained: group.maintained,
|
|
174
|
+
fallbacks: group.fallbacks,
|
|
175
|
+
retainedRows: rows
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return { ...this.#stats, retainedRows, retainedBytes: this.#retainedBytes, groups };
|
|
165
179
|
}
|
|
166
180
|
#freshProbe() {
|
|
167
181
|
let pending = this.#pendingProbe;
|
|
@@ -334,7 +348,11 @@ class LiveQuerySet {
|
|
|
334
348
|
execution: void 0,
|
|
335
349
|
deliveries: 0,
|
|
336
350
|
maintenance: void 0,
|
|
337
|
-
unmaintainable: false
|
|
351
|
+
unmaintainable: false,
|
|
352
|
+
reasons: [],
|
|
353
|
+
reruns: 0,
|
|
354
|
+
maintained: 0,
|
|
355
|
+
fallbacks: 0
|
|
338
356
|
};
|
|
339
357
|
this.#groups.set(key, group);
|
|
340
358
|
this.#indexGroup(group, dependencies);
|
|
@@ -399,9 +417,10 @@ class LiveQuerySet {
|
|
|
399
417
|
try {
|
|
400
418
|
if (this.#incremental && this.#host.executeMaintainable !== void 0 && !group.unmaintainable) {
|
|
401
419
|
const maintained = await this.#host.executeMaintainable(group.query, context);
|
|
402
|
-
if (maintained === void 0)
|
|
420
|
+
if (maintained === void 0 || "declined" in maintained) {
|
|
403
421
|
group.unmaintainable = true;
|
|
404
|
-
|
|
422
|
+
group.reasons = maintained?.declined ?? ["the host declined to maintain it"];
|
|
423
|
+
} else {
|
|
405
424
|
candidate = maintained;
|
|
406
425
|
executed = maintained.result;
|
|
407
426
|
maintenance = maintained.state;
|
|
@@ -665,6 +684,7 @@ class LiveQuerySet {
|
|
|
665
684
|
if (catalogChanged) {
|
|
666
685
|
group.maintenance = void 0;
|
|
667
686
|
group.unmaintainable = false;
|
|
687
|
+
group.reasons = [];
|
|
668
688
|
try {
|
|
669
689
|
await this.#refreshDependencies(group);
|
|
670
690
|
} catch (error) {
|
|
@@ -719,10 +739,14 @@ class LiveQuerySet {
|
|
|
719
739
|
if (!this.#stillOpen() || this.#groups.get(group.key) !== group)
|
|
720
740
|
return;
|
|
721
741
|
}
|
|
722
|
-
if (execution !== void 0)
|
|
742
|
+
if (execution !== void 0) {
|
|
723
743
|
this.#stats.maintained += 1;
|
|
724
|
-
|
|
744
|
+
group.maintained += 1;
|
|
745
|
+
} else {
|
|
725
746
|
this.#stats.reruns += 1;
|
|
747
|
+
group.reruns += 1;
|
|
748
|
+
if (group.maintenance !== void 0)
|
|
749
|
+
group.fallbacks += 1;
|
|
726
750
|
execution = await this.#executeGroup(group, {
|
|
727
751
|
probe: current,
|
|
728
752
|
memoize: groupMemoizes(group)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const DEFAULT_SCOPE_WRITE_SET_BUDGET_BYTES = 64 * 1024 * 1024;
|
|
2
|
+
const scopeWriteSetTestHooks = {
|
|
3
|
+
defaultBudgetBytes: DEFAULT_SCOPE_WRITE_SET_BUDGET_BYTES,
|
|
4
|
+
budgetBytes: DEFAULT_SCOPE_WRITE_SET_BUDGET_BYTES
|
|
5
|
+
};
|
|
6
|
+
function composeScopeEffect(base, next) {
|
|
7
|
+
switch (next.kind) {
|
|
8
|
+
case "delete":
|
|
9
|
+
return next;
|
|
10
|
+
case "insert":
|
|
11
|
+
return base?.kind === "delete" ? { kind: "upsert", values: next.values } : next;
|
|
12
|
+
case "upsert":
|
|
13
|
+
return base?.kind === "insert" ? { kind: "insert", values: next.values } : next;
|
|
14
|
+
case "update": {
|
|
15
|
+
if (base === void 0)
|
|
16
|
+
return next;
|
|
17
|
+
if (base.kind === "delete")
|
|
18
|
+
throw new Error("A deleted key cannot take an update");
|
|
19
|
+
const values = base.values.slice();
|
|
20
|
+
next.values.forEach((value, position) => {
|
|
21
|
+
if (value !== void 0)
|
|
22
|
+
values[position] = value;
|
|
23
|
+
});
|
|
24
|
+
return { kind: base.kind, values };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function netScopeEffect(entry) {
|
|
29
|
+
return entry.pending === void 0 ? entry.staged : composeScopeEffect(entry.staged, entry.pending);
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
DEFAULT_SCOPE_WRITE_SET_BUDGET_BYTES,
|
|
33
|
+
composeScopeEffect,
|
|
34
|
+
netScopeEffect,
|
|
35
|
+
scopeWriteSetTestHooks
|
|
36
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type RpcScope, type WorkerStoreFactory, type WorkerStoreOptions } from "./worker-server.js";
|
|
2
|
-
export {
|
|
2
|
+
export { forgetStoreChoice, openAutoStore, resolveAutoStoreKind, type AutoStoreKind, } from "./auto-store.js";
|
|
3
|
+
export { exposeDatabase, singleStoreFactory, MAX_WORKER_HANDLES_PER_CONNECTION, type DatabaseInitPayload, type ExposeDatabaseOptions, type OpenedStore, type OpenedStoreKind, type RpcScope, type StoreDescriptor, type WireDatabaseOptions, type WorkerStoreFactory, type WorkerStoreOptions, } from "./worker-server.js";
|
|
3
4
|
export interface AttachDatabaseWorkerOptions extends WorkerStoreOptions {
|
|
4
5
|
/**
|
|
5
6
|
* Replaces the stock store factory. `singleStoreFactory()` builds one that opens a single
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { attachWorkerHost } from "./worker-server.js";
|
|
2
|
+
import { forgetStoreChoice, openAutoStore, resolveAutoStoreKind } from "./auto-store.js";
|
|
2
3
|
import { exposeDatabase, singleStoreFactory, MAX_WORKER_HANDLES_PER_CONNECTION } from "./worker-server.js";
|
|
3
4
|
function attachDatabaseWorker(scope, options = {}) {
|
|
4
5
|
attachWorkerHost(scope, options.createStore ?? createStore, options);
|
|
5
6
|
}
|
|
6
7
|
async function createStore(descriptor, options) {
|
|
8
|
+
if (descriptor.kind === "auto") {
|
|
9
|
+
const { openAutoStore: openAutoStore2 } = await import("./auto-store.js");
|
|
10
|
+
return openAutoStore2(descriptor.name, async (kind) => {
|
|
11
|
+
const opened = await createStore(kind === "opfs" ? {
|
|
12
|
+
kind,
|
|
13
|
+
name: descriptor.name,
|
|
14
|
+
...descriptor.opfs?.durability === void 0 ? {} : { durability: descriptor.opfs.durability }
|
|
15
|
+
} : { kind, name: descriptor.name, ...descriptor.indexeddb }, options);
|
|
16
|
+
return opened;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
7
19
|
if (descriptor.kind === "memory") {
|
|
8
20
|
const { MemoryBlockStore } = await import("../storage/memory.js");
|
|
9
21
|
return new MemoryBlockStore();
|
|
@@ -12,7 +24,8 @@ async function createStore(descriptor, options) {
|
|
|
12
24
|
const { OpfsBlockStore } = await import("../storage/opfs/index.js");
|
|
13
25
|
return OpfsBlockStore.open({
|
|
14
26
|
name: descriptor.name,
|
|
15
|
-
...descriptor.durability === void 0 ? {} : { durability: descriptor.durability }
|
|
27
|
+
...descriptor.durability === void 0 ? {} : { durability: descriptor.durability },
|
|
28
|
+
...options.onDiagnostic === void 0 ? {} : { onDiagnostic: options.onDiagnostic }
|
|
16
29
|
});
|
|
17
30
|
}
|
|
18
31
|
const { IndexedDbBlockStore } = await import("../storage/indexeddb.js");
|
|
@@ -27,5 +40,8 @@ export {
|
|
|
27
40
|
MAX_WORKER_HANDLES_PER_CONNECTION,
|
|
28
41
|
attachDatabaseWorker,
|
|
29
42
|
exposeDatabase,
|
|
43
|
+
forgetStoreChoice,
|
|
44
|
+
openAutoStore,
|
|
45
|
+
resolveAutoStoreKind,
|
|
30
46
|
singleStoreFactory
|
|
31
47
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BlockStore } from "../storage/types.js";
|
|
2
|
+
import { type WorkerErrorKind } from "../worker-protocol/index.js";
|
|
2
3
|
import { MinnowDatabase, type MinnowDatabaseOptions } from "./database.js";
|
|
3
4
|
/**
|
|
4
5
|
* Worker-side host for the main-thread client. exposeDatabase() answers the client's RPC frames
|
|
@@ -24,12 +25,40 @@ export type StoreDescriptor = {
|
|
|
24
25
|
kind: "opfs";
|
|
25
26
|
name: string;
|
|
26
27
|
durability?: "relaxed" | "strict";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* OPFS where the worker can hold synchronous access handles, IndexedDB elsewhere. The choice
|
|
31
|
+
* is made once per name and remembered, so the database never reopens empty on the other
|
|
32
|
+
* store; `client.storeKind()` reports which one opened.
|
|
33
|
+
*/
|
|
34
|
+
| {
|
|
35
|
+
kind: "auto";
|
|
36
|
+
name: string;
|
|
37
|
+
opfs?: {
|
|
38
|
+
durability?: "relaxed" | "strict";
|
|
39
|
+
};
|
|
40
|
+
indexeddb?: {
|
|
41
|
+
durability?: IDBTransactionDurability;
|
|
42
|
+
uniqueKeyCacheBytes?: number;
|
|
43
|
+
};
|
|
27
44
|
};
|
|
45
|
+
/** The store kinds a worker can actually open; `auto` resolves to one of the durable two. */
|
|
46
|
+
export type OpenedStoreKind = "indexeddb" | "opfs" | "memory";
|
|
47
|
+
/** What a store factory may return when the descriptor did not name the kind it opened. */
|
|
48
|
+
export interface OpenedStore {
|
|
49
|
+
store: BlockStore;
|
|
50
|
+
kind: OpenedStoreKind;
|
|
51
|
+
}
|
|
28
52
|
/** The cloneable subset of MinnowDatabaseOptions; function-valued seams stay worker-side. */
|
|
29
53
|
export type WireDatabaseOptions = Pick<MinnowDatabaseOptions, "compression" | "targetBlockBytes" | "rowsPerBlock" | "maxCommitRetries" | "coordinateWrites" | "spillOwnerLeaseMs" | "transactionOwnerLeaseMs" | "transactionIdleTimeoutMs" | "bufferPoolBytes" | "executionMemoryBudgetBytes" | "autoCollectDebtLimitCommits" | "autoCollect" | "autoCompact">;
|
|
30
54
|
export interface DatabaseInitPayload {
|
|
31
55
|
store: StoreDescriptor;
|
|
32
56
|
options?: WireDatabaseOptions;
|
|
57
|
+
/**
|
|
58
|
+
* How often the worker reports on a call it is still working on; the client derives it
|
|
59
|
+
* from its own deadline so a short deadline still hears from a long call in time.
|
|
60
|
+
*/
|
|
61
|
+
keepaliveIntervalMs?: number;
|
|
33
62
|
}
|
|
34
63
|
/** The slice of DedicatedWorkerGlobalScope (or MessagePort) the host needs. */
|
|
35
64
|
export interface RpcScope {
|
|
@@ -42,6 +71,17 @@ export interface RpcScope {
|
|
|
42
71
|
transfer: ArrayBuffer[];
|
|
43
72
|
}): void;
|
|
44
73
|
}
|
|
74
|
+
/** Reports a worker failure that belongs to no request to the main thread. */
|
|
75
|
+
export type WorkerErrorReporter = (kind: WorkerErrorKind, error: unknown, context: string) => void;
|
|
76
|
+
/**
|
|
77
|
+
* The one place every failure that is nobody's call answer goes. It posts a diagnostic event
|
|
78
|
+
* frame the client turns into `onWorkerError` (or a `console.error` on the main thread), and on
|
|
79
|
+
* a real worker global it also catches what would otherwise be an uncaught exception, an
|
|
80
|
+
* unhandled rejection, or an unreadable inbound frame. Those three are marked handled so the
|
|
81
|
+
* browser does not log them a second time under the worker's own name; the main thread's copy
|
|
82
|
+
* carries the same message, stack, and cause.
|
|
83
|
+
*/
|
|
84
|
+
export declare function workerErrorReporter(scope: RpcScope): WorkerErrorReporter;
|
|
45
85
|
export interface ExposeDatabaseOptions {
|
|
46
86
|
/** Called when the client sends its dispose frame, after every handle has been closed. */
|
|
47
87
|
onDispose?: () => void | Promise<void>;
|
|
@@ -56,18 +96,28 @@ export interface ExposeDatabaseOptions {
|
|
|
56
96
|
* on the tab the user is looking at.
|
|
57
97
|
*/
|
|
58
98
|
onVisibility?: (visible: boolean) => void;
|
|
99
|
+
/** The kind of store behind the database, reported to the client when it connects. */
|
|
100
|
+
storeKind?: OpenedStoreKind;
|
|
101
|
+
/** Milliseconds between keepalive reports on a call in progress; defaults to five seconds. */
|
|
102
|
+
keepaliveIntervalMs?: number;
|
|
59
103
|
}
|
|
60
104
|
/** What a worker store factory may need beyond the descriptor itself. */
|
|
61
105
|
export interface WorkerStoreOptions {
|
|
62
106
|
/** Test seam for environments without a global IndexedDB (e.g. fake-indexeddb). */
|
|
63
107
|
indexedDB?: IDBFactory;
|
|
108
|
+
/**
|
|
109
|
+
* Hears a store's background failures — a failed checkpoint, a broken election — so they reach
|
|
110
|
+
* the main thread. The stock host wires it to the worker error reporter; a factory passes it
|
|
111
|
+
* to a store that accepts one (`OpfsBlockStoreOptions.onDiagnostic`).
|
|
112
|
+
*/
|
|
113
|
+
onDiagnostic?: (error: unknown, context: string) => void;
|
|
64
114
|
}
|
|
65
115
|
/**
|
|
66
116
|
* Builds the block store named by a client's init frame. The stock entry's factory loads the
|
|
67
117
|
* adapter for any of the three kinds on demand; a per-store entry supplies one that imports a
|
|
68
118
|
* single adapter statically and refuses the rest.
|
|
69
119
|
*/
|
|
70
|
-
export type WorkerStoreFactory = (descriptor: StoreDescriptor, options: WorkerStoreOptions) => BlockStore | Promise<BlockStore>;
|
|
120
|
+
export type WorkerStoreFactory = (descriptor: StoreDescriptor, options: WorkerStoreOptions) => BlockStore | OpenedStore | Promise<BlockStore | OpenedStore>;
|
|
71
121
|
/** Hard per-connection ceiling for resident worker resources abandoned by a client. */
|
|
72
122
|
export declare const MAX_WORKER_HANDLES_PER_CONNECTION = 256;
|
|
73
123
|
/**
|
|
@@ -91,3 +141,5 @@ export declare function attachWorkerHost(scope: RpcScope, createStore: WorkerSto
|
|
|
91
141
|
export declare function singleStoreFactory<Kind extends StoreDescriptor["kind"]>(kind: Kind, open: (descriptor: Extract<StoreDescriptor, {
|
|
92
142
|
kind: Kind;
|
|
93
143
|
}>, options: WorkerStoreOptions) => BlockStore | Promise<BlockStore>): WorkerStoreFactory;
|
|
144
|
+
/** The refusal a worker entry gives an init frame for a store it does not bundle. */
|
|
145
|
+
export declare function unsupportedStoreKindError(bundled: StoreDescriptor["kind"], requested: string): Error;
|