Skip to content

Commit eefcfcf

Browse files
hotlongCopilot
andcommitted
fix(service-cloud): plumb startupTimeout through lazyPlugin wrapper, externalize pg
While verifying the new out-of-band migration path against Neon, three real bugs surfaced that explained why the prior cf-cold-start fixes weren't enough: 1. lazyPlugin() wrapper drops startupTimeout The control-plane preset wraps every plugin in a lazyPlugin proxy so heavy deps load on init() instead of parse-time. The wrapper exposed only { name, init, start, stop } — the kernel reads plugin.startupTimeout at start time, so even after we set ObjectQLPlugin.startupTimeout = 120_000, the wrapper's missing property caused the kernel to fall back to its 30s default. Add an opt-in startupTimeout argument to lazyPlugin() and pass it through for ObjectQL (120s in production, 600s in OS_MIGRATE_AND_EXIT mode for latency-disadvantaged operators migrating across regions). 2. service-cloud bundles pg tsup was compiling the dynamic import('pg') into a CJS chunk that failed at runtime with 'Dynamic require of "events" is not supported'. Mark pg / pg-native / pg-cloudflare as external so they resolve from node_modules at runtime. 3. service-cloud doesn't declare pg pnpm strict isolation means apps/cloud declaring pg doesn't make it resolvable from packages/services/service-cloud/dist/index.js. Add pg as an optionalDependency on service-cloud so pnpm symlinks it into the package's own node_modules. Verified: pnpm --filter @objectstack/cloud migrate now runs schema sync against Neon end-to-end and exits 0 (4m26s first run, 3m53s idempotent second run). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a7cc292 commit eefcfcf

4 files changed

Lines changed: 42 additions & 3 deletions

File tree

packages/services/service-cloud/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
"zod": "^4.4.3",
4141
"@objectstack/service-storage": "workspace:*"
4242
},
43+
"optionalDependencies": {
44+
"pg": "^8.20.0"
45+
},
4346
"devDependencies": {
4447
"@types/node": "^22.19.18",
4548
"tsup": "^8.5.1",

packages/services/service-cloud/src/control-plane-preset.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,21 @@ export interface ControlPlanePresetConfig {
2828
authPlugins?: Record<string, unknown>;
2929
}
3030

31-
/** Create a lazy-proxy plugin that defers its import to init(). */
32-
function lazyPlugin(name: string, factory: (ctx: any) => Promise<any>): any {
31+
/**
32+
* Create a lazy-proxy plugin that defers its import to init().
33+
*
34+
* `startupTimeout` is set on the **wrapper** (not the inner plugin) so the
35+
* kernel honours it during Phase 2 start. The kernel reads
36+
* `plugin.startupTimeout` from the registered plugin object, but the inner
37+
* implementation is not constructed until init() — by which time the
38+
* registration is already locked in. Without forwarding the budget on the
39+
* wrapper, heavy plugins like `ObjectQLPlugin` (which does N×CREATE TABLE
40+
* round-trips against a remote DB) inherit the kernel's default 30s and
41+
* time out on cold Neon/Turso boots.
42+
*/
43+
function lazyPlugin(name: string, factory: (ctx: any) => Promise<any>, opts?: { startupTimeout?: number }): any {
3344
let impl: any = null;
34-
return {
45+
const wrapper: any = {
3546
name,
3647
async init(ctx: any) {
3748
impl = await factory(ctx);
@@ -44,6 +55,10 @@ function lazyPlugin(name: string, factory: (ctx: any) => Promise<any>): any {
4455
if (impl?.stop) await impl.stop(ctx);
4556
},
4657
};
58+
if (typeof opts?.startupTimeout === 'number' && opts.startupTimeout > 0) {
59+
wrapper.startupTimeout = opts.startupTimeout;
60+
}
61+
return wrapper;
4762
}
4863

4964
/**
@@ -67,11 +82,20 @@ export function createControlPlanePlugins(cfg: ControlPlanePresetConfig): any[]
6782

6883
return [
6984
// ── 1. ObjectQL ────────────────────────────────────────────────────────
85+
// Migration mode (`OS_MIGRATE_AND_EXIT=1`) gets a 10-minute startup
86+
// budget because schema sync from a developer laptop to a remote DB
87+
// can be much slower than from a colocated container. Without this,
88+
// operators in latency-disadvantaged regions (e.g. Asia → Neon US East
89+
// at ~300ms RTT × 30 tables × 2 phases) hit the 120s kernel ceiling
90+
// before all DDL completes. Production cold-boot still uses 120s.
7091
lazyPlugin('com.objectstack.engine.objectql', async () => {
7192
const { ObjectQLPlugin } = await import('@objectstack/objectql');
7293
const plugin = new ObjectQLPlugin();
7394
oqlRef.ql = (plugin as any).ql ?? plugin;
7495
return plugin;
96+
}, {
97+
startupTimeout:
98+
process.env.OS_MIGRATE_AND_EXIT === '1' ? 600_000 : 120_000,
7599
}),
76100

77101
// ── 2. Datasource mapping (no heavy deps) ─────────────────────────────

packages/services/service-cloud/tsup.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,13 @@ export default defineConfig({
1919
'@objectstack/plugin-audit',
2020
'@objectstack/service-tenant',
2121
'@objectstack/service-package',
22+
// Native / CJS-heavy DB drivers that can't survive being bundled
23+
// into ESM (they use `require('events')` etc. internally and rely
24+
// on Node's actual module graph, not esbuild's). Resolved at runtime
25+
// from the host app's node_modules — declared as optional deps so
26+
// pnpm makes them available to this package.
27+
'pg',
28+
'pg-native',
29+
'pg-cloudflare',
2230
],
2331
});

pnpm-lock.yaml

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)