@vibesdotdev/localdb 0.0.2 → 1.0.0-rc.2
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/LICENSE +18 -0
- package/README.md +123 -2
- package/dist/cli/commands/_shared.consumer.d.ts +1 -3
- package/dist/cli/commands/_shared.consumer.d.ts.map +1 -1
- package/dist/cli/commands/_shared.consumer.js +1 -11
- package/dist/cli/commands/_shared.consumer.js.map +1 -1
- package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.descriptor.js +1 -1
- package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.descriptor.js.map +1 -1
- package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.impl.consumer.d.ts +9 -3
- package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.impl.consumer.d.ts.map +1 -1
- package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.impl.consumer.js +59 -45
- package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.impl.consumer.js.map +1 -1
- package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.descriptor.js +1 -1
- package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.descriptor.js.map +1 -1
- package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.impl.d.ts +8 -3
- package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.impl.d.ts.map +1 -1
- package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.impl.js +12 -10
- package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.impl.js.map +1 -1
- package/dist/cli/commands/pools/dev.localdb.pools.cli-command.descriptor.js +1 -1
- package/dist/cli/commands/pools/dev.localdb.pools.cli-command.descriptor.js.map +1 -1
- package/dist/cli/commands/pools/dev.localdb.pools.cli-command.impl.d.ts +6 -3
- package/dist/cli/commands/pools/dev.localdb.pools.cli-command.impl.d.ts.map +1 -1
- package/dist/cli/commands/pools/dev.localdb.pools.cli-command.impl.js +8 -7
- package/dist/cli/commands/pools/dev.localdb.pools.cli-command.impl.js.map +1 -1
- package/dist/cli/commands/query/dev.localdb.query.cli-command.descriptor.js +1 -1
- package/dist/cli/commands/query/dev.localdb.query.cli-command.descriptor.js.map +1 -1
- package/dist/cli/commands/query/dev.localdb.query.cli-command.impl.consumer.d.ts +13 -3
- package/dist/cli/commands/query/dev.localdb.query.cli-command.impl.consumer.d.ts.map +1 -1
- package/dist/cli/commands/query/dev.localdb.query.cli-command.impl.consumer.js +66 -51
- package/dist/cli/commands/query/dev.localdb.query.cli-command.impl.consumer.js.map +1 -1
- package/dist/cli/commands/status/dev.localdb.status.cli-command.descriptor.js +1 -1
- package/dist/cli/commands/status/dev.localdb.status.cli-command.descriptor.js.map +1 -1
- package/dist/cli/commands/status/dev.localdb.status.cli-command.impl.consumer.d.ts +9 -3
- package/dist/cli/commands/status/dev.localdb.status.cli-command.impl.consumer.d.ts.map +1 -1
- package/dist/cli/commands/status/dev.localdb.status.cli-command.impl.consumer.js +41 -29
- package/dist/cli/commands/status/dev.localdb.status.cli-command.impl.consumer.js.map +1 -1
- package/dist/cli/inputs.d.ts +25 -0
- package/dist/cli/inputs.d.ts.map +1 -0
- package/dist/cli/inputs.js +81 -0
- package/dist/cli/inputs.js.map +1 -0
- package/dist/cli/types.d.ts +45 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +14 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/core/connection-pool.d.ts +204 -46
- package/dist/core/connection-pool.d.ts.map +1 -1
- package/dist/core/connection-pool.js +610 -101
- package/dist/core/connection-pool.js.map +1 -1
- package/dist/core/database.d.ts +243 -63
- package/dist/core/database.d.ts.map +1 -1
- package/dist/core/database.js +937 -203
- package/dist/core/database.js.map +1 -1
- package/dist/core/holders.d.ts +15 -0
- package/dist/core/holders.d.ts.map +1 -0
- package/dist/core/holders.js +4 -0
- package/dist/core/holders.js.map +1 -0
- package/dist/core/host-kinds.d.ts +22 -0
- package/dist/core/host-kinds.d.ts.map +1 -0
- package/dist/core/host-kinds.js +52 -0
- package/dist/core/host-kinds.js.map +1 -0
- package/dist/core/live-runtime.d.ts +17 -0
- package/dist/core/live-runtime.d.ts.map +1 -0
- package/dist/core/live-runtime.js +28 -0
- package/dist/core/live-runtime.js.map +1 -0
- package/dist/core/migration-registry.d.ts +115 -80
- package/dist/core/migration-registry.d.ts.map +1 -1
- package/dist/core/migration-registry.js +340 -199
- package/dist/core/migration-registry.js.map +1 -1
- package/dist/core/open-connection.d.ts +11 -0
- package/dist/core/open-connection.d.ts.map +1 -0
- package/dist/core/open-connection.js +70 -0
- package/dist/core/open-connection.js.map +1 -0
- package/dist/core/outcome.d.ts +21 -0
- package/dist/core/outcome.d.ts.map +1 -0
- package/dist/core/outcome.js +22 -0
- package/dist/core/outcome.js.map +1 -0
- package/dist/core/pool-api.d.ts +19 -0
- package/dist/core/pool-api.d.ts.map +1 -0
- package/dist/core/pool-api.js +59 -0
- package/dist/core/pool-api.js.map +1 -0
- package/dist/core/slow-statement.d.ts +81 -0
- package/dist/core/slow-statement.d.ts.map +1 -0
- package/dist/core/slow-statement.js +149 -0
- package/dist/core/slow-statement.js.map +1 -0
- package/dist/core/wal-policy.d.ts +86 -0
- package/dist/core/wal-policy.d.ts.map +1 -0
- package/dist/core/wal-policy.js +115 -0
- package/dist/core/wal-policy.js.map +1 -0
- package/dist/dev.localdb.cli-group.descriptor.d.ts +1 -1
- package/dist/dev.localdb.cli-group.descriptor.d.ts.map +1 -1
- package/dist/dev.localdb.context.descriptor.d.ts +15 -1
- package/dist/dev.localdb.context.descriptor.d.ts.map +1 -1
- package/dist/dev.localdb.context.descriptor.js +1 -1
- package/dist/dev.localdb.context.descriptor.js.map +1 -1
- package/dist/dev.localdb.context.impl.consumer.d.ts +12 -5
- package/dist/dev.localdb.context.impl.consumer.d.ts.map +1 -1
- package/dist/dev.localdb.context.impl.consumer.js +14 -5
- package/dist/dev.localdb.context.impl.consumer.js.map +1 -1
- package/dist/driver/sqlite-driver.base.d.ts +107 -0
- package/dist/driver/sqlite-driver.base.d.ts.map +1 -0
- package/dist/driver/sqlite-driver.base.js +426 -0
- package/dist/driver/sqlite-driver.base.js.map +1 -0
- package/dist/driver/sqlite-driver.d.ts +154 -0
- package/dist/driver/sqlite-driver.d.ts.map +1 -0
- package/dist/driver/sqlite-driver.js +103 -0
- package/dist/driver/sqlite-driver.js.map +1 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/localdb.client.descriptor.d.ts +10 -0
- package/dist/localdb.client.descriptor.d.ts.map +1 -0
- package/dist/localdb.client.descriptor.js +13 -0
- package/dist/localdb.client.descriptor.js.map +1 -0
- package/dist/localdb.client.impl.consumer.d.ts +11 -0
- package/dist/localdb.client.impl.consumer.d.ts.map +1 -0
- package/dist/localdb.client.impl.consumer.js +11 -0
- package/dist/localdb.client.impl.consumer.js.map +1 -0
- package/dist/localdb.plugin.d.ts +2 -2
- package/dist/localdb.plugin.d.ts.map +1 -1
- package/dist/localdb.plugin.js +58 -10
- package/dist/localdb.plugin.js.map +1 -1
- package/dist/location.d.ts +6 -0
- package/dist/location.d.ts.map +1 -0
- package/dist/location.js +6 -0
- package/dist/location.js.map +1 -0
- package/dist/migrations/load-sql.d.ts +25 -21
- package/dist/migrations/load-sql.d.ts.map +1 -1
- package/dist/migrations/load-sql.js +91 -76
- package/dist/migrations/load-sql.js.map +1 -1
- package/dist/pool.d.ts +3 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +3 -0
- package/dist/pool.js.map +1 -0
- package/dist/schemas/api.d.ts +133 -59
- package/dist/schemas/api.d.ts.map +1 -1
- package/dist/schemas/api.js +1 -1
- package/dist/schemas/api.js.map +1 -1
- package/dist/schemas/migrations.d.ts +123 -0
- package/dist/schemas/migrations.d.ts.map +1 -0
- package/dist/schemas/migrations.js +55 -0
- package/dist/schemas/migrations.js.map +1 -0
- package/dist/schemas/pool.d.ts +27 -0
- package/dist/schemas/pool.d.ts.map +1 -0
- package/dist/schemas/pool.js +7 -0
- package/dist/schemas/pool.js.map +1 -0
- package/dist/schemas/sqlite-driver-base.d.ts +50 -0
- package/dist/schemas/sqlite-driver-base.d.ts.map +1 -0
- package/dist/schemas/sqlite-driver-base.js +2 -0
- package/dist/schemas/sqlite-driver-base.js.map +1 -0
- package/dist/testing/sqlite-driver-conformance.d.ts +42 -0
- package/dist/testing/sqlite-driver-conformance.d.ts.map +1 -0
- package/dist/testing/sqlite-driver-conformance.js +471 -0
- package/dist/testing/sqlite-driver-conformance.js.map +1 -0
- package/package.json +78 -104
- package/dist/core/error-context.d.ts +0 -2
- package/dist/core/error-context.d.ts.map +0 -1
- package/dist/core/error-context.js +0 -17
- package/dist/core/error-context.js.map +0 -1
- package/dist/core/runtime.d.ts +0 -3
- package/dist/core/runtime.d.ts.map +0 -1
- package/dist/core/runtime.js +0 -17
- package/dist/core/runtime.js.map +0 -1
- package/dist/localdb.cloud.plugin.d.ts +0 -17
- package/dist/localdb.cloud.plugin.d.ts.map +0 -1
- package/dist/localdb.cloud.plugin.js +0 -23
- package/dist/localdb.cloud.plugin.js.map +0 -1
- package/src/cli/commands/_shared.consumer.ts +0 -20
- package/src/cli/commands/migrate/dev.localdb.migrate.cli-command.descriptor.ts +0 -20
- package/src/cli/commands/migrate/dev.localdb.migrate.cli-command.impl.consumer.ts +0 -97
- package/src/cli/commands/namespaces/dev.localdb.namespaces.cli-command.descriptor.ts +0 -17
- package/src/cli/commands/namespaces/dev.localdb.namespaces.cli-command.impl.ts +0 -46
- package/src/cli/commands/pools/dev.localdb.pools.cli-command.descriptor.ts +0 -16
- package/src/cli/commands/pools/dev.localdb.pools.cli-command.impl.ts +0 -37
- package/src/cli/commands/query/dev.localdb.query.cli-command.descriptor.ts +0 -21
- package/src/cli/commands/query/dev.localdb.query.cli-command.impl.consumer.ts +0 -69
- package/src/cli/commands/status/dev.localdb.status.cli-command.descriptor.ts +0 -20
- package/src/cli/commands/status/dev.localdb.status.cli-command.impl.consumer.ts +0 -93
- package/src/core/connection-pool.ts +0 -240
- package/src/core/database.ts +0 -419
- package/src/core/error-context.ts +0 -19
- package/src/core/migration-registry.ts +0 -321
- package/src/core/runtime.ts +0 -17
- package/src/dev.localdb.cli-group.descriptor.ts +0 -20
- package/src/dev.localdb.context.descriptor.ts +0 -13
- package/src/dev.localdb.context.impl.consumer.ts +0 -12
- package/src/index.ts +0 -28
- package/src/localdb.cloud.plugin.ts +0 -24
- package/src/localdb.plugin.ts +0 -43
- package/src/migrations/atlas/001-initial-schema.sql +0 -173
- package/src/migrations/atlas/002-lang-server-fields.sql +0 -12
- package/src/migrations/atlas/003-config-id-dedup.sql +0 -31
- package/src/migrations/atlas/004-fix-on-conflict-constraints.sql +0 -25
- package/src/migrations/atlas/005-diagnostics.sql +0 -66
- package/src/migrations/atlas/006-diagnostic-summaries.sql +0 -65
- package/src/migrations/atlas/007-diagnostic-slice.sql +0 -83
- package/src/migrations/load-sql.ts +0 -133
- package/src/schemas/api.ts +0 -92
|
@@ -7,140 +7,534 @@
|
|
|
7
7
|
* Features:
|
|
8
8
|
* - Singleton pattern per database path
|
|
9
9
|
* - Automatic WAL mode and busy timeout configuration
|
|
10
|
+
* - A bounded WAL (D-WAL, see `wal-policy.ts`): `journal_size_limit`; one
|
|
11
|
+
* non-blocking checkpoint attempt per idle window (TRUNCATE, falling back to
|
|
12
|
+
* RESTART after a busy attempt) with busy attempts backing off by whole
|
|
13
|
+
* windows; an attempt on release to zero references and at close; observed
|
|
14
|
+
* `(busy, log, checkpointed)` results with busy outcomes logged; and a bound
|
|
15
|
+
* on how long a transaction may stay open
|
|
10
16
|
* - Connection reuse across consumers
|
|
11
|
-
* -
|
|
17
|
+
* - Slow-statement reporting: a statement at or over `SLOW_STATEMENT_MS` on any
|
|
18
|
+
* pooled connection is recorded with its operation and table, and the tick
|
|
19
|
+
* reports it (see `slow-statement.ts`) — value-free, and never from the
|
|
20
|
+
* statement's own path
|
|
21
|
+
* - Explicit, host-owned cleanup
|
|
12
22
|
* - Connection statistics and monitoring
|
|
23
|
+
*
|
|
24
|
+
* The pool never deletes a raw `-wal` or `-shm` file.
|
|
25
|
+
*
|
|
26
|
+
* Hosts sharing one pool have at most one connection per path: two connections to one
|
|
27
|
+
* file on the same thread can wait on each other inside SQLite's synchronous
|
|
28
|
+
* busy handler. A composition root that boots several runtimes in one process
|
|
29
|
+
* creates the pool explicitly (`createConnectionPool`) and hands it to every
|
|
30
|
+
* runtime it boots (`shareConnectionPool`), so those runtimes — including
|
|
31
|
+
* reload-generation forks, which inherit the registration — share the one
|
|
32
|
+
* connection per path. A process that boots a single runtime uses the
|
|
33
|
+
* runtime-owned default the LocalDB plugin registers instead. Each call
|
|
34
|
+
* reports on the runtime it was handed (C083a), never on an ambient default.
|
|
35
|
+
*
|
|
36
|
+
* Maintenance is owned by a runtime lifecycle (RC07). A connection records the
|
|
37
|
+
* runtimes that acquired it and runs its maintenance tick on the lifecycle of
|
|
38
|
+
* one of them that is live and persistent; when that lifecycle ends, the tick
|
|
39
|
+
* ends with it and moves to the next live persistent holder. The reload
|
|
40
|
+
* generation dispatcher reads whichever generation serves: it stays a holder
|
|
41
|
+
* while it serves a live one, so its tick follows it from a retiring
|
|
42
|
+
* generation to the next. A connection no persistent runtime
|
|
43
|
+
* holds gets no tick: it checkpoints on release to zero references and on
|
|
44
|
+
* close/shutdown instead, until a persistent runtime acquires it again.
|
|
13
45
|
*/
|
|
14
|
-
import {
|
|
46
|
+
import { CapabilityError, createRuntimeKind } from '@vibesdotdev/kernel';
|
|
15
47
|
import { getLogger } from '@vibesdotdev/logging';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
48
|
+
import { PoolDescriptorSchema } from "../schemas/pool.js";
|
|
49
|
+
export { PoolDescriptorSchema } from "../schemas/pool.js";
|
|
50
|
+
import { sqliteDriverOf } from "./host-kinds.js";
|
|
51
|
+
import { assertOpenedConnectionLive, openConfiguredDatabase } from "./open-connection.js";
|
|
52
|
+
import { createSlowStatementRecorder, instrumentConnection, slowStatementFields, SLOW_STATEMENT_MS } from "./slow-statement.js";
|
|
53
|
+
import { assertRuntimeLive } from "./live-runtime.js";
|
|
54
|
+
import { releasesHolder } from "./holders.js";
|
|
55
|
+
import { attempt } from "./outcome.js";
|
|
56
|
+
import { applyLocalDatabasePragmas, checkpointBackoffWindows, checkpointWal, MAX_TRANSACTION_AGE_MS, nextCheckpointMode, WAL_BUSY_STARVATION_THRESHOLD, WAL_MAINTENANCE_INTERVAL_MS } from "./wal-policy.js";
|
|
57
|
+
/** The pool's Logging on the runtime one call serves (C083a). */
|
|
58
|
+
function poolLogger(runtime) {
|
|
59
|
+
return getLogger('localdb:pool', runtime);
|
|
60
|
+
}
|
|
61
|
+
export class SQLiteConnectionPool {
|
|
19
62
|
connections = new Map();
|
|
20
|
-
|
|
63
|
+
/** Physical path custody spans every runtime-local manager using this pool. */
|
|
64
|
+
databasePathLocks = new Map();
|
|
21
65
|
STALE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
66
|
+
/** Opens in flight, by path: concurrent first acquisitions share one connection. */
|
|
67
|
+
opening = new Map();
|
|
68
|
+
/** Serialize managed initialization, migration, release and reset against the same physical file. */
|
|
69
|
+
async withDatabasePathLock(dbPath, task) {
|
|
70
|
+
const prior = this.databasePathLocks.get(dbPath) ?? Promise.resolve();
|
|
71
|
+
let release;
|
|
72
|
+
const current = new Promise(resolve => { release = resolve; });
|
|
73
|
+
const queued = prior.then(() => current);
|
|
74
|
+
this.databasePathLocks.set(dbPath, queued);
|
|
75
|
+
await prior;
|
|
76
|
+
try {
|
|
77
|
+
return await task();
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
release?.();
|
|
81
|
+
if (this.databasePathLocks.get(dbPath) === queued)
|
|
82
|
+
this.databasePathLocks.delete(dbPath);
|
|
83
|
+
}
|
|
28
84
|
}
|
|
29
85
|
/**
|
|
30
|
-
* Get or create a database connection with optimal configuration
|
|
86
|
+
* Get or create a database connection with optimal configuration, opened
|
|
87
|
+
* through the SQLite driver the calling runtime's host registered. The
|
|
88
|
+
* calling runtime becomes a holder of the connection and reports the open.
|
|
89
|
+
* A runtime whose disposal began is refused before any connection opens
|
|
90
|
+
* (`CANCELLED` `runtime-disposed`), and again if it began while the open
|
|
91
|
+
* was in flight. Cancellation before publication closes the startup handle.
|
|
31
92
|
*/
|
|
32
|
-
getConnection(dbPath, consumer = 'unknown') {
|
|
93
|
+
async getConnection(runtime, dbPath, consumer = 'unknown') {
|
|
94
|
+
const lifecycle = runtime.lifecycle;
|
|
95
|
+
assertRuntimeLive(runtime, 'connection', lifecycle);
|
|
33
96
|
let pooled = this.connections.get(dbPath);
|
|
97
|
+
let created = false;
|
|
34
98
|
if (!pooled) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
99
|
+
const pending = this.opening.get(dbPath);
|
|
100
|
+
if (pending) {
|
|
101
|
+
pooled = await pending;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const opening = this.open(runtime, dbPath, consumer);
|
|
105
|
+
this.opening.set(dbPath, opening);
|
|
106
|
+
try {
|
|
107
|
+
pooled = await opening;
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
this.opening.delete(dbPath);
|
|
111
|
+
}
|
|
112
|
+
created = true;
|
|
113
|
+
}
|
|
114
|
+
assertRuntimeLive(runtime, 'connection', lifecycle);
|
|
115
|
+
// Disposal may have swept an unheld open while this borrower awaited it.
|
|
116
|
+
// A live borrower acquires the current physical handle rather than a closed one.
|
|
117
|
+
if (this.connections.get(dbPath) !== pooled)
|
|
118
|
+
return this.getConnection(runtime, dbPath, consumer);
|
|
47
119
|
}
|
|
120
|
+
this.hold(runtime, dbPath, pooled);
|
|
48
121
|
pooled.refCount++;
|
|
49
122
|
pooled.lastUsed = Date.now();
|
|
123
|
+
if (created)
|
|
124
|
+
poolLogger(runtime).debug(`Created connection: ${dbPath} (${consumer})`);
|
|
50
125
|
return pooled.db;
|
|
51
126
|
}
|
|
127
|
+
/** Open and pool the connection at `dbPath` with the LocalDB pragmas, through the runtime's driver. */
|
|
128
|
+
async open(runtime, dbPath, consumer) {
|
|
129
|
+
const lifecycle = runtime.lifecycle;
|
|
130
|
+
const driver = await sqliteDriverOf(runtime);
|
|
131
|
+
const opened = await openConfiguredDatabase(runtime, () => driver.open(dbPath), applyLocalDatabasePragmas, lifecycle);
|
|
132
|
+
const slowStatements = createSlowStatementRecorder();
|
|
133
|
+
// Timed here, at the one place every statement in this process reaches, so
|
|
134
|
+
// the report names the consumer that opened the connection and the table
|
|
135
|
+
// that statement touched. Reported by the tick, never from here.
|
|
136
|
+
const db = instrumentConnection(opened, slowStatements);
|
|
137
|
+
const pooled = {
|
|
138
|
+
db,
|
|
139
|
+
slowStatements,
|
|
140
|
+
refCount: 0,
|
|
141
|
+
lastUsed: Date.now(),
|
|
142
|
+
createdAt: Date.now(),
|
|
143
|
+
consumer,
|
|
144
|
+
holders: new Set(),
|
|
145
|
+
consecutiveCheckpointBusy: 0,
|
|
146
|
+
nextCheckpointMode: 'TRUNCATE',
|
|
147
|
+
nextIdleCheckpointAt: Number.NEGATIVE_INFINITY
|
|
148
|
+
};
|
|
149
|
+
// Publication is synchronous with the captured-lifecycle check. A cancelled
|
|
150
|
+
// startup closes here and can never publish an unheld physical connection.
|
|
151
|
+
assertOpenedConnectionLive(runtime, opened, lifecycle);
|
|
152
|
+
this.connections.set(dbPath, pooled);
|
|
153
|
+
return pooled;
|
|
154
|
+
}
|
|
52
155
|
/**
|
|
53
|
-
*
|
|
156
|
+
* Record `runtime` as a holder of the pooled connection at `dbPath`, so its
|
|
157
|
+
* maintenance can run on (or move to) that runtime. No connection there:
|
|
158
|
+
* nothing to hold.
|
|
54
159
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
160
|
+
retain(runtime, dbPath) {
|
|
161
|
+
const pooled = this.connections.get(dbPath);
|
|
162
|
+
if (pooled)
|
|
163
|
+
this.hold(runtime, dbPath, pooled);
|
|
164
|
+
}
|
|
165
|
+
hold(runtime, dbPath, pooled) {
|
|
166
|
+
// A runtime that ended holds nothing: drop it so a long-lived connection
|
|
167
|
+
// never retains the runtimes that passed through it.
|
|
168
|
+
for (const holder of pooled.holders) {
|
|
169
|
+
if (holder.lifecycle.signal.aborted)
|
|
170
|
+
pooled.holders.delete(holder);
|
|
171
|
+
}
|
|
172
|
+
pooled.holders.add(runtime);
|
|
173
|
+
this.ensureMaintenance(dbPath, pooled);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Keep the connection's tick on a live persistent holder. A tick whose
|
|
177
|
+
* lifecycle (the one it started on) is still live, on a runtime that still
|
|
178
|
+
* holds the connection, stays where it is;
|
|
179
|
+
* otherwise the first live persistent holder takes it, and holders that
|
|
180
|
+
* ended are dropped.
|
|
181
|
+
*/
|
|
182
|
+
ensureMaintenance(dbPath, pooled) {
|
|
183
|
+
const current = pooled.maintenance;
|
|
184
|
+
// A live tick stays where it is while its runtime still holds the connection.
|
|
185
|
+
if (current && !current.signal.aborted && pooled.holders.has(current.runtime))
|
|
186
|
+
return;
|
|
187
|
+
current?.stop();
|
|
188
|
+
pooled.maintenance = undefined;
|
|
189
|
+
for (const holder of pooled.holders) {
|
|
190
|
+
if (holder.lifecycle.signal.aborted) {
|
|
191
|
+
pooled.holders.delete(holder);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
// A finite host starts no background work (`lifecycle.interval` refuses it).
|
|
195
|
+
if (holder.scope.lifecycle === 'finite')
|
|
196
|
+
continue;
|
|
197
|
+
pooled.maintenance = this.maintainOn(holder, dbPath, pooled);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Run the connection's tick on the lifecycle `runtime` reads now. The tick
|
|
203
|
+
* ends when that lifecycle ends (it clears every interval). At that moment
|
|
204
|
+
* the runtime is dropped as a holder unless it now reads a live lifecycle
|
|
205
|
+
* (the reload generation dispatcher, whose `lifecycle` follows whichever
|
|
206
|
+
* generation serves), and the tick moves to the next live persistent
|
|
207
|
+
* holder, that runtime included.
|
|
208
|
+
*/
|
|
209
|
+
maintainOn(runtime, dbPath, pooled) {
|
|
210
|
+
const { lifecycle } = runtime;
|
|
211
|
+
const { signal } = lifecycle;
|
|
212
|
+
const stopTick = lifecycle.interval(() => this.tick(runtime, dbPath, pooled), WAL_MAINTENANCE_INTERVAL_MS);
|
|
213
|
+
const moveOn = () => {
|
|
214
|
+
// ensureMaintenance drops the holders that ended (a runtime whose lifecycle follows
|
|
215
|
+
// another, the dispatcher, still holds while it reads a live one) and moves the tick on.
|
|
216
|
+
if (this.connections.get(dbPath) === pooled)
|
|
217
|
+
this.ensureMaintenance(dbPath, pooled);
|
|
218
|
+
};
|
|
219
|
+
signal.addEventListener('abort', moveOn, { once: true });
|
|
220
|
+
return {
|
|
221
|
+
runtime,
|
|
222
|
+
signal,
|
|
223
|
+
stop: () => {
|
|
224
|
+
stopTick();
|
|
225
|
+
signal.removeEventListener('abort', moveOn);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** One maintenance tick for one connection. It never throws into the timer: a failure is logged with its cause. */
|
|
230
|
+
tick(runtime, dbPath, pooled) {
|
|
231
|
+
const pass = attempt(() => this.maintainConnection(runtime, dbPath, pooled, Date.now()));
|
|
232
|
+
if (pass.failed) {
|
|
233
|
+
poolLogger(runtime).error('[LocalDB] Pool maintenance tick failed', {
|
|
234
|
+
reason: 'localdb-maintenance-failed',
|
|
235
|
+
dbPath,
|
|
236
|
+
consumer: pooled.consumer,
|
|
237
|
+
error: pass.cause
|
|
238
|
+
});
|
|
239
|
+
}
|
|
66
240
|
}
|
|
67
241
|
/**
|
|
68
242
|
* Release a connection (decrement reference count)
|
|
69
243
|
* Connection remains in pool for reuse
|
|
70
244
|
*/
|
|
71
|
-
releaseConnection(dbPath) {
|
|
245
|
+
releaseConnection(runtime, dbPath) {
|
|
72
246
|
const pooled = this.connections.get(dbPath);
|
|
73
247
|
if (pooled) {
|
|
74
|
-
|
|
75
|
-
|
|
248
|
+
const refCount = Math.max(0, pooled.refCount - 1);
|
|
249
|
+
const lastUsed = Date.now();
|
|
250
|
+
// Last reference gone: the WAL does not self-checkpoint when holders
|
|
251
|
+
// reach zero, so attempt a reset now — unless the connection is
|
|
252
|
+
// backing off after busy attempts; its next idle window will try.
|
|
253
|
+
if (refCount === 0 &&
|
|
254
|
+
!pooled.db.inTransaction &&
|
|
255
|
+
lastUsed >= pooled.nextIdleCheckpointAt) {
|
|
256
|
+
this.observeCheckpoint(runtime, dbPath, pooled, 'release', lastUsed);
|
|
257
|
+
}
|
|
258
|
+
// Commit the reference release only after its checkpoint/reporting work succeeds.
|
|
259
|
+
pooled.refCount = refCount;
|
|
260
|
+
pooled.lastUsed = lastUsed;
|
|
76
261
|
}
|
|
77
262
|
}
|
|
78
263
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
264
|
+
* Run one observed, non-blocking checkpoint attempt and record its outcome,
|
|
265
|
+
* reported on `runtime`. A busy attempt is logged at warn (a run of
|
|
266
|
+
* {@link WAL_BUSY_STARVATION_THRESHOLD} at error, as starvation), switches
|
|
267
|
+
* the next attempt's mode, and backs the connection off by whole windows. A
|
|
268
|
+
* checkpoint SQLite itself rejects is logged at error with its cause and
|
|
269
|
+
* never blocks the caller's close or tick.
|
|
81
270
|
*/
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
271
|
+
observeCheckpoint(runtime, dbPath, pooled, trigger, now) {
|
|
272
|
+
const logger = poolLogger(runtime);
|
|
273
|
+
const attempted = attempt(() => checkpointWal(pooled.db, pooled.nextCheckpointMode));
|
|
274
|
+
if (attempted.failed) {
|
|
275
|
+
logger.error('[LocalDB] WAL checkpoint failed', {
|
|
276
|
+
reason: 'wal-checkpoint-failed',
|
|
277
|
+
dbPath,
|
|
278
|
+
trigger,
|
|
279
|
+
consumer: pooled.consumer,
|
|
280
|
+
error: attempted.cause
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const outcome = attempted.value;
|
|
285
|
+
pooled.lastCheckpoint = { ...outcome, at: now, trigger };
|
|
286
|
+
const { busy, log, checkpointed, mode } = outcome;
|
|
287
|
+
pooled.nextCheckpointMode = nextCheckpointMode(mode, outcome.status);
|
|
288
|
+
const fields = { dbPath, trigger, consumer: pooled.consumer, mode, busy, log, checkpointed };
|
|
289
|
+
if (outcome.status === 'busy') {
|
|
290
|
+
pooled.consecutiveCheckpointBusy++;
|
|
291
|
+
const consecutive = pooled.consecutiveCheckpointBusy;
|
|
292
|
+
pooled.nextIdleCheckpointAt = now + checkpointBackoffWindows(consecutive) * WAL_MAINTENANCE_INTERVAL_MS;
|
|
293
|
+
if (consecutive >= WAL_BUSY_STARVATION_THRESHOLD) {
|
|
294
|
+
logger.error('[LocalDB] WAL checkpoint starved: another holder keeps frames pinned', {
|
|
295
|
+
reason: 'wal-checkpoint-starved',
|
|
296
|
+
consecutiveBusy: consecutive,
|
|
297
|
+
...fields
|
|
298
|
+
});
|
|
93
299
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
300
|
+
else {
|
|
301
|
+
// Every frame copied back and only the truncate/restart was blocked by another
|
|
302
|
+
// process's reader: routine on a shared file. Frames left behind are worth a warning.
|
|
303
|
+
const report = checkpointed >= log ? logger.debug.bind(logger) : logger.warn.bind(logger);
|
|
304
|
+
report('[LocalDB] WAL checkpoint busy', {
|
|
305
|
+
reason: 'wal-checkpoint-busy',
|
|
306
|
+
consecutiveBusy: consecutive,
|
|
307
|
+
...fields
|
|
308
|
+
});
|
|
97
309
|
}
|
|
98
310
|
}
|
|
311
|
+
else {
|
|
312
|
+
pooled.consecutiveCheckpointBusy = 0;
|
|
313
|
+
pooled.nextIdleCheckpointAt = Number.NEGATIVE_INFINITY;
|
|
314
|
+
logger.debug('[LocalDB] WAL checkpoint', { status: outcome.status, ...fields });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Bound how long a pooled connection may sit inside a transaction. SQLite
|
|
319
|
+
* is synchronous, so a transaction a tick can observe spans an `await`; one
|
|
320
|
+
* older than {@link MAX_TRANSACTION_AGE_MS} pins every WAL frame written
|
|
321
|
+
* after it began, in every process. It is rolled back and reported; the
|
|
322
|
+
* owner's later COMMIT then fails loudly rather than the WAL growing silently.
|
|
323
|
+
*
|
|
324
|
+
* @returns whether the connection is still inside a transaction.
|
|
325
|
+
*/
|
|
326
|
+
enforceTransactionBound(runtime, dbPath, pooled, now) {
|
|
327
|
+
if (!pooled.db.inTransaction) {
|
|
328
|
+
pooled.transactionObservedAt = undefined;
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
pooled.transactionObservedAt ??= now;
|
|
332
|
+
const ageMs = now - pooled.transactionObservedAt;
|
|
333
|
+
if (ageMs < MAX_TRANSACTION_AGE_MS)
|
|
334
|
+
return true;
|
|
335
|
+
const logger = poolLogger(runtime);
|
|
336
|
+
const rolledBack = attempt(() => pooled.db.exec('ROLLBACK;'));
|
|
337
|
+
if (rolledBack.failed) {
|
|
338
|
+
logger.error('[LocalDB] Failed to roll back an expired transaction', {
|
|
339
|
+
reason: 'localdb-transaction-expiry-failed',
|
|
340
|
+
dbPath,
|
|
341
|
+
consumer: pooled.consumer,
|
|
342
|
+
observedAgeMs: ageMs,
|
|
343
|
+
error: rolledBack.cause
|
|
344
|
+
});
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
pooled.transactionObservedAt = undefined;
|
|
348
|
+
logger.error('[LocalDB] Rolled back a transaction held open past its bound', {
|
|
349
|
+
reason: 'localdb-transaction-expired',
|
|
350
|
+
dbPath,
|
|
351
|
+
consumer: pooled.consumer,
|
|
352
|
+
observedAgeMs: ageMs,
|
|
353
|
+
maxTransactionAgeMs: MAX_TRANSACTION_AGE_MS
|
|
354
|
+
});
|
|
99
355
|
return false;
|
|
100
356
|
}
|
|
101
357
|
/**
|
|
102
|
-
*
|
|
358
|
+
* Report the statements this window recorded as slow, on `runtime` and once
|
|
359
|
+
* per connection. Each report carries the connection's consumer and database
|
|
360
|
+
* with the statement's operation and table; a window that recorded more than
|
|
361
|
+
* it kept says so rather than reading as a complete count.
|
|
103
362
|
*/
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
363
|
+
reportSlowStatements(runtime, dbPath, pooled) {
|
|
364
|
+
const { reports, dropped } = pooled.slowStatements.drain();
|
|
365
|
+
if (reports.length === 0 && dropped === 0)
|
|
366
|
+
return;
|
|
367
|
+
poolLogger(runtime).warn('[LocalDB] Slow statements held the thread', {
|
|
368
|
+
reason: 'localdb-slow-statement',
|
|
369
|
+
dbPath,
|
|
370
|
+
consumer: pooled.consumer,
|
|
371
|
+
thresholdMs: SLOW_STATEMENT_MS,
|
|
372
|
+
dropped,
|
|
373
|
+
statements: reports.map(slowStatementFields)
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* One idle window for one connection: report what it recorded as slow, bound
|
|
378
|
+
* an open transaction, close the connection when it has been unused for 30+
|
|
379
|
+
* minutes with zero references, or make at most one observed checkpoint
|
|
380
|
+
* attempt when it is not inside a transaction and not backing off. Every step
|
|
381
|
+
* reports its own failure.
|
|
382
|
+
*/
|
|
383
|
+
maintainConnection(runtime, dbPath, pooled, now) {
|
|
384
|
+
// First, so a window that returns early below still reports what it saw.
|
|
385
|
+
this.reportSlowStatements(runtime, dbPath, pooled);
|
|
386
|
+
const inTransaction = this.enforceTransactionBound(runtime, dbPath, pooled, now);
|
|
387
|
+
if (inTransaction)
|
|
388
|
+
return;
|
|
389
|
+
if (pooled.refCount === 0 && now - pooled.lastUsed > this.STALE_THRESHOLD_MS) {
|
|
390
|
+
// The close runs its own observed checkpoint.
|
|
391
|
+
const closed = attempt(() => this.closePooled(runtime, dbPath, pooled, 'close'));
|
|
392
|
+
if (closed.failed) {
|
|
393
|
+
poolLogger(runtime).error('[LocalDB] Failed to close a stale connection', {
|
|
394
|
+
reason: 'localdb-stale-close-failed',
|
|
395
|
+
dbPath,
|
|
396
|
+
consumer: pooled.consumer,
|
|
397
|
+
error: closed.cause
|
|
398
|
+
});
|
|
110
399
|
}
|
|
400
|
+
return;
|
|
111
401
|
}
|
|
402
|
+
if (now < pooled.nextIdleCheckpointAt)
|
|
403
|
+
return;
|
|
404
|
+
this.observeCheckpoint(runtime, dbPath, pooled, 'idle', now);
|
|
112
405
|
}
|
|
113
406
|
/**
|
|
114
|
-
*
|
|
407
|
+
* One maintenance pass over every pooled connection, reported on `runtime`.
|
|
408
|
+
* Each connection's lifecycle tick runs the same window for that connection
|
|
409
|
+
* alone; hosts and tests may call this directly.
|
|
115
410
|
*/
|
|
116
|
-
|
|
117
|
-
|
|
411
|
+
maintain(runtime, now = Date.now()) {
|
|
412
|
+
for (const [dbPath, pooled] of this.connections)
|
|
413
|
+
this.maintainConnection(runtime, dbPath, pooled, now);
|
|
118
414
|
}
|
|
119
415
|
/**
|
|
120
|
-
*
|
|
416
|
+
* Close one pooled connection and remove it from the pool, reported on
|
|
417
|
+
* `runtime`. A connection still inside a transaction is closed without a
|
|
418
|
+
* checkpoint, and SQLite discards that transaction: the discard is
|
|
419
|
+
* reported, never silent. A close SQLite refuses throws
|
|
420
|
+
* `localdb-close-failed` (the original error as `cause`) and the connection
|
|
421
|
+
* stays pooled, still maintained, for a retry.
|
|
121
422
|
*/
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
423
|
+
closePooled(runtime, dbPath, pooled, trigger) {
|
|
424
|
+
const logger = poolLogger(runtime);
|
|
425
|
+
if (pooled.db.inTransaction) {
|
|
426
|
+
logger.error('[LocalDB] Closing a connection inside an open transaction discards that transaction', {
|
|
427
|
+
reason: 'localdb-transaction-discarded',
|
|
428
|
+
dbPath,
|
|
429
|
+
trigger,
|
|
430
|
+
consumer: pooled.consumer
|
|
431
|
+
});
|
|
126
432
|
}
|
|
433
|
+
else {
|
|
434
|
+
// Observed and logged; a busy or failed checkpoint never blocks close.
|
|
435
|
+
this.observeCheckpoint(runtime, dbPath, pooled, trigger, Date.now());
|
|
436
|
+
}
|
|
437
|
+
const closed = attempt(() => pooled.db.close());
|
|
438
|
+
if (closed.failed) {
|
|
439
|
+
throw new CapabilityError(`Failed to close LocalDB connection '${dbPath}'`, 'INTERNAL', {
|
|
440
|
+
reason: 'localdb-close-failed',
|
|
441
|
+
retryable: true,
|
|
442
|
+
details: { dbPath, trigger },
|
|
443
|
+
cause: closed.cause
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
pooled.maintenance?.stop();
|
|
447
|
+
pooled.maintenance = undefined;
|
|
448
|
+
pooled.holders.clear();
|
|
449
|
+
this.connections.delete(dbPath);
|
|
450
|
+
logger.debug(`[LocalDB] Closed connection (${trigger}): ${dbPath}`);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Refuse destructive replacement while another lifecycle or pooled reference
|
|
454
|
+
* holds the file. Only the requesting instance's actual open connection may
|
|
455
|
+
* account for one reference; legacy/raw borrowers keep their custody too.
|
|
456
|
+
* Call under the database path lock, before releasing or closing anything.
|
|
457
|
+
*/
|
|
458
|
+
assertCanReplaceDatabase(runtime, dbPath, ownedConnection) {
|
|
459
|
+
const pooled = this.connections.get(dbPath);
|
|
460
|
+
if (!pooled)
|
|
461
|
+
return;
|
|
462
|
+
const signal = runtime.lifecycle.signal;
|
|
463
|
+
const foreignHolders = [...pooled.holders].filter(holder => !holder.lifecycle.signal.aborted && holder.lifecycle.signal !== signal).length;
|
|
464
|
+
const ownedReferences = pooled.db === ownedConnection ? 1 : 0;
|
|
465
|
+
if (foreignHolders === 0 && pooled.refCount <= ownedReferences)
|
|
466
|
+
return;
|
|
467
|
+
throw new CapabilityError('LocalDB cannot replace a database with outstanding borrowers.', 'CONFLICT', {
|
|
468
|
+
reason: 'localdb-database-in-use',
|
|
469
|
+
retryable: false,
|
|
470
|
+
details: { dbPath, foreignHolders, references: pooled.refCount, ownedReferences },
|
|
471
|
+
recoveryHint: 'Release the other database borrowers before requesting reset or recovery.'
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Give back one pooled reference no live runtime holds: every recorded
|
|
476
|
+
* holder ended (or none is recorded) while the reference count is still
|
|
477
|
+
* nonzero — typically a client of a runtime that ended without releasing.
|
|
478
|
+
* Returns whether a reference was given back. A connection another live
|
|
479
|
+
* runtime still holds is never touched.
|
|
480
|
+
*/
|
|
481
|
+
releaseOrphanedConnection(dbPath) {
|
|
482
|
+
const pooled = this.connections.get(dbPath);
|
|
483
|
+
if (!pooled || pooled.refCount === 0)
|
|
484
|
+
return false;
|
|
485
|
+
if ([...pooled.holders].some((holder) => !holder.lifecycle.signal.aborted))
|
|
486
|
+
return false;
|
|
487
|
+
pooled.refCount = Math.max(0, pooled.refCount - 1);
|
|
488
|
+
pooled.lastUsed = Date.now();
|
|
489
|
+
return true;
|
|
127
490
|
}
|
|
128
491
|
/**
|
|
129
|
-
* Close
|
|
492
|
+
* Close a specific connection immediately. Only closes if the reference
|
|
493
|
+
* count is zero, unless forced. Returns whether it closed; a close SQLite
|
|
494
|
+
* refuses throws `localdb-close-failed` and leaves the connection pooled.
|
|
130
495
|
*/
|
|
131
|
-
|
|
132
|
-
this.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
496
|
+
closeConnection(runtime, dbPath, force = false) {
|
|
497
|
+
const pooled = this.connections.get(dbPath);
|
|
498
|
+
if (!pooled)
|
|
499
|
+
return false;
|
|
500
|
+
if (!force && pooled.refCount > 0)
|
|
501
|
+
return false;
|
|
502
|
+
this.closePooled(runtime, dbPath, pooled, 'close');
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Give back every connection `runtime` holds (typically at its disposal),
|
|
507
|
+
* reported on `runtime`; runtimes that ended hold nothing. A connection
|
|
508
|
+
* another live runtime still holds (another root, or the root a disposing
|
|
509
|
+
* reload generation was forked from) stays pooled for it, its maintenance moving to that
|
|
510
|
+
* holder; every other connection closes. Every close is attempted; the ones
|
|
511
|
+
* SQLite refuses stay pooled and are reported together in one AggregateError
|
|
512
|
+
* so the runtime owner can report them and retry.
|
|
513
|
+
*/
|
|
514
|
+
closeAll(runtime, retainedPaths = new Set()) {
|
|
515
|
+
const failures = [];
|
|
516
|
+
for (const [dbPath, pooled] of this.connections) {
|
|
517
|
+
if (retainedPaths.has(dbPath))
|
|
518
|
+
continue;
|
|
519
|
+
for (const holder of pooled.holders) {
|
|
520
|
+
if (releasesHolder(holder, runtime) || holder.lifecycle.signal.aborted)
|
|
521
|
+
pooled.holders.delete(holder);
|
|
138
522
|
}
|
|
139
|
-
|
|
140
|
-
|
|
523
|
+
if (pooled.holders.size > 0) {
|
|
524
|
+
this.ensureMaintenance(dbPath, pooled);
|
|
525
|
+
continue;
|
|
141
526
|
}
|
|
527
|
+
const closed = attempt(() => this.closePooled(runtime, dbPath, pooled, 'shutdown'));
|
|
528
|
+
if (closed.failed)
|
|
529
|
+
failures.push(closed.cause);
|
|
142
530
|
}
|
|
143
|
-
|
|
531
|
+
if (failures.length > 0) {
|
|
532
|
+
throw new AggregateError(failures, `Failed to close ${failures.length} LocalDB connection${failures.length === 1 ? '' : 's'}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/** Whether a managed handle still names the pooled connection at this path. */
|
|
536
|
+
ownsConnection(dbPath, db) {
|
|
537
|
+
return this.connections.get(dbPath)?.db === db;
|
|
144
538
|
}
|
|
145
539
|
/**
|
|
146
540
|
* Get pool statistics
|
|
@@ -150,19 +544,35 @@ class SQLiteConnectionPool {
|
|
|
150
544
|
let idle = 0;
|
|
151
545
|
const connections = [];
|
|
152
546
|
const now = Date.now();
|
|
153
|
-
for (const [path, pooled] of this.connections
|
|
547
|
+
for (const [path, pooled] of this.connections) {
|
|
154
548
|
if (pooled.refCount > 0) {
|
|
155
549
|
active++;
|
|
156
550
|
}
|
|
157
551
|
else {
|
|
158
552
|
idle++;
|
|
159
553
|
}
|
|
160
|
-
|
|
554
|
+
const entry = {
|
|
161
555
|
path,
|
|
162
556
|
refCount: pooled.refCount,
|
|
163
557
|
ageMs: now - pooled.createdAt,
|
|
164
|
-
consumer: pooled.consumer
|
|
165
|
-
|
|
558
|
+
consumer: pooled.consumer,
|
|
559
|
+
maintained: pooled.maintenance !== undefined,
|
|
560
|
+
holders: pooled.holders.size,
|
|
561
|
+
consecutiveCheckpointBusy: pooled.consecutiveCheckpointBusy
|
|
562
|
+
};
|
|
563
|
+
const last = pooled.lastCheckpoint;
|
|
564
|
+
if (last) {
|
|
565
|
+
entry.lastCheckpoint = {
|
|
566
|
+
at: last.at,
|
|
567
|
+
trigger: last.trigger,
|
|
568
|
+
status: last.status,
|
|
569
|
+
mode: last.mode,
|
|
570
|
+
busy: last.busy,
|
|
571
|
+
log: last.log,
|
|
572
|
+
checkpointed: last.checkpointed
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
connections.push(entry);
|
|
166
576
|
}
|
|
167
577
|
return {
|
|
168
578
|
total: this.connections.size,
|
|
@@ -171,21 +581,120 @@ class SQLiteConnectionPool {
|
|
|
171
581
|
connections
|
|
172
582
|
};
|
|
173
583
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* The `localdb/pool` kind: the process's physical SQLite connections, owned
|
|
587
|
+
* through the kernel. The pool holds live host handles, so — unlike migration
|
|
588
|
+
* declarations, which each runtime owns separately — runtimes that must open
|
|
589
|
+
* the same file share one pool instance resolved from the runtime they are
|
|
590
|
+
* handed. No module-level pool instance exists: every instance is created
|
|
591
|
+
* explicitly (`createConnectionPool`, the plugin default, or a shared owner a
|
|
592
|
+
* composition root registers) and resolved through the kernel below.
|
|
593
|
+
*/
|
|
594
|
+
export const LOCALDB_POOL_KIND = 'localdb/pool';
|
|
595
|
+
/** The runtime-owned default pool, created per root runtime by the LocalDB plugin. */
|
|
596
|
+
export const DEFAULT_POOL_ID = 'pool';
|
|
597
|
+
/**
|
|
598
|
+
* An explicitly shared pool, handed to each runtime by the composition root
|
|
599
|
+
* that boots them. Preferred over the default whenever it is registered.
|
|
600
|
+
*/
|
|
601
|
+
export const SHARED_POOL_ID = 'shared';
|
|
602
|
+
/** The kind LocalDB registers; an implementation that is not a pool is refused `implementation-invalid`. */
|
|
603
|
+
export const poolKind = createRuntimeKind({
|
|
604
|
+
id: LOCALDB_POOL_KIND,
|
|
605
|
+
descriptorSchema: PoolDescriptorSchema,
|
|
606
|
+
validateImplementation: (impl) => impl instanceof SQLiteConnectionPool
|
|
607
|
+
});
|
|
608
|
+
/**
|
|
609
|
+
* Create the physical connection pool a composition root hands to the runtimes
|
|
610
|
+
* it boots. One pool per process per database set: every runtime handed the
|
|
611
|
+
* same pool shares the one connection per path.
|
|
612
|
+
*/
|
|
613
|
+
export function createConnectionPool() {
|
|
614
|
+
return new SQLiteConnectionPool();
|
|
615
|
+
}
|
|
616
|
+
/** `UNAVAILABLE localdb-pool-unavailable`: the runtime registered no connection pool. */
|
|
617
|
+
function poolUnavailable() {
|
|
618
|
+
return new CapabilityError('This runtime registered no LocalDB connection pool (localdb/pool).', 'UNAVAILABLE', {
|
|
619
|
+
reason: 'localdb-pool-unavailable',
|
|
620
|
+
retryable: false,
|
|
621
|
+
recoveryHint: 'Compose the LocalDB plugin, which registers a runtime-owned default pool.'
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Resolve the runtime's physical connection pool through the kernel on every
|
|
626
|
+
* call, so current selection, invalidation, and implementation caching remain
|
|
627
|
+
* owned by the runtime: an explicitly shared pool when the composition root
|
|
628
|
+
* registered one, else the runtime-owned default the LocalDB plugin
|
|
629
|
+
* registered. Anything else (a refused loader, an invalid implementation)
|
|
630
|
+
* propagates unwrapped.
|
|
631
|
+
*/
|
|
632
|
+
export async function poolOf(runtime) {
|
|
633
|
+
if (!runtime.hasKind(LOCALDB_POOL_KIND))
|
|
634
|
+
throw poolUnavailable();
|
|
635
|
+
// SAFETY: kernel RuntimeRef.query returns AssetQuery; its minimal plugin port omits the public sortBy method.
|
|
636
|
+
const selected = await runtime.query(LOCALDB_POOL_KIND)
|
|
637
|
+
.sortBy('priority', 'desc').select();
|
|
638
|
+
if (!selected)
|
|
639
|
+
throw poolUnavailable();
|
|
640
|
+
return selected.impl;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* The already-resolved pool, or `undefined` when no resolution warmed the
|
|
644
|
+
* kernel cache yet. Synchronous cover for call sites that hold no async
|
|
645
|
+
* context after an async call resolved the pool first.
|
|
646
|
+
*/
|
|
647
|
+
export function tryPoolOf(runtime) {
|
|
648
|
+
if (!runtime.hasKind(LOCALDB_POOL_KIND))
|
|
649
|
+
return undefined;
|
|
650
|
+
// SAFETY: kernel RuntimeRef.query returns AssetQuery; its minimal plugin port omits the public sortBy method.
|
|
651
|
+
return runtime.query(LOCALDB_POOL_KIND).sortBy('priority', 'desc').peek?.();
|
|
652
|
+
}
|
|
653
|
+
const DEFAULT_POOL_DESCRIPTION = 'Runtime-owned default LocalDB connection pool';
|
|
654
|
+
const SHARED_POOL_DESCRIPTION = 'Explicitly shared LocalDB connection pool for every runtime one composition root boots';
|
|
655
|
+
/**
|
|
656
|
+
* Register the runtime-owned default pool, unless one is already registered:
|
|
657
|
+
* reload-generation forks inherit their root's registrations, so they keep
|
|
658
|
+
* sharing the root's pool instead of minting a second connection per path.
|
|
659
|
+
*/
|
|
660
|
+
export function ensureDefaultPool(runtime) {
|
|
661
|
+
if (!runtime.hasKind(LOCALDB_POOL_KIND))
|
|
662
|
+
runtime.registerKind(poolKind);
|
|
663
|
+
if (runtime.query(LOCALDB_POOL_KIND).withId(DEFAULT_POOL_ID).descriptors().length > 0)
|
|
664
|
+
return;
|
|
665
|
+
runtime.registerDescriptor(LOCALDB_POOL_KIND, {
|
|
666
|
+
kind: LOCALDB_POOL_KIND,
|
|
667
|
+
id: DEFAULT_POOL_ID,
|
|
668
|
+
description: DEFAULT_POOL_DESCRIPTION
|
|
669
|
+
});
|
|
670
|
+
// The registration owns the instance: inherited loaders must return the
|
|
671
|
+
// same physical owner to forks, rather than construct one per client cache.
|
|
672
|
+
const pool = new SQLiteConnectionPool();
|
|
673
|
+
runtime.registerLoader(LOCALDB_POOL_KIND, DEFAULT_POOL_ID, async () => ({ impl: pool }));
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Hand one explicitly created pool to a runtime: every runtime the
|
|
677
|
+
* composition root boots against the same database files shares the one
|
|
678
|
+
* connection per path. Fork generations inherit the registration, so they
|
|
679
|
+
* share it too.
|
|
680
|
+
*/
|
|
681
|
+
export function shareConnectionPool(runtime, pool) {
|
|
682
|
+
if (!runtime.hasKind(LOCALDB_POOL_KIND))
|
|
683
|
+
runtime.registerKind(poolKind);
|
|
684
|
+
const shared = runtime.query(LOCALDB_POOL_KIND).withId(SHARED_POOL_ID);
|
|
685
|
+
if (shared.peek?.() === pool)
|
|
686
|
+
return;
|
|
687
|
+
if (shared.descriptors().length > 0 || tryPoolOf(runtime)) {
|
|
688
|
+
throw new CapabilityError('The LocalDB pool owner has already been selected.', 'CONFLICT', {
|
|
689
|
+
reason: 'localdb-pool-owner-selected', retryable: false,
|
|
690
|
+
recoveryHint: 'Register the shared owner during composition, before LocalDB activation or pool resolution.'
|
|
186
691
|
});
|
|
187
692
|
}
|
|
693
|
+
runtime.registerDescriptor(LOCALDB_POOL_KIND, {
|
|
694
|
+
kind: LOCALDB_POOL_KIND,
|
|
695
|
+
id: SHARED_POOL_ID,
|
|
696
|
+
description: SHARED_POOL_DESCRIPTION
|
|
697
|
+
});
|
|
698
|
+
runtime.registerLoader(LOCALDB_POOL_KIND, SHARED_POOL_ID, async () => ({ impl: pool }), { priority: 100 });
|
|
188
699
|
}
|
|
189
|
-
// Singleton instance
|
|
190
|
-
export const connectionPool = new SQLiteConnectionPool();
|
|
191
700
|
//# sourceMappingURL=connection-pool.js.map
|