@xnetjs/sqlite 0.0.3 → 0.1.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/{adapter-t2aqQ__n.d.ts → adapter-imtxkmXZ.d.ts} +22 -1
- package/dist/adapters/electron.d.ts +66 -6
- package/dist/adapters/electron.js +629 -47
- package/dist/adapters/expo.d.ts +6 -2
- package/dist/adapters/expo.js +8 -1
- package/dist/adapters/memory.d.ts +6 -2
- package/dist/adapters/memory.js +8 -1
- package/dist/adapters/reader-thread.d.ts +78 -0
- package/dist/adapters/reader-thread.js +57 -0
- package/dist/adapters/web-proxy.d.ts +54 -2
- package/dist/adapters/web-proxy.js +458 -55
- package/dist/adapters/web-router-worker.d.ts +76 -0
- package/dist/adapters/web-router-worker.js +91 -0
- package/dist/adapters/web-worker.d.ts +45 -1
- package/dist/adapters/web-worker.js +232 -17
- package/dist/adapters/web.d.ts +82 -3
- package/dist/adapters/web.js +7 -4
- package/dist/browser-support.d.ts +10 -1
- package/dist/browser-support.js +5 -1
- package/dist/chunk-5HC5V73T.js +191 -0
- package/dist/{chunk-DCY4LAUD.js → chunk-BBZDKLA3.js} +246 -13
- package/dist/chunk-CGI2YBZY.js +16 -0
- package/dist/{chunk-2OK46ZBU.js → chunk-S6MT6KCI.js} +22 -1
- package/dist/chunk-SV475UL5.js +44 -0
- package/dist/{chunk-NVD7G7DZ.js → chunk-W3L4FXU5.js} +11 -2
- package/dist/index.d.ts +93 -8
- package/dist/index.js +92 -10
- package/dist/schema-C4gufY3B.d.ts +35 -0
- package/dist/types-BTabr_VP.d.ts +225 -0
- package/dist/worker-scheduler-D04DqMmR.d.ts +56 -0
- package/package.json +5 -1
- package/dist/schema-BEgIA-rZ.d.ts +0 -35
- package/dist/types-DMrj3K4o.d.ts +0 -132
package/dist/adapters/web.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
WebSQLiteAdapter,
|
|
3
3
|
createWebSQLiteAdapter,
|
|
4
|
-
resetWebSQLiteOpfsStorage
|
|
5
|
-
|
|
4
|
+
resetWebSQLiteOpfsStorage,
|
|
5
|
+
stepIncrementalVacuumToCompletion
|
|
6
|
+
} from "../chunk-BBZDKLA3.js";
|
|
7
|
+
import "../chunk-SV475UL5.js";
|
|
6
8
|
import "../chunk-CBU263LI.js";
|
|
7
|
-
import "../chunk-
|
|
9
|
+
import "../chunk-W3L4FXU5.js";
|
|
8
10
|
export {
|
|
9
11
|
WebSQLiteAdapter,
|
|
10
12
|
createWebSQLiteAdapter,
|
|
11
|
-
resetWebSQLiteOpfsStorage
|
|
13
|
+
resetWebSQLiteOpfsStorage,
|
|
14
|
+
stepIncrementalVacuumToCompletion
|
|
12
15
|
};
|
|
@@ -118,5 +118,14 @@ declare function requestPersistentStorage(options?: PersistentStorageRequestOpti
|
|
|
118
118
|
* ```
|
|
119
119
|
*/
|
|
120
120
|
declare function showUnsupportedBrowserMessage(reason: string): void;
|
|
121
|
+
/**
|
|
122
|
+
* Record that this session opened on the non-durable `:memory:` fallback
|
|
123
|
+
* (another tab/worker held the OPFS handles, or OPFS is unavailable).
|
|
124
|
+
* Multi-tab leadership routing (0263) should drive this count to ~zero —
|
|
125
|
+
* the counter is how we verify that in the field. Returns the new total.
|
|
126
|
+
*/
|
|
127
|
+
declare function recordMemoryFallbackSession(): number;
|
|
128
|
+
/** Total sessions on this device that fell back to `:memory:` storage. */
|
|
129
|
+
declare function getMemoryFallbackSessionCount(): number;
|
|
121
130
|
|
|
122
|
-
export { type BrowserSupport, type PersistentStorageRequestOptions, type PersistentStorageStatus, checkBrowserSupport, checkPersistentStorage, isSilentPersistRequestSafe, requestPersistentStorage, showUnsupportedBrowserMessage, watchPersistentStoragePermission };
|
|
131
|
+
export { type BrowserSupport, type PersistentStorageRequestOptions, type PersistentStorageStatus, checkBrowserSupport, checkPersistentStorage, getMemoryFallbackSessionCount, isSilentPersistRequestSafe, recordMemoryFallbackSession, requestPersistentStorage, showUnsupportedBrowserMessage, watchPersistentStoragePermission };
|
package/dist/browser-support.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
checkBrowserSupport,
|
|
3
3
|
checkPersistentStorage,
|
|
4
|
+
getMemoryFallbackSessionCount,
|
|
4
5
|
isSilentPersistRequestSafe,
|
|
6
|
+
recordMemoryFallbackSession,
|
|
5
7
|
requestPersistentStorage,
|
|
6
8
|
showUnsupportedBrowserMessage,
|
|
7
9
|
watchPersistentStoragePermission
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-S6MT6KCI.js";
|
|
9
11
|
export {
|
|
10
12
|
checkBrowserSupport,
|
|
11
13
|
checkPersistentStorage,
|
|
14
|
+
getMemoryFallbackSessionCount,
|
|
12
15
|
isSilentPersistRequestSafe,
|
|
16
|
+
recordMemoryFallbackSession,
|
|
13
17
|
requestPersistentStorage,
|
|
14
18
|
showUnsupportedBrowserMessage,
|
|
15
19
|
watchPersistentStoragePermission
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// src/adapters/worker-scheduler.ts
|
|
2
|
+
var LANE_ORDER = ["interactive", "bulk", "write"];
|
|
3
|
+
function nowMs() {
|
|
4
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
5
|
+
}
|
|
6
|
+
function firstOpGapFields(report, openedAtMs) {
|
|
7
|
+
return {
|
|
8
|
+
firstOpAfterOpen: true,
|
|
9
|
+
idleBeforeFirstOpMs: Math.round(report.enqueuedAt - openedAtMs),
|
|
10
|
+
sinceOpenMs: Math.round(report.startedAt - openedAtMs)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function percentileMs(samples, p) {
|
|
14
|
+
if (samples.length === 0) return 0;
|
|
15
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
16
|
+
const rank = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p / 100 * sorted.length) - 1));
|
|
17
|
+
return Math.round(sorted[rank]);
|
|
18
|
+
}
|
|
19
|
+
var SCHEDULER_STATS_SAMPLE_CAP = 512;
|
|
20
|
+
var LaneStatsAccumulator = class {
|
|
21
|
+
ops = 0;
|
|
22
|
+
maxExecMs = 0;
|
|
23
|
+
queueSamples = [];
|
|
24
|
+
execSamples = [];
|
|
25
|
+
cursor = 0;
|
|
26
|
+
record(queueMs, execMs) {
|
|
27
|
+
this.ops += 1;
|
|
28
|
+
if (execMs > this.maxExecMs) this.maxExecMs = execMs;
|
|
29
|
+
if (this.queueSamples.length < SCHEDULER_STATS_SAMPLE_CAP) {
|
|
30
|
+
this.queueSamples.push(queueMs);
|
|
31
|
+
this.execSamples.push(execMs);
|
|
32
|
+
} else {
|
|
33
|
+
this.queueSamples[this.cursor] = queueMs;
|
|
34
|
+
this.execSamples[this.cursor] = execMs;
|
|
35
|
+
this.cursor = (this.cursor + 1) % SCHEDULER_STATS_SAMPLE_CAP;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
stats() {
|
|
39
|
+
return {
|
|
40
|
+
ops: this.ops,
|
|
41
|
+
queueP50Ms: percentileMs(this.queueSamples, 50),
|
|
42
|
+
queueP95Ms: percentileMs(this.queueSamples, 95),
|
|
43
|
+
execP50Ms: percentileMs(this.execSamples, 50),
|
|
44
|
+
execP95Ms: percentileMs(this.execSamples, 95),
|
|
45
|
+
maxExecMs: Math.round(this.maxExecMs)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
reset() {
|
|
49
|
+
this.ops = 0;
|
|
50
|
+
this.maxExecMs = 0;
|
|
51
|
+
this.queueSamples.length = 0;
|
|
52
|
+
this.execSamples.length = 0;
|
|
53
|
+
this.cursor = 0;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var WorkerScheduler = class {
|
|
57
|
+
/**
|
|
58
|
+
* @param onOp optional per-operation timing reporter (boot diagnostics, 0229).
|
|
59
|
+
*/
|
|
60
|
+
constructor(onOp) {
|
|
61
|
+
this.onOp = onOp;
|
|
62
|
+
}
|
|
63
|
+
queues = {
|
|
64
|
+
interactive: [],
|
|
65
|
+
bulk: [],
|
|
66
|
+
write: []
|
|
67
|
+
};
|
|
68
|
+
/** In-flight + queued reads keyed by `coalesceKey`, to collapse duplicates. */
|
|
69
|
+
coalesced = /* @__PURE__ */ new Map();
|
|
70
|
+
running = false;
|
|
71
|
+
/** Aggregated op timing per lane (exploration 0263). */
|
|
72
|
+
laneStats = {
|
|
73
|
+
interactive: new LaneStatsAccumulator(),
|
|
74
|
+
bulk: new LaneStatsAccumulator(),
|
|
75
|
+
write: new LaneStatsAccumulator()
|
|
76
|
+
};
|
|
77
|
+
coalescedHits = 0;
|
|
78
|
+
/**
|
|
79
|
+
* Enqueue `fn` on `lane`. Returns a promise that settles with `fn`'s result.
|
|
80
|
+
*
|
|
81
|
+
* When `coalesceKey` is provided (reads only — they're idempotent), an
|
|
82
|
+
* identical key that is already queued or in flight returns the SAME promise
|
|
83
|
+
* instead of enqueuing a second execution. The key is dropped once that
|
|
84
|
+
* execution settles, so a later identical read re-runs against fresh data.
|
|
85
|
+
*/
|
|
86
|
+
schedule(lane, fn, coalesceKey, label, detail) {
|
|
87
|
+
if (coalesceKey !== void 0) {
|
|
88
|
+
const inflight = this.coalesced.get(coalesceKey);
|
|
89
|
+
if (inflight) {
|
|
90
|
+
this.coalescedHits += 1;
|
|
91
|
+
return inflight;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const promise = new Promise((resolve, reject) => {
|
|
95
|
+
this.queues[lane].push({
|
|
96
|
+
run: fn,
|
|
97
|
+
resolve,
|
|
98
|
+
reject,
|
|
99
|
+
lane,
|
|
100
|
+
label,
|
|
101
|
+
detail,
|
|
102
|
+
enqueuedAt: nowMs()
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
if (coalesceKey !== void 0) {
|
|
106
|
+
this.coalesced.set(coalesceKey, promise);
|
|
107
|
+
const clear = () => {
|
|
108
|
+
if (this.coalesced.get(coalesceKey) === promise) this.coalesced.delete(coalesceKey);
|
|
109
|
+
};
|
|
110
|
+
promise.then(clear, clear);
|
|
111
|
+
}
|
|
112
|
+
void this.pump();
|
|
113
|
+
return promise;
|
|
114
|
+
}
|
|
115
|
+
/** Current queue depths and in-flight flag. */
|
|
116
|
+
snapshot() {
|
|
117
|
+
return {
|
|
118
|
+
interactive: this.queues.interactive.length,
|
|
119
|
+
bulk: this.queues.bulk.length,
|
|
120
|
+
write: this.queues.write.length,
|
|
121
|
+
inFlight: this.running
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Cumulative op counts, coalesce hits, and per-lane latency percentiles. */
|
|
125
|
+
opStats() {
|
|
126
|
+
const lanes = {
|
|
127
|
+
interactive: this.laneStats.interactive.stats(),
|
|
128
|
+
bulk: this.laneStats.bulk.stats(),
|
|
129
|
+
write: this.laneStats.write.stats()
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
ops: lanes.interactive.ops + lanes.bulk.ops + lanes.write.ops,
|
|
133
|
+
coalescedHits: this.coalescedHits,
|
|
134
|
+
lanes
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Zero the op-stats counters for a focused before/after measurement. */
|
|
138
|
+
resetOpStats() {
|
|
139
|
+
this.coalescedHits = 0;
|
|
140
|
+
for (const lane of LANE_ORDER) {
|
|
141
|
+
this.laneStats[lane].reset();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Pull the next job, highest-priority non-empty lane first (FIFO within a lane). */
|
|
145
|
+
next() {
|
|
146
|
+
for (const lane of LANE_ORDER) {
|
|
147
|
+
const job = this.queues[lane].shift();
|
|
148
|
+
if (job) return job;
|
|
149
|
+
}
|
|
150
|
+
return void 0;
|
|
151
|
+
}
|
|
152
|
+
/** Drain the queues one job at a time. Re-entrant-safe via the `running` latch. */
|
|
153
|
+
async pump() {
|
|
154
|
+
if (this.running) return;
|
|
155
|
+
this.running = true;
|
|
156
|
+
try {
|
|
157
|
+
let job;
|
|
158
|
+
while (job = this.next()) {
|
|
159
|
+
const startedAt = nowMs();
|
|
160
|
+
try {
|
|
161
|
+
job.resolve(await job.run());
|
|
162
|
+
} catch (err) {
|
|
163
|
+
job.reject(err);
|
|
164
|
+
} finally {
|
|
165
|
+
const endedAt = nowMs();
|
|
166
|
+
const queueMs = startedAt - job.enqueuedAt;
|
|
167
|
+
const execMs = endedAt - startedAt;
|
|
168
|
+
this.laneStats[job.lane].record(queueMs, execMs);
|
|
169
|
+
if (this.onOp) {
|
|
170
|
+
this.onOp({
|
|
171
|
+
lane: job.lane,
|
|
172
|
+
label: job.label,
|
|
173
|
+
detail: job.detail,
|
|
174
|
+
queueMs,
|
|
175
|
+
execMs,
|
|
176
|
+
enqueuedAt: job.enqueuedAt,
|
|
177
|
+
startedAt
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} finally {
|
|
183
|
+
this.running = false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export {
|
|
189
|
+
firstOpGapFields,
|
|
190
|
+
WorkerScheduler
|
|
191
|
+
};
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectOpfsCapability
|
|
3
|
+
} from "./chunk-SV475UL5.js";
|
|
1
4
|
import {
|
|
2
5
|
isSQLiteCorruptionError
|
|
3
6
|
} from "./chunk-CBU263LI.js";
|
|
4
7
|
import {
|
|
5
8
|
SCHEMA_DDL,
|
|
6
9
|
SCHEMA_VERSION
|
|
7
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-W3L4FXU5.js";
|
|
8
11
|
|
|
9
12
|
// src/adapters/opfs-retry.ts
|
|
10
13
|
function isOpfsLockError(err) {
|
|
@@ -31,6 +34,68 @@ async function withOpfsLockRetry(fn, options = {}) {
|
|
|
31
34
|
throw lastErr;
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
// src/adapters/stmt-cache.ts
|
|
38
|
+
var DEFAULT_STMT_CACHE_CAPACITY = 64;
|
|
39
|
+
function hasInteriorSemicolon(sql) {
|
|
40
|
+
const trimmed = sql.replace(/[\s;]+$/, "");
|
|
41
|
+
return trimmed.includes(";");
|
|
42
|
+
}
|
|
43
|
+
var StmtCache = class {
|
|
44
|
+
constructor(capacity = DEFAULT_STMT_CACHE_CAPACITY) {
|
|
45
|
+
this.capacity = capacity;
|
|
46
|
+
if (capacity < 1) {
|
|
47
|
+
throw new Error("StmtCache capacity must be >= 1");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
entries = /* @__PURE__ */ new Map();
|
|
51
|
+
get size() {
|
|
52
|
+
return this.entries.size;
|
|
53
|
+
}
|
|
54
|
+
/** Look up a cached statement, refreshing its recency. */
|
|
55
|
+
get(sql) {
|
|
56
|
+
const stmt = this.entries.get(sql);
|
|
57
|
+
if (stmt !== void 0) {
|
|
58
|
+
this.entries.delete(sql);
|
|
59
|
+
this.entries.set(sql, stmt);
|
|
60
|
+
}
|
|
61
|
+
return stmt;
|
|
62
|
+
}
|
|
63
|
+
/** Insert a statement, finalizing the least-recently-used one if over capacity. */
|
|
64
|
+
set(sql, stmt) {
|
|
65
|
+
const existing = this.entries.get(sql);
|
|
66
|
+
if (existing !== void 0 && existing !== stmt) {
|
|
67
|
+
this.safeFinalize(existing);
|
|
68
|
+
}
|
|
69
|
+
this.entries.delete(sql);
|
|
70
|
+
this.entries.set(sql, stmt);
|
|
71
|
+
while (this.entries.size > this.capacity) {
|
|
72
|
+
const oldest = this.entries.keys().next().value;
|
|
73
|
+
const evicted = this.entries.get(oldest);
|
|
74
|
+
this.entries.delete(oldest);
|
|
75
|
+
if (evicted !== void 0) {
|
|
76
|
+
this.safeFinalize(evicted);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Finalize and drop every cached statement. Called on DDL (`exec`) — where a
|
|
82
|
+
* dropped table would otherwise leave poisoned handles — and on `close()`.
|
|
83
|
+
*/
|
|
84
|
+
clear() {
|
|
85
|
+
for (const stmt of this.entries.values()) {
|
|
86
|
+
this.safeFinalize(stmt);
|
|
87
|
+
}
|
|
88
|
+
this.entries.clear();
|
|
89
|
+
}
|
|
90
|
+
/** finalize() must never throw into a caller that is already unwinding. */
|
|
91
|
+
safeFinalize(stmt) {
|
|
92
|
+
try {
|
|
93
|
+
stmt.finalize();
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
34
99
|
// src/adapters/web.ts
|
|
35
100
|
var WEB_SQLITE_VFS_NAME = "opfs-sahpool";
|
|
36
101
|
var WEB_SQLITE_VFS_DIRECTORY = ".xnet-sqlite";
|
|
@@ -43,6 +108,9 @@ function log(...args) {
|
|
|
43
108
|
console.log(...args);
|
|
44
109
|
}
|
|
45
110
|
}
|
|
111
|
+
function nowMs() {
|
|
112
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
113
|
+
}
|
|
46
114
|
function getDatabasePath(config) {
|
|
47
115
|
return config.path.startsWith("/") ? config.path : `/${config.path}`;
|
|
48
116
|
}
|
|
@@ -89,18 +157,54 @@ var WebSQLiteAdapter = class {
|
|
|
89
157
|
poolUtil = null;
|
|
90
158
|
_config = null;
|
|
91
159
|
inTransaction = false;
|
|
160
|
+
/**
|
|
161
|
+
* Monotonic time of the open transaction's last operation. Lets the worker
|
|
162
|
+
* host distinguish a LIVE manual transaction from one abandoned by a client
|
|
163
|
+
* that died between BEGIN and COMMIT (PowerSync-style lease recovery, 0263).
|
|
164
|
+
*/
|
|
165
|
+
lastTransactionActivityAt = 0;
|
|
92
166
|
storageMode = "memory";
|
|
167
|
+
/**
|
|
168
|
+
* Per-phase timing of the last {@link open} call, or null until it runs.
|
|
169
|
+
* Read by the worker host to emit `[xNet] sqlite open phases` (exploration
|
|
170
|
+
* 0253). Diagnostic only — never affects behaviour.
|
|
171
|
+
*/
|
|
172
|
+
openPhaseTimings = null;
|
|
173
|
+
/** ms spent in `applySchema()` during {@link createWebSQLiteAdapter}, for the open-phases line. */
|
|
174
|
+
schemaApplyMs = 0;
|
|
175
|
+
/** How many times the OPFS lock-retry backoff fired during the last {@link open} (0 = first try). */
|
|
176
|
+
openRetryAttempts = 0;
|
|
177
|
+
/**
|
|
178
|
+
* Prepared-statement LRU for the hot query/run path (exploration 0263).
|
|
179
|
+
* `db.exec()` re-parses the SQL on every call; the hot path repeats a small
|
|
180
|
+
* statement set, so cached `oo1.Stmt` handles skip the parse+prepare cost.
|
|
181
|
+
* Cleared (finalizing all handles) on {@link exec} — the DDL/script path —
|
|
182
|
+
* and on {@link close}.
|
|
183
|
+
*/
|
|
184
|
+
stmts = new StmtCache();
|
|
185
|
+
/** Per-phase timing of the last {@link open}, or null if it hasn't run. */
|
|
186
|
+
getOpenPhaseTimings() {
|
|
187
|
+
return this.openPhaseTimings ? { ...this.openPhaseTimings } : null;
|
|
188
|
+
}
|
|
93
189
|
async open(config) {
|
|
94
190
|
if (this.db !== null) {
|
|
95
191
|
throw new Error("Database already open. Call close() first.");
|
|
96
192
|
}
|
|
97
193
|
log("[WebSQLiteAdapter] Starting open()...");
|
|
194
|
+
const openStartedAt = nowMs();
|
|
195
|
+
let afterImport = openStartedAt;
|
|
196
|
+
let afterInit = openStartedAt;
|
|
197
|
+
let afterVfsInstall = openStartedAt;
|
|
198
|
+
let afterReserveCapacity = openStartedAt;
|
|
199
|
+
let afterDbOpen = openStartedAt;
|
|
98
200
|
log("[WebSQLiteAdapter] Importing sqlite-wasm...");
|
|
99
201
|
const sqlite3InitModule = (await import("@sqlite.org/sqlite-wasm")).default;
|
|
100
202
|
log("[WebSQLiteAdapter] sqlite-wasm imported");
|
|
203
|
+
afterImport = nowMs();
|
|
101
204
|
log("[WebSQLiteAdapter] Initializing sqlite3 module...");
|
|
102
205
|
this.sqlite3 = await sqlite3InitModule();
|
|
103
206
|
log("[WebSQLiteAdapter] sqlite3 module initialized");
|
|
207
|
+
afterInit = nowMs();
|
|
104
208
|
try {
|
|
105
209
|
await withOpfsLockRetry(
|
|
106
210
|
async () => {
|
|
@@ -113,17 +217,21 @@ var WebSQLiteAdapter = class {
|
|
|
113
217
|
clearOnInit: false
|
|
114
218
|
});
|
|
115
219
|
log("[WebSQLiteAdapter] OPFS-SAHPool VFS installed");
|
|
220
|
+
afterVfsInstall = nowMs();
|
|
116
221
|
log("[WebSQLiteAdapter] Reserving capacity...");
|
|
117
222
|
await this.poolUtil.reserveMinimumCapacity(10);
|
|
118
223
|
log("[WebSQLiteAdapter] Capacity reserved");
|
|
224
|
+
afterReserveCapacity = nowMs();
|
|
119
225
|
const dbPath = getDatabasePath(config);
|
|
120
226
|
log("[WebSQLiteAdapter] Opening database at", dbPath);
|
|
121
227
|
this.db = new this.poolUtil.OpfsSAHPoolDb(dbPath, "c");
|
|
122
228
|
this.storageMode = "opfs";
|
|
123
229
|
log("[WebSQLiteAdapter] Database opened with OPFS-SAHPool");
|
|
230
|
+
afterDbOpen = nowMs();
|
|
124
231
|
},
|
|
125
232
|
{
|
|
126
233
|
onRetry: (attempt, retryErr) => {
|
|
234
|
+
this.openRetryAttempts = attempt;
|
|
127
235
|
console.warn(
|
|
128
236
|
`[WebSQLiteAdapter] OPFS access handles are busy (attempt ${attempt}) \u2014 a previous tab/worker is likely still releasing them; retrying before any in-memory fallback.`,
|
|
129
237
|
retryErr
|
|
@@ -132,7 +240,12 @@ var WebSQLiteAdapter = class {
|
|
|
132
240
|
}
|
|
133
241
|
);
|
|
134
242
|
} catch (err) {
|
|
135
|
-
|
|
243
|
+
const capability = detectOpfsCapability();
|
|
244
|
+
if (capability.mode === "async-opfs") {
|
|
245
|
+
console.info("[WebSQLiteAdapter] Sync access handles unavailable \u2014 " + capability.reason);
|
|
246
|
+
} else {
|
|
247
|
+
console.warn("[WebSQLiteAdapter] OPFS-SAHPool not available, trying OPFS direct mode:", err);
|
|
248
|
+
}
|
|
136
249
|
const dbPath = getDatabasePath(config);
|
|
137
250
|
const opfsDbCtor = this.sqlite3?.oo1?.OpfsDb;
|
|
138
251
|
if (typeof opfsDbCtor === "function") {
|
|
@@ -157,6 +270,7 @@ var WebSQLiteAdapter = class {
|
|
|
157
270
|
}
|
|
158
271
|
}
|
|
159
272
|
this._config = config;
|
|
273
|
+
const beforePragmas = nowMs();
|
|
160
274
|
if (config.foreignKeys !== false) {
|
|
161
275
|
this.execSync("PRAGMA foreign_keys = ON");
|
|
162
276
|
}
|
|
@@ -170,16 +284,44 @@ var WebSQLiteAdapter = class {
|
|
|
170
284
|
} catch (err) {
|
|
171
285
|
log("[WebSQLiteAdapter] page_size pragma not applied:", err);
|
|
172
286
|
}
|
|
287
|
+
try {
|
|
288
|
+
this.execSync("PRAGMA auto_vacuum = INCREMENTAL");
|
|
289
|
+
} catch (err) {
|
|
290
|
+
log("[WebSQLiteAdapter] auto_vacuum pragma not applied:", err);
|
|
291
|
+
}
|
|
173
292
|
this.execSync("PRAGMA synchronous = NORMAL");
|
|
174
293
|
this.execSync("PRAGMA cache_size = -262144");
|
|
294
|
+
try {
|
|
295
|
+
this.execSync("PRAGMA mmap_size = 268435456");
|
|
296
|
+
} catch (err) {
|
|
297
|
+
log("[WebSQLiteAdapter] mmap_size pragma not applied:", err);
|
|
298
|
+
}
|
|
175
299
|
this.execSync("PRAGMA temp_store = MEMORY");
|
|
176
300
|
try {
|
|
177
301
|
this.execSync("PRAGMA journal_mode = TRUNCATE");
|
|
178
302
|
} catch (err) {
|
|
179
303
|
log("[WebSQLiteAdapter] journal_mode pragma not applied:", err);
|
|
180
304
|
}
|
|
305
|
+
try {
|
|
306
|
+
this.execSync("PRAGMA analysis_limit = 400");
|
|
307
|
+
this.execSync("PRAGMA optimize = 0x10002");
|
|
308
|
+
} catch (err) {
|
|
309
|
+
log("[WebSQLiteAdapter] open-time optimize not applied:", err);
|
|
310
|
+
}
|
|
311
|
+
const afterPragmas = nowMs();
|
|
312
|
+
const round = (a, b) => Math.round(b - a);
|
|
313
|
+
this.openPhaseTimings = {
|
|
314
|
+
wasmImportMs: round(openStartedAt, afterImport),
|
|
315
|
+
wasmInitMs: round(afterImport, afterInit),
|
|
316
|
+
vfsInstallMs: round(afterInit, afterVfsInstall),
|
|
317
|
+
reserveCapacityMs: round(afterVfsInstall, afterReserveCapacity),
|
|
318
|
+
dbOpenMs: round(afterReserveCapacity, afterDbOpen),
|
|
319
|
+
pragmasMs: round(beforePragmas, afterPragmas),
|
|
320
|
+
totalOpenMs: round(openStartedAt, afterPragmas)
|
|
321
|
+
};
|
|
181
322
|
}
|
|
182
323
|
async close() {
|
|
324
|
+
this.stmts.clear();
|
|
183
325
|
if (this.db) {
|
|
184
326
|
try {
|
|
185
327
|
this.execSync("PRAGMA optimize");
|
|
@@ -199,29 +341,98 @@ var WebSQLiteAdapter = class {
|
|
|
199
341
|
getStorageMode() {
|
|
200
342
|
return this.storageMode;
|
|
201
343
|
}
|
|
344
|
+
/** True while a manual BEGIN…COMMIT/ROLLBACK transaction is open. */
|
|
345
|
+
isInTransaction() {
|
|
346
|
+
return this.inTransaction;
|
|
347
|
+
}
|
|
348
|
+
/** ms since the open transaction last executed an operation (0 when none). */
|
|
349
|
+
transactionIdleMs() {
|
|
350
|
+
return this.inTransaction ? nowMs() - this.lastTransactionActivityAt : 0;
|
|
351
|
+
}
|
|
352
|
+
/** Stamp transaction liveness — called by ops that run inside one. */
|
|
353
|
+
touchTransaction() {
|
|
354
|
+
if (this.inTransaction) {
|
|
355
|
+
this.lastTransactionActivityAt = nowMs();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Fetch (or prepare and cache) the statement for `sql`, or null when the SQL
|
|
360
|
+
* must bypass the cache: `db.prepare()` compiles only the FIRST statement of
|
|
361
|
+
* a multi-statement string, whereas `db.exec()` runs them all — serving such
|
|
362
|
+
* SQL from the cache would silently drop statements.
|
|
363
|
+
*/
|
|
364
|
+
getCachedStmt(sql) {
|
|
365
|
+
if (hasInteriorSemicolon(sql)) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
let stmt = this.stmts.get(sql);
|
|
369
|
+
if (stmt === void 0) {
|
|
370
|
+
stmt = this.db.prepare(sql);
|
|
371
|
+
this.stmts.set(sql, stmt);
|
|
372
|
+
}
|
|
373
|
+
return stmt;
|
|
374
|
+
}
|
|
202
375
|
async query(sql, params) {
|
|
203
376
|
this.ensureOpen();
|
|
377
|
+
this.touchTransaction();
|
|
204
378
|
const rows = [];
|
|
205
|
-
this.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
379
|
+
const stmt = this.getCachedStmt(sql);
|
|
380
|
+
if (stmt === null) {
|
|
381
|
+
this.db.exec({
|
|
382
|
+
sql,
|
|
383
|
+
bind: params,
|
|
384
|
+
rowMode: "object",
|
|
385
|
+
callback: (row) => {
|
|
386
|
+
rows.push(row);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
return rows;
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
if (params && params.length > 0) {
|
|
393
|
+
stmt.bind(params);
|
|
211
394
|
}
|
|
212
|
-
|
|
395
|
+
while (stmt.step()) {
|
|
396
|
+
rows.push(stmt.get({}));
|
|
397
|
+
}
|
|
398
|
+
} finally {
|
|
399
|
+
stmt.reset();
|
|
400
|
+
stmt.clearBindings();
|
|
401
|
+
}
|
|
213
402
|
return rows;
|
|
214
403
|
}
|
|
215
404
|
async queryOne(sql, params) {
|
|
216
405
|
const rows = await this.query(sql, params);
|
|
217
406
|
return rows[0] ?? null;
|
|
218
407
|
}
|
|
408
|
+
async queryBatch(reads) {
|
|
409
|
+
const results = [];
|
|
410
|
+
for (const read of reads) {
|
|
411
|
+
results.push(await this.query(read.sql, read.params));
|
|
412
|
+
}
|
|
413
|
+
return results;
|
|
414
|
+
}
|
|
219
415
|
async run(sql, params) {
|
|
220
416
|
this.ensureOpen();
|
|
221
|
-
this.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
417
|
+
this.touchTransaction();
|
|
418
|
+
const stmt = this.getCachedStmt(sql);
|
|
419
|
+
if (stmt === null) {
|
|
420
|
+
this.db.exec({
|
|
421
|
+
sql,
|
|
422
|
+
bind: params
|
|
423
|
+
});
|
|
424
|
+
} else {
|
|
425
|
+
try {
|
|
426
|
+
if (params && params.length > 0) {
|
|
427
|
+
stmt.bind(params);
|
|
428
|
+
}
|
|
429
|
+
while (stmt.step()) {
|
|
430
|
+
}
|
|
431
|
+
} finally {
|
|
432
|
+
stmt.reset();
|
|
433
|
+
stmt.clearBindings();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
225
436
|
return {
|
|
226
437
|
changes: this.sqlite3.capi.sqlite3_changes(this.db.pointer),
|
|
227
438
|
lastInsertRowid: this.sqlite3.capi.sqlite3_last_insert_rowid(this.db.pointer)
|
|
@@ -229,6 +440,7 @@ var WebSQLiteAdapter = class {
|
|
|
229
440
|
}
|
|
230
441
|
async exec(sql) {
|
|
231
442
|
this.ensureOpen();
|
|
443
|
+
this.stmts.clear();
|
|
232
444
|
this.execSync(sql);
|
|
233
445
|
}
|
|
234
446
|
execSync(sql) {
|
|
@@ -391,6 +603,7 @@ var WebSQLiteAdapter = class {
|
|
|
391
603
|
}
|
|
392
604
|
this.execSync("BEGIN IMMEDIATE");
|
|
393
605
|
this.inTransaction = true;
|
|
606
|
+
this.lastTransactionActivityAt = nowMs();
|
|
394
607
|
}
|
|
395
608
|
async commit() {
|
|
396
609
|
if (!this.inTransaction) {
|
|
@@ -471,6 +684,10 @@ var WebSQLiteAdapter = class {
|
|
|
471
684
|
async vacuum() {
|
|
472
685
|
await this.exec("VACUUM");
|
|
473
686
|
}
|
|
687
|
+
async incrementalVacuum(maxPages) {
|
|
688
|
+
this.ensureOpen();
|
|
689
|
+
return stepIncrementalVacuumToCompletion(this.db, maxPages);
|
|
690
|
+
}
|
|
474
691
|
async checkpoint() {
|
|
475
692
|
return 0;
|
|
476
693
|
}
|
|
@@ -480,15 +697,31 @@ var WebSQLiteAdapter = class {
|
|
|
480
697
|
}
|
|
481
698
|
}
|
|
482
699
|
};
|
|
700
|
+
function stepIncrementalVacuumToCompletion(db, maxPages) {
|
|
701
|
+
const limit = maxPages !== void 0 && maxPages > 0 ? Math.floor(maxPages) : Infinity;
|
|
702
|
+
const stmt = db.prepare("PRAGMA incremental_vacuum");
|
|
703
|
+
let freed = 0;
|
|
704
|
+
try {
|
|
705
|
+
while (freed < limit && stmt.step()) {
|
|
706
|
+
freed++;
|
|
707
|
+
}
|
|
708
|
+
} finally {
|
|
709
|
+
stmt.finalize();
|
|
710
|
+
}
|
|
711
|
+
return freed;
|
|
712
|
+
}
|
|
483
713
|
async function createWebSQLiteAdapter(config) {
|
|
484
714
|
const adapter = new WebSQLiteAdapter();
|
|
485
715
|
await adapter.open(config);
|
|
716
|
+
const schemaStartedAt = nowMs();
|
|
486
717
|
await adapter.applySchema(SCHEMA_VERSION, SCHEMA_DDL);
|
|
718
|
+
adapter.schemaApplyMs = Math.round(nowMs() - schemaStartedAt);
|
|
487
719
|
return adapter;
|
|
488
720
|
}
|
|
489
721
|
|
|
490
722
|
export {
|
|
491
723
|
resetWebSQLiteOpfsStorage,
|
|
492
724
|
WebSQLiteAdapter,
|
|
725
|
+
stepIncrementalVacuumToCompletion,
|
|
493
726
|
createWebSQLiteAdapter
|
|
494
727
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/adapters/boot-log-bridge.ts
|
|
2
|
+
var BOOT_LOG_KEY = "__xnetSqliteBootLog";
|
|
3
|
+
function bootLogMessage(args) {
|
|
4
|
+
return { [BOOT_LOG_KEY]: args };
|
|
5
|
+
}
|
|
6
|
+
function readBootLogArgs(data) {
|
|
7
|
+
if (typeof data === "object" && data !== null && Array.isArray(data[BOOT_LOG_KEY])) {
|
|
8
|
+
return data[BOOT_LOG_KEY];
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
bootLogMessage,
|
|
15
|
+
readBootLogArgs
|
|
16
|
+
};
|
|
@@ -247,6 +247,25 @@ function escapeHtml(text) {
|
|
|
247
247
|
div.textContent = text;
|
|
248
248
|
return div.innerHTML;
|
|
249
249
|
}
|
|
250
|
+
var MEMORY_FALLBACK_COUNT_KEY = "xnet:sqlite:memory-fallback-count";
|
|
251
|
+
function recordMemoryFallbackSession() {
|
|
252
|
+
try {
|
|
253
|
+
const count = getMemoryFallbackSessionCount() + 1;
|
|
254
|
+
localStorage.setItem(MEMORY_FALLBACK_COUNT_KEY, String(count));
|
|
255
|
+
return count;
|
|
256
|
+
} catch {
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function getMemoryFallbackSessionCount() {
|
|
261
|
+
try {
|
|
262
|
+
const raw = localStorage.getItem(MEMORY_FALLBACK_COUNT_KEY);
|
|
263
|
+
const parsed = raw === null ? 0 : Number.parseInt(raw, 10);
|
|
264
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
265
|
+
} catch {
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
250
269
|
|
|
251
270
|
export {
|
|
252
271
|
checkBrowserSupport,
|
|
@@ -254,5 +273,7 @@ export {
|
|
|
254
273
|
isSilentPersistRequestSafe,
|
|
255
274
|
watchPersistentStoragePermission,
|
|
256
275
|
requestPersistentStorage,
|
|
257
|
-
showUnsupportedBrowserMessage
|
|
276
|
+
showUnsupportedBrowserMessage,
|
|
277
|
+
recordMemoryFallbackSession,
|
|
278
|
+
getMemoryFallbackSessionCount
|
|
258
279
|
};
|