@happyvertical/smrt-core 0.49.7 → 0.50.0
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/dist/dispatch/bus.d.ts.map +1 -1
- package/dist/dispatch/bus.js +15 -12
- package/dist/dispatch/bus.js.map +1 -1
- package/dist/dispatch/collections/DispatchSubscriptions.d.ts +7 -2
- package/dist/dispatch/collections/DispatchSubscriptions.d.ts.map +1 -1
- package/dist/dispatch/collections/DispatchSubscriptions.js +9 -8
- package/dist/dispatch/collections/DispatchSubscriptions.js.map +1 -1
- package/dist/dispatch/collections/Dispatches.d.ts +12 -2
- package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
- package/dist/dispatch/collections/Dispatches.js +14 -8
- package/dist/dispatch/collections/Dispatches.js.map +1 -1
- package/dist/dispatch/types.d.ts +16 -1
- package/dist/dispatch/types.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrations/backfill-tracker.d.ts +7 -0
- package/dist/migrations/backfill-tracker.d.ts.map +1 -1
- package/dist/migrations/backfill-tracker.js +20 -1
- package/dist/migrations/backfill-tracker.js.map +1 -1
- package/dist/registry/types.d.ts +21 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +3 -3
- package/dist/system/bootstrap.d.ts +23 -0
- package/dist/system/bootstrap.d.ts.map +1 -1
- package/dist/system/bootstrap.js +52 -17
- package/dist/system/bootstrap.js.map +1 -1
- package/dist/vite-plugin/index.d.ts +3 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +9 -2
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts +26 -5
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +48 -14
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/package.json +10 -10
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backfill-tracker.d.ts","sourceRoot":"","sources":["../../src/migrations/backfill-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAU5D,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,iBAAiB,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;GAKG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;gBAC1C,KAAK,EAAE,OAAO;CAI3B;AAQD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;IACvC,OAAO,CAAC,iBAAiB,CAA8B;gBAE3C,OAAO,EAAE,sBAAsB;IAI3C;;;;OAIG;IACH,MAAM,CAAC,qBAAqB,CAC1B,EAAE,EAAE,iBAAiB,EACrB,MAAM,EAAE,iBAAiB,GACxB,IAAI;IAOP;;;;;;OAMG;IACH,MAAM,CAAC,oBAAoB,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI;IAOxD,mEAAmE;IACnE,MAAM,CAAC,wBAAwB,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI;YAI9C,WAAW;IASzB;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"backfill-tracker.d.ts","sourceRoot":"","sources":["../../src/migrations/backfill-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAU5D,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,iBAAiB,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;GAKG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;gBAC1C,KAAK,EAAE,OAAO;CAI3B;AAQD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;IACvC,OAAO,CAAC,iBAAiB,CAA8B;gBAE3C,OAAO,EAAE,sBAAsB;IAI3C;;;;OAIG;IACH,MAAM,CAAC,qBAAqB,CAC1B,EAAE,EAAE,iBAAiB,EACrB,MAAM,EAAE,iBAAiB,GACxB,IAAI;IAOP;;;;;;OAMG;IACH,MAAM,CAAC,oBAAoB,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI;IAOxD,mEAAmE;IACnE,MAAM,CAAC,wBAAwB,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI;YAI9C,WAAW;IASzB;;;;;OAKG;YACW,sBAAsB;IAWpC;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAiDjC,iDAAiD;IAC3C,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQ/C;;;;OAIG;IACG,aAAa,CACjB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO,GAC3D,OAAO,CAAC,IAAI,CAAC;IAWhB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACG,YAAY,CAAC,CAAC,EAClB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO,GAC3D,OAAO,CAAC;QAAE,GAAG,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,GAAG,EAAE,IAAI,CAAC;QAAC,MAAM,EAAE,CAAC,CAAA;KAAE,CAAC;IAOnE,kDAAkD;IAC5C,WAAW,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;YAYhC,kBAAkB;IAgBhC,OAAO,CAAC,mBAAmB;CAM5B"}
|
|
@@ -49,6 +49,22 @@ var BackfillTracker = class {
|
|
|
49
49
|
await assertPostgresSystemTimestampsCurrent(this.db);
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
+
* A cold runtime process must be able to adopt a migration-created tracker
|
|
53
|
+
* using its ordinary read privilege. PostgreSQL requires schema CREATE even
|
|
54
|
+
* for CREATE TABLE IF NOT EXISTS, so do not issue that DDL until absence is
|
|
55
|
+
* positively classified.
|
|
56
|
+
*/
|
|
57
|
+
async hasUsableExistingTable() {
|
|
58
|
+
try {
|
|
59
|
+
await this.db.query("SELECT 1 FROM _smrt_backfills LIMIT 1");
|
|
60
|
+
await assertPostgresSystemTimestampsCurrent(this.db);
|
|
61
|
+
return true;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (isMissingBackfillTableError(error)) return false;
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
52
68
|
* Create the `_smrt_backfills` table if it doesn't already exist.
|
|
53
69
|
* Safe to call repeatedly and concurrently.
|
|
54
70
|
*/
|
|
@@ -69,7 +85,10 @@ var BackfillTracker = class {
|
|
|
69
85
|
}
|
|
70
86
|
}
|
|
71
87
|
if (isRootDatabase(this.db)) {
|
|
72
|
-
const initialization =
|
|
88
|
+
const initialization = (async () => {
|
|
89
|
+
if (await this.hasUsableExistingTable()) return;
|
|
90
|
+
await this.createTable();
|
|
91
|
+
})().then(() => void 0).catch((error) => {
|
|
73
92
|
if (getSharedInitialization(target) === initialization) deleteSharedInitialization(target);
|
|
74
93
|
throw error;
|
|
75
94
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backfill-tracker.js","names":[],"sources":["../../src/migrations/backfill-tracker.ts"],"sourcesContent":["/**\n * Tracks application-specific data backfills (not schema migrations).\n *\n * Sits next to `MigrationTracker` but is intentionally minimal — backfills\n * have no checksum, no rollback semantics, no DDL. They're \"this data\n * correction has run, don't run it again\" markers, keyed by a stable\n * backfill name the app chooses.\n *\n * Backed by the `_smrt_backfills` system table (see\n * `../system/schema.ts`). The table is created lazily on first use —\n * every public method (`isApplied`, `recordApplied`, `runIfPending`,\n * `listApplied`) awaits `initialize()` itself, so consumers never need\n * to call `initialize()` explicitly.\n */\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n assertPostgresSystemTimestampsCurrent,\n getDatabaseEngine,\n} from '../system/compatibility.js';\nimport {\n CREATE_SMRT_BACKFILLS_TABLE,\n getSystemTableDDLForEngine,\n} from '../system/schema.js';\n\nexport interface BackfillTrackerOptions {\n db: DatabaseInterface;\n}\n\nexport interface BackfillRecord {\n name: string;\n appliedAt: string;\n description: string | null;\n packageName: string | null;\n}\n\n/**\n * A tracker query proved that its inherited root initialization is stale.\n *\n * Transaction coordinators must roll back the current transaction, refresh\n * the root tracker outside that transaction, and retry with a fresh handle.\n */\nexport class BackfillTableUnavailableError extends Error {\n constructor(cause: unknown) {\n super('The shared _smrt_backfills table is unavailable.', { cause });\n this.name = 'BackfillTableUnavailableError';\n }\n}\n\n// Only root adapters can establish a durable shared initialization. A tracker\n// created around a transaction handle may have its DDL rolled back later, so\n// transaction-local successes must never poison this cache.\nconst initializationByObject = new WeakMap<object, Promise<void>>();\nconst inheritedInitializationByHandle = new WeakMap<object, object>();\n\nexport class BackfillTracker {\n private readonly db: DatabaseInterface;\n private initializePromise: Promise<void> | null = null;\n\n constructor(options: BackfillTrackerOptions) {\n this.db = options.db;\n }\n\n /**\n * Mark a transaction handle as inheriting a table initialized durably by its\n * root adapter. Framework transaction coordinators use this after root DDL\n * commits so transaction-local trackers can skip redundant `CREATE TABLE`.\n */\n static inheritInitialization(\n db: DatabaseInterface,\n rootDb: DatabaseInterface,\n ): void {\n inheritedInitializationByHandle.set(\n db as object,\n databaseTargetIdentity(rootDb),\n );\n }\n\n /**\n * Suppress lazy DDL on a caller-owned transaction.\n *\n * Marker reads may proceed when the table already exists. A missing table\n * raises `BackfillTableUnavailableError` so the caller can fail closed or\n * retry with a root adapter.\n */\n static requireExistingTable(db: DatabaseInterface): void {\n inheritedInitializationByHandle.set(\n db as object,\n databaseTargetIdentity(db),\n );\n }\n\n /** Forget a root initialization after a schema lifecycle reset. */\n static invalidateInitialization(db: DatabaseInterface): void {\n deleteSharedInitialization(databaseTargetIdentity(db));\n }\n\n private async createTable(): Promise<void> {\n const ddl = getSystemTableDDLForEngine(\n CREATE_SMRT_BACKFILLS_TABLE,\n getDatabaseEngine(this.db),\n );\n await this.db.query(ddl);\n await assertPostgresSystemTimestampsCurrent(this.db);\n }\n\n /**\n * Create the `_smrt_backfills` table if it doesn't already exist.\n * Safe to call repeatedly and concurrently.\n */\n async initialize(): Promise<void> {\n if (inheritedInitializationByHandle.has(this.db as object)) {\n return;\n }\n\n const target = databaseTargetIdentity(this.db);\n const existing = getSharedInitialization(target);\n if (existing) {\n await existing;\n if (!isRootDatabase(this.db)) return;\n try {\n await this.db.query('SELECT 1 FROM _smrt_backfills LIMIT 1');\n return;\n } catch (error) {\n if (!isMissingBackfillTableError(error)) throw error;\n if (getSharedInitialization(target) === existing) {\n deleteSharedInitialization(target);\n }\n return this.initialize();\n }\n }\n\n if (isRootDatabase(this.db)) {\n const initialization = this.createTable()\n .then(() => undefined)\n .catch((error) => {\n if (getSharedInitialization(target) === initialization) {\n deleteSharedInitialization(target);\n }\n throw error;\n });\n setSharedInitialization(target, initialization);\n return initialization;\n }\n\n if (!this.initializePromise) {\n this.initializePromise = this.createTable()\n .then(() => undefined)\n .catch((error) => {\n this.initializePromise = null;\n throw error;\n });\n }\n return this.initializePromise;\n }\n\n /** Has the backfill named `name` already run? */\n async isApplied(name: string): Promise<boolean> {\n const result = await this.queryBackfillTable(\n 'SELECT 1 FROM _smrt_backfills WHERE name = ? LIMIT 1',\n name,\n );\n return result.rows.length > 0;\n }\n\n /**\n * Record that the backfill named `name` ran. Uses INSERT ... ON CONFLICT\n * so callers don't need to check `isApplied` first if they don't care\n * about distinguishing \"already applied\" from \"just applied\".\n */\n async recordApplied(\n name: string,\n options: { description?: string; packageName?: string } = {},\n ): Promise<void> {\n await this.queryBackfillTable(\n `INSERT INTO _smrt_backfills (name, description, package_name)\n VALUES (?, ?, ?)\n ON CONFLICT (name) DO NOTHING`,\n name,\n options.description ?? null,\n options.packageName ?? null,\n );\n }\n\n /**\n * Convenience wrapper: if `name` hasn't been applied, run `fn` and\n * record the result. Returns `{ ran, result }`:\n * - `{ ran: true, result: T }` — backfill was pending and `fn` ran\n * - `{ ran: false, result: null }` — backfill was already applied\n *\n * The discriminator avoids the `T | null` ambiguity an earlier shape had\n * (a backfill that legitimately returns `null` was indistinguishable\n * from \"already applied\"). Callers check `ran` to decide whether `fn`\n * executed; `result` carries whatever `fn` returned.\n *\n * This is the common pattern an app's migrate orchestration follows:\n * a list of `(name, runner)` pairs, each guarded by isApplied/run/record.\n *\n * **Single-runner only.** This is a check-then-act pattern with no\n * locking: two processes that call `runIfPending` for the same `name`\n * concurrently can both observe `isApplied === false`, both execute\n * `fn`, and only then race to `recordApplied`. The `ON CONFLICT DO\n * NOTHING` in `recordApplied` keeps the table clean but does NOT prevent\n * `fn` from running twice. Acceptable when backfills are run from a\n * single deploy-time migration step (the common case); if you need to\n * fan out backfills across multiple workers, wrap the call in your own\n * mutex (e.g. a Postgres advisory lock keyed by backfill name).\n *\n * **`fn` MUST be idempotent.** The order is `fn` → `recordApplied`, with\n * no surrounding transaction. If `fn` succeeds but `recordApplied`\n * throws (DB connection drops, transient error), the work has happened\n * but is not recorded — the next call sees `isApplied === false` and\n * re-runs `fn`. For non-idempotent work (slug rewrites that consume\n * source rows, one-shot lookups), wrap the entire call in a transaction\n * yourself or structure `fn` so a second execution is a no-op.\n */\n async runIfPending<T>(\n name: string,\n fn: () => Promise<T>,\n options: { description?: string; packageName?: string } = {},\n ): Promise<{ ran: false; result: null } | { ran: true; result: T }> {\n if (await this.isApplied(name)) return { ran: false, result: null };\n const result = await fn();\n await this.recordApplied(name, options);\n return { ran: true, result };\n }\n\n /** List every recorded backfill, oldest first. */\n async listApplied(): Promise<BackfillRecord[]> {\n const result = await this.queryBackfillTable(\n 'SELECT name, applied_at, description, package_name FROM _smrt_backfills ORDER BY applied_at ASC, name ASC',\n );\n return result.rows.map((row) => ({\n name: String(row.name ?? ''),\n appliedAt: normalizeAppliedAt(row.applied_at),\n description: row.description == null ? null : String(row.description),\n packageName: row.package_name == null ? null : String(row.package_name),\n }));\n }\n\n private async queryBackfillTable(sql: string, ...params: unknown[]) {\n await this.initialize();\n try {\n return await this.db.query(sql, ...params);\n } catch (error) {\n if (!isMissingBackfillTableError(error)) throw error;\n const inheritedRoot = inheritedInitializationByHandle.get(\n this.db as object,\n );\n this.resetInitialization(inheritedRoot);\n if (inheritedRoot) throw new BackfillTableUnavailableError(error);\n await this.initialize();\n return this.db.query(sql, ...params);\n }\n }\n\n private resetInitialization(inheritedRoot?: object): void {\n this.initializePromise = null;\n inheritedInitializationByHandle.delete(this.db as object);\n deleteSharedInitialization(databaseTargetIdentity(this.db));\n if (inheritedRoot) deleteSharedInitialization(inheritedRoot);\n }\n}\n\nfunction databaseTargetIdentity(db: DatabaseInterface): object {\n const client = db.client;\n return client !== null &&\n (typeof client === 'object' || typeof client === 'function')\n ? client\n : db;\n}\n\nfunction isRootDatabase(db: DatabaseInterface): boolean {\n return typeof db.beginTransaction === 'function';\n}\n\nfunction getSharedInitialization(target: object): Promise<void> | undefined {\n return initializationByObject.get(target);\n}\n\nfunction setSharedInitialization(\n target: object,\n initialization: Promise<void>,\n): void {\n initializationByObject.set(target, initialization);\n}\n\nfunction deleteSharedInitialization(target: object): void {\n initializationByObject.delete(target);\n}\n\nfunction isMissingBackfillTableError(error: unknown): boolean {\n const pending: unknown[] = [error];\n const seen = new Set<unknown>();\n const missingPattern =\n /(?:no such table|does not exist|doesn't exist|unknown table|not found)/iu;\n while (pending.length > 0) {\n const current = pending.pop();\n if (typeof current === 'string') {\n if (current.includes('_smrt_backfills') && missingPattern.test(current)) {\n return true;\n }\n continue;\n }\n if (!current || typeof current !== 'object' || seen.has(current)) continue;\n seen.add(current);\n if (\n current instanceof Error &&\n current.message.includes('_smrt_backfills') &&\n missingPattern.test(current.message)\n ) {\n return true;\n }\n const details = current as {\n cause?: unknown;\n context?: unknown;\n details?: unknown;\n originalError?: unknown;\n };\n pending.push(\n details.cause,\n details.context,\n details.details,\n details.originalError,\n );\n if (current instanceof AggregateError) pending.push(...current.errors);\n }\n return false;\n}\n\n/**\n * Convert a raw `applied_at` cell into an ISO 8601 string.\n *\n * Different DB drivers return this column in different shapes:\n * - Postgres `node-pg` returns a JS `Date` directly.\n * - SQLite drivers typically return the `CURRENT_TIMESTAMP` literal\n * `'YYYY-MM-DD HH:MM:SS'` (no `T`, no timezone). V8's `Date`\n * constructor parses this as local time, but that's\n * implementation-defined and other runtimes may return Invalid Date.\n * - A corrupted row could return any string.\n *\n * If the value parses to a valid Date we return `toISOString()`. If it\n * doesn't (or the column is null/empty), we fall back to the raw string —\n * better to surface a non-ISO timestamp than crash `listApplied()` with a\n * `RangeError` and take down the caller.\n */\nfunction normalizeAppliedAt(value: unknown): string {\n if (value == null || value === '') return '';\n const raw = String(value);\n const parsed = new Date(raw);\n if (Number.isNaN(parsed.getTime())) {\n return raw;\n }\n return parsed.toISOString();\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"backfill-tracker.js","names":[],"sources":["../../src/migrations/backfill-tracker.ts"],"sourcesContent":["/**\n * Tracks application-specific data backfills (not schema migrations).\n *\n * Sits next to `MigrationTracker` but is intentionally minimal — backfills\n * have no checksum, no rollback semantics, no DDL. They're \"this data\n * correction has run, don't run it again\" markers, keyed by a stable\n * backfill name the app chooses.\n *\n * Backed by the `_smrt_backfills` system table (see\n * `../system/schema.ts`). The table is created lazily on first use —\n * every public method (`isApplied`, `recordApplied`, `runIfPending`,\n * `listApplied`) awaits `initialize()` itself, so consumers never need\n * to call `initialize()` explicitly.\n */\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n assertPostgresSystemTimestampsCurrent,\n getDatabaseEngine,\n} from '../system/compatibility.js';\nimport {\n CREATE_SMRT_BACKFILLS_TABLE,\n getSystemTableDDLForEngine,\n} from '../system/schema.js';\n\nexport interface BackfillTrackerOptions {\n db: DatabaseInterface;\n}\n\nexport interface BackfillRecord {\n name: string;\n appliedAt: string;\n description: string | null;\n packageName: string | null;\n}\n\n/**\n * A tracker query proved that its inherited root initialization is stale.\n *\n * Transaction coordinators must roll back the current transaction, refresh\n * the root tracker outside that transaction, and retry with a fresh handle.\n */\nexport class BackfillTableUnavailableError extends Error {\n constructor(cause: unknown) {\n super('The shared _smrt_backfills table is unavailable.', { cause });\n this.name = 'BackfillTableUnavailableError';\n }\n}\n\n// Only root adapters can establish a durable shared initialization. A tracker\n// created around a transaction handle may have its DDL rolled back later, so\n// transaction-local successes must never poison this cache.\nconst initializationByObject = new WeakMap<object, Promise<void>>();\nconst inheritedInitializationByHandle = new WeakMap<object, object>();\n\nexport class BackfillTracker {\n private readonly db: DatabaseInterface;\n private initializePromise: Promise<void> | null = null;\n\n constructor(options: BackfillTrackerOptions) {\n this.db = options.db;\n }\n\n /**\n * Mark a transaction handle as inheriting a table initialized durably by its\n * root adapter. Framework transaction coordinators use this after root DDL\n * commits so transaction-local trackers can skip redundant `CREATE TABLE`.\n */\n static inheritInitialization(\n db: DatabaseInterface,\n rootDb: DatabaseInterface,\n ): void {\n inheritedInitializationByHandle.set(\n db as object,\n databaseTargetIdentity(rootDb),\n );\n }\n\n /**\n * Suppress lazy DDL on a caller-owned transaction.\n *\n * Marker reads may proceed when the table already exists. A missing table\n * raises `BackfillTableUnavailableError` so the caller can fail closed or\n * retry with a root adapter.\n */\n static requireExistingTable(db: DatabaseInterface): void {\n inheritedInitializationByHandle.set(\n db as object,\n databaseTargetIdentity(db),\n );\n }\n\n /** Forget a root initialization after a schema lifecycle reset. */\n static invalidateInitialization(db: DatabaseInterface): void {\n deleteSharedInitialization(databaseTargetIdentity(db));\n }\n\n private async createTable(): Promise<void> {\n const ddl = getSystemTableDDLForEngine(\n CREATE_SMRT_BACKFILLS_TABLE,\n getDatabaseEngine(this.db),\n );\n await this.db.query(ddl);\n await assertPostgresSystemTimestampsCurrent(this.db);\n }\n\n /**\n * A cold runtime process must be able to adopt a migration-created tracker\n * using its ordinary read privilege. PostgreSQL requires schema CREATE even\n * for CREATE TABLE IF NOT EXISTS, so do not issue that DDL until absence is\n * positively classified.\n */\n private async hasUsableExistingTable(): Promise<boolean> {\n try {\n await this.db.query('SELECT 1 FROM _smrt_backfills LIMIT 1');\n await assertPostgresSystemTimestampsCurrent(this.db);\n return true;\n } catch (error) {\n if (isMissingBackfillTableError(error)) return false;\n throw error;\n }\n }\n\n /**\n * Create the `_smrt_backfills` table if it doesn't already exist.\n * Safe to call repeatedly and concurrently.\n */\n async initialize(): Promise<void> {\n if (inheritedInitializationByHandle.has(this.db as object)) {\n return;\n }\n\n const target = databaseTargetIdentity(this.db);\n const existing = getSharedInitialization(target);\n if (existing) {\n await existing;\n if (!isRootDatabase(this.db)) return;\n try {\n await this.db.query('SELECT 1 FROM _smrt_backfills LIMIT 1');\n return;\n } catch (error) {\n if (!isMissingBackfillTableError(error)) throw error;\n if (getSharedInitialization(target) === existing) {\n deleteSharedInitialization(target);\n }\n return this.initialize();\n }\n }\n\n if (isRootDatabase(this.db)) {\n const initialization = (async () => {\n if (await this.hasUsableExistingTable()) return;\n await this.createTable();\n })()\n .then(() => undefined)\n .catch((error) => {\n if (getSharedInitialization(target) === initialization) {\n deleteSharedInitialization(target);\n }\n throw error;\n });\n setSharedInitialization(target, initialization);\n return initialization;\n }\n\n if (!this.initializePromise) {\n this.initializePromise = this.createTable()\n .then(() => undefined)\n .catch((error) => {\n this.initializePromise = null;\n throw error;\n });\n }\n return this.initializePromise;\n }\n\n /** Has the backfill named `name` already run? */\n async isApplied(name: string): Promise<boolean> {\n const result = await this.queryBackfillTable(\n 'SELECT 1 FROM _smrt_backfills WHERE name = ? LIMIT 1',\n name,\n );\n return result.rows.length > 0;\n }\n\n /**\n * Record that the backfill named `name` ran. Uses INSERT ... ON CONFLICT\n * so callers don't need to check `isApplied` first if they don't care\n * about distinguishing \"already applied\" from \"just applied\".\n */\n async recordApplied(\n name: string,\n options: { description?: string; packageName?: string } = {},\n ): Promise<void> {\n await this.queryBackfillTable(\n `INSERT INTO _smrt_backfills (name, description, package_name)\n VALUES (?, ?, ?)\n ON CONFLICT (name) DO NOTHING`,\n name,\n options.description ?? null,\n options.packageName ?? null,\n );\n }\n\n /**\n * Convenience wrapper: if `name` hasn't been applied, run `fn` and\n * record the result. Returns `{ ran, result }`:\n * - `{ ran: true, result: T }` — backfill was pending and `fn` ran\n * - `{ ran: false, result: null }` — backfill was already applied\n *\n * The discriminator avoids the `T | null` ambiguity an earlier shape had\n * (a backfill that legitimately returns `null` was indistinguishable\n * from \"already applied\"). Callers check `ran` to decide whether `fn`\n * executed; `result` carries whatever `fn` returned.\n *\n * This is the common pattern an app's migrate orchestration follows:\n * a list of `(name, runner)` pairs, each guarded by isApplied/run/record.\n *\n * **Single-runner only.** This is a check-then-act pattern with no\n * locking: two processes that call `runIfPending` for the same `name`\n * concurrently can both observe `isApplied === false`, both execute\n * `fn`, and only then race to `recordApplied`. The `ON CONFLICT DO\n * NOTHING` in `recordApplied` keeps the table clean but does NOT prevent\n * `fn` from running twice. Acceptable when backfills are run from a\n * single deploy-time migration step (the common case); if you need to\n * fan out backfills across multiple workers, wrap the call in your own\n * mutex (e.g. a Postgres advisory lock keyed by backfill name).\n *\n * **`fn` MUST be idempotent.** The order is `fn` → `recordApplied`, with\n * no surrounding transaction. If `fn` succeeds but `recordApplied`\n * throws (DB connection drops, transient error), the work has happened\n * but is not recorded — the next call sees `isApplied === false` and\n * re-runs `fn`. For non-idempotent work (slug rewrites that consume\n * source rows, one-shot lookups), wrap the entire call in a transaction\n * yourself or structure `fn` so a second execution is a no-op.\n */\n async runIfPending<T>(\n name: string,\n fn: () => Promise<T>,\n options: { description?: string; packageName?: string } = {},\n ): Promise<{ ran: false; result: null } | { ran: true; result: T }> {\n if (await this.isApplied(name)) return { ran: false, result: null };\n const result = await fn();\n await this.recordApplied(name, options);\n return { ran: true, result };\n }\n\n /** List every recorded backfill, oldest first. */\n async listApplied(): Promise<BackfillRecord[]> {\n const result = await this.queryBackfillTable(\n 'SELECT name, applied_at, description, package_name FROM _smrt_backfills ORDER BY applied_at ASC, name ASC',\n );\n return result.rows.map((row) => ({\n name: String(row.name ?? ''),\n appliedAt: normalizeAppliedAt(row.applied_at),\n description: row.description == null ? null : String(row.description),\n packageName: row.package_name == null ? null : String(row.package_name),\n }));\n }\n\n private async queryBackfillTable(sql: string, ...params: unknown[]) {\n await this.initialize();\n try {\n return await this.db.query(sql, ...params);\n } catch (error) {\n if (!isMissingBackfillTableError(error)) throw error;\n const inheritedRoot = inheritedInitializationByHandle.get(\n this.db as object,\n );\n this.resetInitialization(inheritedRoot);\n if (inheritedRoot) throw new BackfillTableUnavailableError(error);\n await this.initialize();\n return this.db.query(sql, ...params);\n }\n }\n\n private resetInitialization(inheritedRoot?: object): void {\n this.initializePromise = null;\n inheritedInitializationByHandle.delete(this.db as object);\n deleteSharedInitialization(databaseTargetIdentity(this.db));\n if (inheritedRoot) deleteSharedInitialization(inheritedRoot);\n }\n}\n\nfunction databaseTargetIdentity(db: DatabaseInterface): object {\n const client = db.client;\n return client !== null &&\n (typeof client === 'object' || typeof client === 'function')\n ? client\n : db;\n}\n\nfunction isRootDatabase(db: DatabaseInterface): boolean {\n return typeof db.beginTransaction === 'function';\n}\n\nfunction getSharedInitialization(target: object): Promise<void> | undefined {\n return initializationByObject.get(target);\n}\n\nfunction setSharedInitialization(\n target: object,\n initialization: Promise<void>,\n): void {\n initializationByObject.set(target, initialization);\n}\n\nfunction deleteSharedInitialization(target: object): void {\n initializationByObject.delete(target);\n}\n\nfunction isMissingBackfillTableError(error: unknown): boolean {\n const pending: unknown[] = [error];\n const seen = new Set<unknown>();\n const missingPattern =\n /(?:no such table|does not exist|doesn't exist|unknown table|not found)/iu;\n while (pending.length > 0) {\n const current = pending.pop();\n if (typeof current === 'string') {\n if (current.includes('_smrt_backfills') && missingPattern.test(current)) {\n return true;\n }\n continue;\n }\n if (!current || typeof current !== 'object' || seen.has(current)) continue;\n seen.add(current);\n if (\n current instanceof Error &&\n current.message.includes('_smrt_backfills') &&\n missingPattern.test(current.message)\n ) {\n return true;\n }\n const details = current as {\n cause?: unknown;\n context?: unknown;\n details?: unknown;\n originalError?: unknown;\n };\n pending.push(\n details.cause,\n details.context,\n details.details,\n details.originalError,\n );\n if (current instanceof AggregateError) pending.push(...current.errors);\n }\n return false;\n}\n\n/**\n * Convert a raw `applied_at` cell into an ISO 8601 string.\n *\n * Different DB drivers return this column in different shapes:\n * - Postgres `node-pg` returns a JS `Date` directly.\n * - SQLite drivers typically return the `CURRENT_TIMESTAMP` literal\n * `'YYYY-MM-DD HH:MM:SS'` (no `T`, no timezone). V8's `Date`\n * constructor parses this as local time, but that's\n * implementation-defined and other runtimes may return Invalid Date.\n * - A corrupted row could return any string.\n *\n * If the value parses to a valid Date we return `toISOString()`. If it\n * doesn't (or the column is null/empty), we fall back to the raw string —\n * better to surface a non-ISO timestamp than crash `listApplied()` with a\n * `RangeError` and take down the caller.\n */\nfunction normalizeAppliedAt(value: unknown): string {\n if (value == null || value === '') return '';\n const raw = String(value);\n const parsed = new Date(raw);\n if (Number.isNaN(parsed.getTime())) {\n return raw;\n }\n return parsed.toISOString();\n}\n"],"mappings":"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"}
|
package/dist/registry/types.d.ts
CHANGED
|
@@ -445,8 +445,28 @@ export interface SmartObjectConfig {
|
|
|
445
445
|
* exposed via the API. Set this for classes whose CLI is invoked
|
|
446
446
|
* in-process (e.g. admin/security tools intentionally without HTTP
|
|
447
447
|
* routes) rather than over HTTP.
|
|
448
|
+
*
|
|
449
|
+
* `true` acknowledges the whole class -- every command in its
|
|
450
|
+
* effective CLI set loses the coherence check, not just the one(s)
|
|
451
|
+
* that actually need it. Prefer the narrower `string[]` form: it
|
|
452
|
+
* names exactly the methods that legitimately have no API route
|
|
453
|
+
* (e.g. one that takes a non-serializable callback option), while
|
|
454
|
+
* every other command in the class keeps being checked (smrt#2857).
|
|
455
|
+
* A name in the array that isn't a recognized command on this class
|
|
456
|
+
* (a typo, or a stale entry left after a route was added) is
|
|
457
|
+
* itself a build error -- an unrecognized name would otherwise
|
|
458
|
+
* silently grant no exemption at all.
|
|
459
|
+
*
|
|
460
|
+
* The narrowed array form only restores build-*failing* enforcement
|
|
461
|
+
* for the rest of the class when this class also spells out a
|
|
462
|
+
* non-empty `cli.include` -- `validateCliIncludeAgainstApi`'s
|
|
463
|
+
* blast radius has always stopped there (a bare `cli: true`/`cli:
|
|
464
|
+
* {}` class gets a dev-server warning only, not a build failure;
|
|
465
|
+
* see that function's own doc comment). Give the class an explicit
|
|
466
|
+
* `cli.include` if you need the remaining commands to be build-
|
|
467
|
+
* enforced, not just narrow the waiver.
|
|
448
468
|
*/
|
|
449
|
-
skipApiCheck?: boolean;
|
|
469
|
+
skipApiCheck?: boolean | string[];
|
|
450
470
|
/**
|
|
451
471
|
* Whether this CLI surface should be advertised through HTTP discovery
|
|
452
472
|
* (`GET /api/_resources`) for `@happyvertical/smrt-app-cli`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/registry/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/registry/types.ts"],"names":[],"mappings":"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"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-core",
|
|
6
|
-
"packageVersion": "0.
|
|
6
|
+
"packageVersion": "0.50.0",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
10
|
+
"manifest": "20018fc2c5f5ac40308024dbdf785848baaf7793c3b9ac8ee2874641bdc6d1bf",
|
|
11
|
+
"packageJson": "638c62f03e4046e0b28772aeb5cce763eafcac4aba4c8127cdfa2691e6f4d20c",
|
|
12
12
|
"agents": "6c19f8eab9fbcd9c10d0649279f93c8c36c65c18da3402f2a2b49893432125ec",
|
|
13
13
|
"moduleDoc:agents/object-runtime.md": "34a4e740c109467cd5486c4af0e3bf4b63915697721ab584df0e5506ea9290ca",
|
|
14
14
|
"moduleDoc:agents/revision-guard.md": "aa6b1ddb5b6b49fa27ebbe575ec7fcd6d5ceb35ee25acc702f877a226eb9a99a",
|
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
import { DatabaseInterface } from '@happyvertical/sql';
|
|
2
|
+
/**
|
|
3
|
+
* Run `work` serialized against concurrent SMRT system-table bootstrap.
|
|
4
|
+
*
|
|
5
|
+
* `_smrt_dispatch`/`_smrt_dispatch_subscriptions` (and every other
|
|
6
|
+
* framework-owned `_smrt_*` table) are catalog objects shared by every
|
|
7
|
+
* caller against one physical database — including standalone initializers
|
|
8
|
+
* such as `DispatchBus.initialize()` that provision or verify a subset of
|
|
9
|
+
* {@link ALL_SYSTEM_TABLES} outside of a full {@link ensureSystemTables} run
|
|
10
|
+
* (#2861). PostgreSQL DDL against the same object from concurrent sessions is
|
|
11
|
+
* not internally serialized (`CREATE TABLE IF NOT EXISTS` races on
|
|
12
|
+
* `pg_type_typname_nsp_index` under true concurrency; `ALTER TABLE`/`CREATE
|
|
13
|
+
* INDEX` take catalog locks that queue behind each other), so every writer of
|
|
14
|
+
* a system table — full bootstrap or a standalone initializer alike — must
|
|
15
|
+
* take the *same* advisory lock before touching it. Using one shared lock key
|
|
16
|
+
* across all of them is what makes them mutually exclusive rather than each
|
|
17
|
+
* safe only against itself.
|
|
18
|
+
*
|
|
19
|
+
* PostgreSQL: wraps `work` in the same bounded advisory-locked transaction
|
|
20
|
+
* {@link bootstrapSystemTables} uses. Other engines have no comparable
|
|
21
|
+
* catalog race for this codebase's supported deployment shapes, so `work`
|
|
22
|
+
* runs directly against `db`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runSerializedAgainstSystemTableBootstrap<T>(db: DatabaseInterface, typeHint: string | undefined, work: (scopedDb: DatabaseInterface) => Promise<T>): Promise<T>;
|
|
2
25
|
/**
|
|
3
26
|
* Ensure every framework-owned SMRT system table exists before use.
|
|
4
27
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../../src/system/bootstrap.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAG5D,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,oBAAoB,CAAC;AA8H/E;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../../src/system/bootstrap.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAG5D,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,oBAAoB,CAAC;AA8H/E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,wCAAwC,CAAC,CAAC,EAC9D,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,IAAI,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,GAChD,OAAO,CAAC,CAAC,CAAC,CA2LZ;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf"}
|
package/dist/system/bootstrap.js
CHANGED
|
@@ -66,16 +66,29 @@ async function rollbackBootstrap(tx) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
69
|
+
* Run `work` serialized against concurrent SMRT system-table bootstrap.
|
|
70
70
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
71
|
+
* `_smrt_dispatch`/`_smrt_dispatch_subscriptions` (and every other
|
|
72
|
+
* framework-owned `_smrt_*` table) are catalog objects shared by every
|
|
73
|
+
* caller against one physical database — including standalone initializers
|
|
74
|
+
* such as `DispatchBus.initialize()` that provision or verify a subset of
|
|
75
|
+
* {@link ALL_SYSTEM_TABLES} outside of a full {@link ensureSystemTables} run
|
|
76
|
+
* (#2861). PostgreSQL DDL against the same object from concurrent sessions is
|
|
77
|
+
* not internally serialized (`CREATE TABLE IF NOT EXISTS` races on
|
|
78
|
+
* `pg_type_typname_nsp_index` under true concurrency; `ALTER TABLE`/`CREATE
|
|
79
|
+
* INDEX` take catalog locks that queue behind each other), so every writer of
|
|
80
|
+
* a system table — full bootstrap or a standalone initializer alike — must
|
|
81
|
+
* take the *same* advisory lock before touching it. Using one shared lock key
|
|
82
|
+
* across all of them is what makes them mutually exclusive rather than each
|
|
83
|
+
* safe only against itself.
|
|
84
|
+
*
|
|
85
|
+
* PostgreSQL: wraps `work` in the same bounded advisory-locked transaction
|
|
86
|
+
* {@link bootstrapSystemTables} uses. Other engines have no comparable
|
|
87
|
+
* catalog race for this codebase's supported deployment shapes, so `work`
|
|
88
|
+
* runs directly against `db`.
|
|
73
89
|
*/
|
|
74
|
-
async function
|
|
75
|
-
if (getDatabaseEngine(db, typeHint) !== "postgres")
|
|
76
|
-
await bootstrapSystemTables(db, typeHint);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
90
|
+
async function runSerializedAgainstSystemTableBootstrap(db, typeHint, work) {
|
|
91
|
+
if (getDatabaseEngine(db, typeHint) !== "postgres") return work(db);
|
|
79
92
|
const beginTransaction = db.beginTransaction;
|
|
80
93
|
const transaction = db.transaction;
|
|
81
94
|
if (typeof beginTransaction === "function") {
|
|
@@ -84,25 +97,47 @@ async function ensureSystemTables(db, typeHint) {
|
|
|
84
97
|
try {
|
|
85
98
|
for (const sql of SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL) await tx.query(sql);
|
|
86
99
|
await tx.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);
|
|
87
|
-
await
|
|
100
|
+
const result = await work(tx);
|
|
88
101
|
await tx.commit();
|
|
89
|
-
return;
|
|
102
|
+
return result;
|
|
90
103
|
} catch (error) {
|
|
91
104
|
await rollbackBootstrap(tx);
|
|
92
105
|
throw error;
|
|
93
106
|
}
|
|
94
107
|
}
|
|
95
108
|
if (typeof transaction === "function") {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
await
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
if (!(typeof db.commit === "function" && typeof db.rollback === "function")) {
|
|
110
|
+
const priorLockTimeout = getQueryRows(await db.query("SHOW lock_timeout"))[0]?.lock_timeout;
|
|
111
|
+
const priorStatementTimeout = getQueryRows(await db.query("SHOW statement_timeout"))[0]?.statement_timeout;
|
|
112
|
+
return transaction.call(db, async (tx) => {
|
|
113
|
+
for (const sql of SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL) await tx.query(sql);
|
|
114
|
+
await tx.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);
|
|
115
|
+
const result = await work(tx);
|
|
116
|
+
if (typeof priorLockTimeout === "string") await tx.query(`SET LOCAL lock_timeout = '${priorLockTimeout}'`);
|
|
117
|
+
if (typeof priorStatementTimeout === "string") await tx.query(`SET LOCAL statement_timeout = '${priorStatementTimeout}'`);
|
|
118
|
+
return result;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const priorLockTimeout = getQueryRows(await db.query("SHOW lock_timeout"))[0]?.lock_timeout;
|
|
122
|
+
const priorStatementTimeout = getQueryRows(await db.query("SHOW statement_timeout"))[0]?.statement_timeout;
|
|
123
|
+
for (const sql of SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL) await db.query(sql);
|
|
124
|
+
await db.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);
|
|
125
|
+
if (typeof priorLockTimeout === "string") await db.query(`SET LOCAL lock_timeout = '${priorLockTimeout}'`);
|
|
126
|
+
if (typeof priorStatementTimeout === "string") await db.query(`SET LOCAL statement_timeout = '${priorStatementTimeout}'`);
|
|
127
|
+
return work(db);
|
|
102
128
|
}
|
|
103
129
|
throw new Error("Postgres system table bootstrap requires a transaction-capable database adapter");
|
|
104
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Ensure every framework-owned SMRT system table exists before use.
|
|
133
|
+
*
|
|
134
|
+
* PostgreSQL provisioning is serialized in a bounded advisory-locked
|
|
135
|
+
* transaction; other engines use the schema's idempotent DDL directly.
|
|
136
|
+
*/
|
|
137
|
+
async function ensureSystemTables(db, typeHint) {
|
|
138
|
+
await runSerializedAgainstSystemTableBootstrap(db, typeHint, (scopedDb) => bootstrapSystemTables(scopedDb, typeHint));
|
|
139
|
+
}
|
|
105
140
|
//#endregion
|
|
106
|
-
export { ensureSystemTables };
|
|
141
|
+
export { ensureSystemTables, runSerializedAgainstSystemTableBootstrap };
|
|
107
142
|
|
|
108
143
|
//# sourceMappingURL=bootstrap.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrap.js","names":[],"sources":["../../src/system/bootstrap.ts"],"sourcesContent":["/** Canonical, idempotent SMRT system-table provisioning. */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface, TransactionHandle } from '@happyvertical/sql';\nimport {\n ensurePostgresChangeFeedAppendFunction,\n ensurePostgresChangeFeedHelpers,\n} from '../change-feed.js';\nimport {\n assertPostgresSystemTimestampsCurrent,\n ensureBootstrapSystemTableCompatibility,\n getDatabaseEngine,\n tableExists,\n} from './compatibility.js';\nimport { getSystemTableDDL, SMRT_SCHEMA_VERSION } from './schema.js';\n\nconst SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL =\n \"SELECT pg_advisory_xact_lock(hashtext('smrt'), hashtext('system-tables'))\";\n\n/**\n * Timeout budget for the PostgreSQL system-table bootstrap transaction.\n *\n * The runtime pool's session `lock_timeout`/`statement_timeout` (#2377) are\n * sized for request work. This transaction is not request work: it holds the\n * advisory lock across up to 29 sequential DDL round-trips — ~18.85 s on a\n * high-latency link at 650 ms per round trip — and a second replica cold-starting\n * against the same fresh database *waits* on that lock. Both GUCs bound that\n * wait, because `pg_advisory_xact_lock` is an ordinary statement in the lock\n * manager, so at the runtime defaults the second replica would abort with\n * \"canceling statement due to lock timeout\" where it previously waited and\n * succeeded.\n *\n * Five minutes is an order of magnitude above the documented worst case and\n * still bounded — this is a raise, not a disable. `SET LOCAL` scopes it to this\n * transaction, the same lever migrations use for the same reason (#2362).\n */\nconst SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL = [\n \"SET LOCAL lock_timeout = '300000ms'\",\n \"SET LOCAL statement_timeout = '300000ms'\",\n];\nconst logger = createLogger({ level: 'info' });\n\ntype TransactionCapableDatabase = DatabaseInterface & {\n transaction?: <T>(\n this: DatabaseInterface,\n callback: (tx: DatabaseInterface) => Promise<T>,\n ) => Promise<T>;\n};\n\nfunction getQueryRows(result: unknown): Record<string, unknown>[] {\n if (Array.isArray(result)) return result as Record<string, unknown>[];\n if (result && typeof result === 'object' && 'rows' in result) {\n const rows = (result as { rows?: unknown }).rows;\n if (Array.isArray(rows)) return rows as Record<string, unknown>[];\n }\n return [];\n}\n\nasync function isSystemSchemaVersionApplied(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<boolean> {\n const engine = getDatabaseEngine(db, typeHint);\n if (\n engine === 'postgres' &&\n !(await tableExists(db, '_smrt_migrations', typeHint))\n ) {\n return false;\n }\n try {\n const versionParam = engine === 'postgres' ? '$1' : '?';\n const rows = await db.query(\n `SELECT 1 FROM _smrt_migrations WHERE version = ${versionParam} LIMIT 1`,\n SMRT_SCHEMA_VERSION,\n );\n return getQueryRows(rows).length > 0;\n } catch (error) {\n if (engine === 'postgres') throw error;\n return false;\n }\n}\n\nasync function bootstrapSystemTables(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n // #2649: the change-feed helpers changed without a change to the portable\n // system DDL, so a database already stamped with this version would never\n // reach the install below and would keep the deadlocking append function.\n // Ordinary model writes do not call ensureChangeFeedTable(), so nothing else\n // would repair it. One catalog probe when the helpers are already current.\n await ensurePostgresChangeFeedHelpers(db, typeHint);\n\n if (await isSystemSchemaVersionApplied(db, typeHint)) return;\n\n await ensureBootstrapSystemTableCompatibility(db, typeHint);\n const engine = getDatabaseEngine(db, typeHint);\n for (const ddl of getSystemTableDDL(engine)) {\n for (const statement of ddl\n .split(';')\n .map((value) => value.trim())\n .filter(Boolean)) {\n await db.query(statement);\n }\n }\n await ensurePostgresChangeFeedAppendFunction(db, { typeHint });\n await assertPostgresSystemTimestampsCurrent(db, typeHint);\n\n const id = crypto.randomUUID();\n const description = 'Initial SMRT system tables';\n await db.execute`\n INSERT INTO _smrt_migrations (id, version, description)\n VALUES (${id}, ${SMRT_SCHEMA_VERSION}, ${description})\n ON CONFLICT(version) DO NOTHING\n `;\n}\n\nasync function rollbackBootstrap(tx: TransactionHandle): Promise<void> {\n try {\n if (typeof tx.isActive !== 'function' || tx.isActive()) await tx.rollback();\n } catch (error) {\n logger.warn(\n `[smrt] Failed to rollback system table bootstrap transaction: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n}\n\n/**\n * Ensure every framework-owned SMRT system table exists before use.\n *\n * PostgreSQL provisioning is serialized in a bounded advisory-locked\n * transaction; other engines use the schema's idempotent DDL directly.\n */\nexport async function ensureSystemTables(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (getDatabaseEngine(db, typeHint) !== 'postgres') {\n await bootstrapSystemTables(db, typeHint);\n return;\n }\n\n const beginTransaction = db.beginTransaction;\n const transaction = (db as TransactionCapableDatabase).transaction;\n if (typeof beginTransaction === 'function') {\n const tx = await beginTransaction.call(db);\n if (!tx) throw new Error('Database transaction could not be started');\n try {\n // Raise the budget before taking the lock — the wait itself is what the\n // runtime session timeouts would otherwise cancel (#2377).\n for (const sql of SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL) await tx.query(sql);\n await tx.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);\n await bootstrapSystemTables(tx, typeHint);\n await tx.commit();\n return;\n } catch (error) {\n await rollbackBootstrap(tx);\n throw error;\n }\n }\n\n if (typeof transaction === 'function') {\n await transaction.call(db, async (tx) => {\n for (const sql of SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL) await tx.query(sql);\n await tx.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);\n await bootstrapSystemTables(tx, typeHint);\n });\n return;\n }\n\n throw new Error(\n 'Postgres system table bootstrap requires a transaction-capable database adapter',\n );\n}\n"],"mappings":";;;;;;AAgBA,IAAM,kCACJ;;;;;;;;;;;;;;;;;;AAmBF,IAAM,qCAAqC,CACzC,uCACA,0CACF;AACA,IAAM,SAAS,aAAa,EAAE,OAAO,OAAO,CAAC;AAS7C,SAAS,aAAa,QAA4C;CAChE,IAAI,MAAM,QAAQ,MAAM,GAAG,OAAO;CAClC,IAAI,UAAU,OAAO,WAAW,YAAY,UAAU,QAAQ;EAC5D,MAAM,OAAQ,OAA8B;EAC5C,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO;CAClC;CACA,OAAO,CAAC;AACV;AAEA,eAAe,6BACb,IACA,UACkB;CAClB,MAAM,SAAS,kBAAkB,IAAI,QAAQ;CAC7C,IACE,WAAW,cACX,CAAE,MAAM,YAAY,IAAI,oBAAoB,QAAQ,GAEpD,OAAO;CAET,IAAI;EACF,MAAM,eAAe,WAAW,aAAa,OAAO;EAKpD,OAAO,aAAa,MAJD,GAAG,MACpB,kDAAkD,aAAa,WAC/D,mBACF,CACwB,CAAC,CAAC,SAAS;CACrC,SAAS,OAAO;EACd,IAAI,WAAW,YAAY,MAAM;EACjC,OAAO;CACT;AACF;AAEA,eAAe,sBACb,IACA,UACe;CAMf,MAAM,gCAAgC,IAAI,QAAQ;CAElD,IAAI,MAAM,6BAA6B,IAAI,QAAQ,GAAG;CAEtD,MAAM,wCAAwC,IAAI,QAAQ;CAC1D,MAAM,SAAS,kBAAkB,IAAI,QAAQ;CAC7C,KAAK,MAAM,OAAO,kBAAkB,MAAM,GACxC,KAAK,MAAM,aAAa,IACrB,MAAM,GAAG,CAAC,CACV,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,CAC5B,OAAO,OAAO,GACf,MAAM,GAAG,MAAM,SAAS;CAG5B,MAAM,uCAAuC,IAAI,EAAE,SAAS,CAAC;CAC7D,MAAM,sCAAsC,IAAI,QAAQ;CAExD,MAAM,KAAK,OAAO,WAAW;CAE7B,MAAM,GAAG,OAAO;;cAEJ,GAAG,IAAI,oBAAoB,IAAI,6BAAY;;;AAGzD;AAEA,eAAe,kBAAkB,IAAsC;CACrE,IAAI;EACF,IAAI,OAAO,GAAG,aAAa,cAAc,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS;CAC5E,SAAS,OAAO;EACd,OAAO,KACL,iEACE,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAEzD;CACF;AACF;;;;;;;AAQA,eAAsB,mBACpB,IACA,UACe;CACf,IAAI,kBAAkB,IAAI,QAAQ,MAAM,YAAY;EAClD,MAAM,sBAAsB,IAAI,QAAQ;EACxC;CACF;CAEA,MAAM,mBAAmB,GAAG;CAC5B,MAAM,cAAe,GAAkC;CACvD,IAAI,OAAO,qBAAqB,YAAY;EAC1C,MAAM,KAAK,MAAM,iBAAiB,KAAK,EAAE;EACzC,IAAI,CAAC,IAAI,MAAM,IAAI,MAAM,2CAA2C;EACpE,IAAI;GAGF,KAAK,MAAM,OAAO,oCAAoC,MAAM,GAAG,MAAM,GAAG;GACxE,MAAM,GAAG,MAAM,+BAA+B;GAC9C,MAAM,sBAAsB,IAAI,QAAQ;GACxC,MAAM,GAAG,OAAO;GAChB;EACF,SAAS,OAAO;GACd,MAAM,kBAAkB,EAAE;GAC1B,MAAM;EACR;CACF;CAEA,IAAI,OAAO,gBAAgB,YAAY;EACrC,MAAM,YAAY,KAAK,IAAI,OAAO,OAAO;GACvC,KAAK,MAAM,OAAO,oCAAoC,MAAM,GAAG,MAAM,GAAG;GACxE,MAAM,GAAG,MAAM,+BAA+B;GAC9C,MAAM,sBAAsB,IAAI,QAAQ;EAC1C,CAAC;EACD;CACF;CAEA,MAAM,IAAI,MACR,iFACF;AACF"}
|
|
1
|
+
{"version":3,"file":"bootstrap.js","names":[],"sources":["../../src/system/bootstrap.ts"],"sourcesContent":["/** Canonical, idempotent SMRT system-table provisioning. */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface, TransactionHandle } from '@happyvertical/sql';\nimport {\n ensurePostgresChangeFeedAppendFunction,\n ensurePostgresChangeFeedHelpers,\n} from '../change-feed.js';\nimport {\n assertPostgresSystemTimestampsCurrent,\n ensureBootstrapSystemTableCompatibility,\n getDatabaseEngine,\n tableExists,\n} from './compatibility.js';\nimport { getSystemTableDDL, SMRT_SCHEMA_VERSION } from './schema.js';\n\nconst SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL =\n \"SELECT pg_advisory_xact_lock(hashtext('smrt'), hashtext('system-tables'))\";\n\n/**\n * Timeout budget for the PostgreSQL system-table bootstrap transaction.\n *\n * The runtime pool's session `lock_timeout`/`statement_timeout` (#2377) are\n * sized for request work. This transaction is not request work: it holds the\n * advisory lock across up to 29 sequential DDL round-trips — ~18.85 s on a\n * high-latency link at 650 ms per round trip — and a second replica cold-starting\n * against the same fresh database *waits* on that lock. Both GUCs bound that\n * wait, because `pg_advisory_xact_lock` is an ordinary statement in the lock\n * manager, so at the runtime defaults the second replica would abort with\n * \"canceling statement due to lock timeout\" where it previously waited and\n * succeeded.\n *\n * Five minutes is an order of magnitude above the documented worst case and\n * still bounded — this is a raise, not a disable. `SET LOCAL` scopes it to this\n * transaction, the same lever migrations use for the same reason (#2362).\n */\nconst SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL = [\n \"SET LOCAL lock_timeout = '300000ms'\",\n \"SET LOCAL statement_timeout = '300000ms'\",\n];\nconst logger = createLogger({ level: 'info' });\n\ntype TransactionCapableDatabase = DatabaseInterface & {\n transaction?: <T>(\n this: DatabaseInterface,\n callback: (tx: DatabaseInterface) => Promise<T>,\n ) => Promise<T>;\n};\n\nfunction getQueryRows(result: unknown): Record<string, unknown>[] {\n if (Array.isArray(result)) return result as Record<string, unknown>[];\n if (result && typeof result === 'object' && 'rows' in result) {\n const rows = (result as { rows?: unknown }).rows;\n if (Array.isArray(rows)) return rows as Record<string, unknown>[];\n }\n return [];\n}\n\nasync function isSystemSchemaVersionApplied(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<boolean> {\n const engine = getDatabaseEngine(db, typeHint);\n if (\n engine === 'postgres' &&\n !(await tableExists(db, '_smrt_migrations', typeHint))\n ) {\n return false;\n }\n try {\n const versionParam = engine === 'postgres' ? '$1' : '?';\n const rows = await db.query(\n `SELECT 1 FROM _smrt_migrations WHERE version = ${versionParam} LIMIT 1`,\n SMRT_SCHEMA_VERSION,\n );\n return getQueryRows(rows).length > 0;\n } catch (error) {\n if (engine === 'postgres') throw error;\n return false;\n }\n}\n\nasync function bootstrapSystemTables(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n // #2649: the change-feed helpers changed without a change to the portable\n // system DDL, so a database already stamped with this version would never\n // reach the install below and would keep the deadlocking append function.\n // Ordinary model writes do not call ensureChangeFeedTable(), so nothing else\n // would repair it. One catalog probe when the helpers are already current.\n await ensurePostgresChangeFeedHelpers(db, typeHint);\n\n if (await isSystemSchemaVersionApplied(db, typeHint)) return;\n\n await ensureBootstrapSystemTableCompatibility(db, typeHint);\n const engine = getDatabaseEngine(db, typeHint);\n for (const ddl of getSystemTableDDL(engine)) {\n for (const statement of ddl\n .split(';')\n .map((value) => value.trim())\n .filter(Boolean)) {\n await db.query(statement);\n }\n }\n await ensurePostgresChangeFeedAppendFunction(db, { typeHint });\n await assertPostgresSystemTimestampsCurrent(db, typeHint);\n\n const id = crypto.randomUUID();\n const description = 'Initial SMRT system tables';\n await db.execute`\n INSERT INTO _smrt_migrations (id, version, description)\n VALUES (${id}, ${SMRT_SCHEMA_VERSION}, ${description})\n ON CONFLICT(version) DO NOTHING\n `;\n}\n\nasync function rollbackBootstrap(tx: TransactionHandle): Promise<void> {\n try {\n if (typeof tx.isActive !== 'function' || tx.isActive()) await tx.rollback();\n } catch (error) {\n logger.warn(\n `[smrt] Failed to rollback system table bootstrap transaction: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n}\n\n/**\n * Run `work` serialized against concurrent SMRT system-table bootstrap.\n *\n * `_smrt_dispatch`/`_smrt_dispatch_subscriptions` (and every other\n * framework-owned `_smrt_*` table) are catalog objects shared by every\n * caller against one physical database — including standalone initializers\n * such as `DispatchBus.initialize()` that provision or verify a subset of\n * {@link ALL_SYSTEM_TABLES} outside of a full {@link ensureSystemTables} run\n * (#2861). PostgreSQL DDL against the same object from concurrent sessions is\n * not internally serialized (`CREATE TABLE IF NOT EXISTS` races on\n * `pg_type_typname_nsp_index` under true concurrency; `ALTER TABLE`/`CREATE\n * INDEX` take catalog locks that queue behind each other), so every writer of\n * a system table — full bootstrap or a standalone initializer alike — must\n * take the *same* advisory lock before touching it. Using one shared lock key\n * across all of them is what makes them mutually exclusive rather than each\n * safe only against itself.\n *\n * PostgreSQL: wraps `work` in the same bounded advisory-locked transaction\n * {@link bootstrapSystemTables} uses. Other engines have no comparable\n * catalog race for this codebase's supported deployment shapes, so `work`\n * runs directly against `db`.\n */\nexport async function runSerializedAgainstSystemTableBootstrap<T>(\n db: DatabaseInterface,\n typeHint: string | undefined,\n work: (scopedDb: DatabaseInterface) => Promise<T>,\n): Promise<T> {\n if (getDatabaseEngine(db, typeHint) !== 'postgres') {\n return work(db);\n }\n\n const beginTransaction = db.beginTransaction;\n const transaction = (db as TransactionCapableDatabase).transaction;\n\n if (typeof beginTransaction === 'function') {\n const tx = await beginTransaction.call(db);\n if (!tx) throw new Error('Database transaction could not be started');\n try {\n // Raise the budget before taking the lock — the wait itself is what the\n // runtime session timeouts would otherwise cancel (#2377).\n for (const sql of SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL) await tx.query(sql);\n await tx.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);\n const result = await work(tx);\n await tx.commit();\n return result;\n } catch (error) {\n await rollbackBootstrap(tx);\n throw error;\n }\n }\n\n if (typeof transaction === 'function') {\n // `db` has `transaction()` but no `beginTransaction()`. Two distinct\n // shapes reach here, and they are NOT the same case:\n //\n // - `beginTransaction()`'s own returned handle additionally carries\n // `commit`/`rollback`/`isActive` (e.g. `createIsolatedTestDb()`'s\n // per-test transaction, #2861 review Finding 3:\n // `packages/vitest/src/__tests__/\n // issue-2429-postgres-system-tables.optional.test.ts` hands one\n // straight to `createDispatchBus`). That handle already *is* inside a\n // transaction.\n // - Everything else that carries `transaction` alone and neither\n // `commit` nor `rollback` is NOT necessarily already inside one.\n // Issue #35's pre-existing contract\n // (`packages/core/src/__tests__/\n // issue-35-system-tables-initialization.test.ts`) requires a\n // transaction-capable-but-idle adapter of exactly this shape to run\n // its DDL through `db.transaction(cb)`, not directly against `db`.\n // The callback argument of `db.transaction(cb)` itself (#2861 review\n // recall: `class.ts`/`agent.ts` can reach this shape via `db: tx`)\n // carries the identical shape while already being inside the\n // enclosing transaction — `@happyvertical/sql` has no public marker\n // for \"this handle is already inside a transaction\" to tell the two\n // apart (upstream tracked: happyvertical/sdk#1249). Filed rather than\n // silently widened further: opening via `db.transaction(cb)` serves\n // both — for a genuinely idle handle it is an ordinary transaction\n // (issue #35's contract), and for the callback-argument shape\n // `@happyvertical/sql` documents that nesting re-enters the enclosing\n // transaction under a `SAVEPOINT` on the same connection, so the DDL\n // still runs against the one real transaction either way.\n const alreadyInTransaction =\n typeof (db as Partial<TransactionHandle>).commit === 'function' &&\n typeof (db as Partial<TransactionHandle>).rollback === 'function';\n\n if (!alreadyInTransaction) {\n // Capture on `db` *before* opening the nested scope: for the\n // callback-argument shape this reads the enclosing transaction's\n // current budget so it can be restored; for a genuinely idle handle\n // it reads the ambient session default, which the restore below then\n // harmlessly reapplies to the (about to end) fresh transaction.\n const priorLockTimeout = getQueryRows(\n await db.query('SHOW lock_timeout'),\n )[0]?.lock_timeout;\n const priorStatementTimeout = getQueryRows(\n await db.query('SHOW statement_timeout'),\n )[0]?.statement_timeout;\n // Called via `.call(db, ...)` with explicit type arguments — the\n // established pattern for this same `TransactionCapable`-style shape\n // elsewhere in this package (`cascade.ts`'s two `transaction.call<...>`\n // sites) — rather than a bare `transaction(...)` call, which would\n // drop the `this: DatabaseInterface` receiver the declared signature\n // requires. No live `@happyvertical/sql` adapter's `transaction`\n // currently reads `this`, but nothing guarantees that of every\n // `DatabaseInterface` implementer.\n return transaction.call<\n DatabaseInterface,\n [(tx: DatabaseInterface) => Promise<T>],\n Promise<T>\n >(db, async (tx) => {\n for (const sql of SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL) {\n await tx.query(sql);\n }\n await tx.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);\n const result = await work(tx);\n // Restore only on success. On failure — DDL error or a timed-out\n // lock wait — the (sub)transaction is already aborted (PostgreSQL\n // 25P02: no statement but ROLLBACK/ROLLBACK TO SAVEPOINT is\n // accepted), so issuing `SET LOCAL` here would itself error and\n // replace the real failure. That path needs no explicit restore\n // anyway: `db.transaction(cb)` rolls back to the savepoint on a\n // thrown error, and `ROLLBACK TO SAVEPOINT` *does* undo `SET\n // LOCAL` (unlike the `RELEASE SAVEPOINT` success path this\n // restore guards against — confirmed directly against a live\n // PostgreSQL 17 instance).\n if (typeof priorLockTimeout === 'string') {\n await tx.query(`SET LOCAL lock_timeout = '${priorLockTimeout}'`);\n }\n if (typeof priorStatementTimeout === 'string') {\n await tx.query(\n `SET LOCAL statement_timeout = '${priorStatementTimeout}'`,\n );\n }\n return result;\n });\n }\n\n // `pg_advisory_xact_lock` run directly against `db` here (no new\n // transaction, no savepoint) scopes to the transaction `db` is already\n // in and auto-releases only when *that* transaction commits or rolls\n // back — exactly like the branch above, which opens and promptly\n // commits its own. This matters for more than tidiness:\n //\n // - Wrapping `db`'s own `transaction()` (a savepoint) to take the lock\n // was tried and reverted: PostgreSQL does not scope\n // `pg_advisory_xact_lock`/`SET LOCAL` to a savepoint — both live for\n // the rest of the *transaction* — so it leaked the lock hold and a\n // raised timeout budget into the caller's transaction (confirmed:\n // `SHOW lock_timeout` read back the raised `5min` budget instead of\n // the caller's original value after `createDispatchBus()` returned).\n // - A session-scoped `pg_advisory_lock`/`pg_advisory_unlock` pair\n // released right after `work()` was tried next and also reverted:\n // releasing the lock before the caller's own transaction ends does\n // not close the actual race — two callers each holding their own\n // uncommitted transaction still deadlock on PostgreSQL's ordinary\n // catalog lock for the not-yet-visible `CREATE TABLE`, entirely\n // independent of our advisory lock, because that catalog lock is held\n // until the *first* caller's transaction ends, not until it releases\n // our lock (confirmed by direct reproduction).\n //\n // Taking the xact-scoped lock directly on `db` is the only scheme that\n // actually closes this: a second caller's lock acquisition blocks until\n // the first caller's transaction ends, by which point its DDL is either\n // committed (visible) or rolled back (gone) — no catalog-lock stall\n // either way.\n //\n // The wait for this lock still needs the same raised budget the\n // sibling branch above gives itself — every framework-owned connection\n // (`applyPostgresRuntimeTimeouts()`, wired into `SmrtClass`,\n // `createDispatchBus`'s own `getDatabase()` path, and the generators)\n // bakes a request-sized `lock_timeout=10000ms` into the connection by\n // default, so without a raise here a caller-owned transaction that\n // holds this lock for longer than 10s (concurrent callers now correctly\n // serialize on it — see the shapes above) would abort the *next*\n // waiter with \"canceling statement due to lock timeout\", reproducing\n // the #2861 symptom class through the timeout door instead of the\n // catalog-race door. Unlike the sibling branch, this one must not keep\n // the raise for the caller's remaining transaction: capture the\n // caller's current values first and restore them immediately after\n // acquiring the lock, before `work(db)` runs — the raise covers only\n // the wait PostgreSQL's own lock manager enforces, never the caller's\n // subsequent statements.\n const priorLockTimeout = getQueryRows(\n await db.query('SHOW lock_timeout'),\n )[0]?.lock_timeout;\n const priorStatementTimeout = getQueryRows(\n await db.query('SHOW statement_timeout'),\n )[0]?.statement_timeout;\n for (const sql of SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL) await db.query(sql);\n await db.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);\n // Restore only after the lock is actually acquired. `db` here is the\n // caller's real top-level transaction, not a savepoint: if the wait\n // itself times out, PostgreSQL has already aborted that transaction\n // (25P02 — no statement but ROLLBACK is accepted until it ends), so a\n // `finally`-scoped restore would itself error and replace the real\n // lock-timeout failure with a confusing \"transaction is aborted\"\n // error. There is nothing to restore in that case either: the caller's\n // transaction never reaches `work()` and ends via rollback, which\n // discards the raised values along with everything else.\n if (typeof priorLockTimeout === 'string') {\n await db.query(`SET LOCAL lock_timeout = '${priorLockTimeout}'`);\n }\n if (typeof priorStatementTimeout === 'string') {\n await db.query(\n `SET LOCAL statement_timeout = '${priorStatementTimeout}'`,\n );\n }\n return work(db);\n }\n\n throw new Error(\n 'Postgres system table bootstrap requires a transaction-capable database adapter',\n );\n}\n\n/**\n * Ensure every framework-owned SMRT system table exists before use.\n *\n * PostgreSQL provisioning is serialized in a bounded advisory-locked\n * transaction; other engines use the schema's idempotent DDL directly.\n */\nexport async function ensureSystemTables(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n await runSerializedAgainstSystemTableBootstrap(db, typeHint, (scopedDb) =>\n bootstrapSystemTables(scopedDb, typeHint),\n );\n}\n"],"mappings":"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"}
|
|
@@ -97,7 +97,9 @@ export interface SmrtPluginOptions {
|
|
|
97
97
|
/**
|
|
98
98
|
* Validate that every method in `cli.include` is exposed via the API
|
|
99
99
|
* (so HTTP-based CLI consumers can actually reach them). Default: true.
|
|
100
|
-
* Per-class opt-out via `cli: { skipApiCheck: true }
|
|
100
|
+
* Per-class opt-out via `cli: { skipApiCheck: true }` (whole class) or
|
|
101
|
+
* `cli: { skipApiCheck: ['methodName'] }` (only the named methods,
|
|
102
|
+
* smrt#2857).
|
|
101
103
|
*/
|
|
102
104
|
validateCliApiCoherence?: boolean;
|
|
103
105
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EACV,2BAA2B,EAC3B,qBAAqB,EAEtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,MAAM,CAAC;AAClE,OAAO,EAEL,KAAK,6BAA6B,EACnC,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DETERMINISTIC_GENERATED_AT } from "../scanner/types.js";
|
|
2
|
+
import { CRUD_OPERATIONS } from "../generators/custom-action.js";
|
|
2
3
|
import { findCliApiCoherenceViolations, generateSvelteKitRoutes, methodNameToKebab, resolveApiActionRouteConfig, resolveApiActionSet, validateCliIncludeAgainstApi } from "./sveltekit-generator.js";
|
|
3
4
|
import { buildWebMcpToolDefinitions, compareText, generateWebModule, selectWebCollectionEntries } from "./web-collections.js";
|
|
4
5
|
import { buildDomainKnowledgeManifest } from "../knowledge.js";
|
|
@@ -215,10 +216,16 @@ function smrtPlugin(options = {}) {
|
|
|
215
216
|
if (!validateCliApiCoherence) return;
|
|
216
217
|
const violations = findCliApiCoherenceViolations(m);
|
|
217
218
|
if (violations.length === 0) return;
|
|
218
|
-
for (const { className, unreachable } of violations) {
|
|
219
|
+
for (const { className, unreachable, invalidSkipApiCheck } of violations) {
|
|
220
|
+
for (const name of invalidSkipApiCheck ?? []) console.warn(`[smrt] ${className}: cli.skipApiCheck names '${name}', which isn't a CRUD verb or a scanned public method on this class, or isn't part of its current cli.include/cli.exclude surface (a typo, or a stale entry left behind after the command was dropped from cli.include). Build will fail until this is resolved.\n Fix the typo, or remove '${name}' from cli.skipApiCheck -- an\n unrecognized or stale name grants no exemption at all.`);
|
|
219
221
|
const cliConfig = m.objects[className]?.decoratorConfig?.cli;
|
|
220
222
|
const isEnforced = typeof cliConfig === "object" && cliConfig !== null && Array.isArray(cliConfig.include) && cliConfig.include.length > 0;
|
|
221
|
-
|
|
223
|
+
const methods = m.objects[className]?.methods ?? {};
|
|
224
|
+
const isRealCommandName = (name) => CRUD_OPERATIONS.includes(name) || methods[name]?.isPublic === true;
|
|
225
|
+
for (const action of unreachable) {
|
|
226
|
+
const skipApiCheckSuggestion = isRealCommandName(action) ? ` (or \`skipApiCheck: ['${action}']\` to acknowledge only this command)` : "";
|
|
227
|
+
console.warn(`[smrt] ${className}.${action} is exposed as a CLI command but is not exposed via the api.` + (isEnforced ? " Build will fail until this is resolved.\n" : " (Not yet build-blocking for this cli:true/{} class -- see #2638.)\n") + ` Either:\n - Decorate '${action}' with @method({ expose: true }) to route it, or\n - Add '${action}' to api.include, or\n - Remove '${action}' from cli.include / add it to cli.exclude.\n The CLI invokes methods over HTTP; methods without API routes are unreachable.\n A public method is routed by default only when every parameter can be\n built from JSON; see withheldSurfaces in the knowledge artifact for the\n reason this one was withheld (#2686).\n If this CLI is intentionally invoked in-process (no HTTP), set\n \`cli: { skipApiCheck: true }\`${skipApiCheckSuggestion} on the\n @smrt() decorator.`);
|
|
228
|
+
}
|
|
222
229
|
}
|
|
223
230
|
}
|
|
224
231
|
let server;
|