@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,595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy PGLite Manager for PostgresDO
|
|
3
|
+
*
|
|
4
|
+
* SPIKE: Lazy WASM Loading Experiment
|
|
5
|
+
*
|
|
6
|
+
* This module implements a lazy loading variant of PGLiteManager that defers
|
|
7
|
+
* WASM loading until the first query is executed, rather than loading on
|
|
8
|
+
* DO instantiation.
|
|
9
|
+
*
|
|
10
|
+
* ## Hypothesis
|
|
11
|
+
*
|
|
12
|
+
* Loading WASM only on first query (not on DO instantiation) may improve:
|
|
13
|
+
* 1. Cold start perception (faster initial response for non-query endpoints)
|
|
14
|
+
* 2. Memory usage (don't load if no queries made)
|
|
15
|
+
*
|
|
16
|
+
* ## Trade-offs
|
|
17
|
+
*
|
|
18
|
+
* - Eager loading: ~1200ms cold start, all subsequent requests fast
|
|
19
|
+
* - Lazy loading: ~0ms until first query, first query pays ~1200ms, subsequent fast
|
|
20
|
+
*
|
|
21
|
+
* For workloads where:
|
|
22
|
+
* - Many requests hit non-query endpoints (health checks, metadata) -> lazy wins
|
|
23
|
+
* - All requests are queries -> eager wins (same total latency, better predictability)
|
|
24
|
+
* - Mixed workload -> depends on query ratio
|
|
25
|
+
*
|
|
26
|
+
* ## Implementation
|
|
27
|
+
*
|
|
28
|
+
* The manager provides:
|
|
29
|
+
* - `isWASMLoaded()` - Check if WASM is loaded without triggering load
|
|
30
|
+
* - `ensureWASMLoaded()` - Explicitly trigger WASM loading
|
|
31
|
+
* - `query()` - Execute query (lazy loads WASM if needed)
|
|
32
|
+
* - Promise deduplication for concurrent first queries
|
|
33
|
+
*
|
|
34
|
+
* @module worker/lazy-pglite-manager
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type { PostgresConfig } from './types'
|
|
38
|
+
import { PluginManager, type PluginManagerConfig } from './plugin-manager'
|
|
39
|
+
import type { PGliteLike } from './do-pglite-manager'
|
|
40
|
+
|
|
41
|
+
// Import WASM module and data bundle for Workers
|
|
42
|
+
// These are bundled at build time as CompiledWasm and Data modules
|
|
43
|
+
import pgliteWasm from '../pglite-assets/pglite.wasm'
|
|
44
|
+
import pgliteData from '../pglite-assets/pglite.data'
|
|
45
|
+
|
|
46
|
+
// Import Workers-compatible PGLite wrapper
|
|
47
|
+
import { createWorkersPGLite, type WorkersPGLite } from '../pglite/workers-pglite'
|
|
48
|
+
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// Module-Level Lazy WASM State (for hoisting across DO reinstantiation)
|
|
51
|
+
// =============================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Hoisted PGLite instance - survives DO class reinstantiation within same isolate.
|
|
55
|
+
* Same as eager loading, but only populated on first query.
|
|
56
|
+
*/
|
|
57
|
+
let lazyHoistedPglite: WorkersPGLite | null = null
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Promise for in-progress PGLite initialization.
|
|
61
|
+
* Prevents duplicate WASM loading when multiple queries execute concurrently.
|
|
62
|
+
*/
|
|
63
|
+
let lazyHoistedPglitePromise: Promise<WorkersPGLite> | null = null
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Timestamp when WASM was first loaded (for diagnostics)
|
|
67
|
+
*/
|
|
68
|
+
let wasmLoadedAt: number | null = null
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Unique identifier for this module instance (for debugging/diagnostics).
|
|
72
|
+
*/
|
|
73
|
+
const LAZY_MODULE_INSTANCE_ID = Math.random().toString(36).slice(2, 10)
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if the lazy hoisted PGLite instance exists.
|
|
77
|
+
*/
|
|
78
|
+
export function hasLazyHoistedPglite(): boolean {
|
|
79
|
+
return lazyHoistedPglite !== null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get diagnostics about the lazy hoisted WASM state.
|
|
84
|
+
*/
|
|
85
|
+
export function getLazyHoistedPgliteDiagnostics(): {
|
|
86
|
+
hasInstance: boolean
|
|
87
|
+
hasPendingPromise: boolean
|
|
88
|
+
moduleInstanceId: string
|
|
89
|
+
wasmLoadedAt: number | null
|
|
90
|
+
timeSinceLoad: number | null
|
|
91
|
+
} {
|
|
92
|
+
const now = Date.now()
|
|
93
|
+
return {
|
|
94
|
+
hasInstance: lazyHoistedPglite !== null,
|
|
95
|
+
hasPendingPromise: lazyHoistedPglitePromise !== null,
|
|
96
|
+
moduleInstanceId: LAZY_MODULE_INSTANCE_ID,
|
|
97
|
+
wasmLoadedAt,
|
|
98
|
+
timeSinceLoad: wasmLoadedAt !== null ? now - wasmLoadedAt : null,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reset the lazy hoisted PGLite instance.
|
|
104
|
+
* WARNING: This should only be used in tests.
|
|
105
|
+
* @internal
|
|
106
|
+
*/
|
|
107
|
+
export function resetLazyHoistedPglite(): void {
|
|
108
|
+
lazyHoistedPglite = null
|
|
109
|
+
lazyHoistedPglitePromise = null
|
|
110
|
+
wasmLoadedAt = null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Lazily get or create the hoisted PGLite instance.
|
|
115
|
+
*
|
|
116
|
+
* This function is called ONLY when a query is executed, not during
|
|
117
|
+
* manager initialization.
|
|
118
|
+
*/
|
|
119
|
+
async function lazyGetOrCreateHoistedPglite(options: {
|
|
120
|
+
database?: string
|
|
121
|
+
debug?: 0 | 1
|
|
122
|
+
}): Promise<WorkersPGLite> {
|
|
123
|
+
// Fast path: already initialized
|
|
124
|
+
if (lazyHoistedPglite) {
|
|
125
|
+
return lazyHoistedPglite
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Deduplication: return existing promise if initialization in progress
|
|
129
|
+
if (lazyHoistedPglitePromise) {
|
|
130
|
+
return lazyHoistedPglitePromise
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const loadStartTime = performance.now()
|
|
134
|
+
|
|
135
|
+
// Start initialization
|
|
136
|
+
lazyHoistedPglitePromise = createWorkersPGLite({
|
|
137
|
+
wasmModule: pgliteWasm,
|
|
138
|
+
fsBundle: pgliteData,
|
|
139
|
+
database: options.database ?? 'postgres',
|
|
140
|
+
debug: options.debug ?? 0,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
lazyHoistedPglite = await lazyHoistedPglitePromise
|
|
145
|
+
wasmLoadedAt = Date.now()
|
|
146
|
+
|
|
147
|
+
const loadDuration = performance.now() - loadStartTime
|
|
148
|
+
console.log(
|
|
149
|
+
`[LazyPGLiteManager] WASM LOADED (lazy, on first query) - took ${loadDuration.toFixed(2)}ms, instance: ${LAZY_MODULE_INSTANCE_ID}`
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return lazyHoistedPglite
|
|
153
|
+
} finally {
|
|
154
|
+
// Clear the promise after resolution (success or failure)
|
|
155
|
+
lazyHoistedPglitePromise = null
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// =============================================================================
|
|
160
|
+
// Loading State Enum
|
|
161
|
+
// =============================================================================
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* WASM loading state for diagnostics
|
|
165
|
+
*/
|
|
166
|
+
export type WASMLoadingState = 'not_loaded' | 'loading' | 'loaded' | 'error'
|
|
167
|
+
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// Configuration
|
|
170
|
+
// =============================================================================
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Configuration for Lazy PGLite manager
|
|
174
|
+
*/
|
|
175
|
+
export interface LazyPGLiteManagerConfig {
|
|
176
|
+
/** Database name */
|
|
177
|
+
database?: string
|
|
178
|
+
/** Enable debug mode */
|
|
179
|
+
debug?: boolean
|
|
180
|
+
/** Plugin configuration */
|
|
181
|
+
plugins?: PostgresConfig['plugins']
|
|
182
|
+
/** Custom PGLite factory for testing */
|
|
183
|
+
createPGLite?: () => Promise<PGliteLike>
|
|
184
|
+
/**
|
|
185
|
+
* Disable WASM hoisting optimization.
|
|
186
|
+
* When false (default), the PGLite WASM instance is cached at module level.
|
|
187
|
+
* @default false
|
|
188
|
+
*/
|
|
189
|
+
disableHoisting?: boolean
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// =============================================================================
|
|
193
|
+
// LazyPGLiteManager Class
|
|
194
|
+
// =============================================================================
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* LazyPGLiteManager defers WASM loading until the first query.
|
|
198
|
+
*
|
|
199
|
+
* Unlike the eager PGLiteManager, this manager does NOT load WASM during
|
|
200
|
+
* initialize(). WASM is only loaded when query() or ensureWASMLoaded() is called.
|
|
201
|
+
*
|
|
202
|
+
* Benefits:
|
|
203
|
+
* - Non-query endpoints (health, metadata, stats) respond immediately
|
|
204
|
+
* - Memory is not consumed until queries are actually executed
|
|
205
|
+
* - Better cold start perception for mixed workloads
|
|
206
|
+
*
|
|
207
|
+
* Trade-offs:
|
|
208
|
+
* - First query incurs the full WASM loading time (~1200ms)
|
|
209
|
+
* - Less predictable query latency (first vs subsequent)
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```typescript
|
|
213
|
+
* const manager = new LazyPGLiteManager({ database: 'mydb' })
|
|
214
|
+
*
|
|
215
|
+
* // Initialize (fast - no WASM loading)
|
|
216
|
+
* await manager.initialize()
|
|
217
|
+
*
|
|
218
|
+
* // Check if WASM is loaded (without triggering load)
|
|
219
|
+
* console.log(manager.isWASMLoaded()) // false
|
|
220
|
+
*
|
|
221
|
+
* // First query triggers WASM load (~1200ms)
|
|
222
|
+
* const result1 = await manager.query('SELECT 1')
|
|
223
|
+
*
|
|
224
|
+
* // Now WASM is loaded
|
|
225
|
+
* console.log(manager.isWASMLoaded()) // true
|
|
226
|
+
*
|
|
227
|
+
* // Subsequent queries are fast (~ms)
|
|
228
|
+
* const result2 = await manager.query('SELECT 2')
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
export class LazyPGLiteManager {
|
|
232
|
+
private pglite: PGliteLike | null = null
|
|
233
|
+
private initialized = false
|
|
234
|
+
private loadingState: WASMLoadingState = 'not_loaded'
|
|
235
|
+
private loadError: Error | null = null
|
|
236
|
+
private config: LazyPGLiteManagerConfig
|
|
237
|
+
private pluginManager: PluginManager
|
|
238
|
+
|
|
239
|
+
/** Track whether this manager is using the hoisted instance */
|
|
240
|
+
private usingHoistedInstance = false
|
|
241
|
+
|
|
242
|
+
/** Timing metrics for diagnostics */
|
|
243
|
+
private metrics = {
|
|
244
|
+
initializeCalledAt: null as number | null,
|
|
245
|
+
firstQueryAt: null as number | null,
|
|
246
|
+
wasmLoadStartAt: null as number | null,
|
|
247
|
+
wasmLoadEndAt: null as number | null,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
constructor(config: LazyPGLiteManagerConfig = {}) {
|
|
251
|
+
this.config = {
|
|
252
|
+
database: config.database ?? 'postgres',
|
|
253
|
+
debug: config.debug ?? false,
|
|
254
|
+
}
|
|
255
|
+
if (config.plugins !== undefined) {
|
|
256
|
+
this.config.plugins = config.plugins
|
|
257
|
+
}
|
|
258
|
+
if (config.createPGLite !== undefined) {
|
|
259
|
+
this.config.createPGLite = config.createPGLite
|
|
260
|
+
}
|
|
261
|
+
if (config.disableHoisting !== undefined) {
|
|
262
|
+
this.config.disableHoisting = config.disableHoisting
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Build plugin manager config
|
|
266
|
+
const pluginManagerConfig: PluginManagerConfig = {}
|
|
267
|
+
if (config.plugins !== undefined) {
|
|
268
|
+
pluginManagerConfig.plugins = config.plugins
|
|
269
|
+
}
|
|
270
|
+
if (config.debug !== undefined) {
|
|
271
|
+
pluginManagerConfig.debug = config.debug
|
|
272
|
+
}
|
|
273
|
+
this.pluginManager = new PluginManager(pluginManagerConfig)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check if the manager is initialized.
|
|
278
|
+
* Note: This returns true after initialize() is called, even if WASM is not loaded.
|
|
279
|
+
*/
|
|
280
|
+
isInitialized(): boolean {
|
|
281
|
+
return this.initialized
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Check if WASM is loaded (without triggering load).
|
|
286
|
+
* This is the key difference from eager loading.
|
|
287
|
+
*/
|
|
288
|
+
isWASMLoaded(): boolean {
|
|
289
|
+
return this.pglite !== null
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get the current WASM loading state.
|
|
294
|
+
*/
|
|
295
|
+
getLoadingState(): WASMLoadingState {
|
|
296
|
+
return this.loadingState
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get any error that occurred during WASM loading.
|
|
301
|
+
*/
|
|
302
|
+
getLoadError(): Error | null {
|
|
303
|
+
return this.loadError
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get the PGLite instance (throws if not loaded).
|
|
308
|
+
* For lazy loading, prefer using query() which handles loading automatically.
|
|
309
|
+
*/
|
|
310
|
+
getInstance(): PGliteLike {
|
|
311
|
+
if (!this.pglite) {
|
|
312
|
+
throw new Error('PGLite WASM not loaded. Call ensureWASMLoaded() or query() first.')
|
|
313
|
+
}
|
|
314
|
+
return this.pglite
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get the PGLite instance or null if not loaded.
|
|
319
|
+
*/
|
|
320
|
+
getInstanceOrNull(): PGliteLike | null {
|
|
321
|
+
return this.pglite
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get the plugin manager.
|
|
326
|
+
*/
|
|
327
|
+
getPluginManager(): PluginManager {
|
|
328
|
+
return this.pluginManager
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Get timing metrics for diagnostics.
|
|
333
|
+
*/
|
|
334
|
+
getMetrics(): {
|
|
335
|
+
initializeCalledAt: number | null
|
|
336
|
+
firstQueryAt: number | null
|
|
337
|
+
wasmLoadStartAt: number | null
|
|
338
|
+
wasmLoadEndAt: number | null
|
|
339
|
+
wasmLoadDurationMs: number | null
|
|
340
|
+
timeToFirstQuery: number | null
|
|
341
|
+
} {
|
|
342
|
+
const wasmLoadDurationMs =
|
|
343
|
+
this.metrics.wasmLoadStartAt !== null && this.metrics.wasmLoadEndAt !== null
|
|
344
|
+
? this.metrics.wasmLoadEndAt - this.metrics.wasmLoadStartAt
|
|
345
|
+
: null
|
|
346
|
+
|
|
347
|
+
const timeToFirstQuery =
|
|
348
|
+
this.metrics.initializeCalledAt !== null && this.metrics.firstQueryAt !== null
|
|
349
|
+
? this.metrics.firstQueryAt - this.metrics.initializeCalledAt
|
|
350
|
+
: null
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
...this.metrics,
|
|
354
|
+
wasmLoadDurationMs,
|
|
355
|
+
timeToFirstQuery,
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Initialize the manager (does NOT load WASM).
|
|
361
|
+
*
|
|
362
|
+
* This is intentionally fast - WASM loading is deferred to first query.
|
|
363
|
+
* Safe to call multiple times.
|
|
364
|
+
*/
|
|
365
|
+
async initialize(): Promise<void> {
|
|
366
|
+
if (this.initialized) {
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
this.metrics.initializeCalledAt = performance.now()
|
|
371
|
+
this.initialized = true
|
|
372
|
+
|
|
373
|
+
console.log(`[LazyPGLiteManager] Initialized (WASM not loaded yet)`)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Explicitly ensure WASM is loaded.
|
|
378
|
+
*
|
|
379
|
+
* Use this if you want to pre-warm WASM loading before the first query,
|
|
380
|
+
* but still want the option to skip loading entirely for non-query requests.
|
|
381
|
+
*/
|
|
382
|
+
async ensureWASMLoaded(): Promise<void> {
|
|
383
|
+
if (this.pglite) {
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
await this.loadWASM()
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Internal method to load WASM.
|
|
392
|
+
*/
|
|
393
|
+
private async loadWASM(): Promise<PGliteLike> {
|
|
394
|
+
if (this.pglite) {
|
|
395
|
+
return this.pglite
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (this.loadingState === 'loading') {
|
|
399
|
+
// Wait for existing load to complete
|
|
400
|
+
if (lazyHoistedPglitePromise) {
|
|
401
|
+
const result = await lazyHoistedPglitePromise
|
|
402
|
+
this.pglite = result
|
|
403
|
+
this.loadingState = 'loaded'
|
|
404
|
+
return result
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
this.loadingState = 'loading'
|
|
409
|
+
this.metrics.wasmLoadStartAt = performance.now()
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
// Use custom factory if provided (for testing)
|
|
413
|
+
if (this.config.createPGLite) {
|
|
414
|
+
this.usingHoistedInstance = false
|
|
415
|
+
this.pglite = await this.config.createPGLite()
|
|
416
|
+
} else if (this.config.disableHoisting) {
|
|
417
|
+
// Create new instance without hoisting
|
|
418
|
+
this.usingHoistedInstance = false
|
|
419
|
+
console.log(`[LazyPGLiteManager] WASM hoisting disabled - creating new instance`)
|
|
420
|
+
this.pglite = await createWorkersPGLite({
|
|
421
|
+
wasmModule: pgliteWasm,
|
|
422
|
+
fsBundle: pgliteData,
|
|
423
|
+
database: this.config.database ?? 'postgres',
|
|
424
|
+
debug: this.config.debug ? 1 : 0,
|
|
425
|
+
})
|
|
426
|
+
} else {
|
|
427
|
+
// Use the lazy hoisted instance
|
|
428
|
+
const wasmWasAlreadyLoaded = lazyHoistedPglite !== null
|
|
429
|
+
this.pglite = await lazyGetOrCreateHoistedPglite({
|
|
430
|
+
database: this.config.database,
|
|
431
|
+
debug: this.config.debug ? 1 : 0,
|
|
432
|
+
})
|
|
433
|
+
this.usingHoistedInstance = true
|
|
434
|
+
|
|
435
|
+
if (wasmWasAlreadyLoaded) {
|
|
436
|
+
const diagnostics = getLazyHoistedPgliteDiagnostics()
|
|
437
|
+
console.log(
|
|
438
|
+
`[LazyPGLiteManager] WASM REUSED (lazy hoisted) - loaded ${diagnostics.timeSinceLoad}ms ago, instance: ${diagnostics.moduleInstanceId}`
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
this.metrics.wasmLoadEndAt = performance.now()
|
|
444
|
+
this.loadingState = 'loaded'
|
|
445
|
+
|
|
446
|
+
// Wait for ready if needed
|
|
447
|
+
if (this.pglite.waitReady) {
|
|
448
|
+
await this.pglite.waitReady
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Run auto-create for plugins
|
|
452
|
+
await this.pluginManager.runAutoCreate(this.pglite)
|
|
453
|
+
|
|
454
|
+
return this.pglite
|
|
455
|
+
} catch (error) {
|
|
456
|
+
this.loadingState = 'error'
|
|
457
|
+
this.loadError = error instanceof Error ? error : new Error(String(error))
|
|
458
|
+
throw error
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Execute a query (lazy loads WASM if needed).
|
|
464
|
+
*
|
|
465
|
+
* This is the primary method for interacting with the database.
|
|
466
|
+
* On first call, it will trigger WASM loading.
|
|
467
|
+
*/
|
|
468
|
+
async query<T = unknown>(
|
|
469
|
+
sql: string,
|
|
470
|
+
params?: unknown[]
|
|
471
|
+
): Promise<{
|
|
472
|
+
rows: T[]
|
|
473
|
+
fields: { name: string; dataTypeID: number }[]
|
|
474
|
+
affectedRows?: number
|
|
475
|
+
}> {
|
|
476
|
+
// Track first query time
|
|
477
|
+
if (this.metrics.firstQueryAt === null) {
|
|
478
|
+
this.metrics.firstQueryAt = performance.now()
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Lazy load WASM if not loaded
|
|
482
|
+
const pglite = await this.loadWASM()
|
|
483
|
+
|
|
484
|
+
// Execute query
|
|
485
|
+
return pglite.query<T>(sql, params)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Check if PGLite is responsive by running a simple query.
|
|
490
|
+
* This will trigger WASM loading if not already loaded.
|
|
491
|
+
*/
|
|
492
|
+
async checkLiveness(): Promise<boolean> {
|
|
493
|
+
try {
|
|
494
|
+
const result = await this.query('SELECT 1 as alive')
|
|
495
|
+
return result.rows.length > 0
|
|
496
|
+
} catch {
|
|
497
|
+
return false
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Close PGLite instance.
|
|
503
|
+
*/
|
|
504
|
+
async close(timeoutMs: number = 5000): Promise<void> {
|
|
505
|
+
// If using hoisted instance, don't close it
|
|
506
|
+
if (this.usingHoistedInstance) {
|
|
507
|
+
console.log(`[LazyPGLiteManager] Skipping close of hoisted WASM instance (will be reused)`)
|
|
508
|
+
this.pglite = null
|
|
509
|
+
this.initialized = false
|
|
510
|
+
this.usingHoistedInstance = false
|
|
511
|
+
this.loadingState = 'not_loaded'
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (!this.pglite?.close) {
|
|
516
|
+
this.pglite = null
|
|
517
|
+
this.initialized = false
|
|
518
|
+
this.loadingState = 'not_loaded'
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
await Promise.race([
|
|
524
|
+
this.pglite.close(),
|
|
525
|
+
new Promise<void>((_, reject) =>
|
|
526
|
+
setTimeout(() => reject(new Error('PGlite close timeout')), timeoutMs)
|
|
527
|
+
),
|
|
528
|
+
])
|
|
529
|
+
} catch (error) {
|
|
530
|
+
console.error('Error closing PGlite:', error)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
this.pglite = null
|
|
534
|
+
this.initialized = false
|
|
535
|
+
this.loadingState = 'not_loaded'
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Reset manager state (for testing or hibernation recovery).
|
|
540
|
+
*/
|
|
541
|
+
reset(): void {
|
|
542
|
+
this.pglite = null
|
|
543
|
+
this.initialized = false
|
|
544
|
+
this.usingHoistedInstance = false
|
|
545
|
+
this.loadingState = 'not_loaded'
|
|
546
|
+
this.loadError = null
|
|
547
|
+
this.metrics = {
|
|
548
|
+
initializeCalledAt: null,
|
|
549
|
+
firstQueryAt: null,
|
|
550
|
+
wasmLoadStartAt: null,
|
|
551
|
+
wasmLoadEndAt: null,
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Check if this manager is using the hoisted WASM instance.
|
|
557
|
+
*/
|
|
558
|
+
isUsingHoistedInstance(): boolean {
|
|
559
|
+
return this.usingHoistedInstance
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Get comprehensive diagnostics about lazy loading state.
|
|
564
|
+
*/
|
|
565
|
+
getDiagnostics(): {
|
|
566
|
+
initialized: boolean
|
|
567
|
+
wasmLoaded: boolean
|
|
568
|
+
loadingState: WASMLoadingState
|
|
569
|
+
usingHoistedInstance: boolean
|
|
570
|
+
metrics: ReturnType<LazyPGLiteManager['getMetrics']>
|
|
571
|
+
hoisted: {
|
|
572
|
+
hasInstance: boolean
|
|
573
|
+
hasPendingPromise: boolean
|
|
574
|
+
moduleInstanceId: string
|
|
575
|
+
wasmLoadedAt: number | null
|
|
576
|
+
timeSinceLoad: number | null
|
|
577
|
+
}
|
|
578
|
+
} {
|
|
579
|
+
return {
|
|
580
|
+
initialized: this.initialized,
|
|
581
|
+
wasmLoaded: this.pglite !== null,
|
|
582
|
+
loadingState: this.loadingState,
|
|
583
|
+
usingHoistedInstance: this.usingHoistedInstance,
|
|
584
|
+
metrics: this.getMetrics(),
|
|
585
|
+
hoisted: getLazyHoistedPgliteDiagnostics(),
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Factory function to create a LazyPGLiteManager.
|
|
592
|
+
*/
|
|
593
|
+
export function createLazyPGLiteManager(config?: LazyPGLiteManagerConfig): LazyPGLiteManager {
|
|
594
|
+
return new LazyPGLiteManager(config)
|
|
595
|
+
}
|