@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
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
* Handles PGLite instance creation, initialization, and lifecycle.
|
|
5
5
|
* Extracted from PostgresDO to reduce class size and separate concerns.
|
|
6
6
|
*
|
|
7
|
+
* ## WASM Hoisting Pattern
|
|
8
|
+
*
|
|
9
|
+
* This module implements WASM hoisting to reduce "warm start" time from ~1200ms to ~30ms.
|
|
10
|
+
*
|
|
11
|
+
* The problem: In Cloudflare Workers, when a DO class is recreated (but the isolate stays alive),
|
|
12
|
+
* the PGLite WASM needs to be reloaded. WASM initialization takes ~1200ms.
|
|
13
|
+
*
|
|
14
|
+
* The solution: Cache the PGLite instance at module level. The module loads once per isolate,
|
|
15
|
+
* so the PGLite instance survives DO class reinstantiation. This means:
|
|
16
|
+
* - Cold start: ~1200ms (full WASM initialization)
|
|
17
|
+
* - Warm start (isolate alive, DO recreated): ~30ms (reuse cached WASM)
|
|
18
|
+
*
|
|
7
19
|
* @module worker/do-pglite-manager
|
|
8
20
|
*/
|
|
9
21
|
|
|
@@ -16,7 +28,131 @@ import pgliteWasm from '../pglite-assets/pglite.wasm'
|
|
|
16
28
|
import pgliteData from '../pglite-assets/pglite.data'
|
|
17
29
|
|
|
18
30
|
// Import Workers-compatible PGLite wrapper
|
|
19
|
-
import { createWorkersPGLite } from '../pglite/workers-pglite'
|
|
31
|
+
import { createWorkersPGLite, type WorkersPGLite } from '../pglite/workers-pglite'
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// Module-Level WASM Hoisting (outside class)
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// These persist as long as the isolate lives - helps reduce warm start time.
|
|
37
|
+
// The module loads once per isolate, so the PGLite instance survives
|
|
38
|
+
// across DO class reinstantiations within the same isolate.
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Hoisted PGLite instance - survives DO class reinstantiation within same isolate.
|
|
42
|
+
* This is the key to reducing warm start time from ~1200ms to ~30ms.
|
|
43
|
+
*/
|
|
44
|
+
let hoistedPglite: WorkersPGLite | null = null
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Promise for in-progress PGLite initialization.
|
|
48
|
+
* Prevents duplicate WASM loading when multiple DO instances initialize concurrently.
|
|
49
|
+
*/
|
|
50
|
+
let hoistedPglitePromise: Promise<WorkersPGLite> | null = null
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Module load time for diagnostics.
|
|
54
|
+
* Note: Date.now() at module evaluation time in Workers returns 0,
|
|
55
|
+
* so we capture the first access time instead.
|
|
56
|
+
*/
|
|
57
|
+
let MODULE_LOAD_TIME: number | null = null
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Unique identifier for this module instance (for debugging/diagnostics).
|
|
61
|
+
*/
|
|
62
|
+
const MODULE_INSTANCE_ID = Math.random().toString(36).slice(2, 10)
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the module load time (captures on first call since Date.now() is 0 at module load).
|
|
66
|
+
*/
|
|
67
|
+
function getModuleLoadTime(): number {
|
|
68
|
+
if (MODULE_LOAD_TIME === null) {
|
|
69
|
+
MODULE_LOAD_TIME = Date.now()
|
|
70
|
+
}
|
|
71
|
+
return MODULE_LOAD_TIME
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get or create the hoisted PGLite instance.
|
|
76
|
+
*
|
|
77
|
+
* This function implements the singleton pattern with promise deduplication:
|
|
78
|
+
* - If an instance exists, return it immediately
|
|
79
|
+
* - If initialization is in progress, return the existing promise
|
|
80
|
+
* - Otherwise, start initialization and cache both the promise and the result
|
|
81
|
+
*
|
|
82
|
+
* @param options - PGLite creation options (only used if creating new instance)
|
|
83
|
+
* @returns Promise resolving to the PGLite instance
|
|
84
|
+
*/
|
|
85
|
+
async function getOrCreateHoistedPglite(options: {
|
|
86
|
+
database?: string
|
|
87
|
+
debug?: 0 | 1
|
|
88
|
+
}): Promise<WorkersPGLite> {
|
|
89
|
+
// Fast path: already initialized
|
|
90
|
+
if (hoistedPglite) {
|
|
91
|
+
return hoistedPglite
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Deduplication: return existing promise if initialization in progress
|
|
95
|
+
if (hoistedPglitePromise) {
|
|
96
|
+
return hoistedPglitePromise
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Start initialization
|
|
100
|
+
hoistedPglitePromise = createWorkersPGLite({
|
|
101
|
+
wasmModule: pgliteWasm,
|
|
102
|
+
fsBundle: pgliteData,
|
|
103
|
+
database: options.database ?? 'postgres',
|
|
104
|
+
debug: options.debug ?? 0,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
hoistedPglite = await hoistedPglitePromise
|
|
109
|
+
return hoistedPglite
|
|
110
|
+
} finally {
|
|
111
|
+
// Clear the promise after resolution (success or failure)
|
|
112
|
+
hoistedPglitePromise = null
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if the hoisted PGLite instance exists.
|
|
118
|
+
* Useful for diagnostics and testing.
|
|
119
|
+
*/
|
|
120
|
+
export function hasHoistedPglite(): boolean {
|
|
121
|
+
return hoistedPglite !== null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get diagnostics about the hoisted WASM state.
|
|
126
|
+
* Useful for debugging warm start performance.
|
|
127
|
+
*/
|
|
128
|
+
export function getHoistedPgliteDiagnostics(): {
|
|
129
|
+
hasInstance: boolean
|
|
130
|
+
hasPendingPromise: boolean
|
|
131
|
+
moduleInstanceId: string
|
|
132
|
+
moduleLoadTime: number
|
|
133
|
+
moduleAgeMs: number
|
|
134
|
+
} {
|
|
135
|
+
const now = Date.now()
|
|
136
|
+
const loadTime = getModuleLoadTime()
|
|
137
|
+
return {
|
|
138
|
+
hasInstance: hoistedPglite !== null,
|
|
139
|
+
hasPendingPromise: hoistedPglitePromise !== null,
|
|
140
|
+
moduleInstanceId: MODULE_INSTANCE_ID,
|
|
141
|
+
moduleLoadTime: loadTime,
|
|
142
|
+
moduleAgeMs: now - loadTime,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Reset the hoisted PGLite instance.
|
|
148
|
+
* WARNING: This should only be used in tests. In production, the hoisted
|
|
149
|
+
* instance should persist for the lifetime of the isolate.
|
|
150
|
+
* @internal
|
|
151
|
+
*/
|
|
152
|
+
export function resetHoistedPglite(): void {
|
|
153
|
+
hoistedPglite = null
|
|
154
|
+
hoistedPglitePromise = null
|
|
155
|
+
}
|
|
20
156
|
|
|
21
157
|
/**
|
|
22
158
|
* PGlite-like interface
|
|
@@ -50,6 +186,14 @@ export interface PGLiteManagerConfig {
|
|
|
50
186
|
plugins?: PostgresConfig['plugins']
|
|
51
187
|
/** Custom PGLite factory for testing */
|
|
52
188
|
createPGLite?: () => Promise<PGliteLike>
|
|
189
|
+
/**
|
|
190
|
+
* Disable WASM hoisting optimization.
|
|
191
|
+
* When false (default), the PGLite WASM instance is cached at module level
|
|
192
|
+
* and survives DO class reinstantiation for optimal warm start performance.
|
|
193
|
+
* Set to true to create a new WASM instance for each PGLiteManager.
|
|
194
|
+
* @default false
|
|
195
|
+
*/
|
|
196
|
+
disableHoisting?: boolean
|
|
53
197
|
}
|
|
54
198
|
|
|
55
199
|
/**
|
|
@@ -85,6 +229,9 @@ export class PGLiteManager {
|
|
|
85
229
|
private config: PGLiteManagerConfig
|
|
86
230
|
private pluginManager: PluginManager
|
|
87
231
|
|
|
232
|
+
/** Track whether this manager is using the hoisted instance */
|
|
233
|
+
private usingHoistedInstance = false
|
|
234
|
+
|
|
88
235
|
constructor(config: PGLiteManagerConfig = {}) {
|
|
89
236
|
// Build config without undefined values for exactOptionalPropertyTypes
|
|
90
237
|
this.config = {
|
|
@@ -97,6 +244,9 @@ export class PGLiteManager {
|
|
|
97
244
|
if (config.createPGLite !== undefined) {
|
|
98
245
|
this.config.createPGLite = config.createPGLite
|
|
99
246
|
}
|
|
247
|
+
if (config.disableHoisting !== undefined) {
|
|
248
|
+
this.config.disableHoisting = config.disableHoisting
|
|
249
|
+
}
|
|
100
250
|
|
|
101
251
|
// Build plugin manager config without undefined values for exactOptionalPropertyTypes
|
|
102
252
|
const pluginManagerConfig: PluginManagerConfig = {}
|
|
@@ -166,10 +316,15 @@ export class PGLiteManager {
|
|
|
166
316
|
/**
|
|
167
317
|
* Create the PGLite instance
|
|
168
318
|
* Can be overridden for testing
|
|
319
|
+
*
|
|
320
|
+
* By default, uses the module-level hoisted PGLite instance for optimal
|
|
321
|
+
* warm start performance. The hoisted instance survives DO class
|
|
322
|
+
* reinstantiation within the same isolate.
|
|
169
323
|
*/
|
|
170
324
|
protected async createPGLiteInstance(): Promise<PGliteLike> {
|
|
171
|
-
// Use custom factory if provided
|
|
325
|
+
// Use custom factory if provided (for testing)
|
|
172
326
|
if (this.config.createPGLite) {
|
|
327
|
+
this.usingHoistedInstance = false
|
|
173
328
|
return this.config.createPGLite()
|
|
174
329
|
}
|
|
175
330
|
|
|
@@ -180,25 +335,37 @@ export class PGLiteManager {
|
|
|
180
335
|
// as WorkersPGLite doesn't support extension loading yet
|
|
181
336
|
void extensions // Mark as intentionally unused
|
|
182
337
|
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
fsBundle: pgliteData,
|
|
194
|
-
// Database configuration
|
|
195
|
-
debug: this.config.debug ? 1 : 0,
|
|
196
|
-
}
|
|
197
|
-
if (this.config.database !== undefined) {
|
|
198
|
-
pgliteOptions.database = this.config.database
|
|
338
|
+
// If hoisting is disabled, create a new instance each time
|
|
339
|
+
if (this.config.disableHoisting) {
|
|
340
|
+
this.usingHoistedInstance = false
|
|
341
|
+
console.log(`[PGLiteManager] WASM hoisting disabled - creating new instance`)
|
|
342
|
+
return createWorkersPGLite({
|
|
343
|
+
wasmModule: pgliteWasm,
|
|
344
|
+
fsBundle: pgliteData,
|
|
345
|
+
database: this.config.database ?? 'postgres',
|
|
346
|
+
debug: this.config.debug ? 1 : 0,
|
|
347
|
+
})
|
|
199
348
|
}
|
|
200
349
|
|
|
201
|
-
|
|
350
|
+
// Use the hoisted PGLite instance for optimal warm start performance.
|
|
351
|
+
// This is the key optimization: when the DO class is recreated but the
|
|
352
|
+
// isolate stays alive, we reuse the already-initialized WASM instance
|
|
353
|
+
// instead of loading it again (~1200ms -> ~30ms).
|
|
354
|
+
const wasmWasAlreadyLoaded = hoistedPglite !== null
|
|
355
|
+
const pg = await getOrCreateHoistedPglite({
|
|
356
|
+
database: this.config.database,
|
|
357
|
+
debug: this.config.debug ? 1 : 0,
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
this.usingHoistedInstance = true
|
|
361
|
+
|
|
362
|
+
// Log whether we reused the WASM instance (helpful for debugging warm starts)
|
|
363
|
+
if (wasmWasAlreadyLoaded) {
|
|
364
|
+
const diagnostics = getHoistedPgliteDiagnostics()
|
|
365
|
+
console.log(`[PGLiteManager] WASM REUSED - module age: ${diagnostics.moduleAgeMs}ms, instance: ${diagnostics.moduleInstanceId}`)
|
|
366
|
+
} else {
|
|
367
|
+
console.log(`[PGLiteManager] WASM LOADED (cold start) - instance: ${MODULE_INSTANCE_ID}`)
|
|
368
|
+
}
|
|
202
369
|
|
|
203
370
|
return pg
|
|
204
371
|
}
|
|
@@ -261,9 +428,28 @@ export class PGLiteManager {
|
|
|
261
428
|
/**
|
|
262
429
|
* Close PGLite instance
|
|
263
430
|
*
|
|
431
|
+
* Note: When using the hoisted WASM instance (default), this method does NOT
|
|
432
|
+
* close the underlying PGLite instance, as it's shared across DO instances.
|
|
433
|
+
* It only clears the local reference. The hoisted instance persists for the
|
|
434
|
+
* lifetime of the isolate for optimal warm start performance.
|
|
435
|
+
*
|
|
436
|
+
* To actually close the hoisted instance, use resetHoistedPglite() followed
|
|
437
|
+
* by close() on the next manager that initializes. This should only be done
|
|
438
|
+
* in tests or when the isolate is shutting down.
|
|
439
|
+
*
|
|
264
440
|
* @param timeoutMs - Maximum time to wait for close (default: 5000ms)
|
|
265
441
|
*/
|
|
266
442
|
async close(timeoutMs: number = 5000): Promise<void> {
|
|
443
|
+
// If using hoisted instance, don't actually close it - just clear our reference
|
|
444
|
+
// The hoisted instance should survive for the lifetime of the isolate
|
|
445
|
+
if (this.usingHoistedInstance) {
|
|
446
|
+
console.log(`[PGLiteManager] Skipping close of hoisted WASM instance (will be reused)`)
|
|
447
|
+
this.pglite = null
|
|
448
|
+
this.initialized = false
|
|
449
|
+
this.usingHoistedInstance = false
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
|
|
267
453
|
if (!this.pglite?.close) {
|
|
268
454
|
this.pglite = null
|
|
269
455
|
this.initialized = false
|
|
@@ -291,6 +477,36 @@ export class PGLiteManager {
|
|
|
291
477
|
reset(): void {
|
|
292
478
|
this.pglite = null
|
|
293
479
|
this.initialized = false
|
|
480
|
+
this.usingHoistedInstance = false
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Check if this manager is using the hoisted WASM instance
|
|
485
|
+
*/
|
|
486
|
+
isUsingHoistedInstance(): boolean {
|
|
487
|
+
return this.usingHoistedInstance
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Get diagnostics about WASM hoisting and initialization state.
|
|
492
|
+
* Useful for debugging warm start performance.
|
|
493
|
+
*/
|
|
494
|
+
getDiagnostics(): {
|
|
495
|
+
initialized: boolean
|
|
496
|
+
usingHoistedInstance: boolean
|
|
497
|
+
hoisted: {
|
|
498
|
+
hasInstance: boolean
|
|
499
|
+
hasPendingPromise: boolean
|
|
500
|
+
moduleInstanceId: string
|
|
501
|
+
moduleLoadTime: number
|
|
502
|
+
moduleAgeMs: number
|
|
503
|
+
}
|
|
504
|
+
} {
|
|
505
|
+
return {
|
|
506
|
+
initialized: this.initialized,
|
|
507
|
+
usingHoistedInstance: this.usingHoistedInstance,
|
|
508
|
+
hoisted: getHoistedPgliteDiagnostics(),
|
|
509
|
+
}
|
|
294
510
|
}
|
|
295
511
|
}
|
|
296
512
|
|
package/src/worker/entry.ts
CHANGED
|
@@ -152,42 +152,44 @@ function createApp() {
|
|
|
152
152
|
app.post('/query', async (c) => {
|
|
153
153
|
const dbId = c.req.query('db') || 'default'
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const url = new URL(c.req.url)
|
|
161
|
-
url.pathname = '/query'
|
|
162
|
-
url.searchParams.delete('db')
|
|
163
|
-
|
|
164
|
-
const doRequest = new Request(url.toString(), {
|
|
165
|
-
method: 'POST',
|
|
166
|
-
headers: c.req.raw.headers,
|
|
167
|
-
body: c.req.raw.body,
|
|
168
|
-
})
|
|
155
|
+
try {
|
|
156
|
+
const body = await c.req.json<{ sql: string; params?: unknown[] }>()
|
|
157
|
+
if (!body.sql) {
|
|
158
|
+
return c.json({ error: true, code: 'INVALID_REQUEST', message: 'Missing sql field' }, 400)
|
|
159
|
+
}
|
|
169
160
|
|
|
170
|
-
|
|
161
|
+
// Get the Durable Object stub and call RPC method directly
|
|
162
|
+
const doId = c.env.POSTGRES_DO.idFromName(dbId)
|
|
163
|
+
const stub = c.env.POSTGRES_DO.get(doId) as DurableObjectStub<PostgresDO>
|
|
164
|
+
const result = await stub.rpcQuery(body.sql, body.params)
|
|
165
|
+
return c.json(result)
|
|
166
|
+
} catch (error) {
|
|
167
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
168
|
+
return c.json({ error: true, code: 'QUERY_ERROR', message }, 500)
|
|
169
|
+
}
|
|
171
170
|
})
|
|
172
171
|
|
|
173
172
|
// Batch queries
|
|
174
173
|
app.post('/batch', async (c) => {
|
|
175
174
|
const dbId = c.req.query('db') || 'default'
|
|
176
175
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
url.searchParams.delete('db')
|
|
183
|
-
|
|
184
|
-
const doRequest = new Request(url.toString(), {
|
|
185
|
-
method: 'POST',
|
|
186
|
-
headers: c.req.raw.headers,
|
|
187
|
-
body: c.req.raw.body,
|
|
188
|
-
})
|
|
176
|
+
try {
|
|
177
|
+
const body = await c.req.json<{ queries: Array<{ sql: string; params?: unknown[] }>; transaction?: boolean }>()
|
|
178
|
+
if (!body.queries || !Array.isArray(body.queries)) {
|
|
179
|
+
return c.json({ error: true, code: 'INVALID_REQUEST', message: 'Missing queries array' }, 400)
|
|
180
|
+
}
|
|
189
181
|
|
|
190
|
-
|
|
182
|
+
// Get the Durable Object stub and call RPC method directly
|
|
183
|
+
const doId = c.env.POSTGRES_DO.idFromName(dbId)
|
|
184
|
+
const stub = c.env.POSTGRES_DO.get(doId) as DurableObjectStub<PostgresDO>
|
|
185
|
+
const result = body.transaction
|
|
186
|
+
? await stub.rpcBatchTransaction(body.queries)
|
|
187
|
+
: await stub.rpcBatch(body.queries)
|
|
188
|
+
return c.json(result)
|
|
189
|
+
} catch (error) {
|
|
190
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
191
|
+
return c.json({ error: true, code: 'BATCH_ERROR', message }, 500)
|
|
192
|
+
}
|
|
191
193
|
})
|
|
192
194
|
|
|
193
195
|
// ==========================================================================
|
|
@@ -383,8 +385,88 @@ function createApp() {
|
|
|
383
385
|
}
|
|
384
386
|
})
|
|
385
387
|
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
+
// ==========================================================================
|
|
389
|
+
// Dynamic database routes (for postgres.do client compatibility)
|
|
390
|
+
// ==========================================================================
|
|
391
|
+
// This handles URLs like https://db.example.com.ai/neon-example/query
|
|
392
|
+
// where the first path segment is the database ID
|
|
393
|
+
// We use notFound to handle these since Hono matches more specific routes first
|
|
394
|
+
|
|
395
|
+
const RESERVED_PATHS = new Set(['ping', 'health', 'user', 'db', 'rpc', 'query', 'batch'])
|
|
396
|
+
|
|
397
|
+
// 404 handler - also handles dynamic database routes
|
|
398
|
+
// Supports postgres.do client URLs like https://db.api.qa/mydb/query
|
|
399
|
+
app.notFound(async (c) => {
|
|
400
|
+
// Try to handle as dynamic database route: /:dbId/...
|
|
401
|
+
const pathParts = c.req.path.split('/').filter(Boolean)
|
|
402
|
+
if (pathParts.length >= 1) {
|
|
403
|
+
const dbId = pathParts[0]
|
|
404
|
+
|
|
405
|
+
// Skip if it's a reserved path
|
|
406
|
+
if (!RESERVED_PATHS.has(dbId)) {
|
|
407
|
+
const remainingPath = '/' + pathParts.slice(1).join('/')
|
|
408
|
+
|
|
409
|
+
// Get the Durable Object stub
|
|
410
|
+
const doId = c.env.POSTGRES_DO.idFromName(dbId)
|
|
411
|
+
const stub = c.env.POSTGRES_DO.get(doId) as DurableObjectStub<PostgresDO>
|
|
412
|
+
|
|
413
|
+
// Handle specific paths via RPC
|
|
414
|
+
if (remainingPath === '/query' && c.req.method === 'POST') {
|
|
415
|
+
try {
|
|
416
|
+
const body = await c.req.json<{ sql: string; params?: unknown[] }>()
|
|
417
|
+
if (!body.sql) {
|
|
418
|
+
return c.json({ error: true, code: 'INVALID_REQUEST', message: 'Missing sql field' }, 400)
|
|
419
|
+
}
|
|
420
|
+
const result = await stub.rpcQuery(body.sql, body.params)
|
|
421
|
+
return c.json(result)
|
|
422
|
+
} catch (error) {
|
|
423
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
424
|
+
return c.json({ error: true, code: 'QUERY_ERROR', message }, 500)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (remainingPath === '/batch' && c.req.method === 'POST') {
|
|
429
|
+
try {
|
|
430
|
+
const body = await c.req.json<{ queries: Array<{ sql: string; params?: unknown[] }>; transaction?: boolean }>()
|
|
431
|
+
if (!body.queries || !Array.isArray(body.queries)) {
|
|
432
|
+
return c.json({ error: true, code: 'INVALID_REQUEST', message: 'Missing queries array' }, 400)
|
|
433
|
+
}
|
|
434
|
+
const result = body.transaction
|
|
435
|
+
? await stub.rpcBatchTransaction(body.queries)
|
|
436
|
+
: await stub.rpcBatch(body.queries)
|
|
437
|
+
return c.json(result)
|
|
438
|
+
} catch (error) {
|
|
439
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
440
|
+
return c.json({ error: true, code: 'BATCH_ERROR', message }, 500)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (remainingPath === '/transaction' && c.req.method === 'POST') {
|
|
445
|
+
try {
|
|
446
|
+
const body = await c.req.json<{ queries: Array<{ sql: string; params?: unknown[] }>; options?: unknown }>()
|
|
447
|
+
if (!body.queries || !Array.isArray(body.queries)) {
|
|
448
|
+
return c.json({ error: true, code: 'INVALID_REQUEST', message: 'Missing queries array' }, 400)
|
|
449
|
+
}
|
|
450
|
+
const result = await stub.rpcBatchTransaction(body.queries)
|
|
451
|
+
return c.json(result)
|
|
452
|
+
} catch (error) {
|
|
453
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
454
|
+
return c.json({ error: true, code: 'TRANSACTION_ERROR', message }, 500)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// For other paths, return database info
|
|
459
|
+
if (remainingPath === '/' || remainingPath === '') {
|
|
460
|
+
return c.json({
|
|
461
|
+
service: 'postgres.do',
|
|
462
|
+
database: dbId,
|
|
463
|
+
status: 'ready',
|
|
464
|
+
timestamp: new Date().toISOString(),
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
388
470
|
return c.json({
|
|
389
471
|
error: true,
|
|
390
472
|
code: 'NOT_FOUND',
|
package/src/worker/index.ts
CHANGED
|
@@ -373,9 +373,45 @@ export { QueryStatsManager, createQueryStatsManager } from './query-stats-manage
|
|
|
373
373
|
export type { QueryStats, ExtendedQueryStats, QueryStatsConfig } from './query-stats-manager'
|
|
374
374
|
|
|
375
375
|
// PGLite Manager (extracted from PostgresDO for reuse)
|
|
376
|
-
|
|
376
|
+
// Includes WASM hoisting for optimal warm start performance (~1200ms -> ~30ms)
|
|
377
|
+
export {
|
|
378
|
+
PGLiteManager,
|
|
379
|
+
createPGLiteManager,
|
|
380
|
+
// WASM Hoisting utilities
|
|
381
|
+
hasHoistedPglite,
|
|
382
|
+
getHoistedPgliteDiagnostics,
|
|
383
|
+
resetHoistedPglite,
|
|
384
|
+
} from './do-pglite-manager'
|
|
377
385
|
export type { PGLiteManagerConfig } from './do-pglite-manager'
|
|
378
386
|
|
|
387
|
+
// Lazy PGLite Manager (SPIKE: Lazy WASM loading experiment)
|
|
388
|
+
// Defers WASM loading until first query for improved cold start perception
|
|
389
|
+
export {
|
|
390
|
+
LazyPGLiteManager,
|
|
391
|
+
createLazyPGLiteManager,
|
|
392
|
+
// Lazy WASM Hoisting utilities
|
|
393
|
+
hasLazyHoistedPglite,
|
|
394
|
+
getLazyHoistedPgliteDiagnostics,
|
|
395
|
+
resetLazyHoistedPglite,
|
|
396
|
+
} from './lazy-pglite-manager'
|
|
397
|
+
export type { LazyPGLiteManagerConfig, WASMLoadingState } from './lazy-pglite-manager'
|
|
398
|
+
|
|
399
|
+
// Background PGLite Manager (RECOMMENDED: Eager-but-non-blocking pattern)
|
|
400
|
+
// Starts WASM loading immediately but doesn't block - best of both worlds:
|
|
401
|
+
// - Non-query endpoints respond instantly
|
|
402
|
+
// - WASM loads in background (uses ctx.waitUntil)
|
|
403
|
+
// - First query only waits for remaining load time (often zero)
|
|
404
|
+
export {
|
|
405
|
+
BackgroundPGLiteManager,
|
|
406
|
+
createBackgroundPGLiteManager,
|
|
407
|
+
// Background WASM Hoisting utilities
|
|
408
|
+
hasBgHoistedPglite,
|
|
409
|
+
isBgWasmLoading,
|
|
410
|
+
getBgHoistedPgliteDiagnostics,
|
|
411
|
+
resetBgHoistedPglite,
|
|
412
|
+
} from './background-pglite-manager'
|
|
413
|
+
export type { BackgroundPGLiteManagerConfig, BGWASMLoadingState } from './background-pglite-manager'
|
|
414
|
+
|
|
379
415
|
// Query Execution Manager (extracted from PostgresDO for reuse - Task: postgres-an18)
|
|
380
416
|
export { QueryExecutionManager, createQueryExecutionManager } from './query-execution-manager'
|
|
381
417
|
export type { QueryExecutionDependencies, QueryExecutorPGlite } from './query-execution-manager'
|
|
@@ -387,3 +423,37 @@ export type { HealthCheckDependencies, HealthCheckPGlite, ColumnDescription, Dat
|
|
|
387
423
|
// RPC Methods Manager (extracted from PostgresDO for reuse - Task: postgres-an18)
|
|
388
424
|
export { RpcMethodsManager, createRpcMethodsManager } from './rpc-methods-manager'
|
|
389
425
|
export type { RpcMethodsDependencies, RpcMethodsPGlite } from './rpc-methods-manager'
|
|
426
|
+
|
|
427
|
+
// MCP Server (Model Context Protocol)
|
|
428
|
+
export {
|
|
429
|
+
createMCPServer,
|
|
430
|
+
createMCPRoutes,
|
|
431
|
+
createPGBinding,
|
|
432
|
+
PG_BINDING_TYPES,
|
|
433
|
+
searchTool,
|
|
434
|
+
fetchTool,
|
|
435
|
+
doTool,
|
|
436
|
+
getToolDefinitions,
|
|
437
|
+
createToolHandlers,
|
|
438
|
+
createSearchHandler,
|
|
439
|
+
createFetchHandler,
|
|
440
|
+
createDoHandler,
|
|
441
|
+
} from '../mcp'
|
|
442
|
+
export type {
|
|
443
|
+
MCPServer,
|
|
444
|
+
MCPServerConfig,
|
|
445
|
+
MCPAuthContext,
|
|
446
|
+
QueryResult as MCPQueryResult,
|
|
447
|
+
TableInfo,
|
|
448
|
+
ColumnInfo,
|
|
449
|
+
MCPSearchResult,
|
|
450
|
+
MCPFetchResult,
|
|
451
|
+
PGBinding,
|
|
452
|
+
PGTransaction,
|
|
453
|
+
ToolResponse,
|
|
454
|
+
SearchInput,
|
|
455
|
+
FetchInput,
|
|
456
|
+
DoInput,
|
|
457
|
+
Tool as MCPTool,
|
|
458
|
+
QueryExecutor as MCPQueryExecutor,
|
|
459
|
+
} from '../mcp'
|