@xnetjs/sqlite 0.0.2 → 0.1.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.
Files changed (37) hide show
  1. package/dist/{adapter-I7yAV6iu.d.ts → adapter-imtxkmXZ.d.ts} +48 -1
  2. package/dist/adapters/electron.d.ts +70 -6
  3. package/dist/adapters/electron.js +662 -36
  4. package/dist/adapters/expo.d.ts +6 -2
  5. package/dist/adapters/expo.js +34 -6
  6. package/dist/adapters/memory.d.ts +6 -2
  7. package/dist/adapters/memory.js +8 -1
  8. package/dist/adapters/reader-thread.d.ts +78 -0
  9. package/dist/adapters/reader-thread.js +57 -0
  10. package/dist/adapters/web-proxy.d.ts +71 -3
  11. package/dist/adapters/web-proxy.js +554 -39
  12. package/dist/adapters/web-router-worker.d.ts +76 -0
  13. package/dist/adapters/web-router-worker.js +91 -0
  14. package/dist/adapters/web-worker.d.ts +56 -1
  15. package/dist/adapters/web-worker.js +253 -14
  16. package/dist/adapters/web.d.ts +90 -3
  17. package/dist/adapters/web.js +10 -4
  18. package/dist/browser-support.d.ts +65 -1
  19. package/dist/browser-support.js +15 -3
  20. package/dist/chunk-5HC5V73T.js +191 -0
  21. package/dist/chunk-BBZDKLA3.js +727 -0
  22. package/dist/chunk-CBU263LI.js +30 -0
  23. package/dist/chunk-CGI2YBZY.js +16 -0
  24. package/dist/chunk-S6MT6KCI.js +279 -0
  25. package/dist/chunk-SV475UL5.js +44 -0
  26. package/dist/chunk-W3L4FXU5.js +415 -0
  27. package/dist/index.d.ts +111 -8
  28. package/dist/index.js +219 -130
  29. package/dist/schema-C4gufY3B.d.ts +35 -0
  30. package/dist/types-BTabr_VP.d.ts +225 -0
  31. package/dist/worker-scheduler-D04DqMmR.d.ts +56 -0
  32. package/package.json +5 -1
  33. package/dist/chunk-BXYZU3OL.js +0 -245
  34. package/dist/chunk-HIREU5S5.js +0 -193
  35. package/dist/chunk-ZRR5D2OD.js +0 -140
  36. package/dist/schema-CjkXTqxn.d.ts +0 -35
  37. package/dist/types-C_aHfRDF.d.ts +0 -42
@@ -1,5 +1,5 @@
1
- import { S as SQLiteAdapter, P as PreparedStatement } from '../adapter-I7yAV6iu.js';
2
- import { b as SQLiteConfig, a as SQLRow, S as SQLValue, R as RunResult } from '../types-C_aHfRDF.js';
1
+ import { S as SQLiteAdapter, P as PreparedStatement } from '../adapter-imtxkmXZ.js';
2
+ import { b as SQLiteConfig, a as SQLRow, S as SQLValue, c as SQLBatchRead, R as RunResult, l as SQLiteNodeBatchApplyInput, m as SQLiteNodeBatchApplyResult } from '../types-BTabr_VP.js';
3
3
 
4
4
  /**
5
5
  * @xnetjs/sqlite - Web SQLite adapter using @sqlite.org/sqlite-wasm
@@ -8,6 +8,37 @@ import { b as SQLiteConfig, a as SQLRow, S as SQLValue, R as RunResult } from '.
8
8
  * Must run in a Web Worker for OPFS access.
9
9
  */
10
10
 
