@dotdo/postgres 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -1
- package/dist/client/index.d.ts +47 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +47 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/postgres-client.d.ts +273 -0
- package/dist/client/postgres-client.d.ts.map +1 -0
- package/dist/client/postgres-client.js +389 -0
- package/dist/client/postgres-client.js.map +1 -0
- package/dist/client/types.d.ts +167 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +7 -0
- package/dist/client/types.js.map +1 -0
- package/dist/do/index.d.ts +18 -0
- package/dist/do/index.d.ts.map +1 -0
- package/dist/do/index.js +18 -0
- package/dist/do/index.js.map +1 -0
- package/dist/do/postgres.d.ts +110 -0
- package/dist/do/postgres.d.ts.map +1 -0
- package/dist/do/postgres.js +266 -0
- package/dist/do/postgres.js.map +1 -0
- package/dist/do/sql.d.ts +92 -0
- package/dist/do/sql.d.ts.map +1 -0
- package/dist/do/sql.js +204 -0
- package/dist/do/sql.js.map +1 -0
- package/dist/index.d.ts +25 -30
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -30
- package/dist/index.js.map +1 -1
- package/dist/mcp/binding.d.ts +47 -0
- package/dist/mcp/binding.d.ts.map +1 -0
- package/dist/mcp/binding.js +183 -0
- package/dist/mcp/binding.js.map +1 -0
- package/dist/mcp/index.d.ts +92 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +91 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +62 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +278 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +58 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +356 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/types.d.ts +139 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +7 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/pglite/workers-pglite.d.ts +13 -4
- package/dist/pglite/workers-pglite.d.ts.map +1 -1
- package/dist/pglite/workers-pglite.js +110 -5
- package/dist/pglite/workers-pglite.js.map +1 -1
- package/dist/pglite-assets/pglite.data +0 -0
- package/dist/pglite-assets/pglite.wasm +0 -0
- package/dist/worker/auth.d.ts.map +1 -1
- package/dist/worker/auth.js +16 -6
- package/dist/worker/auth.js.map +1 -1
- package/dist/worker/background-pglite-manager.d.ts +243 -0
- package/dist/worker/background-pglite-manager.d.ts.map +1 -0
- package/dist/worker/background-pglite-manager.js +528 -0
- package/dist/worker/background-pglite-manager.js.map +1 -0
- package/dist/worker/do-pglite-manager.d.ts +77 -0
- package/dist/worker/do-pglite-manager.d.ts.map +1 -1
- package/dist/worker/do-pglite-manager.js +189 -12
- package/dist/worker/do-pglite-manager.js.map +1 -1
- package/dist/worker/entry.d.ts.map +1 -1
- package/dist/worker/entry.js +108 -26
- package/dist/worker/entry.js.map +1 -1
- package/dist/worker/index.d.ts +7 -1
- package/dist/worker/index.d.ts.map +1 -1
- package/dist/worker/index.js +19 -1
- package/dist/worker/index.js.map +1 -1
- package/dist/worker/lazy-pglite-manager.d.ts +242 -0
- package/dist/worker/lazy-pglite-manager.d.ts.map +1 -0
- package/dist/worker/lazy-pglite-manager.js +463 -0
- package/dist/worker/lazy-pglite-manager.js.map +1 -0
- package/package.json +20 -6
- package/src/client/index.ts +61 -0
- package/src/client/postgres-client.ts +442 -0
- package/src/client/types.ts +211 -0
- package/src/do/index.ts +18 -0
- package/src/do/postgres.ts +367 -0
- package/src/do/sql.ts +280 -0
- package/src/index.ts +50 -30
- package/src/mcp/binding.ts +236 -0
- package/src/mcp/index.ts +122 -0
- package/src/mcp/server.ts +361 -0
- package/src/mcp/tools.ts +464 -0
- package/src/mcp/types.ts +148 -0
- package/src/pglite/workers-pglite.ts +141 -12
- package/src/pglite-assets/pglite.data +0 -0
- package/src/pglite-assets/pglite.wasm +0 -0
- package/src/worker/auth.ts +17 -6
- package/src/worker/background-pglite-manager.ts +680 -0
- package/src/worker/do-pglite-manager.ts +235 -19
- package/src/worker/entry.ts +112 -30
- package/src/worker/index.ts +71 -1
- package/src/worker/lazy-pglite-manager.ts +595 -0
- package/dist/iceberg/duckdb-wasm.d.ts +0 -447
- package/dist/iceberg/duckdb-wasm.d.ts.map +0 -1
- package/dist/iceberg/duckdb-wasm.js +0 -600
- package/dist/iceberg/duckdb-wasm.js.map +0 -1
- package/dist/iceberg/test-fixtures.d.ts +0 -151
- package/dist/iceberg/test-fixtures.d.ts.map +0 -1
- package/dist/iceberg/test-fixtures.js +0 -446
- package/dist/iceberg/test-fixtures.js.map +0 -1
- package/dist/worker/__mocks__/cloudflare-workers.d.ts +0 -31
- package/dist/worker/__mocks__/cloudflare-workers.d.ts.map +0 -1
- package/dist/worker/__mocks__/cloudflare-workers.js +0 -33
- package/dist/worker/__mocks__/cloudflare-workers.js.map +0 -1
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background PGLite Manager for PostgresDO
|
|
3
|
+
*
|
|
4
|
+
* This module implements a "eager-but-non-blocking" loading pattern:
|
|
5
|
+
* - WASM loading starts immediately on initialize() but doesn't block
|
|
6
|
+
* - Non-query endpoints respond instantly while WASM loads in background
|
|
7
|
+
* - Queries wait for WASM if not ready, but loading already started
|
|
8
|
+
* - Uses ctx.waitUntil() to keep DO alive during background loading
|
|
9
|
+
*
|
|
10
|
+
* ## Why This is Better Than Pure Lazy Loading
|
|
11
|
+
*
|
|
12
|
+
* Pure lazy loading just kicks the can down the road:
|
|
13
|
+
* - First query still pays full ~1200ms WASM load time
|
|
14
|
+
*
|
|
15
|
+
* Background loading gives us the best of both worlds:
|
|
16
|
+
* - Non-query endpoints respond instantly
|
|
17
|
+
* - WASM starts loading immediately
|
|
18
|
+
* - First query only waits for remaining load time (often near-zero)
|
|
19
|
+
* - If query arrives before WASM ready, it waits but loading is already in progress
|
|
20
|
+
*
|
|
21
|
+
* ## Usage with Durable Objects
|
|
22
|
+
*
|
|
23
|
+
* ```typescript
|
|
24
|
+
* export class MyDO extends DurableObject {
|
|
25
|
+
* private manager: BackgroundPGLiteManager
|
|
26
|
+
*
|
|
27
|
+
* constructor(ctx: DurableObjectState, env: Env) {
|
|
28
|
+
* super(ctx, env)
|
|
29
|
+
* this.manager = new BackgroundPGLiteManager({
|
|
30
|
+
* database: 'mydb',
|
|
31
|
+
* waitUntil: (p) => ctx.waitUntil(p), // Keep DO alive during load
|
|
32
|
+
* })
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* async init() {
|
|
36
|
+
* // Starts WASM loading in background, returns immediately
|
|
37
|
+
* await this.manager.initialize()
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* // Health check - responds instantly, triggers background load if needed
|
|
41
|
+
* async ping() {
|
|
42
|
+
* return { ok: true, wasmLoading: this.manager.isLoading() }
|
|
43
|
+
* }
|
|
44
|
+
*
|
|
45
|
+
* // Query - waits for WASM if not ready
|
|
46
|
+
* async query(sql: string) {
|
|
47
|
+
* return this.manager.query(sql)
|
|
48
|
+
* }
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @module worker/background-pglite-manager
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import type { PostgresConfig } from './types'
|
|
56
|
+
import { PluginManager, type PluginManagerConfig } from './plugin-manager'
|
|
57
|
+
import type { PGliteLike } from './do-pglite-manager'
|
|
58
|
+
|
|
59
|
+
// Import WASM module and data bundle for Workers
|
|
60
|
+
import pgliteWasm from '../pglite-assets/pglite.wasm'
|
|
61
|
+
import pgliteData from '../pglite-assets/pglite.data'
|
|
62
|
+
|
|
63
|
+
// Import Workers-compatible PGLite wrapper
|
|
64
|
+
import { createWorkersPGLite, type WorkersPGLite } from '../pglite/workers-pglite'
|
|
65
|
+
|
|
66
|
+
// =============================================================================
|
|
67
|
+
// Module-Level WASM State (for hoisting across DO reinstantiation)
|
|
68
|
+
// =============================================================================
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Hoisted PGLite instance - survives DO class reinstantiation within same isolate.
|
|
72
|
+
*/
|
|
73
|
+
let bgHoistedPglite: WorkersPGLite | null = null
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Promise for in-progress PGLite initialization.
|
|
77
|
+
* Shared across all manager instances for deduplication.
|
|
78
|
+
*/
|
|
79
|
+
let bgHoistedPglitePromise: Promise<WorkersPGLite> | null = null
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Timestamp when WASM loading started
|
|
83
|
+
*/
|
|
84
|
+
let wasmLoadStartedAt: number | null = null
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Timestamp when WASM was loaded
|
|
88
|
+
*/
|
|
89
|
+
let wasmLoadedAt: number | null = null
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Unique identifier for this module instance
|
|
93
|
+
*/
|
|
94
|
+
const BG_MODULE_INSTANCE_ID = Math.random().toString(36).slice(2, 10)
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if the hoisted PGLite instance exists.
|
|
98
|
+
*/
|
|
99
|
+
export function hasBgHoistedPglite(): boolean {
|
|
100
|
+
return bgHoistedPglite !== null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if WASM loading is in progress.
|
|
105
|
+
*/
|
|
106
|
+
export function isBgWasmLoading(): boolean {
|
|
107
|
+
return bgHoistedPglitePromise !== null && bgHoistedPglite === null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get diagnostics about the background hoisted WASM state.
|
|
112
|
+
*/
|
|
113
|
+
export function getBgHoistedPgliteDiagnostics(): {
|
|
114
|
+
hasInstance: boolean
|
|
115
|
+
isLoading: boolean
|
|
116
|
+
moduleInstanceId: string
|
|
117
|
+
wasmLoadStartedAt: number | null
|
|
118
|
+
wasmLoadedAt: number | null
|
|
119
|
+
loadDurationMs: number | null
|
|
120
|
+
timeSinceLoadMs: number | null
|
|
121
|
+
} {
|
|
122
|
+
const now = Date.now()
|
|
123
|
+
return {
|
|
124
|
+
hasInstance: bgHoistedPglite !== null,
|
|
125
|
+
isLoading: bgHoistedPglitePromise !== null && bgHoistedPglite === null,
|
|
126
|
+
moduleInstanceId: BG_MODULE_INSTANCE_ID,
|
|
127
|
+
wasmLoadStartedAt,
|
|
128
|
+
wasmLoadedAt,
|
|
129
|
+
loadDurationMs: wasmLoadStartedAt && wasmLoadedAt ? wasmLoadedAt - wasmLoadStartedAt : null,
|
|
130
|
+
timeSinceLoadMs: wasmLoadedAt !== null ? now - wasmLoadedAt : null,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Reset the hoisted PGLite instance.
|
|
136
|
+
* WARNING: This should only be used in tests.
|
|
137
|
+
* @internal
|
|
138
|
+
*/
|
|
139
|
+
export function resetBgHoistedPglite(): void {
|
|
140
|
+
bgHoistedPglite = null
|
|
141
|
+
bgHoistedPglitePromise = null
|
|
142
|
+
wasmLoadStartedAt = null
|
|
143
|
+
wasmLoadedAt = null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Start WASM loading and return the promise.
|
|
148
|
+
* This is called by initialize() but not awaited.
|
|
149
|
+
*/
|
|
150
|
+
function startWasmLoading(options: {
|
|
151
|
+
database?: string
|
|
152
|
+
debug?: 0 | 1
|
|
153
|
+
}): Promise<WorkersPGLite> {
|
|
154
|
+
// Fast path: already initialized
|
|
155
|
+
if (bgHoistedPglite) {
|
|
156
|
+
return Promise.resolve(bgHoistedPglite)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Deduplication: return existing promise if loading in progress
|
|
160
|
+
if (bgHoistedPglitePromise) {
|
|
161
|
+
return bgHoistedPglitePromise
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
wasmLoadStartedAt = Date.now()
|
|
165
|
+
console.log(`[BackgroundPGLiteManager] Starting WASM load in background - instance: ${BG_MODULE_INSTANCE_ID}`)
|
|
166
|
+
|
|
167
|
+
// Start initialization
|
|
168
|
+
bgHoistedPglitePromise = createWorkersPGLite({
|
|
169
|
+
wasmModule: pgliteWasm,
|
|
170
|
+
fsBundle: pgliteData,
|
|
171
|
+
database: options.database ?? 'postgres',
|
|
172
|
+
debug: options.debug ?? 0,
|
|
173
|
+
}).then((pglite) => {
|
|
174
|
+
bgHoistedPglite = pglite
|
|
175
|
+
wasmLoadedAt = Date.now()
|
|
176
|
+
const loadDuration = wasmLoadedAt - (wasmLoadStartedAt ?? wasmLoadedAt)
|
|
177
|
+
console.log(
|
|
178
|
+
`[BackgroundPGLiteManager] WASM LOADED - took ${loadDuration}ms, instance: ${BG_MODULE_INSTANCE_ID}`
|
|
179
|
+
)
|
|
180
|
+
return pglite
|
|
181
|
+
}).catch((error) => {
|
|
182
|
+
console.error(`[BackgroundPGLiteManager] WASM load failed:`, error)
|
|
183
|
+
// Clear the promise so we can retry
|
|
184
|
+
bgHoistedPglitePromise = null
|
|
185
|
+
throw error
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
return bgHoistedPglitePromise
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get the hoisted instance, waiting for loading if in progress.
|
|
193
|
+
*/
|
|
194
|
+
async function getOrAwaitHoistedPglite(options: {
|
|
195
|
+
database?: string
|
|
196
|
+
debug?: 0 | 1
|
|
197
|
+
}): Promise<WorkersPGLite> {
|
|
198
|
+
// Fast path: already loaded
|
|
199
|
+
if (bgHoistedPglite) {
|
|
200
|
+
return bgHoistedPglite
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Loading in progress - wait for it
|
|
204
|
+
if (bgHoistedPglitePromise) {
|
|
205
|
+
return bgHoistedPglitePromise
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Not started yet - start now (shouldn't happen if initialize() was called)
|
|
209
|
+
return startWasmLoading(options)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// =============================================================================
|
|
213
|
+
// Loading State
|
|
214
|
+
// =============================================================================
|
|
215
|
+
|
|
216
|
+
export type BGWASMLoadingState = 'not_started' | 'loading' | 'loaded' | 'error'
|
|
217
|
+
|
|
218
|
+
// =============================================================================
|
|
219
|
+
// Configuration
|
|
220
|
+
// =============================================================================
|
|
221
|
+
|
|
222
|
+
export interface BackgroundPGLiteManagerConfig {
|
|
223
|
+
/** Database name */
|
|
224
|
+
database?: string
|
|
225
|
+
/** Enable debug mode */
|
|
226
|
+
debug?: boolean
|
|
227
|
+
/** Plugin configuration */
|
|
228
|
+
plugins?: PostgresConfig['plugins']
|
|
229
|
+
/** Custom PGLite factory for testing */
|
|
230
|
+
createPGLite?: () => Promise<PGliteLike>
|
|
231
|
+
/**
|
|
232
|
+
* Disable WASM hoisting optimization.
|
|
233
|
+
* @default false
|
|
234
|
+
*/
|
|
235
|
+
disableHoisting?: boolean
|
|
236
|
+
/**
|
|
237
|
+
* waitUntil function from Durable Object context.
|
|
238
|
+
* Used to keep the DO alive while WASM loads in background.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```typescript
|
|
242
|
+
* new BackgroundPGLiteManager({
|
|
243
|
+
* waitUntil: (p) => ctx.waitUntil(p)
|
|
244
|
+
* })
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
waitUntil?: (promise: Promise<unknown>) => void
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// =============================================================================
|
|
251
|
+
// BackgroundPGLiteManager Class
|
|
252
|
+
// =============================================================================
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* BackgroundPGLiteManager starts WASM loading immediately but doesn't block.
|
|
256
|
+
*
|
|
257
|
+
* This gives the best of both worlds:
|
|
258
|
+
* - Non-query endpoints respond instantly
|
|
259
|
+
* - WASM starts loading on first init
|
|
260
|
+
* - Queries wait only for remaining load time (often zero if loaded)
|
|
261
|
+
* - Uses ctx.waitUntil() to keep DO alive during background loading
|
|
262
|
+
*/
|
|
263
|
+
export class BackgroundPGLiteManager {
|
|
264
|
+
private pglite: PGliteLike | null = null
|
|
265
|
+
private initialized = false
|
|
266
|
+
private loadingState: BGWASMLoadingState = 'not_started'
|
|
267
|
+
private loadError: Error | null = null
|
|
268
|
+
private config: BackgroundPGLiteManagerConfig
|
|
269
|
+
private pluginManager: PluginManager
|
|
270
|
+
private usingHoistedInstance = false
|
|
271
|
+
private loadPromise: Promise<PGliteLike> | null = null
|
|
272
|
+
|
|
273
|
+
/** Timing metrics for diagnostics */
|
|
274
|
+
private metrics = {
|
|
275
|
+
initializeCalledAt: null as number | null,
|
|
276
|
+
wasmLoadStartAt: null as number | null,
|
|
277
|
+
wasmLoadEndAt: null as number | null,
|
|
278
|
+
firstQueryAt: null as number | null,
|
|
279
|
+
firstQueryWaitedMs: null as number | null,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
constructor(config: BackgroundPGLiteManagerConfig = {}) {
|
|
283
|
+
this.config = {
|
|
284
|
+
database: config.database ?? 'postgres',
|
|
285
|
+
debug: config.debug ?? false,
|
|
286
|
+
...config,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const pluginManagerConfig: PluginManagerConfig = {}
|
|
290
|
+
if (config.plugins !== undefined) {
|
|
291
|
+
pluginManagerConfig.plugins = config.plugins
|
|
292
|
+
}
|
|
293
|
+
if (config.debug !== undefined) {
|
|
294
|
+
pluginManagerConfig.debug = config.debug
|
|
295
|
+
}
|
|
296
|
+
this.pluginManager = new PluginManager(pluginManagerConfig)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Initialize the manager.
|
|
301
|
+
*
|
|
302
|
+
* This starts WASM loading in the background but returns immediately.
|
|
303
|
+
* Non-query operations can proceed while WASM loads.
|
|
304
|
+
*
|
|
305
|
+
* If waitUntil is provided, it's used to keep the DO alive during loading.
|
|
306
|
+
*/
|
|
307
|
+
async initialize(): Promise<void> {
|
|
308
|
+
if (this.initialized) {
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
this.metrics.initializeCalledAt = Date.now()
|
|
313
|
+
this.initialized = true
|
|
314
|
+
|
|
315
|
+
// If custom factory provided, use it (for testing)
|
|
316
|
+
if (this.config.createPGLite) {
|
|
317
|
+
this.loadingState = 'loading'
|
|
318
|
+
this.loadPromise = this.config.createPGLite()
|
|
319
|
+
|
|
320
|
+
this.loadPromise.then((pg) => {
|
|
321
|
+
this.pglite = pg
|
|
322
|
+
this.loadingState = 'loaded'
|
|
323
|
+
this.metrics.wasmLoadEndAt = Date.now()
|
|
324
|
+
}).catch((err) => {
|
|
325
|
+
this.loadingState = 'error'
|
|
326
|
+
this.loadError = err
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
if (this.config.waitUntil) {
|
|
330
|
+
this.config.waitUntil(this.loadPromise.catch(() => {}))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
console.log(`[BackgroundPGLiteManager] Initialized (custom factory, loading in background)`)
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// If hoisting is disabled, start loading but don't hoist
|
|
338
|
+
if (this.config.disableHoisting) {
|
|
339
|
+
this.loadingState = 'loading'
|
|
340
|
+
this.metrics.wasmLoadStartAt = Date.now()
|
|
341
|
+
|
|
342
|
+
this.loadPromise = createWorkersPGLite({
|
|
343
|
+
wasmModule: pgliteWasm,
|
|
344
|
+
fsBundle: pgliteData,
|
|
345
|
+
database: this.config.database ?? 'postgres',
|
|
346
|
+
debug: this.config.debug ? 1 : 0,
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
this.loadPromise.then((pg) => {
|
|
350
|
+
this.pglite = pg
|
|
351
|
+
this.loadingState = 'loaded'
|
|
352
|
+
this.metrics.wasmLoadEndAt = Date.now()
|
|
353
|
+
console.log(`[BackgroundPGLiteManager] WASM LOADED (non-hoisted) - took ${this.metrics.wasmLoadEndAt - (this.metrics.wasmLoadStartAt ?? 0)}ms`)
|
|
354
|
+
}).catch((err) => {
|
|
355
|
+
this.loadingState = 'error'
|
|
356
|
+
this.loadError = err
|
|
357
|
+
console.error(`[BackgroundPGLiteManager] WASM load failed:`, err)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
if (this.config.waitUntil) {
|
|
361
|
+
this.config.waitUntil(this.loadPromise.catch(() => {}))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
console.log(`[BackgroundPGLiteManager] Initialized (hoisting disabled, loading in background)`)
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Use hoisted instance
|
|
369
|
+
this.usingHoistedInstance = true
|
|
370
|
+
|
|
371
|
+
// Check if already loaded
|
|
372
|
+
if (bgHoistedPglite) {
|
|
373
|
+
this.pglite = bgHoistedPglite
|
|
374
|
+
this.loadingState = 'loaded'
|
|
375
|
+
console.log(`[BackgroundPGLiteManager] Initialized (WASM already loaded, reusing hoisted instance)`)
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Start loading in background
|
|
380
|
+
this.loadingState = 'loading'
|
|
381
|
+
this.metrics.wasmLoadStartAt = Date.now()
|
|
382
|
+
|
|
383
|
+
const loadPromise = startWasmLoading({
|
|
384
|
+
database: this.config.database,
|
|
385
|
+
debug: this.config.debug ? 1 : 0,
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
// Track when loading completes for this manager
|
|
389
|
+
loadPromise.then((pg) => {
|
|
390
|
+
this.pglite = pg
|
|
391
|
+
this.loadingState = 'loaded'
|
|
392
|
+
this.metrics.wasmLoadEndAt = Date.now()
|
|
393
|
+
}).catch((err) => {
|
|
394
|
+
this.loadingState = 'error'
|
|
395
|
+
this.loadError = err
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
// Use waitUntil to keep DO alive during loading
|
|
399
|
+
if (this.config.waitUntil) {
|
|
400
|
+
this.config.waitUntil(loadPromise.catch(() => {}))
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
console.log(`[BackgroundPGLiteManager] Initialized (starting WASM load in background)`)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Check if WASM is fully loaded and ready for queries.
|
|
408
|
+
*/
|
|
409
|
+
isWASMLoaded(): boolean {
|
|
410
|
+
return this.loadingState === 'loaded' && this.pglite !== null
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Check if WASM is currently loading.
|
|
415
|
+
*/
|
|
416
|
+
isLoading(): boolean {
|
|
417
|
+
return this.loadingState === 'loading'
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Get the current loading state.
|
|
422
|
+
*/
|
|
423
|
+
getLoadingState(): BGWASMLoadingState {
|
|
424
|
+
return this.loadingState
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Get the PGLite instance, waiting for loading to complete if needed.
|
|
429
|
+
* This is called internally by query methods.
|
|
430
|
+
*/
|
|
431
|
+
private async getPglite(): Promise<PGliteLike> {
|
|
432
|
+
// Already loaded
|
|
433
|
+
if (this.pglite) {
|
|
434
|
+
return this.pglite
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Using hoisted instance
|
|
438
|
+
if (this.usingHoistedInstance && !this.config.createPGLite) {
|
|
439
|
+
const waitStart = performance.now()
|
|
440
|
+
const pg = await getOrAwaitHoistedPglite({
|
|
441
|
+
database: this.config.database,
|
|
442
|
+
debug: this.config.debug ? 1 : 0,
|
|
443
|
+
})
|
|
444
|
+
this.pglite = pg
|
|
445
|
+
this.loadingState = 'loaded'
|
|
446
|
+
const waitMs = performance.now() - waitStart
|
|
447
|
+
if (waitMs > 1) {
|
|
448
|
+
console.log(`[BackgroundPGLiteManager] Query waited ${waitMs.toFixed(2)}ms for WASM to finish loading`)
|
|
449
|
+
}
|
|
450
|
+
return pg
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Using custom factory or non-hoisted
|
|
454
|
+
if (this.loadPromise) {
|
|
455
|
+
const waitStart = performance.now()
|
|
456
|
+
const pg = await this.loadPromise
|
|
457
|
+
const waitMs = performance.now() - waitStart
|
|
458
|
+
if (waitMs > 1) {
|
|
459
|
+
console.log(`[BackgroundPGLiteManager] Query waited ${waitMs.toFixed(2)}ms for WASM to finish loading`)
|
|
460
|
+
}
|
|
461
|
+
return pg
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
throw new Error('PGLite not initialized - call initialize() first')
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Execute a query.
|
|
469
|
+
*
|
|
470
|
+
* If WASM is not loaded yet, this will wait for loading to complete.
|
|
471
|
+
* If WASM is already loaded, this returns immediately after query execution.
|
|
472
|
+
*/
|
|
473
|
+
async query<T = unknown>(
|
|
474
|
+
sql: string,
|
|
475
|
+
params?: unknown[]
|
|
476
|
+
): Promise<{
|
|
477
|
+
rows: T[]
|
|
478
|
+
fields: { name: string; dataTypeID: number }[]
|
|
479
|
+
affectedRows?: number
|
|
480
|
+
}> {
|
|
481
|
+
if (!this.initialized) {
|
|
482
|
+
throw new Error('Manager not initialized - call initialize() first')
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (this.loadingState === 'error') {
|
|
486
|
+
throw this.loadError ?? new Error('WASM loading failed')
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Track first query timing
|
|
490
|
+
if (this.metrics.firstQueryAt === null) {
|
|
491
|
+
this.metrics.firstQueryAt = Date.now()
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const pg = await this.getPglite()
|
|
495
|
+
return pg.query<T>(sql, params)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Execute a SQL statement without returning rows.
|
|
500
|
+
*/
|
|
501
|
+
async exec(sql: string): Promise<void> {
|
|
502
|
+
const pg = await this.getPglite()
|
|
503
|
+
if (pg.exec) {
|
|
504
|
+
await pg.exec(sql)
|
|
505
|
+
} else {
|
|
506
|
+
await pg.query(sql)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Check if the manager is initialized (not the same as WASM loaded).
|
|
512
|
+
*/
|
|
513
|
+
isInitialized(): boolean {
|
|
514
|
+
return this.initialized
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Get the PGLite instance if loaded, null otherwise.
|
|
519
|
+
* Does NOT wait for loading - use for checking readiness.
|
|
520
|
+
*/
|
|
521
|
+
getInstanceOrNull(): PGliteLike | null {
|
|
522
|
+
return this.pglite
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Get the PGLite instance (throws if not loaded).
|
|
527
|
+
* Use query() instead to automatically wait for loading.
|
|
528
|
+
*/
|
|
529
|
+
getInstance(): PGliteLike {
|
|
530
|
+
if (!this.pglite) {
|
|
531
|
+
throw new Error('PGLite not loaded yet')
|
|
532
|
+
}
|
|
533
|
+
return this.pglite
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Wait for WASM to be loaded.
|
|
538
|
+
* Call this if you want to ensure WASM is ready before proceeding.
|
|
539
|
+
*/
|
|
540
|
+
async ensureWASMLoaded(): Promise<void> {
|
|
541
|
+
await this.getPglite()
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Check if PGLite is responsive.
|
|
546
|
+
*/
|
|
547
|
+
async checkLiveness(): Promise<boolean> {
|
|
548
|
+
if (!this.isWASMLoaded()) {
|
|
549
|
+
return false
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
const result = await this.pglite!.query('SELECT 1 as alive')
|
|
554
|
+
return result.rows.length > 0
|
|
555
|
+
} catch {
|
|
556
|
+
return false
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Get timing metrics for diagnostics.
|
|
562
|
+
*/
|
|
563
|
+
getMetrics(): {
|
|
564
|
+
initializeCalledAt: number | null
|
|
565
|
+
wasmLoadStartAt: number | null
|
|
566
|
+
wasmLoadEndAt: number | null
|
|
567
|
+
wasmLoadDurationMs: number | null
|
|
568
|
+
firstQueryAt: number | null
|
|
569
|
+
timeFromInitToFirstQueryMs: number | null
|
|
570
|
+
isUsingHoistedInstance: boolean
|
|
571
|
+
} {
|
|
572
|
+
const loadDuration = this.metrics.wasmLoadStartAt && this.metrics.wasmLoadEndAt
|
|
573
|
+
? this.metrics.wasmLoadEndAt - this.metrics.wasmLoadStartAt
|
|
574
|
+
: null
|
|
575
|
+
const initToFirstQuery = this.metrics.initializeCalledAt && this.metrics.firstQueryAt
|
|
576
|
+
? this.metrics.firstQueryAt - this.metrics.initializeCalledAt
|
|
577
|
+
: null
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
...this.metrics,
|
|
581
|
+
wasmLoadDurationMs: loadDuration,
|
|
582
|
+
timeFromInitToFirstQueryMs: initToFirstQuery,
|
|
583
|
+
isUsingHoistedInstance: this.usingHoistedInstance,
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Get comprehensive diagnostics.
|
|
589
|
+
*/
|
|
590
|
+
getDiagnostics(): {
|
|
591
|
+
manager: {
|
|
592
|
+
initialized: boolean
|
|
593
|
+
loadingState: BGWASMLoadingState
|
|
594
|
+
hasInstance: boolean
|
|
595
|
+
usingHoistedInstance: boolean
|
|
596
|
+
error: string | null
|
|
597
|
+
}
|
|
598
|
+
module: ReturnType<typeof getBgHoistedPgliteDiagnostics>
|
|
599
|
+
metrics: ReturnType<BackgroundPGLiteManager['getMetrics']>
|
|
600
|
+
} {
|
|
601
|
+
return {
|
|
602
|
+
manager: {
|
|
603
|
+
initialized: this.initialized,
|
|
604
|
+
loadingState: this.loadingState,
|
|
605
|
+
hasInstance: this.pglite !== null,
|
|
606
|
+
usingHoistedInstance: this.usingHoistedInstance,
|
|
607
|
+
error: this.loadError?.message ?? null,
|
|
608
|
+
},
|
|
609
|
+
module: getBgHoistedPgliteDiagnostics(),
|
|
610
|
+
metrics: this.getMetrics(),
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Close the manager.
|
|
616
|
+
* Note: Does NOT close hoisted instances (they're reused across DO reinstantiations).
|
|
617
|
+
*/
|
|
618
|
+
async close(timeoutMs: number = 5000): Promise<void> {
|
|
619
|
+
// Don't close hoisted instances
|
|
620
|
+
if (this.usingHoistedInstance) {
|
|
621
|
+
console.log(`[BackgroundPGLiteManager] Skipping close of hoisted WASM instance (will be reused)`)
|
|
622
|
+
this.pglite = null
|
|
623
|
+
this.initialized = false
|
|
624
|
+
return
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Close non-hoisted instance
|
|
628
|
+
if (this.pglite?.close) {
|
|
629
|
+
try {
|
|
630
|
+
await Promise.race([
|
|
631
|
+
this.pglite.close(),
|
|
632
|
+
new Promise<void>((_, reject) =>
|
|
633
|
+
setTimeout(() => reject(new Error('Close timeout')), timeoutMs)
|
|
634
|
+
),
|
|
635
|
+
])
|
|
636
|
+
} catch (error) {
|
|
637
|
+
console.error('[BackgroundPGLiteManager] Error closing PGLite:', error)
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
this.pglite = null
|
|
642
|
+
this.initialized = false
|
|
643
|
+
this.loadingState = 'not_started'
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Reset the manager state.
|
|
648
|
+
*/
|
|
649
|
+
reset(): void {
|
|
650
|
+
this.pglite = null
|
|
651
|
+
this.initialized = false
|
|
652
|
+
this.loadingState = 'not_started'
|
|
653
|
+
this.loadError = null
|
|
654
|
+
this.loadPromise = null
|
|
655
|
+
this.usingHoistedInstance = false
|
|
656
|
+
this.metrics = {
|
|
657
|
+
initializeCalledAt: null,
|
|
658
|
+
wasmLoadStartAt: null,
|
|
659
|
+
wasmLoadEndAt: null,
|
|
660
|
+
firstQueryAt: null,
|
|
661
|
+
firstQueryWaitedMs: null,
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Get the plugin manager.
|
|
667
|
+
*/
|
|
668
|
+
getPluginManager(): PluginManager {
|
|
669
|
+
return this.pluginManager
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Factory function to create a BackgroundPGLiteManager.
|
|
675
|
+
*/
|
|
676
|
+
export function createBackgroundPGLiteManager(
|
|
677
|
+
config: BackgroundPGLiteManagerConfig = {}
|
|
678
|
+
): BackgroundPGLiteManager {
|
|
679
|
+
return new BackgroundPGLiteManager(config)
|
|
680
|
+
}
|