@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.
- package/dist/{adapter-I7yAV6iu.d.ts → adapter-imtxkmXZ.d.ts} +48 -1
- package/dist/adapters/electron.d.ts +70 -6
- package/dist/adapters/electron.js +662 -36
- package/dist/adapters/expo.d.ts +6 -2
- package/dist/adapters/expo.js +34 -6
- 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 +71 -3
- package/dist/adapters/web-proxy.js +554 -39
- 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 +56 -1
- package/dist/adapters/web-worker.js +253 -14
- package/dist/adapters/web.d.ts +90 -3
- package/dist/adapters/web.js +10 -4
- package/dist/browser-support.d.ts +65 -1
- package/dist/browser-support.js +15 -3
- package/dist/chunk-5HC5V73T.js +191 -0
- package/dist/chunk-BBZDKLA3.js +727 -0
- package/dist/chunk-CBU263LI.js +30 -0
- package/dist/chunk-CGI2YBZY.js +16 -0
- package/dist/chunk-S6MT6KCI.js +279 -0
- package/dist/chunk-SV475UL5.js +44 -0
- package/dist/chunk-W3L4FXU5.js +415 -0
- package/dist/index.d.ts +111 -8
- package/dist/index.js +219 -130
- 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/chunk-BXYZU3OL.js +0 -245
- package/dist/chunk-HIREU5S5.js +0 -193
- package/dist/chunk-ZRR5D2OD.js +0 -140
- package/dist/schema-CjkXTqxn.d.ts +0 -35
- package/dist/types-C_aHfRDF.d.ts +0 -42
package/dist/adapters/web.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as SQLiteAdapter, P as PreparedStatement } from '../adapter-
|
|
2
|
-
import { b as SQLiteConfig, a as SQLRow, S as SQLValue, R as RunResult } from '../types-
|
|
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 };
|
package/dist/adapters/web.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
WebSQLiteAdapter,
|
|
3
|
-
createWebSQLiteAdapter
|
|
4
|
-
|
|
5
|
-
|
|
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 };
|
package/dist/browser-support.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
checkBrowserSupport,
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
+
};
|