11
+ /**
12
+ * Per-phase timing of {@link WebSQLiteAdapter.open}. Every prior cold-open
13
+ * exploration (0204→0249) timed the layer the stall was *in*, so the cost kept
14
+ * hopping to the next un-timed layer; by the 0253 capture both `queueMs` and
15
+ * `execMs` read 0 and the ~17 s lived entirely in this `open()` window, which no
16
+ * timer bracketed. These fields split it so the dominant sub-phase is named in
17
+ * one boot-log line. All durations are milliseconds, rounded.
18
+ */
19
+ interface OpenPhaseTimings {
20
+ /** Dynamic `import('@sqlite.org/sqlite-wasm')` (bundle parse/download). */
21
+ wasmImportMs: number;
22
+ /** `sqlite3InitModule()` — WASM instantiate. */
23
+ wasmInitMs: number;
24
+ /** `installOpfsSAHPoolVfs()` — acquires the pool's sync access handles; INCLUDES any lock-retry backoff. */
25
+ vfsInstallMs: number;
26
+ /** `reserveMinimumCapacity()` — may acquire more handles / grow the pool. */
27
+ reserveCapacityMs: number;
28
+ /** `new OpfsSAHPoolDb()` — open the database file in the pool. */
29
+ dbOpenMs: number;
30
+ /** All post-open `PRAGMA` settings (page_size/cache/mmap/journal/…). */
31
+ pragmasMs: number;
32
+ /** Whole `open()` span (import → last pragma). */
33
+ totalOpenMs: number;
34
+ }
35
+ /**
36
+ * Remove xNet's OPFS-backed SQLite storage for the supplied database path.
37
+ *
38
+ * This must run in a worker because the SAH-pool VFS uses synchronous OPFS
39
+ * handles. It is intentionally scoped to xNet's SQLite VFS directory.
40
+ */
41
+ declare function resetWebSQLiteOpfsStorage(config: SQLiteConfig): Promise<void>;
11
42
  /**
12
43
  * SQLite adapter for web browsers using @sqlite.org/sqlite-wasm.
13
44
  *
@@ -33,17 +64,58 @@ declare class WebSQLiteAdapter implements SQLiteAdapter {
33
64
  private poolUtil;
34
65
  private _config;
35
66
  private inTransaction;
67
+ /**
68
+ * Monotonic time of the open transaction's last operation. Lets the worker
69
+ * host distinguish a LIVE manual transaction from one abandoned by a client
70
+ * that died between BEGIN and COMMIT (PowerSync-style lease recovery, 0263).
71
+ */
72
+ private lastTransactionActivityAt;
36
73
  private storageMode;
74
+ /**
75
+ * Per-phase timing of the last {@link open} call, or null until it runs.
76
+ * Read by the worker host to emit `[xNet] sqlite open phases` (exploration
77
+ * 0253). Diagnostic only — never affects behaviour.
78
+ */
79
+ private openPhaseTimings;
80
+ /** ms spent in `applySchema()` during {@link createWebSQLiteAdapter}, for the open-phases line. */
81
+ schemaApplyMs: number;
82
+ /** How many times the OPFS lock-retry backoff fired during the last {@link open} (0 = first try). */
83
+ openRetryAttempts: number;
84
+ /**
85
+ * Prepared-statement LRU for the hot query/run path (exploration 0263).
86
+ * `db.exec()` re-parses the SQL on every call; the hot path repeats a small
87
+ * statement set, so cached `oo1.Stmt` handles skip the parse+prepare cost.
88
+ * Cleared (finalizing all handles) on {@link exec} — the DDL/script path —
89
+ * and on {@link close}.
90
+ */
91
+ private stmts;
92
+ /** Per-phase timing of the last {@link open}, or null if it hasn't run. */
93
+ getOpenPhaseTimings(): OpenPhaseTimings | null;
37
94
  open(config: SQLiteConfig): Promise<void>;
38
95
  close(): Promise<void>;
39
96
  isOpen(): boolean;
40
97
  getStorageMode(): 'opfs' | 'memory';
98
+ /** True while a manual BEGIN…COMMIT/ROLLBACK transaction is open. */
99
+ isInTransaction(): boolean;
100
+ /** ms since the open transaction last executed an operation (0 when none). */
101
+ transactionIdleMs(): number;
102
+ /** Stamp transaction liveness — called by ops that run inside one. */
103
+ private touchTransaction;
104
+ /**
105
+ * Fetch (or prepare and cache) the statement for `sql`, or null when the SQL
106
+ * must bypass the cache: `db.prepare()` compiles only the FIRST statement of
107
+ * a multi-statement string, whereas `db.exec()` runs them all — serving such
108
+ * SQL from the cache would silently drop statements.
109
+ */
110
+ private getCachedStmt;
41
111
  query<T extends SQLRow = SQLRow>(sql: string, params?: SQLValue[]): Promise<T[]>;
