@framers/sql-storage-adapter 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/dist/adapters/electron/preload.cjs +289 -0
- package/dist/adapters/electron/renderer.d.ts +9 -0
- package/dist/adapters/electron/renderer.d.ts.map +1 -0
- package/dist/adapters/electron/renderer.js +8 -0
- package/dist/adapters/electron/renderer.js.map +1 -0
- package/dist/cjs/adapters/__tests__/indexedDbAdapter.spec.d.ts +2 -0
- package/dist/cjs/adapters/__tests__/indexedDbAdapter.spec.js +133 -0
- package/dist/cjs/adapters/baseStorageAdapter.d.ts +368 -0
- package/dist/cjs/adapters/baseStorageAdapter.js +744 -0
- package/dist/cjs/adapters/betterSqliteAdapter.d.ts +124 -0
- package/dist/cjs/adapters/betterSqliteAdapter.js +334 -0
- package/dist/cjs/adapters/capacitorSqliteAdapter.d.ts +83 -0
- package/dist/cjs/adapters/capacitorSqliteAdapter.js +250 -0
- package/dist/cjs/adapters/electron/electronMainAdapter.d.ts +251 -0
- package/dist/cjs/adapters/electron/electronMainAdapter.js +392 -0
- package/dist/cjs/adapters/electron/electronRendererAdapter.d.ts +177 -0
- package/dist/cjs/adapters/electron/electronRendererAdapter.js +283 -0
- package/dist/cjs/adapters/electron/index.d.ts +74 -0
- package/dist/cjs/adapters/electron/index.js +86 -0
- package/dist/cjs/adapters/electron/ipc/channels.d.ts +196 -0
- package/dist/cjs/adapters/electron/ipc/channels.js +127 -0
- package/dist/cjs/adapters/electron/ipc/index.d.ts +11 -0
- package/dist/cjs/adapters/electron/ipc/index.js +38 -0
- package/dist/cjs/adapters/electron/ipc/protocol.d.ts +78 -0
- package/dist/cjs/adapters/electron/ipc/protocol.js +341 -0
- package/dist/cjs/adapters/electron/ipc/types.d.ts +248 -0
- package/dist/cjs/adapters/electron/ipc/types.js +15 -0
- package/dist/cjs/adapters/electron/migration/autoMigrator.d.ts +184 -0
- package/dist/cjs/adapters/electron/migration/autoMigrator.js +441 -0
- package/dist/cjs/adapters/electron/migration/index.d.ts +9 -0
- package/dist/cjs/adapters/electron/migration/index.js +29 -0
- package/dist/cjs/adapters/electron/preload.d.ts +126 -0
- package/dist/cjs/adapters/electron/preload.js +233 -0
- package/dist/cjs/adapters/electron/recovery/corruptionDetector.d.ts +214 -0
- package/dist/cjs/adapters/electron/recovery/corruptionDetector.js +359 -0
- package/dist/cjs/adapters/electron/recovery/index.d.ts +11 -0
- package/dist/cjs/adapters/electron/recovery/index.js +34 -0
- package/dist/cjs/adapters/electron/recovery/walCheckpoint.d.ts +186 -0
- package/dist/cjs/adapters/electron/recovery/walCheckpoint.js +263 -0
- package/dist/cjs/adapters/electron/renderer.d.ts +9 -0
- package/dist/cjs/adapters/electron/renderer.js +33 -0
- package/dist/cjs/adapters/electron/window/index.d.ts +9 -0
- package/dist/cjs/adapters/electron/window/index.js +29 -0
- package/dist/cjs/adapters/electron/window/windowManager.d.ts +190 -0
- package/dist/cjs/adapters/electron/window/windowManager.js +321 -0
- package/dist/cjs/adapters/indexedDbAdapter.d.ts +490 -0
- package/dist/cjs/adapters/indexedDbAdapter.js +993 -0
- package/dist/cjs/adapters/postgresAdapter.d.ts +424 -0
- package/dist/cjs/adapters/postgresAdapter.js +889 -0
- package/dist/cjs/adapters/sqlJsAdapter.d.ts +64 -0
- package/dist/cjs/adapters/sqlJsAdapter.js +268 -0
- package/dist/cjs/adapters/supabase.d.ts +58 -0
- package/dist/cjs/adapters/supabase.js +401 -0
- package/dist/cjs/codecs/BrowserBlobCodec.d.ts +14 -0
- package/dist/cjs/codecs/BrowserBlobCodec.js +57 -0
- package/dist/cjs/codecs/NodeBlobCodec.d.ts +14 -0
- package/dist/cjs/codecs/NodeBlobCodec.js +59 -0
- package/dist/cjs/core/contracts/blobCodec.d.ts +15 -0
- package/dist/cjs/core/contracts/blobCodec.js +15 -0
- package/dist/cjs/core/contracts/context.d.ts +221 -0
- package/dist/cjs/core/contracts/context.js +15 -0
- package/dist/cjs/core/contracts/dialect.d.ts +154 -0
- package/dist/cjs/core/contracts/dialect.js +15 -0
- package/dist/cjs/core/contracts/events.d.ts +225 -0
- package/dist/cjs/core/contracts/events.js +15 -0
- package/dist/cjs/core/contracts/exporter.d.ts +13 -0
- package/dist/cjs/core/contracts/exporter.js +15 -0
- package/dist/cjs/core/contracts/extensions.d.ts +73 -0
- package/dist/cjs/core/contracts/extensions.js +15 -0
- package/dist/cjs/core/contracts/features.d.ts +25 -0
- package/dist/cjs/core/contracts/features.js +61 -0
- package/dist/cjs/core/contracts/fts.d.ts +66 -0
- package/dist/cjs/core/contracts/fts.js +15 -0
- package/dist/cjs/core/contracts/hooks.d.ts +481 -0
- package/dist/cjs/core/contracts/hooks.js +140 -0
- package/dist/cjs/core/contracts/index.d.ts +260 -0
- package/dist/cjs/core/contracts/index.js +41 -0
- package/dist/cjs/core/contracts/limitations.d.ts +46 -0
- package/dist/cjs/core/contracts/limitations.js +218 -0
- package/dist/cjs/core/contracts/performance.d.ts +308 -0
- package/dist/cjs/core/contracts/performance.js +144 -0
- package/dist/cjs/core/database.d.ts +220 -0
- package/dist/cjs/core/database.js +136 -0
- package/dist/cjs/core/portable.d.ts +23 -0
- package/dist/cjs/core/portable.js +66 -0
- package/dist/cjs/core/resolver.d.ts +77 -0
- package/dist/cjs/core/resolver.js +235 -0
- package/dist/cjs/dialects/PostgresDialect.d.ts +35 -0
- package/dist/cjs/dialects/PostgresDialect.js +143 -0
- package/dist/cjs/dialects/SqliteDialect.d.ts +35 -0
- package/dist/cjs/dialects/SqliteDialect.js +142 -0
- package/dist/cjs/dialects/shared.d.ts +17 -0
- package/dist/cjs/dialects/shared.js +90 -0
- package/dist/cjs/exporters/PostgresExporter.d.ts +14 -0
- package/dist/cjs/exporters/PostgresExporter.js +61 -0
- package/dist/cjs/exporters/SqliteFileExporter.d.ts +16 -0
- package/dist/cjs/exporters/SqliteFileExporter.js +43 -0
- package/dist/cjs/features/backup/cloudBackup.d.ts +219 -0
- package/dist/cjs/features/backup/cloudBackup.js +209 -0
- package/dist/cjs/features/kv/idb.d.ts +79 -0
- package/dist/cjs/features/kv/idb.js +32 -0
- package/dist/cjs/features/kv/index.d.ts +14 -0
- package/dist/cjs/features/kv/index.js +40 -0
- package/dist/cjs/features/kv/indexedDbKeyValueStore.d.ts +77 -0
- package/dist/cjs/features/kv/indexedDbKeyValueStore.js +116 -0
- package/dist/cjs/features/kv/keyValueStore.d.ts +48 -0
- package/dist/cjs/features/kv/keyValueStore.js +67 -0
- package/dist/cjs/features/kv/sqlKeyValueStore.d.ts +34 -0
- package/dist/cjs/features/kv/sqlKeyValueStore.js +81 -0
- package/dist/cjs/features/kv/valueCodec.d.ts +35 -0
- package/dist/cjs/features/kv/valueCodec.js +426 -0
- package/dist/cjs/features/migrations/dataExport.d.ts +77 -0
- package/dist/cjs/features/migrations/dataExport.js +194 -0
- package/dist/cjs/features/migrations/dataImport.d.ts +54 -0
- package/dist/cjs/features/migrations/dataImport.js +300 -0
- package/dist/cjs/features/migrations/migration.d.ts +89 -0
- package/dist/cjs/features/migrations/migration.js +187 -0
- package/dist/cjs/features/migrations/schemaMigrations.d.ts +93 -0
- package/dist/cjs/features/migrations/schemaMigrations.js +220 -0
- package/dist/cjs/features/sync/conflicts/conflictResolver.d.ts +222 -0
- package/dist/cjs/features/sync/conflicts/conflictResolver.js +366 -0
- package/dist/cjs/features/sync/conflicts/index.d.ts +9 -0
- package/dist/cjs/features/sync/conflicts/index.js +21 -0
- package/dist/cjs/features/sync/crossPlatformSync.d.ts +281 -0
- package/dist/cjs/features/sync/crossPlatformSync.js +551 -0
- package/dist/cjs/features/sync/devices/deviceManager.d.ts +243 -0
- package/dist/cjs/features/sync/devices/deviceManager.js +450 -0
- package/dist/cjs/features/sync/devices/index.d.ts +10 -0
- package/dist/cjs/features/sync/devices/index.js +21 -0
- package/dist/cjs/features/sync/index.d.ts +37 -0
- package/dist/cjs/features/sync/index.js +111 -0
- package/dist/cjs/features/sync/protocol/index.d.ts +11 -0
- package/dist/cjs/features/sync/protocol/index.js +23 -0
- package/dist/cjs/features/sync/protocol/messages.d.ts +348 -0
- package/dist/cjs/features/sync/protocol/messages.js +215 -0
- package/dist/cjs/features/sync/protocol/vectorClock.d.ts +164 -0
- package/dist/cjs/features/sync/protocol/vectorClock.js +256 -0
- package/dist/cjs/features/sync/syncManager.d.ts +348 -0
- package/dist/cjs/features/sync/syncManager.js +455 -0
- package/dist/cjs/features/sync/tables/index.d.ts +10 -0
- package/dist/cjs/features/sync/tables/index.js +21 -0
- package/dist/cjs/features/sync/tables/syncLogManager.d.ts +216 -0
- package/dist/cjs/features/sync/tables/syncLogManager.js +423 -0
- package/dist/cjs/features/sync/transport/httpTransport.d.ts +123 -0
- package/dist/cjs/features/sync/transport/httpTransport.js +346 -0
- package/dist/cjs/features/sync/transport/index.d.ts +12 -0
- package/dist/cjs/features/sync/transport/index.js +25 -0
- package/dist/cjs/features/sync/transport/transport.d.ts +259 -0
- package/dist/cjs/features/sync/transport/transport.js +163 -0
- package/dist/cjs/features/sync/transport/websocketTransport.d.ts +126 -0
- package/dist/cjs/features/sync/transport/websocketTransport.js +342 -0
- package/dist/cjs/fts/PostgresFts.d.ts +29 -0
- package/dist/cjs/fts/PostgresFts.js +77 -0
- package/dist/cjs/fts/SqliteFts5.d.ts +22 -0
- package/dist/cjs/fts/SqliteFts5.js +58 -0
- package/dist/cjs/index.d.ts +30 -0
- package/dist/cjs/index.js +101 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/shared/connectionGate.d.ts +94 -0
- package/dist/cjs/shared/connectionGate.js +77 -0
- package/dist/cjs/shared/errors.d.ts +23 -0
- package/dist/cjs/shared/errors.js +45 -0
- package/dist/cjs/shared/logger.d.ts +26 -0
- package/dist/cjs/shared/logger.js +40 -0
- package/dist/cjs/shared/parameterUtils.d.ts +16 -0
- package/dist/cjs/shared/parameterUtils.js +59 -0
- package/dist/cjs/shared/postgresTarget.d.ts +11 -0
- package/dist/cjs/shared/postgresTarget.js +30 -0
- package/dist/cjs/shared/segment.d.ts +43 -0
- package/dist/cjs/shared/segment.js +63 -0
- package/dist/cjs/shared/serializedTransaction.d.ts +14 -0
- package/dist/cjs/shared/serializedTransaction.js +74 -0
- package/dist/cjs/shared/splitSqlStatements.d.ts +27 -0
- package/dist/cjs/shared/splitSqlStatements.js +163 -0
- package/dist/cjs/shared/sqlPlaceholders.d.ts +48 -0
- package/dist/cjs/shared/sqlPlaceholders.js +263 -0
- package/dist/cjs/types/index.d.ts +29 -0
- package/dist/cjs/types/index.js +33 -0
- package/dist/core/portable.d.ts +23 -0
- package/dist/core/portable.d.ts.map +1 -0
- package/dist/core/portable.js +23 -0
- package/dist/core/portable.js.map +1 -0
- package/dist/features/migrations/schemaMigrations.d.ts +93 -0
- package/dist/features/migrations/schemaMigrations.d.ts.map +1 -0
- package/dist/features/migrations/schemaMigrations.js +216 -0
- package/dist/features/migrations/schemaMigrations.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +139 -24
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
var indexedDbAdapter_exports = {};
|
|
29
|
+
__export(indexedDbAdapter_exports, {
|
|
30
|
+
IndexedDbAdapter: () => IndexedDbAdapter,
|
|
31
|
+
IndexedDbConflictError: () => IndexedDbConflictError
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(indexedDbAdapter_exports);
|
|
34
|
+
var import_sql = __toESM(require("sql.js"));
|
|
35
|
+
var import_parameterUtils = require("../shared/parameterUtils.js");
|
|
36
|
+
var import_sqlPlaceholders = require("../shared/sqlPlaceholders.js");
|
|
37
|
+
var import_serializedTransaction = require("../shared/serializedTransaction.js");
|
|
38
|
+
var import_connectionGate = require("../shared/connectionGate.js");
|
|
39
|
+
var import_segment = require("../shared/segment.js");
|
|
40
|
+
const LONG_WAIT_MS = 1e4;
|
|
41
|
+
const IMAGE_KEY = "db";
|
|
42
|
+
const STAMP_KEY = "db:stamp";
|
|
43
|
+
const MAX_CONFLICT_RETRIES = 50;
|
|
44
|
+
const CONNECTION_PRAGMAS = [
|
|
45
|
+
"foreign_keys",
|
|
46
|
+
"recursive_triggers",
|
|
47
|
+
"automatic_index",
|
|
48
|
+
"cache_size",
|
|
49
|
+
"cache_spill",
|
|
50
|
+
"cell_size_check",
|
|
51
|
+
"ignore_check_constraints",
|
|
52
|
+
"legacy_alter_table",
|
|
53
|
+
"query_only",
|
|
54
|
+
"reverse_unordered_selects",
|
|
55
|
+
"secure_delete",
|
|
56
|
+
"synchronous",
|
|
57
|
+
"temp_store",
|
|
58
|
+
"trusted_schema",
|
|
59
|
+
"busy_timeout"
|
|
60
|
+
];
|
|
61
|
+
let globalSQL = null;
|
|
62
|
+
let globalSQLPromise = null;
|
|
63
|
+
async function getGlobalSQL(config) {
|
|
64
|
+
if (globalSQL) {
|
|
65
|
+
return globalSQL;
|
|
66
|
+
}
|
|
67
|
+
if (globalSQLPromise) {
|
|
68
|
+
return globalSQLPromise;
|
|
69
|
+
}
|
|
70
|
+
globalSQLPromise = (0, import_sql.default)(config).then((sql) => {
|
|
71
|
+
globalSQL = sql;
|
|
72
|
+
return sql;
|
|
73
|
+
});
|
|
74
|
+
return globalSQLPromise;
|
|
75
|
+
}
|
|
76
|
+
class IndexedDbConflictError extends Error {
|
|
77
|
+
constructor(message) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = "IndexedDbConflictError";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const DB_VERSION = 1;
|
|
83
|
+
const firstKeyword = (statement) => {
|
|
84
|
+
const rest = statement.replace(/^(?:\s+|--[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)*/, "");
|
|
85
|
+
return (/^[A-Za-z]+/.exec(rest)?.[0] ?? "").toUpperCase();
|
|
86
|
+
};
|
|
87
|
+
class IndexedDbAdapter {
|
|
88
|
+
/** @throws {Error} When `options.segment` is not a name a segment can take. */
|
|
89
|
+
constructor(options = {}) {
|
|
90
|
+
this.kind = "indexeddb";
|
|
91
|
+
this.capabilities = /* @__PURE__ */ new Set(["transactions", "persistence", "json", "prepared"]);
|
|
92
|
+
this.SQL = null;
|
|
93
|
+
this.db = null;
|
|
94
|
+
this.idb = null;
|
|
95
|
+
this.saveTimer = null;
|
|
96
|
+
this.writeGeneration = 0;
|
|
97
|
+
this.savedGeneration = 0;
|
|
98
|
+
this.saving = null;
|
|
99
|
+
this.savingGeneration = 0;
|
|
100
|
+
this.saveFailureReported = false;
|
|
101
|
+
this.opening = null;
|
|
102
|
+
this.closing = false;
|
|
103
|
+
this.closeRun = null;
|
|
104
|
+
this.longWaitReported = false;
|
|
105
|
+
this.lockWaitReported = false;
|
|
106
|
+
this.loadedStamp = null;
|
|
107
|
+
this.stampCount = 0;
|
|
108
|
+
this.pragmaDefaults = /* @__PURE__ */ new Map();
|
|
109
|
+
this.locks = void 0;
|
|
110
|
+
this.runCoordinatedInTurn = (0, import_serializedTransaction.createSerializedTransactionRunner)();
|
|
111
|
+
this.runSerializedTransaction = (0, import_serializedTransaction.createSerializedTransactionRunner)();
|
|
112
|
+
this.gate = (0, import_connectionGate.createConnectionGate)(null, { withoutContext: "wait" });
|
|
113
|
+
this.longWait = {
|
|
114
|
+
afterMs: LONG_WAIT_MS,
|
|
115
|
+
onLongWait: () => this.reportLongWait()
|
|
116
|
+
};
|
|
117
|
+
this.dbName = (0, import_segment.segmentName)(options.dbName || "app-db", options.segment);
|
|
118
|
+
this.storeName = options.storeName || "sqliteDb";
|
|
119
|
+
this.autoSave = options.autoSave ?? true;
|
|
120
|
+
this.saveIntervalMs = options.saveIntervalMs || 5e3;
|
|
121
|
+
this.sqlJsConfig = options.sqlJsConfig || {};
|
|
122
|
+
this.crossTab = options.crossTab === true ? {} : typeof options.crossTab === "object" && options.crossTab !== null ? options.crossTab : null;
|
|
123
|
+
this.durability = this.crossTab ? "commit" : options.durability ?? "interval";
|
|
124
|
+
}
|
|
125
|
+
/** The name of the Web Lock that coordinates the tabs on this database. */
|
|
126
|
+
get lockName() {
|
|
127
|
+
return `@framers/sql-storage-adapter:indexeddb:${this.dbName}:${this.storeName}`;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Opens IndexedDB and initializes sql.js database.
|
|
131
|
+
* Loads existing database from IndexedDB if present.
|
|
132
|
+
*
|
|
133
|
+
* Uses a global sql.js singleton to prevent race conditions when
|
|
134
|
+
* multiple adapters (e.g., fabric_codex_db and quarry-codex-local)
|
|
135
|
+
* are initialized simultaneously. Two calls at once open once.
|
|
136
|
+
*/
|
|
137
|
+
open(options) {
|
|
138
|
+
if (this.db) {
|
|
139
|
+
return Promise.resolve();
|
|
140
|
+
}
|
|
141
|
+
if (!this.opening) {
|
|
142
|
+
this.foreignKeys = options?.foreignKeys;
|
|
143
|
+
this.opening = this.openNow().finally(() => {
|
|
144
|
+
this.opening = null;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return this.opening;
|
|
148
|
+
}
|
|
149
|
+
async openNow() {
|
|
150
|
+
this.SQL = await getGlobalSQL(this.sqlJsConfig);
|
|
151
|
+
this.idb = await this.openIndexedDbWithRetry();
|
|
152
|
+
const stored = await this.loadFromIndexedDb();
|
|
153
|
+
const db = stored.image ? new this.SQL.Database(stored.image) : new this.SQL.Database();
|
|
154
|
+
this.pragmaDefaults = readPragmas(db);
|
|
155
|
+
this.db = db;
|
|
156
|
+
this.loadedStamp = stored.stamp;
|
|
157
|
+
this.applyConnectionSettings();
|
|
158
|
+
if (this.crossTab) {
|
|
159
|
+
this.locks = this.resolveLocks(this.crossTab);
|
|
160
|
+
} else if (this.autoSave) {
|
|
161
|
+
this.startAutoSave();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** The lock manager crossTab uses, or null for the version stamp alone. */
|
|
165
|
+
resolveLocks(options) {
|
|
166
|
+
if (options.locks !== void 0) {
|
|
167
|
+
return options.locks;
|
|
168
|
+
}
|
|
169
|
+
const scope = globalThis;
|
|
170
|
+
const locks = scope.navigator?.locks;
|
|
171
|
+
if (locks && typeof locks.request === "function") {
|
|
172
|
+
return locks;
|
|
173
|
+
}
|
|
174
|
+
console.warn(`[IndexedDbAdapter] Web Locks are not available here, so "${this.dbName}" coordinates tabs by its version stamp alone: a statement another tab saved over runs again, and a transaction() another tab saved over fails with IndexedDbConflictError.`);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Executes a SQL statement that doesn't return rows (INSERT, UPDATE, DELETE, CREATE, etc.).
|
|
179
|
+
* @param statement SQL statement with ? or :name placeholders
|
|
180
|
+
* @param parameters Positional array or named object
|
|
181
|
+
* @returns Result with changes count and last insert row ID
|
|
182
|
+
*/
|
|
183
|
+
async run(statement, parameters) {
|
|
184
|
+
if (this.crossTab) {
|
|
185
|
+
return this.coordinatedStatement(() => this.runNow(statement, parameters));
|
|
186
|
+
}
|
|
187
|
+
return (0, import_connectionGate.whenAdmitted)(this.gate, () => this.stored(this.runNow(statement, parameters)), void 0, this.longWait);
|
|
188
|
+
}
|
|
189
|
+
runNow(statement, parameters) {
|
|
190
|
+
({ statement, parameters } = (0, import_sqlPlaceholders.toSqliteStatement)(statement, parameters));
|
|
191
|
+
this.ensureOpen();
|
|
192
|
+
const stmt = this.db.prepare(statement);
|
|
193
|
+
try {
|
|
194
|
+
const { named, positional } = (0, import_parameterUtils.normaliseParameters)(parameters);
|
|
195
|
+
if (named) {
|
|
196
|
+
stmt.bind((0, import_parameterUtils.expandNamedParameters)(named));
|
|
197
|
+
} else if (positional) {
|
|
198
|
+
stmt.bind(positional);
|
|
199
|
+
}
|
|
200
|
+
stmt.step();
|
|
201
|
+
const rowIdResult = this.db.exec("SELECT last_insert_rowid() AS id");
|
|
202
|
+
const rawRowId = rowIdResult[0]?.values?.[0]?.[0];
|
|
203
|
+
const lastInsertRowid = this.normaliseRowId(rawRowId);
|
|
204
|
+
this.writeGeneration += 1;
|
|
205
|
+
return {
|
|
206
|
+
changes: this.db.getRowsModified(),
|
|
207
|
+
lastInsertRowid
|
|
208
|
+
};
|
|
209
|
+
} finally {
|
|
210
|
+
stmt.free();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Executes a SQL query and returns the first row.
|
|
215
|
+
* @param statement SELECT statement
|
|
216
|
+
* @param parameters Query parameters
|
|
217
|
+
* @returns First row as object or null if no results
|
|
218
|
+
*/
|
|
219
|
+
async get(statement, parameters) {
|
|
220
|
+
const rows = await this.all(statement, parameters);
|
|
221
|
+
return rows.length > 0 ? rows[0] : null;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Executes a SQL query and returns all matching rows. A statement that
|
|
225
|
+
* writes (`INSERT ... RETURNING`) is saved like a `run()`.
|
|
226
|
+
* @param statement SELECT statement
|
|
227
|
+
* @param parameters Query parameters
|
|
228
|
+
* @returns Array of result rows as objects
|
|
229
|
+
*/
|
|
230
|
+
async all(statement, parameters) {
|
|
231
|
+
if (this.crossTab) {
|
|
232
|
+
return this.coordinatedStatement(() => this.allNow(statement, parameters));
|
|
233
|
+
}
|
|
234
|
+
return (0, import_connectionGate.whenAdmitted)(this.gate, () => {
|
|
235
|
+
const before = this.writeGeneration;
|
|
236
|
+
const rows = this.allNow(statement, parameters);
|
|
237
|
+
return this.writeGeneration === before ? rows : this.stored(rows);
|
|
238
|
+
}, void 0, this.longWait);
|
|
239
|
+
}
|
|
240
|
+
allNow(statement, parameters) {
|
|
241
|
+
({ statement, parameters } = (0, import_sqlPlaceholders.toSqliteStatement)(statement, parameters));
|
|
242
|
+
this.ensureOpen();
|
|
243
|
+
const keyword = firstKeyword(statement);
|
|
244
|
+
const reads = keyword === "SELECT" || keyword === "EXPLAIN" || keyword === "VALUES";
|
|
245
|
+
const mayWrite = keyword === "WITH" || keyword === "PRAGMA";
|
|
246
|
+
const changesBefore = mayWrite ? this.totalChanges() : 0;
|
|
247
|
+
const stmt = this.db.prepare(statement);
|
|
248
|
+
try {
|
|
249
|
+
const { named, positional } = (0, import_parameterUtils.normaliseParameters)(parameters);
|
|
250
|
+
if (named) {
|
|
251
|
+
stmt.bind((0, import_parameterUtils.expandNamedParameters)(named));
|
|
252
|
+
} else if (positional) {
|
|
253
|
+
stmt.bind(positional);
|
|
254
|
+
}
|
|
255
|
+
const results = [];
|
|
256
|
+
const columnNames = stmt.getColumnNames();
|
|
257
|
+
while (stmt.step()) {
|
|
258
|
+
const row = stmt.get();
|
|
259
|
+
const obj = {};
|
|
260
|
+
columnNames.forEach((col, idx) => {
|
|
261
|
+
obj[col] = row[idx];
|
|
262
|
+
});
|
|
263
|
+
results.push(obj);
|
|
264
|
+
}
|
|
265
|
+
if (!reads && (!mayWrite || this.totalChanges() !== changesBefore)) {
|
|
266
|
+
this.writeGeneration += 1;
|
|
267
|
+
}
|
|
268
|
+
return results;
|
|
269
|
+
} finally {
|
|
270
|
+
stmt.free();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/** The rows changed on this connection since it opened. */
|
|
274
|
+
totalChanges() {
|
|
275
|
+
return Number(this.db.exec("SELECT total_changes()")[0]?.values?.[0]?.[0] ?? 0);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Executes a script containing multiple SQL statements.
|
|
279
|
+
*/
|
|
280
|
+
async exec(script) {
|
|
281
|
+
if (this.crossTab) {
|
|
282
|
+
return this.coordinatedStatement(() => this.execNow(script));
|
|
283
|
+
}
|
|
284
|
+
return (0, import_connectionGate.whenAdmitted)(this.gate, () => this.stored(this.execNow(script)), void 0, this.longWait);
|
|
285
|
+
}
|
|
286
|
+
execNow(script) {
|
|
287
|
+
this.ensureOpen();
|
|
288
|
+
try {
|
|
289
|
+
this.db.exec(script);
|
|
290
|
+
} finally {
|
|
291
|
+
this.writeGeneration += 1;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* With `durability: 'commit'`, resolves with `result` once the write that
|
|
296
|
+
* produced it is stored; otherwise with `result` at once.
|
|
297
|
+
*/
|
|
298
|
+
stored(result) {
|
|
299
|
+
if (this.durability !== "commit") {
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
return this.saveThrough(this.writeGeneration).then(() => result);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Executes a callback within a database transaction.
|
|
306
|
+
*
|
|
307
|
+
* Callbacks run one at a time, since every transaction shares the one
|
|
308
|
+
* connection. The callback's statements go through the handle it receives;
|
|
309
|
+
* a statement issued through the adapter itself while the callback runs
|
|
310
|
+
* waits until the transaction ends, so a callback must not await one. The
|
|
311
|
+
* auto-save tick does not save while a transaction is open. With
|
|
312
|
+
* `durability: 'commit'` the transaction resolves once it is stored.
|
|
313
|
+
*/
|
|
314
|
+
async transaction(fn) {
|
|
315
|
+
this.ensureOpen();
|
|
316
|
+
if (this.crossTab) {
|
|
317
|
+
return this.coordinatedTransaction(fn);
|
|
318
|
+
}
|
|
319
|
+
const queued = setTimeout(() => this.reportLongWait(), LONG_WAIT_MS);
|
|
320
|
+
return this.runSerializedTransaction(async () => {
|
|
321
|
+
clearTimeout(queued);
|
|
322
|
+
const result = await this.transactionNow(fn);
|
|
323
|
+
if (this.durability === "commit") {
|
|
324
|
+
await this.saveThrough(this.writeGeneration);
|
|
325
|
+
}
|
|
326
|
+
return result;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
/** Runs one transaction: BEGIN, the callback through its handle, COMMIT or ROLLBACK. */
|
|
330
|
+
transactionNow(fn) {
|
|
331
|
+
return this.gate.hold(async (token) => {
|
|
332
|
+
this.ensureOpen();
|
|
333
|
+
this.db.run("BEGIN TRANSACTION");
|
|
334
|
+
const transactionAdapter = (0, import_serializedTransaction.createSharedConnectionTransactionAdapter)(this.handleStatements(token));
|
|
335
|
+
try {
|
|
336
|
+
const result = await fn(transactionAdapter);
|
|
337
|
+
this.ensureOpen();
|
|
338
|
+
this.db.run("COMMIT");
|
|
339
|
+
this.writeGeneration += 1;
|
|
340
|
+
return result;
|
|
341
|
+
} catch (error) {
|
|
342
|
+
this.rollbackQuietly();
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* The statements of one transaction's handle: they carry the transaction's
|
|
349
|
+
* token, so the gate admits them while the transaction is open.
|
|
350
|
+
*/
|
|
351
|
+
handleStatements(token) {
|
|
352
|
+
return {
|
|
353
|
+
kind: this.kind,
|
|
354
|
+
capabilities: this.capabilities,
|
|
355
|
+
open: async () => {
|
|
356
|
+
},
|
|
357
|
+
close: async () => {
|
|
358
|
+
},
|
|
359
|
+
run: (statement, parameters) => (0, import_connectionGate.whenAdmitted)(this.gate, () => this.runNow(statement, parameters), token),
|
|
360
|
+
get: async (statement, parameters) => {
|
|
361
|
+
const rows = await (0, import_connectionGate.whenAdmitted)(this.gate, () => this.allNow(statement, parameters), token);
|
|
362
|
+
return rows.length > 0 ? rows[0] : null;
|
|
363
|
+
},
|
|
364
|
+
all: (statement, parameters) => (0, import_connectionGate.whenAdmitted)(this.gate, () => this.allNow(statement, parameters), token),
|
|
365
|
+
exec: (script) => (0, import_connectionGate.whenAdmitted)(this.gate, () => this.execNow(script), token),
|
|
366
|
+
// The handle answers a nested transaction() itself and never calls this.
|
|
367
|
+
transaction: (fn) => fn(this)
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/** Rolls back the open transaction; one that already ended (a failed statement, close()) is left alone. */
|
|
371
|
+
rollbackQuietly() {
|
|
372
|
+
if (!this.db)
|
|
373
|
+
return;
|
|
374
|
+
try {
|
|
375
|
+
this.db.run("ROLLBACK");
|
|
376
|
+
} catch {
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Begins a transaction the caller ends with commit() or rollback(). It is not
|
|
381
|
+
* the callback form: other callers' statements are not held out of it. With
|
|
382
|
+
* `crossTab` it is refused: use transaction().
|
|
383
|
+
*/
|
|
384
|
+
async beginTransaction() {
|
|
385
|
+
await this.run("BEGIN");
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Commits the current transaction.
|
|
389
|
+
*/
|
|
390
|
+
async commit() {
|
|
391
|
+
await this.run("COMMIT");
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Rolls back the current transaction.
|
|
395
|
+
*/
|
|
396
|
+
async rollback() {
|
|
397
|
+
await this.run("ROLLBACK");
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Stores every write made so far in IndexedDB. A `transaction()` that is
|
|
401
|
+
* open is waited for and stored with the rest; a transaction a caller began
|
|
402
|
+
* with `BEGIN` cannot be stored without ending it, so flush() rejects while
|
|
403
|
+
* one is open. Resolves at once when nothing is waiting to be saved.
|
|
404
|
+
*/
|
|
405
|
+
async flush() {
|
|
406
|
+
this.ensureOpen();
|
|
407
|
+
if (this.crossTab) {
|
|
408
|
+
await this.coordinatedStatement(() => void 0);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const target = this.writeGeneration;
|
|
412
|
+
while (this.savedGeneration < target && this.db && this.idb) {
|
|
413
|
+
if (this.saving) {
|
|
414
|
+
const covering = this.savingGeneration >= target;
|
|
415
|
+
try {
|
|
416
|
+
await this.saving;
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (covering)
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (this.gate.openToken() !== null) {
|
|
424
|
+
await this.gate.ended();
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (this.inTransaction()) {
|
|
428
|
+
throw new Error(`IndexedDbAdapter: flush() cannot store "${this.dbName}" while a transaction begun with BEGIN is open: sql.js's export would end it. COMMIT or ROLLBACK it first.`);
|
|
429
|
+
}
|
|
430
|
+
await this.startSave();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Closes the database and saves its committed state to IndexedDB.
|
|
435
|
+
*
|
|
436
|
+
* A transaction still open is rolled back, as closing a SQLite connection
|
|
437
|
+
* does, before the save; the save then holds committed writes only. A call
|
|
438
|
+
* made while a close is running waits for that close.
|
|
439
|
+
*/
|
|
440
|
+
close() {
|
|
441
|
+
if (!this.closeRun) {
|
|
442
|
+
this.closeRun = this.closeNow().finally(() => {
|
|
443
|
+
this.closeRun = null;
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return this.closeRun;
|
|
447
|
+
}
|
|
448
|
+
async closeNow() {
|
|
449
|
+
if (this.opening) {
|
|
450
|
+
await this.opening.catch(() => void 0);
|
|
451
|
+
}
|
|
452
|
+
if (this.saveTimer) {
|
|
453
|
+
clearInterval(this.saveTimer);
|
|
454
|
+
this.saveTimer = null;
|
|
455
|
+
}
|
|
456
|
+
if (this.db) {
|
|
457
|
+
this.closing = true;
|
|
458
|
+
try {
|
|
459
|
+
if (this.inTransaction()) {
|
|
460
|
+
this.rollbackQuietly();
|
|
461
|
+
}
|
|
462
|
+
if (this.crossTab) {
|
|
463
|
+
await this.withCoordination(async () => {
|
|
464
|
+
if (this.db && this.hasUnsavedWrites()) {
|
|
465
|
+
await this.saveIfStampIs(this.loadedStamp);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
} else {
|
|
469
|
+
await this.settleSave();
|
|
470
|
+
if (this.hasUnsavedWrites()) {
|
|
471
|
+
await this.startSave();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
this.db?.close();
|
|
475
|
+
this.db = null;
|
|
476
|
+
} finally {
|
|
477
|
+
this.closing = false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (this.idb) {
|
|
481
|
+
this.idb.close();
|
|
482
|
+
this.idb = null;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Exports the database as a Uint8Array (SQLite file format).
|
|
487
|
+
* Can be downloaded or stored externally. With `crossTab` it is this tab's
|
|
488
|
+
* image, which another tab's save can have overtaken.
|
|
489
|
+
*
|
|
490
|
+
* Throws while a transaction is open: sql.js's export closes and reopens the
|
|
491
|
+
* connection, which would end the transaction.
|
|
492
|
+
* @returns SQLite database file as binary
|
|
493
|
+
*/
|
|
494
|
+
exportDatabase() {
|
|
495
|
+
this.ensureOpen();
|
|
496
|
+
if (this.inTransaction()) {
|
|
497
|
+
throw new Error("IndexedDbAdapter: cannot export the database while a transaction is open; sql.js's export would end it.");
|
|
498
|
+
}
|
|
499
|
+
return this.exportImage();
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Applies the connection settings open() was asked for. sql.js loses them
|
|
503
|
+
* whenever export() reopens the connection; a save, an export, an import
|
|
504
|
+
* and a reload set them again with the other settings a caller changed
|
|
505
|
+
* (exportImage, replaceDatabase).
|
|
506
|
+
*/
|
|
507
|
+
applyConnectionSettings() {
|
|
508
|
+
if (this.foreignKeys !== void 0) {
|
|
509
|
+
this.db.run(`PRAGMA foreign_keys = ${this.foreignKeys ? "ON" : "OFF"};`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Imports a database from a Uint8Array (SQLite file).
|
|
514
|
+
* Replaces the current database once any open transaction() has ended, and
|
|
515
|
+
* saves it to IndexedDB before it resolves; with `crossTab`, in place of
|
|
516
|
+
* what every tab stored. Bytes that are not a SQLite database are refused,
|
|
517
|
+
* and the adapter keeps its database.
|
|
518
|
+
* @param data SQLite database file
|
|
519
|
+
*/
|
|
520
|
+
async importDatabase(data) {
|
|
521
|
+
this.ensureOpen();
|
|
522
|
+
if (this.crossTab) {
|
|
523
|
+
await this.withCoordination(async () => {
|
|
524
|
+
this.swapIn(data);
|
|
525
|
+
await this.saveIfStampIs(void 0);
|
|
526
|
+
});
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
await (0, import_connectionGate.whenAdmitted)(this.gate, () => {
|
|
530
|
+
this.swapIn(data);
|
|
531
|
+
return this.startSave();
|
|
532
|
+
}, void 0, this.longWait);
|
|
533
|
+
}
|
|
534
|
+
/** Replaces the database with an image, once sql.js has read it as a SQLite database. */
|
|
535
|
+
swapIn(data) {
|
|
536
|
+
this.ensureOpen();
|
|
537
|
+
const imported = new this.SQL.Database(data);
|
|
538
|
+
try {
|
|
539
|
+
imported.exec("SELECT count(*) FROM sqlite_master");
|
|
540
|
+
} catch (error) {
|
|
541
|
+
imported.close();
|
|
542
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
543
|
+
throw new Error(`IndexedDbAdapter: the image to import is not a SQLite database that sql.js can read (${reason}).`);
|
|
544
|
+
}
|
|
545
|
+
this.replaceDatabase(imported);
|
|
546
|
+
this.writeGeneration += 1;
|
|
547
|
+
}
|
|
548
|
+
/** Puts `next` in place of the database, with the connection settings the current one has. */
|
|
549
|
+
replaceDatabase(next) {
|
|
550
|
+
const settings = this.changedPragmas();
|
|
551
|
+
this.db.close();
|
|
552
|
+
this.db = next;
|
|
553
|
+
this.applyPragmas(settings);
|
|
554
|
+
}
|
|
555
|
+
// ==========================================================================
|
|
556
|
+
// Across tabs
|
|
557
|
+
// ==========================================================================
|
|
558
|
+
/**
|
|
559
|
+
* Runs `work` with this tab holding the database: under the Web Lock, or,
|
|
560
|
+
* without Web Locks, after this adapter's earlier coordinated work.
|
|
561
|
+
*/
|
|
562
|
+
withCoordination(work) {
|
|
563
|
+
const waiting = setTimeout(() => this.reportLockWait(), LONG_WAIT_MS);
|
|
564
|
+
const start = () => {
|
|
565
|
+
clearTimeout(waiting);
|
|
566
|
+
return work();
|
|
567
|
+
};
|
|
568
|
+
const locks = this.locks;
|
|
569
|
+
const held = locks ? locks.request(this.lockName, { mode: "exclusive" }, start) : this.runCoordinatedInTurn(start);
|
|
570
|
+
return held.finally(() => clearTimeout(waiting));
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* One statement across tabs: on the newest stored image, and stored before
|
|
574
|
+
* it resolves. Without Web Locks another tab can save between the load and
|
|
575
|
+
* the save; the statement then runs again on that tab's image.
|
|
576
|
+
*/
|
|
577
|
+
coordinatedStatement(statement) {
|
|
578
|
+
return this.withCoordination(async () => {
|
|
579
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
580
|
+
this.ensureOpen();
|
|
581
|
+
await this.syncFromStore();
|
|
582
|
+
const result = statement();
|
|
583
|
+
if (this.inTransaction()) {
|
|
584
|
+
this.rollbackQuietly();
|
|
585
|
+
throw new Error(`IndexedDbAdapter: "${this.dbName}" coordinates tabs, so a transaction begun with BEGIN through run() or exec() cannot be held open; it was rolled back. Use transaction().`);
|
|
586
|
+
}
|
|
587
|
+
if (!this.hasUnsavedWrites() || await this.saveIfStampIs(this.loadedStamp)) {
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
if (attempt >= MAX_CONFLICT_RETRIES) {
|
|
591
|
+
await this.syncFromStore();
|
|
592
|
+
throw new IndexedDbConflictError(`IndexedDbAdapter: other tabs saved "${this.dbName}" before each of ${attempt} attempts to store a statement; nothing of it is stored.`);
|
|
593
|
+
}
|
|
594
|
+
await this.syncFromStore();
|
|
595
|
+
await new Promise((resolve) => setTimeout(resolve, Math.random() * 4 * attempt));
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* One transaction across tabs: on the newest stored image, and stored before
|
|
601
|
+
* it resolves. Without Web Locks another tab can save while the callback
|
|
602
|
+
* runs; the transaction is then refused, nothing of it is stored, and the
|
|
603
|
+
* other tab's image is loaded for the caller to run it again.
|
|
604
|
+
*/
|
|
605
|
+
coordinatedTransaction(fn) {
|
|
606
|
+
return this.withCoordination(async () => {
|
|
607
|
+
this.ensureOpen();
|
|
608
|
+
await this.syncFromStore();
|
|
609
|
+
const result = await this.runSerializedTransaction(() => this.transactionNow(fn));
|
|
610
|
+
if (this.hasUnsavedWrites() && !await this.saveIfStampIs(this.loadedStamp)) {
|
|
611
|
+
await this.syncFromStore();
|
|
612
|
+
throw new IndexedDbConflictError(`IndexedDbAdapter: another tab saved "${this.dbName}" while this transaction ran, so nothing of it is stored. That tab's writes are loaded; run the transaction again.`);
|
|
613
|
+
}
|
|
614
|
+
return result;
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
/** Loads the stored image when another tab (or adapter) has saved since this one loaded or saved. */
|
|
618
|
+
async syncFromStore() {
|
|
619
|
+
const stored = await this.readIfChanged(this.loadedStamp);
|
|
620
|
+
if (!stored || !this.db) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
this.replaceDatabase(stored.image ? new this.SQL.Database(stored.image) : new this.SQL.Database());
|
|
624
|
+
this.loadedStamp = stored.stamp;
|
|
625
|
+
this.savedGeneration = this.writeGeneration;
|
|
626
|
+
}
|
|
627
|
+
/** Reads the stored stamp, and the image with it when the stamp is not `known`; null when it is. */
|
|
628
|
+
readIfChanged(known) {
|
|
629
|
+
return this.withTransactionRetry(() => new Promise((resolve, reject) => {
|
|
630
|
+
const tx = this.idb.transaction(this.storeName, "readonly");
|
|
631
|
+
const store = tx.objectStore(this.storeName);
|
|
632
|
+
let answer = null;
|
|
633
|
+
const stampRequest = store.get(STAMP_KEY);
|
|
634
|
+
stampRequest.onsuccess = () => {
|
|
635
|
+
const stamp = typeof stampRequest.result === "string" ? stampRequest.result : null;
|
|
636
|
+
if (stamp === known)
|
|
637
|
+
return;
|
|
638
|
+
const imageRequest = store.get(IMAGE_KEY);
|
|
639
|
+
imageRequest.onsuccess = () => {
|
|
640
|
+
answer = { image: imageRequest.result ?? null, stamp };
|
|
641
|
+
};
|
|
642
|
+
};
|
|
643
|
+
tx.oncomplete = () => resolve(answer);
|
|
644
|
+
tx.onabort = () => reject(tx.error ?? new Error(`IndexedDbAdapter: reading "${this.dbName}" was aborted.`));
|
|
645
|
+
}));
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Stores the image when the stored stamp is still `expected`, in one
|
|
649
|
+
* IndexedDB transaction, so no other tab's save lands between the check and
|
|
650
|
+
* the write. `undefined` stores it whatever the stamp is. Resolves with
|
|
651
|
+
* whether it stored the image.
|
|
652
|
+
*/
|
|
653
|
+
async saveIfStampIs(expected) {
|
|
654
|
+
const generation = this.writeGeneration;
|
|
655
|
+
const image = this.exportImage();
|
|
656
|
+
const stamp = this.nextStamp();
|
|
657
|
+
const stored = await this.withTransactionRetry(() => new Promise((resolve, reject) => {
|
|
658
|
+
const tx = this.idb.transaction(this.storeName, "readwrite");
|
|
659
|
+
const store = tx.objectStore(this.storeName);
|
|
660
|
+
let wrote = false;
|
|
661
|
+
const write = () => {
|
|
662
|
+
store.put(image, IMAGE_KEY);
|
|
663
|
+
store.put(stamp, STAMP_KEY);
|
|
664
|
+
wrote = true;
|
|
665
|
+
};
|
|
666
|
+
if (expected === void 0) {
|
|
667
|
+
write();
|
|
668
|
+
} else {
|
|
669
|
+
const current = store.get(STAMP_KEY);
|
|
670
|
+
current.onsuccess = () => {
|
|
671
|
+
const found = typeof current.result === "string" ? current.result : null;
|
|
672
|
+
if (found === expected)
|
|
673
|
+
write();
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
tx.oncomplete = () => resolve(wrote);
|
|
677
|
+
tx.onabort = () => reject(tx.error ?? new Error(`IndexedDbAdapter: saving "${this.dbName}" to IndexedDB was aborted.`));
|
|
678
|
+
}));
|
|
679
|
+
if (stored) {
|
|
680
|
+
this.loadedStamp = stamp;
|
|
681
|
+
this.savedGeneration = Math.max(this.savedGeneration, generation);
|
|
682
|
+
}
|
|
683
|
+
return stored;
|
|
684
|
+
}
|
|
685
|
+
/** A stamp no other save of this or another tab writes. */
|
|
686
|
+
nextStamp() {
|
|
687
|
+
this.stampCount += 1;
|
|
688
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}-${this.stampCount}`;
|
|
689
|
+
}
|
|
690
|
+
// ==========================================================================
|
|
691
|
+
// Saving
|
|
692
|
+
// ==========================================================================
|
|
693
|
+
/**
|
|
694
|
+
* Exports the database for a save, keeping the connection settings a
|
|
695
|
+
* caller made: sql.js's export() closes the connection and opens it again.
|
|
696
|
+
*/
|
|
697
|
+
exportImage() {
|
|
698
|
+
const settings = this.changedPragmas();
|
|
699
|
+
const image = this.db.export();
|
|
700
|
+
this.applyPragmas(settings);
|
|
701
|
+
return image;
|
|
702
|
+
}
|
|
703
|
+
/** The connection settings that differ from a fresh connection's. */
|
|
704
|
+
changedPragmas() {
|
|
705
|
+
const changed = /* @__PURE__ */ new Map();
|
|
706
|
+
if (!this.db)
|
|
707
|
+
return changed;
|
|
708
|
+
for (const [name, value] of readPragmas(this.db)) {
|
|
709
|
+
if (this.pragmaDefaults.get(name) !== value)
|
|
710
|
+
changed.set(name, value);
|
|
711
|
+
}
|
|
712
|
+
return changed;
|
|
713
|
+
}
|
|
714
|
+
/** Sets connection settings on the database, after a save or a reload. */
|
|
715
|
+
applyPragmas(settings) {
|
|
716
|
+
for (const [name, value] of settings) {
|
|
717
|
+
const literal = typeof value === "number" ? String(value) : `'${String(value).replace(/'/g, "''")}'`;
|
|
718
|
+
try {
|
|
719
|
+
this.db.exec(`PRAGMA ${name} = ${literal}`);
|
|
720
|
+
} catch {
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Opens or creates the IndexedDB database.
|
|
726
|
+
*/
|
|
727
|
+
openIndexedDb() {
|
|
728
|
+
return new Promise((resolve, reject) => {
|
|
729
|
+
const req = indexedDB.open(this.dbName, DB_VERSION);
|
|
730
|
+
req.onupgradeneeded = () => {
|
|
731
|
+
const db = req.result;
|
|
732
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
733
|
+
db.createObjectStore(this.storeName);
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
req.onsuccess = () => resolve(req.result);
|
|
737
|
+
req.onerror = () => reject(req.error);
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Opens IndexedDB with retry logic and exponential backoff.
|
|
742
|
+
* Handles race conditions when multiple adapters initialize simultaneously.
|
|
743
|
+
*
|
|
744
|
+
* The "NotFoundError" for object stores typically occurs when:
|
|
745
|
+
* 1. Two adapters try to open/upgrade simultaneously
|
|
746
|
+
* 2. One adapter opens the DB while another is still upgrading
|
|
747
|
+
* 3. Schema version mismatch during concurrent initialization
|
|
748
|
+
*
|
|
749
|
+
* @param maxRetries Maximum number of retry attempts (default: 3)
|
|
750
|
+
* @param baseDelayMs Base delay in milliseconds (default: 100)
|
|
751
|
+
*/
|
|
752
|
+
async openIndexedDbWithRetry(maxRetries = 3, baseDelayMs = 100) {
|
|
753
|
+
let lastError;
|
|
754
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
755
|
+
try {
|
|
756
|
+
return await this.openIndexedDb();
|
|
757
|
+
} catch (error) {
|
|
758
|
+
lastError = error;
|
|
759
|
+
const isNotFoundError = error instanceof DOMException && error.name === "NotFoundError";
|
|
760
|
+
const isVersionError = error instanceof DOMException && error.name === "VersionError";
|
|
761
|
+
const isRetryable = isNotFoundError || isVersionError;
|
|
762
|
+
if (!isRetryable || attempt === maxRetries) {
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
const delay = baseDelayMs * Math.pow(2, attempt);
|
|
766
|
+
console.warn(`[IndexedDbAdapter] Retrying IndexedDB open for "${this.dbName}" (attempt ${attempt + 1}/${maxRetries + 1}, delay: ${delay}ms)`, error);
|
|
767
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
throw lastError;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Loads the SQLite database and its stamp from IndexedDB, in one
|
|
774
|
+
* transaction, so the two belong together.
|
|
775
|
+
*/
|
|
776
|
+
async loadFromIndexedDb() {
|
|
777
|
+
if (!this.idb)
|
|
778
|
+
return { image: null, stamp: null };
|
|
779
|
+
return this.withTransactionRetry(() => new Promise((resolve, reject) => {
|
|
780
|
+
const tx = this.idb.transaction(this.storeName, "readonly");
|
|
781
|
+
const store = tx.objectStore(this.storeName);
|
|
782
|
+
const image = store.get(IMAGE_KEY);
|
|
783
|
+
const stamp = store.get(STAMP_KEY);
|
|
784
|
+
tx.oncomplete = () => resolve({
|
|
785
|
+
image: image.result ?? null,
|
|
786
|
+
stamp: typeof stamp.result === "string" ? stamp.result : null
|
|
787
|
+
});
|
|
788
|
+
tx.onabort = () => reject(tx.error ?? new Error(`IndexedDbAdapter: reading "${this.dbName}" was aborted.`));
|
|
789
|
+
}));
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Writes the current image to IndexedDB, with a new stamp. Never called
|
|
793
|
+
* while a transaction is open (see saveIfIdle): sql.js's export() closes and
|
|
794
|
+
* reopens the connection, which ends an open transaction.
|
|
795
|
+
*
|
|
796
|
+
* Settles when the IndexedDB transaction completes, not when its put request
|
|
797
|
+
* succeeds: the transaction can still abort after that (a quota error at
|
|
798
|
+
* commit), and the image is then not stored. On abort the writes stay
|
|
799
|
+
* unsaved, so the next save writes them. A write made after the export stays
|
|
800
|
+
* unsaved too.
|
|
801
|
+
*/
|
|
802
|
+
async saveToIndexedDb() {
|
|
803
|
+
if (!this.idb || !this.db)
|
|
804
|
+
return;
|
|
805
|
+
await this.saveIfStampIs(void 0);
|
|
806
|
+
}
|
|
807
|
+
/** Whether writes were made since the image the last completed save exported. */
|
|
808
|
+
hasUnsavedWrites() {
|
|
809
|
+
return this.writeGeneration !== this.savedGeneration;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Whether a transaction is open on the connection: one transaction() is
|
|
813
|
+
* running, or one the caller began with BEGIN or SAVEPOINT. sql.js does not
|
|
814
|
+
* export sqlite3_get_autocommit, so the adapter asks SQLite: BEGIN fails
|
|
815
|
+
* inside a transaction, and outside one it is rolled back at once. A probe
|
|
816
|
+
* that fails for any reason counts as open, so the adapter does not export.
|
|
817
|
+
*/
|
|
818
|
+
inTransaction() {
|
|
819
|
+
if (this.gate.openToken() !== null)
|
|
820
|
+
return true;
|
|
821
|
+
if (!this.db)
|
|
822
|
+
return false;
|
|
823
|
+
try {
|
|
824
|
+
this.db.run("BEGIN");
|
|
825
|
+
} catch {
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
this.db.run("ROLLBACK");
|
|
830
|
+
} catch {
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Starts a save of the unsaved writes, unless a save is running or a
|
|
837
|
+
* transaction is open; the auto-save tick tries again later.
|
|
838
|
+
*/
|
|
839
|
+
saveIfIdle() {
|
|
840
|
+
if (this.saving)
|
|
841
|
+
return this.saving;
|
|
842
|
+
if (!this.db || !this.idb || !this.hasUnsavedWrites() || this.inTransaction()) {
|
|
843
|
+
return Promise.resolve();
|
|
844
|
+
}
|
|
845
|
+
return this.startSave();
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Resolves once the writes up to `generation` are stored. A save in
|
|
849
|
+
* progress that holds them is waited for; an open transaction() is waited
|
|
850
|
+
* for, since its own save holds them; while a transaction a caller began
|
|
851
|
+
* with BEGIN is open, nothing can be stored, and its COMMIT stores them.
|
|
852
|
+
* Rejects when the save that held them failed.
|
|
853
|
+
*/
|
|
854
|
+
async saveThrough(generation) {
|
|
855
|
+
while (this.savedGeneration < generation && this.db && this.idb) {
|
|
856
|
+
if (this.saving) {
|
|
857
|
+
const covering = this.savingGeneration >= generation;
|
|
858
|
+
try {
|
|
859
|
+
await this.saving;
|
|
860
|
+
} catch (error) {
|
|
861
|
+
if (covering)
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if (this.gate.openToken() !== null) {
|
|
867
|
+
await this.gate.ended();
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (this.inTransaction()) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
await this.startSave();
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Starts a save at once and keeps it as the save in progress. The caller has
|
|
878
|
+
* made sure no transaction is open: the export would end it.
|
|
879
|
+
*/
|
|
880
|
+
startSave() {
|
|
881
|
+
this.savingGeneration = this.writeGeneration;
|
|
882
|
+
const saving = this.saveToIndexedDb().finally(() => {
|
|
883
|
+
if (this.saving === saving)
|
|
884
|
+
this.saving = null;
|
|
885
|
+
});
|
|
886
|
+
this.saving = saving;
|
|
887
|
+
return saving;
|
|
888
|
+
}
|
|
889
|
+
/** Waits for a save in progress to settle, whether it succeeded or not. */
|
|
890
|
+
async settleSave() {
|
|
891
|
+
if (this.saving) {
|
|
892
|
+
await this.saving.catch(() => void 0);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
reportLongWait() {
|
|
896
|
+
if (this.longWaitReported)
|
|
897
|
+
return;
|
|
898
|
+
this.longWaitReported = true;
|
|
899
|
+
console.warn(`[IndexedDbAdapter] A statement on "${this.dbName}" has waited ${LONG_WAIT_MS / 1e3} s for an open transaction to end. Inside a transaction() callback, run statements through the callback's handle: one run through the adapter itself waits for the transaction, and the transaction waits for it.`);
|
|
900
|
+
}
|
|
901
|
+
reportLockWait() {
|
|
902
|
+
if (this.lockWaitReported)
|
|
903
|
+
return;
|
|
904
|
+
this.lockWaitReported = true;
|
|
905
|
+
console.warn(`[IndexedDbAdapter] A statement on "${this.dbName}" has waited ${LONG_WAIT_MS / 1e3} s for its turn across tabs: another tab's transaction holds the lock, or a transaction() of this tab whose callback runs a statement through the adapter itself instead of the callback's handle waits on itself.`);
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Wraps a transaction operation with retry logic.
|
|
909
|
+
* Handles "NotFoundError" by reopening IndexedDB connection.
|
|
910
|
+
*/
|
|
911
|
+
async withTransactionRetry(operation, maxRetries = 2) {
|
|
912
|
+
let lastError;
|
|
913
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
914
|
+
try {
|
|
915
|
+
return await operation();
|
|
916
|
+
} catch (error) {
|
|
917
|
+
lastError = error;
|
|
918
|
+
const isNotFoundError = error instanceof DOMException && error.name === "NotFoundError";
|
|
919
|
+
if (!isNotFoundError || attempt === maxRetries) {
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
console.warn(`[IndexedDbAdapter] Transaction failed for "${this.dbName}", reopening connection (attempt ${attempt + 1}/${maxRetries + 1})`, error);
|
|
923
|
+
try {
|
|
924
|
+
if (this.idb) {
|
|
925
|
+
this.idb.close();
|
|
926
|
+
}
|
|
927
|
+
this.idb = await this.openIndexedDbWithRetry();
|
|
928
|
+
} catch (reopenError) {
|
|
929
|
+
console.error("[IndexedDbAdapter] Failed to reopen IndexedDB:", reopenError);
|
|
930
|
+
throw lastError;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
throw lastError;
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Starts auto-save interval.
|
|
938
|
+
*/
|
|
939
|
+
startAutoSave() {
|
|
940
|
+
this.saveTimer = setInterval(() => {
|
|
941
|
+
void this.autoSaveTick();
|
|
942
|
+
}, this.saveIntervalMs);
|
|
943
|
+
}
|
|
944
|
+
/** One auto-save tick: a failed save is reported once and tried again on the next tick. */
|
|
945
|
+
async autoSaveTick() {
|
|
946
|
+
try {
|
|
947
|
+
await this.saveIfIdle();
|
|
948
|
+
this.saveFailureReported = false;
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if (!this.saveFailureReported) {
|
|
951
|
+
this.saveFailureReported = true;
|
|
952
|
+
console.warn(`[IndexedDbAdapter] Saving "${this.dbName}" to IndexedDB failed; the next tick tries again.`, error);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Ensures the database is open.
|
|
958
|
+
*/
|
|
959
|
+
ensureOpen() {
|
|
960
|
+
if (!this.db || this.closing) {
|
|
961
|
+
throw new Error("IndexedDbAdapter: Database not open. Call open() first.");
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Normalizes last insert row ID to string or number.
|
|
966
|
+
*/
|
|
967
|
+
normaliseRowId(value) {
|
|
968
|
+
if (typeof value === "number" || typeof value === "string") {
|
|
969
|
+
return value;
|
|
970
|
+
}
|
|
971
|
+
if (typeof value === "bigint") {
|
|
972
|
+
return value.toString();
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
const readPragmas = (db) => {
|
|
978
|
+
const values = /* @__PURE__ */ new Map();
|
|
979
|
+
for (const name of CONNECTION_PRAGMAS) {
|
|
980
|
+
try {
|
|
981
|
+
const value = db.exec(`PRAGMA ${name}`)[0]?.values?.[0]?.[0];
|
|
982
|
+
if (value !== void 0)
|
|
983
|
+
values.set(name, value);
|
|
984
|
+
} catch {
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return values;
|
|
988
|
+
};
|
|
989
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
990
|
+
0 && (module.exports = {
|
|
991
|
+
IndexedDbAdapter,
|
|
992
|
+
IndexedDbConflictError
|
|
993
|
+
});
|