42
112
  queryOne<T extends SQLRow = SQLRow>(sql: string, params?: SQLValue[]): Promise<T | null>;
113
+ queryBatch(reads: SQLBatchRead[]): Promise<SQLRow[][]>;
43
114
  run(sql: string, params?: SQLValue[]): Promise<RunResult>;
44
115
  exec(sql: string): Promise<void>;
45
116
  private execSync;
46
117
  transaction<T>(fn: () => Promise<T>): Promise<T>;
118
+ applyNodeBatch(input: SQLiteNodeBatchApplyInput): Promise<SQLiteNodeBatchApplyResult>;
47
119
  beginTransaction(): Promise<void>;
48
120
  commit(): Promise<void>;
49
121
  rollback(): Promise<void>;
@@ -53,12 +125,27 @@ declare class WebSQLiteAdapter implements SQLiteAdapter {
53
125
  applySchema(version: number, sql: string): Promise<boolean>;
54
126
  getDatabaseSize(): Promise<number>;
55
127
  vacuum(): Promise<void>;
128
+ incrementalVacuum(maxPages?: number): Promise<number>;
56
129
  checkpoint(): Promise<number>;
57
130
  private ensureOpen;
58
131
  }
132
+ /**
133
+ * Step `PRAGMA incremental_vacuum` to completion (or `maxPages`), returning
134
+ * the number of pages freed. SQLite frees ONE page per `sqlite3_step` of this
135
+ * pragma; the oo1 `exec` path steps a statement only once when it collects no
136
+ * rows, so `exec('PRAGMA incremental_vacuum')` silently freed a single page
137
+ * per call — the compaction reclaim was a near-no-op (caught by the local
138
+ * bloat-seed validation, 2026-07-05). Exported for the engine-level test.
139
+ */
140
+ declare function stepIncrementalVacuumToCompletion(db: {
141
+ prepare(sql: string): {
142
+ step(): boolean;
143
+ finalize(): unknown;
144
+ };
145
+ }, maxPages?: number): number;
59
146
  /**
60
147
  * Create a WebSQLiteAdapter with schema applied.
61
148
  */
62
149
  declare function createWebSQLiteAdapter(config: SQLiteConfig): Promise<WebSQLiteAdapter>;
63
150
 
64
- export { WebSQLiteAdapter, createWebSQLiteAdapter };
151
+ export { type OpenPhaseTimings, WebSQLiteAdapter, createWebSQLiteAdapter, resetWebSQLiteOpfsStorage, stepIncrementalVacuumToCompletion };
@@ -1,9 +1,15 @@
1
1
  import {
2
2
  WebSQLiteAdapter,
3
- createWebSQLiteAdapter
4
- } from "../chunk-BXYZU3OL.js";
5
- import "../chunk-HIREU5S5.js";
3
+ createWebSQLiteAdapter,
4
+ resetWebSQLiteOpfsStorage,
5
+ stepIncrementalVacuumToCompletion
6
+ } from "../chunk-BBZDKLA3.js";
7
+ import "../chunk-SV475UL5.js";
8
+ import "../chunk-CBU263LI.js";
9
+ import "../chunk-W3L4FXU5.js";
6
10
  export {
7
11
  WebSQLiteAdapter,
8
- createWebSQLiteAdapter
12
+ createWebSQLiteAdapter,
13
+ resetWebSQLiteOpfsStorage,
14
+ stepIncrementalVacuumToCompletion
9
15
  };
@@ -16,6 +16,30 @@ interface BrowserSupport {
16
16
  /** Warning message for soft failures (app can still work with fallback) */
17
17
  warning?: string;
18
18
  }
19
+ interface PersistentStorageStatus {
20
+ /** Browser exposes persistence APIs. */
21
+ supported: boolean;
22
+ /** Whether the site is currently persisted. */
23
+ persisted: boolean | null;
24
+ /** Whether the explicit persistence request was granted. */
25
+ granted: boolean | null;
26
+ /** Whether this status was produced by an explicit persistence request. */
27
+ requested: boolean;
28
+ /** Whether the app can present a user action to request persistence. */
29
+ requestable: boolean;
30
+ /** Durable-storage state for UI and diagnostics. */
31
+ state: 'granted' | 'not-granted' | 'unsupported' | 'error';
32
+ /** User-facing explanation of the result. */
33
+ message: string;
34
+ /** Optional usage estimate in bytes. */
35
+ usageBytes?: number;
36
+ /** Optional quota estimate in bytes. */
37
+ quotaBytes?: number;
38
+ }
39
+ interface PersistentStorageRequestOptions {
40
+ /** Request persistent mode instead of only checking current state. */
41
+ request?: boolean;
42
+ }
19
43
  /**
20
44
  * Check if the current browser supports SQLite-WASM with OPFS.
21
45
  *
@@ -46,6 +70,37 @@ interface BrowserSupport {
46
70
  * ```
47
71
  */
48
72
  declare function checkBrowserSupport(): Promise<BrowserSupport>;
73
+ /**
74
+ * Check whether persistent storage is already enabled without prompting or
75
+ * spending a browser heuristic-based permission request during startup.
76
+ */
77
+ declare function checkPersistentStorage(): Promise<PersistentStorageStatus>;
78
+ /**
79
+ * Whether `navigator.storage.persist()` can be called without showing the
80
+ * user a permission prompt.
81
+ *
82
+ * Chromium and WebKit decide the request silently from heuristics (install,
83
+ * notification permission, engagement) and re-evaluate it fresh on every
84
+ * call, so requesting at startup is free. Gecko (desktop Firefox) shows a
85
+ * modal doorhanger, so requests there must stay behind a user gesture.
86
+ * Firefox on iOS (FxiOS) runs WebKit, but we treat anything Firefox-branded
87
+ * as prompt-capable — the worst case is skipping a free request.
88
+ */
89
+ declare function isSilentPersistRequestSafe(): boolean;
90
+ /**
91
+ * Watch the `persistent-storage` permission for changes (e.g. a grant that
92
+ * lands mid-session after the user enables notifications or installs the
93
+ * app). Querying is free — it never spends or triggers a request.
94
+ *
95
+ * Calls `onChange` with the new state whenever the browser reports a
96
+ * change. No-op on browsers without the Permissions API or the
97
+ * `persistent-storage` permission name. Returns an unsubscribe function.
98
+ */
99
+ declare function watchPersistentStoragePermission(onChange: (state: PermissionState) => void): () => void;
100
+ /**
101
+ * Request persistent storage where supported and summarize the result.
102
+ */
103
+ declare function requestPersistentStorage(options?: PersistentStorageRequestOptions): Promise<PersistentStorageStatus>;
49
104
  /**
50
105
  * Show an unsupported browser message to the user.
51
106
  *
@@ -63,5 +118,14 @@ declare function checkBrowserSupport(): Promise<BrowserSupport>;
63
118
  * ```
64
119
  */
65
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;
66
130
 
67
- export { type BrowserSupport, checkBrowserSupport, showUnsupportedBrowserMessage };
131
+ export { type BrowserSupport, type PersistentStorageRequestOptions, type PersistentStorageStatus, checkBrowserSupport, checkPersistentStorage, getMemoryFallbackSessionCount, isSilentPersistRequestSafe, recordMemoryFallbackSession, requestPersistentStorage, showUnsupportedBrowserMessage, watchPersistentStoragePermission };
@@ -1,8 +1,20 @@
1
1
  import {
2
2
  checkBrowserSupport,
3
- showUnsupportedBrowserMessage
4
- } from "./chunk-ZRR5D2OD.js";
3
+ checkPersistentStorage,
4
+ getMemoryFallbackSessionCount,
5
+ isSilentPersistRequestSafe,
6
+ recordMemoryFallbackSession,
7
+ requestPersistentStorage,
8
+ showUnsupportedBrowserMessage,
9
+ watchPersistentStoragePermission
10
+ } from "./chunk-S6MT6KCI.js";
5
11
  export {
6
12
  checkBrowserSupport,
7
- showUnsupportedBrowserMessage
13
+ checkPersistentStorage,
14
+ getMemoryFallbackSessionCount,
15
+ isSilentPersistRequestSafe,
16
+ recordMemoryFallbackSession,
17
+ requestPersistentStorage,
18
+ showUnsupportedBrowserMessage,
19
+ watchPersistentStoragePermission
8
20
  };
@@ -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
+ };