@happyvertical/smrt-core 0.57.31 → 0.57.33
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 +25 -1
- package/agents/data-query.md +21 -0
- package/dist/browser.d.ts +3 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +4 -3
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +1 -1
- package/dist/class.js.map +1 -1
- package/dist/consumer-plugin/index.d.ts +2 -2
- package/dist/consumer-plugin/index.d.ts.map +1 -1
- package/dist/consumer-plugin/index.js.map +1 -1
- package/dist/data-query.d.ts +16 -1
- package/dist/data-query.d.ts.map +1 -1
- package/dist/data-query.js +104 -4
- package/dist/data-query.js.map +1 -1
- package/dist/db-errors.d.ts +7 -0
- package/dist/db-errors.d.ts.map +1 -1
- package/dist/db-errors.js +34 -1
- package/dist/db-errors.js.map +1 -1
- package/dist/dispatch/bus.d.ts +3 -0
- package/dist/dispatch/bus.d.ts.map +1 -1
- package/dist/dispatch/bus.js +27 -4
- package/dist/dispatch/bus.js.map +1 -1
- package/dist/dispatch/index.d.ts +1 -1
- package/dist/dispatch/index.d.ts.map +1 -1
- package/dist/dispatch/types.d.ts +17 -0
- package/dist/dispatch/types.d.ts.map +1 -1
- package/dist/external-agent-usage.d.ts +90 -0
- package/dist/external-agent-usage.d.ts.map +1 -0
- package/dist/external-agent-usage.js +272 -0
- package/dist/external-agent-usage.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- 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/smrt-knowledge.json +5 -6
- package/dist/system/retention.d.ts.map +1 -1
- package/dist/system/retention.js +1 -0
- package/dist/system/retention.js.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +18 -0
- package/dist/testing/database.js.map +1 -1
- package/dist/vite-plugin/asset-url-compatibility.d.ts +2 -2
- package/dist/vite-plugin/asset-url-compatibility.d.ts.map +1 -1
- package/dist/vite-plugin/asset-url-compatibility.js +13 -3
- package/dist/vite-plugin/asset-url-compatibility.js.map +1 -1
- package/dist/vite-plugin/index.d.ts +5 -2
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/plugin-contract.d.ts +164 -0
- package/dist/vite-plugin/plugin-contract.d.ts.map +1 -0
- package/dist/vite-plugin/sveltekit-route-coordinator.d.ts +5 -5
- package/dist/vite-plugin/sveltekit-route-coordinator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-route-coordinator.js.map +1 -1
- package/dist/vite-plugin/worker-registration.d.ts +4 -4
- package/dist/vite-plugin/worker-registration.d.ts.map +1 -1
- package/dist/vite-plugin/worker-registration.js.map +1 -1
- package/dist/vite-preset/index.d.ts +2 -2
- package/dist/vite-preset/index.d.ts.map +1 -1
- package/dist/vite-preset/index.js +9 -1
- package/dist/vite-preset/index.js.map +1 -1
- package/package.json +4 -5
package/dist/db-errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db-errors.js","names":[],"sources":["../src/db-errors.ts"],"sourcesContent":["/**\n * Driver-error classification for the persistence path (#2366).\n *\n * `@happyvertical/sql` does not normalize driver errors: every adapter catches\n * the driver failure and rethrows\n * `DatabaseError('Failed to upsert record into table', { …, originalError })`,\n * where `originalError` is the **string** produced by the SDK's `formatDbError`\n * (`\"<driver message>, code=<driver code>, detail=…, hint=…\"`). The typed\n * message therefore carries none of the constraint wording, which is why\n * matching on `error.message` alone silently stopped recognizing constraint\n * violations and let {@link ErrorUtils.withRetry} retry them.\n *\n * This module walks the whole causal chain — `cause`, `context.originalError`\n * (Error *or* formatted string), `originalError`, `details.causeMessages`, and\n * `AggregateError.errors` — collecting every driver code and message it finds,\n * then classifies on the codes first (PostgreSQL SQLSTATE, SQLite result\n * codes, Node network errno) and only falls back to dialect wording for DuckDB,\n * which reports no machine-readable code.\n *\n * The classification separates two independent questions:\n * - `deterministic` — a retry cannot change the outcome (constraint violation,\n * bad input syntax, aborted transaction, missing table). Fail fast.\n * - `retryable` — a retry plausibly can succeed (serialization failure,\n * deadlock, lock timeout, connection loss, `SQLITE_BUSY`).\n *\n * They are not complements: `kind: 'unknown'` means \"no opinion\", leaving the\n * caller's own default policy in charge.\n *\n * @module\n */\n\n/** Coarse classification of a database driver failure. */\nexport type DatabaseErrorKind =\n | 'unique_violation'\n | 'not_null_violation'\n | 'foreign_key_violation'\n | 'check_violation'\n | 'exclusion_violation'\n | 'invalid_input'\n | 'aborted_transaction'\n | 'undefined_object'\n | 'insufficient_privilege'\n | 'syntax_error'\n | 'transient'\n | 'unknown';\n\n/** Structured result of {@link classifyDatabaseError}. */\nexport interface DatabaseErrorClassification {\n /** What kind of failure this is, or `'unknown'` when nothing matched. */\n kind: DatabaseErrorKind;\n /** True when retrying cannot change the outcome. */\n deterministic: boolean;\n /** True when retrying plausibly can succeed. */\n retryable: boolean;\n /** PostgreSQL SQLSTATE recovered from the chain, when present. */\n sqlstate?: string;\n /** Raw driver code that produced the classification, when present. */\n driverCode?: string;\n /** Every driver code found anywhere in the chain, in discovery order. */\n driverCodes: readonly string[];\n /**\n * Every message found anywhere in the chain, in discovery order. Includes\n * the SDK's formatted `originalError` strings and the driver's own\n * `detail`/`hint` fields, which carry the PostgreSQL\n * `Key (col)=(value) already exists.` line used for column recovery.\n */\n driverMessages: readonly string[];\n /**\n * Column named directly by the driver (`node-postgres` sets `error.column`\n * on a `23502`), when one was reported. More reliable than parsing it back\n * out of the message.\n */\n column?: string;\n /** Constraint named directly by the driver (`error.constraint`). */\n constraint?: string;\n}\n\n/** Maximum chain depth walked, guarding against pathological nesting. */\nconst MAX_CHAIN_DEPTH = 12;\n\n/** `formatDbError` renders driver codes as `code=<value>` inside its string. */\nconst FORMATTED_CODE_PATTERN = /(?:^|[\\s,;([{])code=([A-Za-z0-9_]+)/g;\n\n/**\n * PostgreSQL SQLSTATE codes whose failure is fully determined by the statement\n * and its data. Retrying re-executes the same deterministic rejection.\n */\nconst DETERMINISTIC_SQLSTATES = new Map<string, DatabaseErrorKind>([\n // Class 23 — integrity constraint violation\n ['23000', 'check_violation'], // integrity_constraint_violation (generic)\n ['23001', 'foreign_key_violation'], // restrict_violation\n ['23502', 'not_null_violation'],\n ['23503', 'foreign_key_violation'],\n ['23505', 'unique_violation'],\n ['23514', 'check_violation'],\n // An EXCLUDE constraint is not a unique index: the conflicting rows differ on\n // the constrained columns and merely overlap under the operator. Reporting it\n // as `unique_violation` would make `save()` raise\n // VALIDATION_UNIQUE_CONSTRAINT naming a field that did not collide, so it\n // keeps its own kind and stays on the generic DatabaseError path.\n ['23P01', 'exclusion_violation'],\n // Class 22 — data exception (bad literal, overflow, bad uuid cast)\n ['22001', 'invalid_input'], // string_data_right_truncation\n ['22003', 'invalid_input'], // numeric_value_out_of_range\n ['22007', 'invalid_input'], // invalid_datetime_format\n ['22008', 'invalid_input'], // datetime_field_overflow\n ['22023', 'invalid_input'], // invalid_parameter_value\n ['22P02', 'invalid_input'], // invalid_text_representation — the bad-uuid case\n ['22P03', 'invalid_input'], // invalid_binary_representation\n // Class 25 — invalid transaction state. Only `25P02` belongs here:\n // `25P03 idle_in_transaction_session_timeout` means the server *terminated\n // the session*, which is a connection-level failure a fresh connection can\n // survive, so it lives in TRANSIENT_SQLSTATES instead. Calling it\n // deterministic would both block that retry and make\n // `isAbortedTransactionError()` report a timed-out session as an OIDC race\n // conflict in the profiles coordinator.\n ['25P02', 'aborted_transaction'], // in_failed_sql_transaction\n // Class 42 — syntax error or access rule violation\n ['42501', 'insufficient_privilege'],\n ['42601', 'syntax_error'],\n ['42703', 'undefined_object'], // undefined_column\n ['42804', 'invalid_input'], // datatype_mismatch\n ['42883', 'undefined_object'], // undefined_function\n ['42P01', 'undefined_object'], // undefined_table\n ['42P07', 'undefined_object'], // duplicate_table\n // invalid_column_reference — \"no unique or exclusion constraint matching the\n // ON CONFLICT specification\". A conflict target that does not match a real\n // unique index is a schema defect, not contention; retrying it four times\n // only delays the report (see epic #2382 finding A4).\n ['42P10', 'undefined_object'],\n ['3D000', 'undefined_object'], // invalid_catalog_name\n ['3F000', 'undefined_object'], // invalid_schema_name\n]);\n\n/**\n * PostgreSQL SQLSTATE codes describing a contention or availability failure.\n * The same statement may well succeed on a later attempt.\n */\nconst TRANSIENT_SQLSTATES = new Set([\n '25P03', // idle_in_transaction_session_timeout — the session was terminated\n '40001', // serialization_failure\n '40003', // statement_completion_unknown\n '40P01', // deadlock_detected\n '53200', // out_of_memory\n '53300', // too_many_connections\n '55P03', // lock_not_available\n '57014', // query_canceled\n '57P01', // admin_shutdown\n '57P02', // crash_shutdown\n '57P03', // cannot_connect_now\n '58030', // io_error\n '08000', // connection_exception\n '08001', // sqlclient_unable_to_establish_sqlconnection\n '08003', // connection_does_not_exist\n '08004', // sqlserver_rejected_establishment_of_sqlconnection\n '08006', // connection_failure\n '08007', // transaction_resolution_unknown\n '08P01', // protocol_violation\n]);\n\n/**\n * SQLite extended result codes. `better-sqlite3`/`node:sqlite` frequently\n * report the bare `SQLITE_CONSTRAINT` parent code, so the constraint sub-kind\n * is resolved from the message instead (see {@link classifyDialectMessage}).\n */\nconst DETERMINISTIC_SQLITE_CODES = new Map<string, DatabaseErrorKind>([\n ['SQLITE_CONSTRAINT_UNIQUE', 'unique_violation'],\n ['SQLITE_CONSTRAINT_PRIMARYKEY', 'unique_violation'],\n ['SQLITE_CONSTRAINT_ROWID', 'unique_violation'],\n ['SQLITE_CONSTRAINT_NOTNULL', 'not_null_violation'],\n ['SQLITE_CONSTRAINT_FOREIGNKEY', 'foreign_key_violation'],\n ['SQLITE_CONSTRAINT_CHECK', 'check_violation'],\n ['SQLITE_CONSTRAINT_TRIGGER', 'check_violation'],\n ['SQLITE_MISMATCH', 'invalid_input'],\n ['SQLITE_RANGE', 'invalid_input'],\n ['SQLITE_TOOBIG', 'invalid_input'],\n ['SQLITE_READONLY', 'insufficient_privilege'],\n ['SQLITE_AUTH', 'insufficient_privilege'],\n]);\n\n/** SQLite result codes that clear once contention or a lock resolves. */\nconst TRANSIENT_SQLITE_CODES = new Set([\n 'SQLITE_BUSY',\n 'SQLITE_BUSY_SNAPSHOT',\n 'SQLITE_BUSY_TIMEOUT',\n 'SQLITE_LOCKED',\n 'SQLITE_LOCKED_SHAREDCACHE',\n 'SQLITE_PROTOCOL',\n]);\n\n/** Node/libuv socket errno values raised when a pooled connection drops. */\nconst TRANSIENT_NETWORK_CODES = new Set([\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'ECONNABORTED',\n 'EPIPE',\n 'ETIMEDOUT',\n 'ENETUNREACH',\n 'ENETRESET',\n 'EHOSTUNREACH',\n 'EAI_AGAIN',\n 'ENOTFOUND',\n]);\n\n/**\n * Dialect wording, in precedence order. Only DuckDB has no machine-readable\n * code, but SQLite's bare `SQLITE_CONSTRAINT` and PostgreSQL errors that have\n * lost their code through string formatting are recovered here too.\n *\n * NOT NULL is tested before UNIQUE because DuckDB phrases both with the same\n * `Constraint Error … violates` shape, and foreign-key/check wording must not\n * be mistaken for a unique violation (#1578).\n */\nconst DIALECT_MESSAGE_RULES: ReadonlyArray<{\n kind: DatabaseErrorKind;\n pattern: RegExp;\n}> = [\n {\n kind: 'aborted_transaction',\n pattern: /current transaction is aborted/i,\n },\n {\n kind: 'not_null_violation',\n pattern:\n /NOT NULL constraint failed|null value in column .* violates not-null/i,\n },\n {\n // SQLite reports `FOREIGN KEY constraint failed` under the bare\n // `SQLITE_CONSTRAINT` parent code, so without this arm the sub-kind\n // recovery below would mislabel every SQLite FK failure.\n kind: 'foreign_key_violation',\n pattern: /violates foreign key constraint|FOREIGN KEY constraint failed/i,\n },\n {\n kind: 'check_violation',\n pattern: /violates check constraint|CHECK constraint failed/i,\n },\n {\n kind: 'unique_violation',\n pattern:\n /UNIQUE constraint failed|violates unique constraint|violates primary key constraint|duplicate key value violates|PRIMARY KEY or UNIQUE constraint violation/i,\n },\n {\n kind: 'invalid_input',\n pattern: /invalid input syntax for type|value too long for type/i,\n },\n {\n // Deliberately narrow: a bare /does not exist/ matches far too much to be\n // safe as a fail-fast signal.\n kind: 'undefined_object',\n pattern:\n /no such table|no such column|(?:relation|column|table|function) \"[^\"]*\" does not exist/i,\n },\n {\n kind: 'transient',\n pattern:\n /database is locked|database table is locked|connection terminated|server closed the connection|too many clients|deadlock detected|could not serialize access/i,\n },\n];\n\n/** SMRT-level codes that already express a driver classification. */\nconst SMRT_CODE_KINDS = new Map<string, DatabaseErrorKind>([\n ['VALIDATION_UNIQUE_CONSTRAINT', 'unique_violation'],\n ['DB_CONSTRAINT_VIOLATION', 'check_violation'],\n]);\n\n/**\n * Reporting precedence among deterministic kinds, most specific first.\n *\n * `aborted_transaction` ranks last on purpose: PostgreSQL reports `25P02` for\n * every statement issued after the transaction already failed, so a chain\n * carrying both it and the constraint that caused the abort must name the\n * constraint. Every kind here is non-retryable, so ranking changes only what\n * is reported, never whether the operation is retried.\n */\nconst DETERMINISTIC_KIND_RANK = new Map<DatabaseErrorKind, number>([\n ['unique_violation', 0],\n ['not_null_violation', 1],\n ['foreign_key_violation', 2],\n ['check_violation', 3],\n ['exclusion_violation', 4],\n ['invalid_input', 5],\n ['undefined_object', 6],\n ['insufficient_privilege', 7],\n ['syntax_error', 8],\n ['aborted_transaction', 9],\n]);\n\nfunction rankKind(kind: DatabaseErrorKind): number {\n return DETERMINISTIC_KIND_RANK.get(kind) ?? Number.MAX_SAFE_INTEGER;\n}\n\ninterface ChainSignals {\n codes: string[];\n messages: string[];\n columns: string[];\n constraints: string[];\n}\n\nfunction pushUnique(collection: string[], value: unknown): void {\n if (typeof value !== 'string') return;\n const entry = value.trim();\n if (entry && !collection.includes(entry)) {\n collection.push(entry);\n }\n}\n\nfunction pushCode(signals: ChainSignals, value: unknown): void {\n pushUnique(signals.codes, value);\n}\n\nfunction pushMessage(signals: ChainSignals, value: unknown): void {\n if (typeof value !== 'string') return;\n const message = value.trim();\n if (!message || signals.messages.includes(message)) return;\n signals.messages.push(message);\n\n // `formatDbError` folds the driver's own `code` into its string, so the\n // machine-readable classification survives even after stringification.\n FORMATTED_CODE_PATTERN.lastIndex = 0;\n let match = FORMATTED_CODE_PATTERN.exec(message);\n while (match) {\n pushCode(signals, match[1]);\n match = FORMATTED_CODE_PATTERN.exec(message);\n }\n}\n\n/**\n * Walks an error's causal chain, collecting every driver code and message.\n *\n * Visits `cause`, `context.originalError`, `originalError`,\n * `details.causeMessage`/`details.causeMessages` and `AggregateError.errors`,\n * because the SDK, core and consumer wrappers each nest the driver error\n * differently. Cycles and depth are bounded.\n */\nfunction collectChainSignals(error: unknown): ChainSignals {\n const signals: ChainSignals = {\n codes: [],\n messages: [],\n columns: [],\n constraints: [],\n };\n const seen = new Set<unknown>();\n const pending: Array<{ value: unknown; depth: number }> = [\n { value: error, depth: 0 },\n ];\n\n while (pending.length > 0) {\n const entry = pending.pop();\n if (!entry) break;\n const { value, depth } = entry;\n if (value === null || value === undefined || depth > MAX_CHAIN_DEPTH) {\n continue;\n }\n\n if (typeof value === 'string') {\n pushMessage(signals, value);\n continue;\n }\n if (typeof value !== 'object' || seen.has(value)) continue;\n seen.add(value);\n\n const node = value as {\n code?: unknown;\n sqlState?: unknown;\n sqlstate?: unknown;\n message?: unknown;\n detail?: unknown;\n hint?: unknown;\n column?: unknown;\n constraint?: unknown;\n cause?: unknown;\n context?: { originalError?: unknown } | unknown;\n originalError?: unknown;\n details?: unknown;\n errors?: unknown;\n };\n\n pushCode(signals, node.code);\n pushCode(signals, node.sqlState);\n pushCode(signals, node.sqlstate);\n pushMessage(signals, node.message);\n // `node-postgres` surfaces the PostgreSQL error fields as properties. On\n // the direct INSERT path nothing stringifies them into the message, so the\n // column would otherwise be unrecoverable (`error.detail` carries\n // \"Key (sku)=(s1) already exists.\" for a 23505; `error.column` names the\n // column outright for a 23502).\n pushMessage(signals, node.detail);\n pushMessage(signals, node.hint);\n pushUnique(signals.columns, node.column);\n pushUnique(signals.constraints, node.constraint);\n\n const details = node.details as\n | { causeMessage?: unknown; causeMessages?: unknown }\n | undefined;\n if (details && typeof details === 'object') {\n pushMessage(signals, details.causeMessage);\n if (Array.isArray(details.causeMessages)) {\n for (const causeMessage of details.causeMessages) {\n pushMessage(signals, causeMessage);\n }\n }\n }\n\n const context = node.context as { originalError?: unknown } | undefined;\n const next = [\n node.cause,\n node.originalError,\n context && typeof context === 'object'\n ? context.originalError\n : undefined,\n ];\n for (const child of next) {\n if (child !== null && child !== undefined) {\n pending.push({ value: child, depth: depth + 1 });\n }\n }\n if (Array.isArray(node.errors)) {\n for (const child of node.errors) {\n pending.push({ value: child, depth: depth + 1 });\n }\n }\n }\n\n return signals;\n}\n\n/**\n * Classifies a raw dialect message. Exposed so\n * `SmrtObject.classifyConstraintError()` and this module share one set of\n * regexes rather than drifting apart.\n */\nexport function classifyDialectMessage(message: string): DatabaseErrorKind {\n if (!message) return 'unknown';\n for (const rule of DIALECT_MESSAGE_RULES) {\n if (rule.pattern.test(message)) return rule.kind;\n }\n return 'unknown';\n}\n\nfunction isSqlState(code: string): boolean {\n return /^[0-9A-Z]{5}$/.test(code);\n}\n\n/**\n * Classifies a database failure by walking its causal chain.\n *\n * Codes win over wording: a PostgreSQL SQLSTATE or SQLite result code is\n * authoritative, and dialect message matching only runs when no code in the\n * chain was recognized (DuckDB, or an error already flattened to a string).\n *\n * Constraint violations are reported ahead of `aborted_transaction` so a chain\n * carrying both surfaces the root cause rather than the downstream symptom;\n * both are deterministic, so retry behavior is identical either way.\n *\n * @param error - Any thrown value. Non-database errors classify as `'unknown'`.\n * @returns The structured classification; never throws.\n */\nexport function classifyDatabaseError(\n error: unknown,\n): DatabaseErrorClassification {\n const signals = collectChainSignals(error);\n const sqlstate = signals.codes.find(\n (code) =>\n isSqlState(code) &&\n (DETERMINISTIC_SQLSTATES.has(code) || TRANSIENT_SQLSTATES.has(code)),\n );\n\n const base = {\n driverCodes: signals.codes,\n driverMessages: signals.messages,\n ...(sqlstate ? { sqlstate } : {}),\n ...(signals.columns[0] ? { column: signals.columns[0] } : {}),\n ...(signals.constraints[0] ? { constraint: signals.constraints[0] } : {}),\n };\n\n // Deterministic codes first: a chain that carries both a constraint code and\n // a downstream contention code must fail fast, not retry.\n const deterministicCandidates: Array<{\n code: string;\n kind: DatabaseErrorKind;\n }> = [];\n for (const code of signals.codes) {\n const kind =\n DETERMINISTIC_SQLSTATES.get(code) ??\n DETERMINISTIC_SQLITE_CODES.get(code) ??\n // SMRT's own typed codes: a `ValidationError` already carrying\n // `VALIDATION_UNIQUE_CONSTRAINT` is the classified form of the same\n // driver failure.\n SMRT_CODE_KINDS.get(code) ??\n // A bare `SQLITE_CONSTRAINT` carries no sub-kind; recover it from the\n // message, which SQLite always phrases explicitly.\n (code === 'SQLITE_CONSTRAINT'\n ? (signals.messages\n .map(classifyDialectMessage)\n .find((candidate) => DETERMINISTIC_KIND_RANK.has(candidate)) ??\n 'check_violation')\n : undefined);\n if (kind) deterministicCandidates.push({ code, kind });\n }\n\n if (deterministicCandidates.length > 0) {\n // Rank rather than take the first discovered: an error whose chain holds\n // both `23505` and the `25P02` it caused must report the root constraint,\n // not the downstream symptom.\n const best = deterministicCandidates.reduce((winner, candidate) =>\n rankKind(candidate.kind) < rankKind(winner.kind) ? candidate : winner,\n );\n return {\n ...base,\n kind: best.kind,\n deterministic: true,\n retryable: false,\n driverCode: best.code,\n };\n }\n\n for (const code of signals.codes) {\n if (TRANSIENT_SQLSTATES.has(code) || TRANSIENT_SQLITE_CODES.has(code)) {\n return {\n ...base,\n kind: 'transient',\n deterministic: false,\n retryable: true,\n driverCode: code,\n };\n }\n if (TRANSIENT_NETWORK_CODES.has(code)) {\n return {\n ...base,\n kind: 'transient',\n deterministic: false,\n retryable: true,\n driverCode: code,\n };\n }\n }\n\n // No recognized code anywhere — fall back to dialect wording. This is the\n // only path DuckDB ever takes.\n const messageKinds = signals.messages\n .map(classifyDialectMessage)\n .filter((kind) => kind !== 'unknown');\n const deterministicFromMessage = messageKinds\n .filter((kind) => DETERMINISTIC_KIND_RANK.has(kind))\n .sort((left, right) => rankKind(left) - rankKind(right))[0];\n if (deterministicFromMessage) {\n return {\n ...base,\n kind: deterministicFromMessage,\n deterministic: true,\n retryable: false,\n };\n }\n if (messageKinds.includes('transient')) {\n return {\n ...base,\n kind: 'transient',\n deterministic: false,\n retryable: true,\n };\n }\n\n return {\n ...base,\n kind: 'unknown',\n deterministic: false,\n retryable: false,\n };\n}\n\n/**\n * True when retrying the statement cannot change the outcome.\n *\n * Used by {@link ErrorUtils.withRetry} to fail fast instead of burning the\n * full 500 + 1000 + 2000 ms backoff on a constraint violation.\n */\nexport function isDeterministicDatabaseError(error: unknown): boolean {\n return classifyDatabaseError(error).deterministic;\n}\n\n/** True when the failure is contention or availability and may clear. */\nexport function isTransientDatabaseError(error: unknown): boolean {\n return classifyDatabaseError(error).retryable;\n}\n\n/** True when a unique or primary-key constraint was violated. */\nexport function isUniqueViolationError(error: unknown): boolean {\n return classifyDatabaseError(error).kind === 'unique_violation';\n}\n\n/** True when a NOT NULL constraint was violated. */\nexport function isNotNullViolationError(error: unknown): boolean {\n return classifyDatabaseError(error).kind === 'not_null_violation';\n}\n\n/**\n * True when the failure came from running inside an already-aborted\n * PostgreSQL transaction (SQLSTATE `25P02`).\n *\n * Detected independently of {@link DatabaseErrorKind} so a chain that also\n * carries the root constraint code still answers `true` here.\n */\nexport function isAbortedTransactionError(error: unknown): boolean {\n const signals = collectChainSignals(error);\n // `25P02` only. `25P03` is a terminated session, not a failed transaction,\n // and reporting it here would make the profiles coordinator treat an\n // idle-timeout disconnect as a provisioning race conflict.\n if (signals.codes.includes('25P02')) {\n return true;\n }\n return signals.messages.some((message) =>\n /current transaction is aborted/i.test(message),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"db-errors.js","names":[],"sources":["../src/db-errors.ts"],"sourcesContent":["/**\n * Driver-error classification for the persistence path (#2366).\n *\n * `@happyvertical/sql` does not normalize driver errors: every adapter catches\n * the driver failure and rethrows\n * `DatabaseError('Failed to upsert record into table', { …, originalError })`,\n * where `originalError` is the **string** produced by the SDK's `formatDbError`\n * (`\"<driver message>, code=<driver code>, detail=…, hint=…\"`). The typed\n * message therefore carries none of the constraint wording, which is why\n * matching on `error.message` alone silently stopped recognizing constraint\n * violations and let {@link ErrorUtils.withRetry} retry them.\n *\n * This module walks the whole causal chain — `cause`, `context.originalError`\n * (Error *or* formatted string), `originalError`, `details.causeMessages`, and\n * `AggregateError.errors` — collecting every driver code and message it finds,\n * then classifies on the codes first (PostgreSQL SQLSTATE, SQLite result\n * codes, Node network errno) and only falls back to dialect wording for DuckDB,\n * which reports no machine-readable code.\n *\n * The classification separates two independent questions:\n * - `deterministic` — a retry cannot change the outcome (constraint violation,\n * bad input syntax, aborted transaction, missing table). Fail fast.\n * - `retryable` — a retry plausibly can succeed (serialization failure,\n * deadlock, lock timeout, connection loss, `SQLITE_BUSY`).\n *\n * They are not complements: `kind: 'unknown'` means \"no opinion\", leaving the\n * caller's own default policy in charge.\n *\n * @module\n */\n\n/** Coarse classification of a database driver failure. */\nexport type DatabaseErrorKind =\n | 'unique_violation'\n | 'not_null_violation'\n | 'foreign_key_violation'\n | 'check_violation'\n | 'exclusion_violation'\n | 'invalid_input'\n | 'aborted_transaction'\n | 'undefined_object'\n | 'insufficient_privilege'\n | 'syntax_error'\n | 'transient'\n | 'unknown';\n\n/** Structured result of {@link classifyDatabaseError}. */\nexport interface DatabaseErrorClassification {\n /** What kind of failure this is, or `'unknown'` when nothing matched. */\n kind: DatabaseErrorKind;\n /** True when retrying cannot change the outcome. */\n deterministic: boolean;\n /** True when retrying plausibly can succeed. */\n retryable: boolean;\n /** PostgreSQL SQLSTATE recovered from the chain, when present. */\n sqlstate?: string;\n /** Raw driver code that produced the classification, when present. */\n driverCode?: string;\n /** Every driver code found anywhere in the chain, in discovery order. */\n driverCodes: readonly string[];\n /**\n * Every message found anywhere in the chain, in discovery order. Includes\n * the SDK's formatted `originalError` strings and the driver's own\n * `detail`/`hint` fields, which carry the PostgreSQL\n * `Key (col)=(value) already exists.` line used for column recovery.\n */\n driverMessages: readonly string[];\n /**\n * Column named directly by the driver (`node-postgres` sets `error.column`\n * on a `23502`), when one was reported. More reliable than parsing it back\n * out of the message.\n */\n column?: string;\n /** Constraint named directly by the driver (`error.constraint`). */\n constraint?: string;\n}\n\n/** Maximum chain depth walked, guarding against pathological nesting. */\nconst MAX_CHAIN_DEPTH = 12;\n\n/** `formatDbError` renders driver codes as `code=<value>` inside its string. */\nconst FORMATTED_CODE_PATTERN = /(?:^|[\\s,;([{])code=([A-Za-z0-9_]+)/g;\n\n/**\n * PostgreSQL SQLSTATE codes whose failure is fully determined by the statement\n * and its data. Retrying re-executes the same deterministic rejection.\n */\nconst DETERMINISTIC_SQLSTATES = new Map<string, DatabaseErrorKind>([\n // Class 23 — integrity constraint violation\n ['23000', 'check_violation'], // integrity_constraint_violation (generic)\n ['23001', 'foreign_key_violation'], // restrict_violation\n ['23502', 'not_null_violation'],\n ['23503', 'foreign_key_violation'],\n ['23505', 'unique_violation'],\n ['23514', 'check_violation'],\n // An EXCLUDE constraint is not a unique index: the conflicting rows differ on\n // the constrained columns and merely overlap under the operator. Reporting it\n // as `unique_violation` would make `save()` raise\n // VALIDATION_UNIQUE_CONSTRAINT naming a field that did not collide, so it\n // keeps its own kind and stays on the generic DatabaseError path.\n ['23P01', 'exclusion_violation'],\n // Class 22 — data exception (bad literal, overflow, bad uuid cast)\n ['22001', 'invalid_input'], // string_data_right_truncation\n ['22003', 'invalid_input'], // numeric_value_out_of_range\n ['22007', 'invalid_input'], // invalid_datetime_format\n ['22008', 'invalid_input'], // datetime_field_overflow\n ['22023', 'invalid_input'], // invalid_parameter_value\n ['22P02', 'invalid_input'], // invalid_text_representation — the bad-uuid case\n ['22P03', 'invalid_input'], // invalid_binary_representation\n // Class 25 — invalid transaction state. Only `25P02` belongs here:\n // `25P03 idle_in_transaction_session_timeout` means the server *terminated\n // the session*, which is a connection-level failure a fresh connection can\n // survive, so it lives in TRANSIENT_SQLSTATES instead. Calling it\n // deterministic would both block that retry and make\n // `isAbortedTransactionError()` report a timed-out session as an OIDC race\n // conflict in the profiles coordinator.\n ['25P02', 'aborted_transaction'], // in_failed_sql_transaction\n // Class 42 — syntax error or access rule violation\n ['42501', 'insufficient_privilege'],\n ['42601', 'syntax_error'],\n ['42703', 'undefined_object'], // undefined_column\n ['42804', 'invalid_input'], // datatype_mismatch\n ['42883', 'undefined_object'], // undefined_function\n ['42P01', 'undefined_object'], // undefined_table\n ['42P07', 'undefined_object'], // duplicate_table\n // invalid_column_reference — \"no unique or exclusion constraint matching the\n // ON CONFLICT specification\". A conflict target that does not match a real\n // unique index is a schema defect, not contention; retrying it four times\n // only delays the report (see epic #2382 finding A4).\n ['42P10', 'undefined_object'],\n ['3D000', 'undefined_object'], // invalid_catalog_name\n ['3F000', 'undefined_object'], // invalid_schema_name\n]);\n\n/**\n * PostgreSQL SQLSTATE codes describing a contention or availability failure.\n * The same statement may well succeed on a later attempt.\n */\nconst TRANSIENT_SQLSTATES = new Set([\n '25P03', // idle_in_transaction_session_timeout — the session was terminated\n '40001', // serialization_failure\n '40003', // statement_completion_unknown\n '40P01', // deadlock_detected\n '53200', // out_of_memory\n '53300', // too_many_connections\n '55P03', // lock_not_available\n '57014', // query_canceled\n '57P01', // admin_shutdown\n '57P02', // crash_shutdown\n '57P03', // cannot_connect_now\n '58030', // io_error\n '08000', // connection_exception\n '08001', // sqlclient_unable_to_establish_sqlconnection\n '08003', // connection_does_not_exist\n '08004', // sqlserver_rejected_establishment_of_sqlconnection\n '08006', // connection_failure\n '08007', // transaction_resolution_unknown\n '08P01', // protocol_violation\n]);\n\n/**\n * SQLite extended result codes. `better-sqlite3`/`node:sqlite` frequently\n * report the bare `SQLITE_CONSTRAINT` parent code, so the constraint sub-kind\n * is resolved from the message instead (see {@link classifyDialectMessage}).\n */\nconst DETERMINISTIC_SQLITE_CODES = new Map<string, DatabaseErrorKind>([\n ['SQLITE_CONSTRAINT_UNIQUE', 'unique_violation'],\n ['SQLITE_CONSTRAINT_PRIMARYKEY', 'unique_violation'],\n ['SQLITE_CONSTRAINT_ROWID', 'unique_violation'],\n ['SQLITE_CONSTRAINT_NOTNULL', 'not_null_violation'],\n ['SQLITE_CONSTRAINT_FOREIGNKEY', 'foreign_key_violation'],\n ['SQLITE_CONSTRAINT_CHECK', 'check_violation'],\n ['SQLITE_CONSTRAINT_TRIGGER', 'check_violation'],\n ['SQLITE_MISMATCH', 'invalid_input'],\n ['SQLITE_RANGE', 'invalid_input'],\n ['SQLITE_TOOBIG', 'invalid_input'],\n ['SQLITE_READONLY', 'insufficient_privilege'],\n ['SQLITE_AUTH', 'insufficient_privilege'],\n]);\n\n/** SQLite result codes that clear once contention or a lock resolves. */\nconst TRANSIENT_SQLITE_CODES = new Set([\n 'SQLITE_BUSY',\n 'SQLITE_BUSY_SNAPSHOT',\n 'SQLITE_BUSY_TIMEOUT',\n 'SQLITE_LOCKED',\n 'SQLITE_LOCKED_SHAREDCACHE',\n 'SQLITE_PROTOCOL',\n]);\n\n/** Node/libuv socket errno values raised when a pooled connection drops. */\nconst TRANSIENT_NETWORK_CODES = new Set([\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'ECONNABORTED',\n 'EPIPE',\n 'ETIMEDOUT',\n 'ENETUNREACH',\n 'ENETRESET',\n 'EHOSTUNREACH',\n 'EAI_AGAIN',\n 'ENOTFOUND',\n]);\n\n/**\n * Dialect wording, in precedence order. Only DuckDB has no machine-readable\n * code, but SQLite's bare `SQLITE_CONSTRAINT` and PostgreSQL errors that have\n * lost their code through string formatting are recovered here too.\n *\n * NOT NULL is tested before UNIQUE because DuckDB phrases both with the same\n * `Constraint Error … violates` shape, and foreign-key/check wording must not\n * be mistaken for a unique violation (#1578).\n */\nconst DIALECT_MESSAGE_RULES: ReadonlyArray<{\n kind: DatabaseErrorKind;\n pattern: RegExp;\n}> = [\n {\n kind: 'aborted_transaction',\n pattern: /current transaction is aborted/i,\n },\n {\n kind: 'not_null_violation',\n pattern:\n /NOT NULL constraint failed|null value in column .* violates not-null/i,\n },\n {\n // SQLite reports `FOREIGN KEY constraint failed` under the bare\n // `SQLITE_CONSTRAINT` parent code, so without this arm the sub-kind\n // recovery below would mislabel every SQLite FK failure.\n kind: 'foreign_key_violation',\n pattern: /violates foreign key constraint|FOREIGN KEY constraint failed/i,\n },\n {\n kind: 'check_violation',\n pattern: /violates check constraint|CHECK constraint failed/i,\n },\n {\n kind: 'unique_violation',\n pattern:\n /UNIQUE constraint failed|violates unique constraint|violates primary key constraint|duplicate key value violates|PRIMARY KEY or UNIQUE constraint violation/i,\n },\n {\n kind: 'invalid_input',\n pattern: /invalid input syntax for type|value too long for type/i,\n },\n {\n // Deliberately narrow: a bare /does not exist/ matches far too much to be\n // safe as a fail-fast signal.\n kind: 'undefined_object',\n pattern:\n /no such table|no such column|(?:relation|column|table|function) \"[^\"]*\" does not exist/i,\n },\n {\n kind: 'transient',\n pattern:\n /database is locked|database table is locked|connection terminated|server closed the connection|too many clients|deadlock detected|could not serialize access/i,\n },\n];\n\n/** SMRT-level codes that already express a driver classification. */\nconst SMRT_CODE_KINDS = new Map<string, DatabaseErrorKind>([\n ['VALIDATION_UNIQUE_CONSTRAINT', 'unique_violation'],\n ['DB_CONSTRAINT_VIOLATION', 'check_violation'],\n]);\n\n/**\n * Reporting precedence among deterministic kinds, most specific first.\n *\n * `aborted_transaction` ranks last on purpose: PostgreSQL reports `25P02` for\n * every statement issued after the transaction already failed, so a chain\n * carrying both it and the constraint that caused the abort must name the\n * constraint. Every kind here is non-retryable, so ranking changes only what\n * is reported, never whether the operation is retried.\n */\nconst DETERMINISTIC_KIND_RANK = new Map<DatabaseErrorKind, number>([\n ['unique_violation', 0],\n ['not_null_violation', 1],\n ['foreign_key_violation', 2],\n ['check_violation', 3],\n ['exclusion_violation', 4],\n ['invalid_input', 5],\n ['undefined_object', 6],\n ['insufficient_privilege', 7],\n ['syntax_error', 8],\n ['aborted_transaction', 9],\n]);\n\nfunction rankKind(kind: DatabaseErrorKind): number {\n return DETERMINISTIC_KIND_RANK.get(kind) ?? Number.MAX_SAFE_INTEGER;\n}\n\ninterface ChainSignals {\n codes: string[];\n messages: string[];\n columns: string[];\n constraints: string[];\n}\n\nfunction pushUnique(collection: string[], value: unknown): void {\n if (typeof value !== 'string') return;\n const entry = value.trim();\n if (entry && !collection.includes(entry)) {\n collection.push(entry);\n }\n}\n\nfunction pushCode(signals: ChainSignals, value: unknown): void {\n pushUnique(signals.codes, value);\n}\n\nfunction pushMessage(signals: ChainSignals, value: unknown): void {\n if (typeof value !== 'string') return;\n const message = value.trim();\n if (!message || signals.messages.includes(message)) return;\n signals.messages.push(message);\n\n // `formatDbError` folds the driver's own `code` into its string, so the\n // machine-readable classification survives even after stringification.\n FORMATTED_CODE_PATTERN.lastIndex = 0;\n let match = FORMATTED_CODE_PATTERN.exec(message);\n while (match) {\n pushCode(signals, match[1]);\n match = FORMATTED_CODE_PATTERN.exec(message);\n }\n}\n\n/**\n * Walks an error's causal chain, collecting every driver code and message.\n *\n * Visits `cause`, `context.originalError`, `originalError`,\n * `details.causeMessage`/`details.causeMessages` and `AggregateError.errors`,\n * because the SDK, core and consumer wrappers each nest the driver error\n * differently. Cycles and depth are bounded.\n */\nfunction collectChainSignals(error: unknown): ChainSignals {\n const signals: ChainSignals = {\n codes: [],\n messages: [],\n columns: [],\n constraints: [],\n };\n const seen = new Set<unknown>();\n const pending: Array<{ value: unknown; depth: number }> = [\n { value: error, depth: 0 },\n ];\n\n while (pending.length > 0) {\n const entry = pending.pop();\n if (!entry) break;\n const { value, depth } = entry;\n if (value === null || value === undefined || depth > MAX_CHAIN_DEPTH) {\n continue;\n }\n\n if (typeof value === 'string') {\n pushMessage(signals, value);\n continue;\n }\n if (typeof value !== 'object' || seen.has(value)) continue;\n seen.add(value);\n\n const node = value as {\n code?: unknown;\n sqlState?: unknown;\n sqlstate?: unknown;\n message?: unknown;\n detail?: unknown;\n hint?: unknown;\n column?: unknown;\n constraint?: unknown;\n cause?: unknown;\n context?: { originalError?: unknown } | unknown;\n originalError?: unknown;\n details?: unknown;\n errors?: unknown;\n };\n\n pushCode(signals, node.code);\n pushCode(signals, node.sqlState);\n pushCode(signals, node.sqlstate);\n pushMessage(signals, node.message);\n // `node-postgres` surfaces the PostgreSQL error fields as properties. On\n // the direct INSERT path nothing stringifies them into the message, so the\n // column would otherwise be unrecoverable (`error.detail` carries\n // \"Key (sku)=(s1) already exists.\" for a 23505; `error.column` names the\n // column outright for a 23502).\n pushMessage(signals, node.detail);\n pushMessage(signals, node.hint);\n pushUnique(signals.columns, node.column);\n pushUnique(signals.constraints, node.constraint);\n\n const details = node.details as\n | { causeMessage?: unknown; causeMessages?: unknown }\n | undefined;\n if (details && typeof details === 'object') {\n pushMessage(signals, details.causeMessage);\n if (Array.isArray(details.causeMessages)) {\n for (const causeMessage of details.causeMessages) {\n pushMessage(signals, causeMessage);\n }\n }\n }\n\n const context = node.context as { originalError?: unknown } | undefined;\n const next = [\n node.cause,\n node.originalError,\n context && typeof context === 'object'\n ? context.originalError\n : undefined,\n ];\n for (const child of next) {\n if (child !== null && child !== undefined) {\n pending.push({ value: child, depth: depth + 1 });\n }\n }\n if (Array.isArray(node.errors)) {\n for (const child of node.errors) {\n pending.push({ value: child, depth: depth + 1 });\n }\n }\n }\n\n return signals;\n}\n\n/**\n * Classifies a raw dialect message. Exposed so\n * `SmrtObject.classifyConstraintError()` and this module share one set of\n * regexes rather than drifting apart.\n */\nexport function classifyDialectMessage(message: string): DatabaseErrorKind {\n if (!message) return 'unknown';\n for (const rule of DIALECT_MESSAGE_RULES) {\n if (rule.pattern.test(message)) return rule.kind;\n }\n return 'unknown';\n}\n\nfunction isSqlState(code: string): boolean {\n return /^[0-9A-Z]{5}$/.test(code);\n}\n\n/**\n * Classifies a database failure by walking its causal chain.\n *\n * Codes win over wording: a PostgreSQL SQLSTATE or SQLite result code is\n * authoritative, and dialect message matching only runs when no code in the\n * chain was recognized (DuckDB, or an error already flattened to a string).\n *\n * Constraint violations are reported ahead of `aborted_transaction` so a chain\n * carrying both surfaces the root cause rather than the downstream symptom;\n * both are deterministic, so retry behavior is identical either way.\n *\n * @param error - Any thrown value. Non-database errors classify as `'unknown'`.\n * @returns The structured classification; never throws.\n */\nexport function classifyDatabaseError(\n error: unknown,\n): DatabaseErrorClassification {\n const signals = collectChainSignals(error);\n const sqlstate = signals.codes.find(\n (code) =>\n isSqlState(code) &&\n (DETERMINISTIC_SQLSTATES.has(code) || TRANSIENT_SQLSTATES.has(code)),\n );\n\n const base = {\n driverCodes: signals.codes,\n driverMessages: signals.messages,\n ...(sqlstate ? { sqlstate } : {}),\n ...(signals.columns[0] ? { column: signals.columns[0] } : {}),\n ...(signals.constraints[0] ? { constraint: signals.constraints[0] } : {}),\n };\n\n // Deterministic codes first: a chain that carries both a constraint code and\n // a downstream contention code must fail fast, not retry.\n const deterministicCandidates: Array<{\n code: string;\n kind: DatabaseErrorKind;\n }> = [];\n for (const code of signals.codes) {\n const kind =\n DETERMINISTIC_SQLSTATES.get(code) ??\n DETERMINISTIC_SQLITE_CODES.get(code) ??\n // SMRT's own typed codes: a `ValidationError` already carrying\n // `VALIDATION_UNIQUE_CONSTRAINT` is the classified form of the same\n // driver failure.\n SMRT_CODE_KINDS.get(code) ??\n // A bare `SQLITE_CONSTRAINT` carries no sub-kind; recover it from the\n // message, which SQLite always phrases explicitly.\n (code === 'SQLITE_CONSTRAINT'\n ? (signals.messages\n .map(classifyDialectMessage)\n .find((candidate) => DETERMINISTIC_KIND_RANK.has(candidate)) ??\n 'check_violation')\n : undefined);\n if (kind) deterministicCandidates.push({ code, kind });\n }\n\n if (deterministicCandidates.length > 0) {\n // Rank rather than take the first discovered: an error whose chain holds\n // both `23505` and the `25P02` it caused must report the root constraint,\n // not the downstream symptom.\n const best = deterministicCandidates.reduce((winner, candidate) =>\n rankKind(candidate.kind) < rankKind(winner.kind) ? candidate : winner,\n );\n return {\n ...base,\n kind: best.kind,\n deterministic: true,\n retryable: false,\n driverCode: best.code,\n };\n }\n\n for (const code of signals.codes) {\n if (TRANSIENT_SQLSTATES.has(code) || TRANSIENT_SQLITE_CODES.has(code)) {\n return {\n ...base,\n kind: 'transient',\n deterministic: false,\n retryable: true,\n driverCode: code,\n };\n }\n if (TRANSIENT_NETWORK_CODES.has(code)) {\n return {\n ...base,\n kind: 'transient',\n deterministic: false,\n retryable: true,\n driverCode: code,\n };\n }\n }\n\n // No recognized code anywhere — fall back to dialect wording. This is the\n // only path DuckDB ever takes.\n const messageKinds = signals.messages\n .map(classifyDialectMessage)\n .filter((kind) => kind !== 'unknown');\n const deterministicFromMessage = messageKinds\n .filter((kind) => DETERMINISTIC_KIND_RANK.has(kind))\n .sort((left, right) => rankKind(left) - rankKind(right))[0];\n if (deterministicFromMessage) {\n return {\n ...base,\n kind: deterministicFromMessage,\n deterministic: true,\n retryable: false,\n };\n }\n if (messageKinds.includes('transient')) {\n return {\n ...base,\n kind: 'transient',\n deterministic: false,\n retryable: true,\n };\n }\n\n return {\n ...base,\n kind: 'unknown',\n deterministic: false,\n retryable: false,\n };\n}\n\n/**\n * True when retrying the statement cannot change the outcome.\n *\n * Used by {@link ErrorUtils.withRetry} to fail fast instead of burning the\n * full 500 + 1000 + 2000 ms backoff on a constraint violation.\n */\nexport function isDeterministicDatabaseError(error: unknown): boolean {\n return classifyDatabaseError(error).deterministic;\n}\n\n/** True when the failure is contention or availability and may clear. */\nexport function isTransientDatabaseError(error: unknown): boolean {\n return classifyDatabaseError(error).retryable;\n}\n\n/**\n * Codes that mean the connection itself failed or the server would not take\n * it yet: SQLSTATE class `08`, server shutdown/startup `57P01`–`57P03`,\n * `53300 too_many_connections`, and the socket codes a server that is still\n * starting or settling produces. Deliberately narrower than\n * `TRANSIENT_NETWORK_CODES`: an unresolvable or unreachable host\n * (`ENOTFOUND`, `EHOSTUNREACH`, `ENETUNREACH`) is a configuration error and\n * must fail at once (#3657).\n */\nconst CONNECTION_SQLSTATES = new Set(['53300', '57P01', '57P02', '57P03']);\nconst CONNECTION_SOCKET_CODES = new Set([\n 'ECONNREFUSED',\n 'ECONNRESET',\n 'ETIMEDOUT',\n 'EAI_AGAIN',\n]);\nconst CONNECTION_MESSAGE =\n /connection terminated|server closed the connection|too many clients|the database system is (?:starting up|shutting down|in recovery mode)/i;\n\n/**\n * True when the failure is a transient *connection* failure: the database\n * dropped, refused or could not yet accept the connection (#3657). Narrower\n * than {@link isTransientDatabaseError}, which also covers contention\n * (deadlocks, serialization, locks). Used to retry startup bootstrap only.\n */\nexport function isTransientConnectionError(error: unknown): boolean {\n const classification = classifyDatabaseError(error);\n if (classification.deterministic) return false;\n return (\n classification.driverCodes.some(\n (code) =>\n CONNECTION_SQLSTATES.has(code) ||\n CONNECTION_SOCKET_CODES.has(code) ||\n /^08[0-9A-Z]{3}$/.test(code),\n ) ||\n classification.driverMessages.some((message) =>\n CONNECTION_MESSAGE.test(message),\n )\n );\n}\n\n/** True when a unique or primary-key constraint was violated. */\nexport function isUniqueViolationError(error: unknown): boolean {\n return classifyDatabaseError(error).kind === 'unique_violation';\n}\n\n/** True when a NOT NULL constraint was violated. */\nexport function isNotNullViolationError(error: unknown): boolean {\n return classifyDatabaseError(error).kind === 'not_null_violation';\n}\n\n/**\n * True when the failure came from running inside an already-aborted\n * PostgreSQL transaction (SQLSTATE `25P02`).\n *\n * Detected independently of {@link DatabaseErrorKind} so a chain that also\n * carries the root constraint code still answers `true` here.\n */\nexport function isAbortedTransactionError(error: unknown): boolean {\n const signals = collectChainSignals(error);\n // `25P02` only. `25P03` is a terminated session, not a failed transaction,\n // and reporting it here would make the profiles coordinator treat an\n // idle-timeout disconnect as a provisioning race conflict.\n if (signals.codes.includes('25P02')) {\n return true;\n }\n return signals.messages.some((message) =>\n /current transaction is aborted/i.test(message),\n );\n}\n"],"mappings":"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"}
|
package/dist/dispatch/bus.d.ts
CHANGED
|
@@ -329,6 +329,9 @@ export declare class DispatchBus {
|
|
|
329
329
|
/**
|
|
330
330
|
* Create a DispatchBus instance
|
|
331
331
|
*
|
|
332
|
+
* Opening the database and `initialize()` are retried on a transient
|
|
333
|
+
* connection failure; see {@link DispatchBusOptions.startupRetry}.
|
|
334
|
+
*
|
|
332
335
|
* @param options - Bus configuration
|
|
333
336
|
* @returns Initialized DispatchBus
|
|
334
337
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../../src/dispatch/bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../../src/dispatch/bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAkB5D,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAKxE,OAAO,KAAK,EACV,kBAAkB,EAElB,sBAAsB,EACtB,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EACf,mBAAmB,EAEnB,sBAAsB,EACtB,oBAAoB,EACpB,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAapB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,QAAQ,CAA+C;IAC/D,OAAO,CAAC,WAAW,CAAkB;IAErC;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAiB;IAE5D,wEAAwE;IACxE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAO;IAEhD;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IAa7B;;OAEG;gBACS,EAAE,EAAE,iBAAiB;IAIjC;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAkFjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoDG;IACG,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,QAAQ,CAAC;IA0EpB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI;IAenD;;;;;;OAMG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO;IAcvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAsCG;IACG,SAAS,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmCjE;;;;;OAKG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACG,OAAO,CACX,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,CAAC;IAyIlB;;;;;;;OAOG;IACG,KAAK,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,MAAM,CAAC;IAqBhE;;;;;OAKG;IACG,OAAO,CACX,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,qBAAqB,CAAC;IAYjC;;;;;;;;;;;;;;;;;;OAkBG;IACG,IAAI,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAgBlE;;;;;;;;OAQG;IACG,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAU/C;;OAEG;IACG,iBAAiB,CACrB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAkBlC;;OAEG;IACH,OAAO,CAAC,cAAc;CAqCvB;AAiCD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,WAAW,CAAC,CAkDtB"}
|
package/dist/dispatch/bus.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isTransientConnectionError } from "../db-errors.js";
|
|
1
2
|
import { getDatabase } from "../host.js";
|
|
2
3
|
import { resolveDispatchTenantId, resolveDispatchTenantScope } from "./tenant-resolver.js";
|
|
3
4
|
import { CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE, CREATE_SMRT_DISPATCH_TABLE, getSystemTableDDLForEngine } from "../system/schema.js";
|
|
@@ -513,8 +514,30 @@ var DispatchBus = class DispatchBus {
|
|
|
513
514
|
}
|
|
514
515
|
};
|
|
515
516
|
/**
|
|
517
|
+
* Run one startup step, retrying it with exponential backoff while it fails
|
|
518
|
+
* with a transient connection error (#3657). Anything else, or the last
|
|
519
|
+
* attempt's failure, is thrown as is.
|
|
520
|
+
*/
|
|
521
|
+
async function withStartupRetry(step, retry) {
|
|
522
|
+
const attempts = retry === false ? 1 : Math.max(1, Math.floor(retry?.attempts ?? 5));
|
|
523
|
+
const maxDelayMs = retry ? retry.maxDelayMs ?? 5e3 : 5e3;
|
|
524
|
+
let delayMs = retry ? retry.initialDelayMs ?? 500 : 500;
|
|
525
|
+
for (let attempt = 1;; attempt++) try {
|
|
526
|
+
return await step();
|
|
527
|
+
} catch (error) {
|
|
528
|
+
if (attempt >= attempts || !isTransientConnectionError(error)) throw error;
|
|
529
|
+
const wait = Math.min(delayMs, maxDelayMs);
|
|
530
|
+
logger.warn(`DispatchBus: transient connection failure during startup (attempt ${attempt} of ${attempts}); retrying in ${wait} ms`, { error: error instanceof Error ? error.message : String(error) });
|
|
531
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
532
|
+
delayMs *= 2;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
516
536
|
* Create a DispatchBus instance
|
|
517
537
|
*
|
|
538
|
+
* Opening the database and `initialize()` are retried on a transient
|
|
539
|
+
* connection failure; see {@link DispatchBusOptions.startupRetry}.
|
|
540
|
+
*
|
|
518
541
|
* @param options - Bus configuration
|
|
519
542
|
* @returns Initialized DispatchBus
|
|
520
543
|
*/
|
|
@@ -522,15 +545,15 @@ async function createDispatchBus(options = {}) {
|
|
|
522
545
|
const dbConfig = options.db || options.persistence;
|
|
523
546
|
if (!dbConfig) throw new Error("DispatchBus requires a database configuration");
|
|
524
547
|
let db;
|
|
525
|
-
if (typeof dbConfig === "string") db = await getDatabase(applyPostgresRuntimeTimeouts({ url: dbConfig }));
|
|
548
|
+
if (typeof dbConfig === "string") db = await withStartupRetry(() => getDatabase(applyPostgresRuntimeTimeouts({ url: dbConfig })), options.startupRetry);
|
|
526
549
|
else if ("query" in dbConfig) db = dbConfig;
|
|
527
|
-
else if ("type" in dbConfig && "url" in dbConfig) db = await getDatabase(applyPostgresRuntimeTimeouts({
|
|
550
|
+
else if ("type" in dbConfig && "url" in dbConfig) db = await withStartupRetry(() => getDatabase(applyPostgresRuntimeTimeouts({
|
|
528
551
|
type: dbConfig.type,
|
|
529
552
|
url: dbConfig.url
|
|
530
|
-
}));
|
|
553
|
+
})), options.startupRetry);
|
|
531
554
|
else throw new Error("Invalid database configuration for DispatchBus");
|
|
532
555
|
const bus = new DispatchBus(db);
|
|
533
|
-
await bus.initialize();
|
|
556
|
+
await withStartupRetry(() => bus.initialize(), options.startupRetry);
|
|
534
557
|
return bus;
|
|
535
558
|
}
|
|
536
559
|
//#endregion
|
package/dist/dispatch/bus.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bus.js","names":[],"sources":["../../src/dispatch/bus.ts"],"sourcesContent":["/**\n * DispatchBus - Central hub for inter-agent communication\n *\n * The DispatchBus provides both in-memory handlers and persistent subscriptions\n * for asynchronous agent-to-agent messaging.\n *\n * @example\n * ```typescript\n * const bus = await createDispatchBus({ db: { type: 'sqlite', url: 'app.db' } });\n *\n * // In-memory handler (immediate)\n * bus.on('campaign.completed', async (payload, metadata) => {\n * console.log(`Campaign ${payload.campaignId} completed`);\n * });\n *\n * // Persistent subscription (processed later)\n * await bus.subscribe({ signalType: 'campaign.*', subscriber: 'fiscus' });\n *\n * // Emit a dispatch\n * await bus.emit('campaign.completed', { campaignId: '123' }, { source: 'suasor' });\n *\n * // Process pending dispatches\n * await bus.process('fiscus');\n * ```\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { getDatabase } from '../host.js';\nimport { applyPostgresRuntimeTimeouts } from '../postgres-timeouts.js';\nimport { runSerializedAgainstSystemTableBootstrap } from '../system/bootstrap.js';\nimport {\n assertPostgresSystemTimestampsCurrent,\n ensureDispatchSubscriptionsSystemTableCompatibility,\n ensureDispatchSystemTableCompatibility,\n getDatabaseEngine,\n} from '../system/compatibility.js';\nimport {\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n CREATE_SMRT_DISPATCH_TABLE,\n getSystemTableDDLForEngine,\n} from '../system/schema.js';\nimport { DispatchCollection } from './collections/Dispatches.js';\nimport { DispatchSubscriptionCollection } from './collections/DispatchSubscriptions.js';\nimport { Dispatch } from './models/Dispatch.js';\nimport { DispatchSubscription } from './models/DispatchSubscription.js';\nimport {\n resolveDispatchTenantId,\n resolveDispatchTenantScope,\n} from './tenant-resolver.js';\nimport type {\n DispatchBusOptions,\n DispatchCleanupOptions,\n DispatchCleanupResult,\n DispatchEmitOptions,\n DispatchHandler,\n DispatchListOptions,\n DispatchMetadata,\n DispatchProcessOptions,\n DispatchRetryOptions,\n DispatchSubscribeOptions,\n} from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Registered in-memory handler\n */\ninterface RegisteredHandler {\n pattern: string;\n handler: DispatchHandler;\n subscription: DispatchSubscription;\n}\n\n/**\n * Central hub for inter-agent messaging with both in-memory and persistent delivery.\n *\n * The `DispatchBus` combines two complementary delivery models:\n *\n * - **In-memory handlers** (`on(pattern, handler)`) — called synchronously (fire-and-forget)\n * when a dispatch is emitted. Fast, but lost on process restart.\n * - **Persistent subscriptions** (`subscribe({ signalType, subscriber })`) — stored in\n * `_smrt_dispatch_subscriptions` and processed later by calling `process(subscriber, handler)`.\n * Survive restarts; suitable for background workers and scheduled agents.\n *\n * Signal types support single-segment wildcards: `'campaign.*'` matches `'campaign.completed'`\n * and `'campaign.failed'`, but not `'campaign.phase.two'`.\n *\n * Dispatch lifecycle: `pending → processing → completed` (or `failed` on handler error).\n * Use `retry()` to reset failed dispatches back to `pending`.\n *\n * Create instances via `createDispatchBus()` — do not use `new DispatchBus()` directly\n * in application code.\n *\n * @example\n * ```typescript\n * const bus = await createDispatchBus({ db: myDb });\n *\n * // In-memory: immediate, fire-and-forget\n * bus.on('invoice.paid', async (payload) => {\n * console.log('Invoice paid:', payload.invoiceId);\n * });\n *\n * // Persistent: processed by 'notifications' agent on next run\n * await bus.subscribe({ signalType: 'invoice.*', subscriber: 'notifications' });\n *\n * // Emit (notifies in-memory handlers immediately, stores persistent dispatch)\n * await bus.emit('invoice.paid', { invoiceId: 'inv-001' }, { source: 'billing' });\n *\n * // Later, in the notifications agent:\n * await bus.process('notifications', async (payload) => {\n * await sendEmail(payload.invoiceId);\n * });\n * ```\n */\nexport class DispatchBus {\n private db: DatabaseInterface;\n private handlers: Map<string, RegisteredHandler[]> = new Map();\n private initialized: boolean = false;\n\n /**\n * Reserved subscriber name used internally for in-memory `on()` handlers.\n * Callers may not register persistent subscriptions under this name, nor\n * assert it as an emit `source`, so they cannot impersonate the in-memory\n * pseudo-subscriber (S5 #1398).\n */\n private static readonly RESERVED_SUBSCRIBER = '_in_memory_';\n\n /** Maximum stored length of a caller-asserted dispatch source label. */\n private static readonly MAX_SOURCE_LENGTH = 256;\n\n /**\n * Normalize a caller-asserted `source` into untrusted metadata.\n *\n * `source` is a declared label, not an authenticated identity, so it must not\n * be trusted for authorization. This caps its length and **rejects** the\n * reserved internal sentinel (`_in_memory_`) by throwing — the contract is\n * that the sentinel is not an accepted source, so a caller cannot quietly\n * impersonate the in-memory subscriber pseudo-source (S5 #1398). Empty/missing\n * values default to `'unknown'` (unchanged behavior).\n *\n * @throws Error if `source` is the reserved in-memory sentinel.\n */\n private static sanitizeSource(source: string | undefined): string {\n const raw = (source ?? '').trim();\n if (!raw) {\n return 'unknown';\n }\n if (raw === DispatchBus.RESERVED_SUBSCRIBER) {\n throw new Error(\n `DispatchBus.emit: \"${DispatchBus.RESERVED_SUBSCRIBER}\" is a reserved source and cannot be asserted`,\n );\n }\n return raw.slice(0, DispatchBus.MAX_SOURCE_LENGTH);\n }\n\n /**\n * Create a new DispatchBus (use createDispatchBus factory instead)\n */\n constructor(db: DatabaseInterface) {\n this.db = db;\n }\n\n /**\n * Initialize the dispatch tables\n */\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n // `_smrt_dispatch`/`_smrt_dispatch_subscriptions` are framework-owned\n // system tables (part of `ALL_SYSTEM_TABLES`), but this standalone\n // initializer provisions/verifies them independently of a full\n // `ensureSystemTables()` bootstrap. Without serialization, concurrent\n // `DispatchBus` instances against the same already-warmed PostgreSQL\n // database (e.g. multiple `Suasor`/`Agent` instantiations, or concurrent\n // test files sharing one database) race on unguarded DDL — proven to\n // throw `duplicate key value violates unique constraint\n // \"pg_type_typname_nsp_index\"` on concurrent `CREATE TABLE IF NOT\n // EXISTS` (#2861). Fresh DDL always creates timezone-aware columns (see\n // `getSystemTableDDLForEngine()`), so this race does not itself produce a\n // legacy-typed column; a stale `assertPostgresSystemTimestampsCurrent()`\n // guard requires a *separate* concurrent actor to have put some\n // `_smrt_*` table into a legacy-typed state (e.g. a test deliberately\n // simulating a pre-migration database), which this lock does not by\n // itself rule out — see the issue for that residual risk. Running this\n // under the same advisory lock `ensureSystemTables()` uses makes every\n // writer of these tables mutually exclusive instead of merely\n // idempotent, which is what the proven race requires.\n //\n // Resolve the engine from `this.db` — the caller-supplied handle — once,\n // up front. `runSerializedAgainstSystemTableBootstrap()` hands the\n // callback a *different* handle on PostgreSQL (the lock-holding\n // transaction it opens), and that handle is not guaranteed to carry the\n // `url`/`type` (or `config.url`/`config.type`) fields engine detection\n // reads (see the `db.config?.url`-style adapters `getDatabaseEngine()`\n // already supports elsewhere in this package). Recomputing the engine\n // on the transaction handle can therefore silently disagree with the\n // outer gate that just decided `postgres` — `detectEngine('', undefined)`\n // falls back to `sqlite`, which would create the dispatch tables with\n // `TIMESTAMP` instead of `TIMESTAMPTZ` and skip the PostgreSQL timestamp\n // guard entirely. Threading the one resolved `engine` through — as\n // `ensureSystemTables()`/`bootstrapSystemTables()` already thread\n // `typeHint` — keeps the inner detection bound to the outer decision.\n const engine = getDatabaseEngine(this.db);\n await runSerializedAgainstSystemTableBootstrap(\n this.db,\n engine,\n async (tx) => {\n // Create dispatch tables if they don't exist\n const dispatchExists = await DispatchCollection.tableExists(tx, engine);\n if (!dispatchExists) {\n // Split the DDL into separate statements and execute each\n const statements = getSystemTableDDLForEngine(\n CREATE_SMRT_DISPATCH_TABLE,\n engine,\n )\n .split(';')\n .filter((s) => s.trim());\n for (const stmt of statements) {\n await tx.query(stmt);\n }\n }\n await assertPostgresSystemTimestampsCurrent(tx, engine);\n await ensureDispatchSystemTableCompatibility(tx, engine);\n\n const subsExists = await DispatchSubscriptionCollection.tableExists(\n tx,\n engine,\n );\n if (!subsExists) {\n const statements = getSystemTableDDLForEngine(\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n engine,\n )\n .split(';')\n .filter((s) => s.trim());\n for (const stmt of statements) {\n await tx.query(stmt);\n }\n }\n await assertPostgresSystemTimestampsCurrent(tx, engine);\n await ensureDispatchSubscriptionsSystemTableCompatibility(tx, engine);\n },\n );\n\n this.initialized = true;\n }\n\n /**\n * Emits a dispatch message.\n *\n * Two things happen atomically (from the caller's perspective):\n * 1. One or more `Dispatch` rows are inserted with `status: 'pending'` into `_smrt_dispatch`.\n * 2. All matching in-memory handlers (registered via `on()`) are invoked\n * fire-and-forget (errors are caught and logged, not thrown back to the caller).\n *\n * Persistent subscriptions registered via `subscribe()` will see this dispatch\n * the next time `process(subscriber, handler)` is called for their subscriber name.\n *\n * **Delivery modes** affect how dispatches are created:\n * - `compete` subscribers share a single dispatch (`target_subscriber = NULL`).\n * First subscriber to `process()` claims it (at-most-once delivery).\n * - `fanout` subscribers each get their own dispatch copy (`target_subscriber` set\n * to the subscriber name), so each processes independently.\n *\n * If no subscriptions exist, one dispatch is created for future processing.\n *\n * **Security (S5 #1398):**\n * - `tenant_id` is derived **server-side** from the active tenant context and\n * is never read from caller options, so it cannot be spoofed. Subscribers in\n * a different tenant cannot see or claim this dispatch (see {@link process}).\n * When there is no active tenant context (system/global), `tenant_id` is\n * `NULL` and the dispatch is visible to all scopes — preserving pre-tenancy\n * behavior.\n * - `options.source` is **caller-asserted, untrusted metadata** — it is a\n * declared label only and must not be relied on as an authenticated emitter\n * identity. It is length-capped and the reserved internal sentinel\n * (`_in_memory_`) is rejected so a caller cannot impersonate the in-memory\n * subscriber pseudo-source.\n *\n * @param type - Signal type string, e.g. `'campaign.completed'` or `'invoice.paid'`\n * @param payload - Any JSON-serializable data to attach to the dispatch\n * @param options.source - Declared (untrusted) name of the emitting agent/component (default `'unknown'`)\n * @param options.sourceId - Optional ID of the specific emitting entity\n * @param options.metadata - Optional additional JSON metadata for the dispatch record\n * @returns A persisted `Dispatch` instance (the compete dispatch, or the first fanout copy if fanout-only)\n *\n * @example\n * ```typescript\n * const dispatch = await bus.emit(\n * 'campaign.completed',\n * { campaignId: 'cmp-001', impressions: 10_000 },\n * { source: 'suasor', sourceId: agentId },\n * );\n * console.log(dispatch.id); // UUID of the created dispatch record\n * ```\n *\n * @see {@link on} for in-memory handlers\n * @see {@link subscribe} for persistent subscriptions\n * @see {@link process} to consume pending dispatches\n */\n async emit(\n type: string,\n payload: unknown,\n options: DispatchEmitOptions = {},\n ): Promise<Dispatch> {\n await this.initialize();\n\n // Derive the tenant scope server-side from the active context. This is the\n // trust anchor for cross-tenant isolation (S5 #1398) — it is never taken\n // from caller options, so it cannot be spoofed. `undefined`/`null` means\n // \"no tenant context\" → tenant_id stays NULL (global dispatch).\n const tenantId = resolveDispatchTenantId() ?? null;\n\n // Treat the caller-asserted source as untrusted metadata.\n const source = DispatchBus.sanitizeSource(options.source);\n\n // Query subscriptions to determine if fan-out copies are needed.\n // This runs on every emit() — uses SQL-level exact matching with\n // in-memory fallback only for wildcard subscriptions to minimize I/O.\n // Scoped to the active tenant so emit only fans out to this tenant's\n // (and, for global emits, global) subscriptions (S5 #1398).\n const matchingSubs = await DispatchSubscriptionCollection.findBySignalType(\n this.db,\n type,\n resolveDispatchTenantScope(),\n );\n\n // Separate into compete and fanout subscribers\n const fanoutSubs = matchingSubs.filter((s) => s.delivery === 'fanout');\n const competeSubs = matchingSubs.filter((s) => s.delivery !== 'fanout');\n\n // Create the base dispatch record\n const dispatch = new Dispatch({\n type,\n source,\n source_id: options.sourceId || null,\n payload: JSON.stringify(payload || {}),\n metadata: JSON.stringify(options.metadata || {}),\n status: 'pending',\n correlation_id: options.correlationId || null,\n tenant_id: tenantId,\n });\n\n // Track first persisted dispatch to return to caller\n let returnDispatch = dispatch;\n\n // For fanout subscribers: create a per-subscriber dispatch copy\n for (const sub of fanoutSubs) {\n const fanoutDispatch = new Dispatch({\n type,\n source,\n source_id: options.sourceId || null,\n payload: JSON.stringify(payload || {}),\n metadata: JSON.stringify(options.metadata || {}),\n status: 'pending',\n target_subscriber: sub.subscriber,\n correlation_id: options.correlationId || null,\n tenant_id: tenantId,\n });\n await DispatchCollection.insert(this.db, fanoutDispatch);\n // If no compete dispatch will be persisted, return the first fanout copy\n if (competeSubs.length === 0 && matchingSubs.length > 0) {\n returnDispatch = fanoutDispatch;\n }\n }\n\n // For compete subscribers (or if no subscriptions found): insert the original\n // dispatch with target_subscriber = NULL so any compete subscriber can claim it\n if (competeSubs.length > 0 || matchingSubs.length === 0) {\n await DispatchCollection.insert(this.db, dispatch);\n }\n\n // Notify in-memory handlers immediately (fire-and-forget)\n this.notifyHandlers(type, payload, dispatch.getMetadata());\n\n return returnDispatch;\n }\n\n /**\n * Registers an in-memory handler for a signal type pattern.\n *\n * In-memory handlers are called immediately (fire-and-forget) during `emit()`.\n * They are stored only in memory and lost on process restart — use `subscribe()`\n * for durable, restart-safe subscriptions.\n *\n * Pattern matching supports a single-segment wildcard `*`:\n * - `'campaign.*'` matches `'campaign.completed'` and `'campaign.failed'`\n * - `'campaign.*'` does **not** match `'campaign.phase.two'`\n *\n * Multiple handlers may be registered for the same pattern — all will be called.\n *\n * @param pattern - Signal type pattern, optionally with a trailing `.*` wildcard\n * @param handler - Async or sync function to call with `(payload, metadata)`\n *\n * @example\n * ```typescript\n * bus.on('invoice.*', async (payload, metadata) => {\n * console.log(`Invoice event from ${metadata.source}:`, payload);\n * });\n * ```\n *\n * @see {@link off} to remove a handler\n * @see {@link subscribe} for persistent (restart-safe) subscriptions\n */\n on(pattern: string, handler: DispatchHandler): void {\n const subscription = new DispatchSubscription({\n signal_type: pattern,\n subscriber: '_in_memory_',\n handler: 'callback',\n });\n\n const registered: RegisteredHandler = { pattern, handler, subscription };\n\n if (!this.handlers.has(pattern)) {\n this.handlers.set(pattern, []);\n }\n this.handlers.get(pattern)?.push(registered);\n }\n\n /**\n * Remove an in-memory handler\n *\n * @param pattern - Signal type pattern\n * @param handler - Handler function to remove\n * @returns True if handler was found and removed\n */\n off(pattern: string, handler: DispatchHandler): boolean {\n const handlers = this.handlers.get(pattern);\n if (!handlers) return false;\n\n const index = handlers.findIndex((h) => h.handler === handler);\n if (index === -1) return false;\n\n handlers.splice(index, 1);\n if (handlers.length === 0) {\n this.handlers.delete(pattern);\n }\n return true;\n }\n\n /**\n * Creates or updates a persistent subscription in the database.\n *\n * Persistent subscriptions survive process restarts. When `process(subscriber, handler)`\n * is called later, all pending dispatches matching this subscriber's signal patterns\n * will be delivered.\n *\n * Calling `subscribe()` with the same `signalType`/`subscriber` pair is idempotent\n * (upsert) — it is safe to call on every agent startup.\n *\n * Set `delivery: 'fanout'` to give each subscriber their own dispatch copy.\n * Default is `'compete'` (at-most-once, first subscriber to process claims it).\n *\n * @param options.signalType - Signal type pattern to subscribe to (wildcards supported)\n * @param options.subscriber - Name that identifies this subscriber (e.g. agent class name)\n * @param options.handler - Optional method name on the subscriber to call (default `'handleDispatch'`)\n * @param options.delivery - `'compete'` (default) for at-most-once, `'fanout'` for per-subscriber copies\n * @param options.enabled - If `false`, subscription is created but disabled (default `true`)\n *\n * @example\n * ```typescript\n * // Subscribe on agent startup (idempotent)\n * await bus.subscribe({ signalType: 'campaign.*', subscriber: 'FiscusAgent' });\n *\n * // Fan-out: each subscriber gets their own dispatch copy\n * await bus.subscribe({\n * signalType: 'campaign.*',\n * subscriber: 'AuditorAgent',\n * delivery: 'fanout',\n * });\n *\n * // Later, process matching pending dispatches\n * await bus.process('FiscusAgent', async (payload, metadata) => { ... });\n * ```\n *\n * @see {@link process} to consume pending dispatches for this subscriber\n * @see {@link unsubscribe} to remove the subscription\n * @see {@link on} for non-persistent in-memory handling\n */\n async subscribe(options: DispatchSubscribeOptions): Promise<void> {\n await this.initialize();\n\n // Gate the subscriber namespace (S5 #1398): require a concrete subscriber\n // name and reject the reserved internal sentinel so callers cannot\n // register a persistent subscription that masquerades as the in-memory\n // pseudo-subscriber.\n const subscriber = (options.subscriber ?? '').trim();\n if (!subscriber) {\n throw new Error('DispatchBus.subscribe requires a non-empty subscriber');\n }\n if (subscriber === DispatchBus.RESERVED_SUBSCRIBER) {\n throw new Error(\n `DispatchBus.subscribe: \"${DispatchBus.RESERVED_SUBSCRIBER}\" is a reserved subscriber name`,\n );\n }\n const signalType = (options.signalType ?? '').trim();\n if (!signalType) {\n throw new Error('DispatchBus.subscribe requires a non-empty signalType');\n }\n\n const subscription = new DispatchSubscription({\n signal_type: signalType,\n subscriber,\n handler: options.handler || 'handleDispatch',\n delivery: options.delivery || 'compete',\n enabled: options.enabled !== false ? 1 : 0,\n // Server-derived tenant scope (never from caller options). NULL when there\n // is no active tenant context (global subscription).\n tenant_id: resolveDispatchTenantId() ?? null,\n });\n\n await DispatchSubscriptionCollection.upsert(this.db, subscription);\n }\n\n /**\n * Remove a persistent subscription\n *\n * @param signalType - Signal type pattern\n * @param subscriber - Subscriber name\n */\n async unsubscribe(signalType: string, subscriber: string): Promise<void> {\n await this.initialize();\n // Scoped to the active tenant so one tenant cannot remove another's\n // subscription (S5 #1398).\n await DispatchSubscriptionCollection.deleteByKey(\n this.db,\n signalType,\n subscriber,\n resolveDispatchTenantScope(),\n );\n }\n\n /**\n * Processes pending dispatches for a named subscriber.\n *\n * For each matching `pending` dispatch:\n * 1. Sets status to `processing`\n * 2. Calls `handler(payload, metadata)`\n * 3. On success: sets status to `completed`\n * 4. On error: sets status to `failed` (stores the error message)\n *\n * Uses a wildcard-aware query strategy: subscriptions with `*` patterns fetch\n * all pending dispatches and filter in memory; exact-match subscriptions use\n * a direct SQL `IN` query for efficiency.\n *\n * @param subscriber - The subscriber name (must match a `subscribe()` call)\n * @param handler - Function to call for each pending dispatch\n * @param options.limit - Maximum dispatches to process in one call (default 100)\n * @param options.signalTypes - Optional filter to process only specific signal types\n * @returns Number of dispatches successfully processed (excludes failed)\n *\n * @example\n * ```typescript\n * const count = await bus.process('FiscusAgent', async (payload, metadata) => {\n * if (metadata.signalType === 'campaign.completed') {\n * await generateInvoice(payload.campaignId);\n * }\n * });\n * console.log(`Processed ${count} dispatches`);\n * ```\n *\n * @see {@link subscribe} to register the persistent subscription first\n * @see {@link retry} to reset failed dispatches back to pending\n */\n async process(\n subscriber: string,\n handler: DispatchHandler,\n options: DispatchProcessOptions = {},\n ): Promise<number> {\n await this.initialize();\n\n // Resolve the active tenant scope server-side (S5 #1398). This distinguishes\n // three states (see resolveDispatchTenantScope):\n // - tenancy off → no filter; system/global + non-tenant deployments behave\n // exactly as before.\n // - active tenant T → only T's dispatches plus global (NULL) dispatches are\n // claimable, so a subscriber in tenant A cannot see/claim tenant B's.\n // - tenancy on but no active tenant → fail-closed to global (NULL) rows\n // only; a missing context never leaks other tenants' dispatches.\n const tenantScope = resolveDispatchTenantScope();\n\n // Get subscriber's subscriptions, scoped to the active tenant so a tenant-A\n // processor only matches tenant-A (and, when global, global) subscriptions.\n const subscriptions = await DispatchSubscriptionCollection.findBySubscriber(\n this.db,\n subscriber,\n true,\n tenantScope,\n );\n\n if (subscriptions.length === 0) {\n return 0;\n }\n\n // Get signal types (including wildcards)\n const signalTypes = subscriptions.map((s) => s.signalType);\n\n // For non-wildcard subscriptions, we can query directly\n // For wildcards, we need to get all pending and filter\n const hasWildcards = signalTypes.some((t) => t.includes('*'));\n\n // Helper: check if a dispatch should be visible to this subscriber\n const isVisibleToSubscriber = (dispatch: Dispatch): boolean => {\n // Tenant isolation (defense in depth — the SQL queries below also filter).\n if (tenantScope.enforced) {\n if (tenantScope.tenantId === null) {\n // Tenancy on, no active tenant → only global (NULL) rows are visible.\n if (dispatch.tenantId !== null) {\n return false;\n }\n } else if (\n dispatch.tenantId !== null &&\n dispatch.tenantId !== tenantScope.tenantId\n ) {\n // Active tenant → that tenant's rows plus global (NULL) rows only.\n return false;\n }\n }\n if (dispatch.targetSubscriber === subscriber) {\n // Targeted at this subscriber (fanout copy) — always visible\n return true;\n }\n if (dispatch.targetSubscriber !== null) {\n // Targeted at a different subscriber — not visible\n return false;\n }\n // Null target (compete dispatch): only visible if this subscriber\n // has a compete subscription matching this dispatch type\n const matchingSub = subscriptions.find((sub) =>\n sub.matches(dispatch.type),\n );\n return matchingSub?.delivery === 'compete';\n };\n\n let pendingDispatches: Dispatch[];\n if (hasWildcards) {\n // Get all pending dispatches and filter by subscription patterns + target\n const allPending = await DispatchCollection.list(this.db, {\n status: 'pending',\n limit: options.limit || 100,\n tenantScope,\n });\n\n pendingDispatches = allPending.filter(\n (dispatch) =>\n subscriptions.some((sub) => sub.matches(dispatch.type)) &&\n isVisibleToSubscriber(dispatch),\n );\n } else {\n // Direct query for exact signal types (with target_subscriber filter)\n const raw = await DispatchCollection.findPending(\n this.db,\n signalTypes,\n options.limit || 100,\n subscriber,\n tenantScope,\n );\n // Post-filter for delivery mode correctness\n pendingDispatches = raw.filter(isVisibleToSubscriber);\n }\n\n // Filter by specific signal types if provided\n if (options.signalTypes && options.signalTypes.length > 0) {\n pendingDispatches = pendingDispatches.filter((d) =>\n options.signalTypes?.includes(d.type),\n );\n }\n\n let processed = 0;\n\n for (const dispatch of pendingDispatches) {\n // Atomically claim the dispatch (S5 #1398). The conditional UPDATE\n // (WHERE id = ? AND status = 'pending' [AND tenant predicate]) closes the\n // TOCTOU window: if a competing worker — or a system-scoped processor —\n // already moved this row out of 'pending', our claim affects no rows and\n // we skip it, so a compete/global dispatch is processed at most once.\n const claimed = await DispatchCollection.claim(\n this.db,\n dispatch,\n tenantScope,\n );\n if (!claimed) {\n continue;\n }\n\n try {\n // Invoke handler\n await handler(dispatch.payload, dispatch.getMetadata());\n\n // Mark as completed\n dispatch.markCompleted(subscriber);\n await DispatchCollection.update(this.db, dispatch);\n processed++;\n } catch (error) {\n // Mark as failed\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n dispatch.markFailed(errorMessage);\n await DispatchCollection.update(this.db, dispatch);\n }\n }\n\n return processed;\n }\n\n /**\n * Retry failed dispatches\n *\n * Resets failed dispatches to pending status so they can be processed again.\n *\n * @param options - Retry options\n * @returns Number of dispatches reset\n */\n async retry(options: DispatchRetryOptions = {}): Promise<number> {\n await this.initialize();\n\n // Tenant isolation (S5 #1398): only the active tenant's failed dispatches\n // (plus global ones) are eligible — a tenant's retry() must not reset\n // another tenant's rows. Scope is derived server-side; callers cannot widen\n // it.\n const retryable = await DispatchCollection.findRetryable(\n this.db,\n options,\n resolveDispatchTenantScope(),\n );\n\n for (const dispatch of retryable) {\n dispatch.resetForRetry();\n await DispatchCollection.update(this.db, dispatch);\n }\n\n return retryable.length;\n }\n\n /**\n * Clean up old dispatches\n *\n * @param options - Cleanup options\n * @returns Number of dispatches deleted\n */\n async cleanup(\n options: DispatchCleanupOptions = {},\n ): Promise<DispatchCleanupResult> {\n await this.initialize();\n // Tenant isolation (S5 #1398): only the active tenant's dispatches (plus\n // global ones) are deleted — a tenant's cleanup() must not delete another\n // tenant's rows. Scope is derived server-side; callers cannot widen it.\n return DispatchCollection.cleanup(\n this.db,\n options,\n resolveDispatchTenantScope(),\n );\n }\n\n /**\n * Lists dispatches, scoped to the active tenant context (S5 #1398).\n *\n * The tenant scope is derived **server-side** from the active context and\n * **overrides any caller-supplied scope** — callers cannot select another\n * tenant or widen visibility to all tenants. Scoping rules:\n *\n * - tenancy off → no tenant filter (pre-tenancy behavior).\n * - active tenant T → that tenant's dispatches only. Global (NULL-tenant)\n * dispatches are system-only on reads (#3301): a global row's payload (for\n * example a delivery emitted outside tenant context) may carry another\n * tenant's recipients/subjects. Processing (`process`) still lets a tenant\n * worker claim global work.\n * - tenancy on but no active tenant → global (NULL) dispatches only\n * (fail-closed; never all tenants).\n *\n * @param options - Non-tenant list filters (status/type/source/etc.). Any\n * `tenantScope` field is ignored and replaced with the server-derived one.\n */\n async list(options: DispatchListOptions = {}): Promise<Dispatch[]> {\n await this.initialize();\n // Strip any caller-supplied scope and inject the server-derived one.\n const { tenantScope: _ignoredCallerScope, ...safeOptions } = options;\n // `includeGlobal: false` — global (NULL-tenant) rows are system-only on\n // this inspection read (#3301).\n return DispatchCollection.list(\n this.db,\n {\n ...safeOptions,\n tenantScope: resolveDispatchTenantScope(),\n },\n false,\n );\n }\n\n /**\n * Gets a dispatch by ID, enforcing the active tenant scope (S5 #1398).\n *\n * Applies the same server-derived tenant predicate as {@link list}: a\n * subscriber in tenant A cannot fetch tenant B's dispatch, or a global\n * (NULL-tenant) dispatch, by id (#3301), and when tenancy is on with no\n * active tenant only global (NULL) dispatches are returned. Returns `null`\n * when the dispatch exists but is out of scope.\n */\n async get(id: string): Promise<Dispatch | null> {\n await this.initialize();\n return DispatchCollection.getScoped(\n this.db,\n id,\n resolveDispatchTenantScope(),\n false,\n );\n }\n\n /**\n * List all subscriptions\n */\n async listSubscriptions(\n subscriber?: string,\n ): Promise<DispatchSubscription[]> {\n await this.initialize();\n\n // Scoped to the active tenant so listing never leaks another tenant's\n // subscriptions (S5 #1398).\n const tenantScope = resolveDispatchTenantScope();\n\n if (subscriber) {\n return DispatchSubscriptionCollection.findBySubscriber(\n this.db,\n subscriber,\n false,\n tenantScope,\n );\n }\n return DispatchSubscriptionCollection.list(this.db, false, tenantScope);\n }\n\n /**\n * Notify in-memory handlers (fire-and-forget)\n */\n private notifyHandlers(\n type: string,\n payload: unknown,\n metadata: DispatchMetadata,\n ): void {\n // Collect all matching handlers\n const matchingHandlers: RegisteredHandler[] = [];\n\n for (const [, handlers] of this.handlers) {\n for (const registered of handlers) {\n if (registered.subscription.matches(type)) {\n matchingHandlers.push(registered);\n }\n }\n }\n\n // Fire-and-forget - don't await\n for (const registered of matchingHandlers) {\n try {\n const result = registered.handler(payload, metadata);\n // Handle both sync and async handlers\n if (result && typeof result.catch === 'function') {\n void result.catch((error: unknown) => {\n logger.error(\n `DispatchBus: Handler for \"${registered.pattern}\" failed`,\n { error },\n );\n });\n }\n } catch (error) {\n logger.error(\n `DispatchBus: Handler for \"${registered.pattern}\" failed`,\n { error },\n );\n }\n }\n }\n}\n\n/**\n * Create a DispatchBus instance\n *\n * @param options - Bus configuration\n * @returns Initialized DispatchBus\n */\nexport async function createDispatchBus(\n options: DispatchBusOptions = {},\n): Promise<DispatchBus> {\n // Resolve database configuration\n const dbConfig = options.db || options.persistence;\n\n if (!dbConfig) {\n throw new Error('DispatchBus requires a database configuration');\n }\n\n // Get or create database interface\n let db: DatabaseInterface;\n if (typeof dbConfig === 'string') {\n // `DispatchBusOptions.db` does not admit a bare string, but this branch is\n // live code and must not be the one unbounded pool a reader finds while\n // auditing this file (#2377).\n db = await getDatabase(\n applyPostgresRuntimeTimeouts({\n url: dbConfig,\n }) as Parameters<typeof getDatabase>[0],\n );\n } else if ('query' in dbConfig) {\n // Already a DatabaseInterface (has query method)\n db = dbConfig as DatabaseInterface;\n } else if ('type' in dbConfig && 'url' in dbConfig) {\n // Database config object - use getDatabase with type and url.\n // Bounded like every other runtime pool so a dispatch-bus connection is\n // not the one unbounded PostgreSQL client in the process (#2377).\n db = await getDatabase(\n applyPostgresRuntimeTimeouts({\n type: dbConfig.type as 'sqlite' | 'postgres' | 'duckdb',\n url: dbConfig.url,\n }) as Parameters<typeof getDatabase>[0],\n );\n } else {\n throw new Error('Invalid database configuration for DispatchBus');\n }\n\n const bus = new DispatchBus(db);\n await bus.initialize();\n\n return bus;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bus.js","names":[],"sources":["../../src/dispatch/bus.ts"],"sourcesContent":["/**\n * DispatchBus - Central hub for inter-agent communication\n *\n * The DispatchBus provides both in-memory handlers and persistent subscriptions\n * for asynchronous agent-to-agent messaging.\n *\n * @example\n * ```typescript\n * const bus = await createDispatchBus({ db: { type: 'sqlite', url: 'app.db' } });\n *\n * // In-memory handler (immediate)\n * bus.on('campaign.completed', async (payload, metadata) => {\n * console.log(`Campaign ${payload.campaignId} completed`);\n * });\n *\n * // Persistent subscription (processed later)\n * await bus.subscribe({ signalType: 'campaign.*', subscriber: 'fiscus' });\n *\n * // Emit a dispatch\n * await bus.emit('campaign.completed', { campaignId: '123' }, { source: 'suasor' });\n *\n * // Process pending dispatches\n * await bus.process('fiscus');\n * ```\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { isTransientConnectionError } from '../db-errors.js';\nimport { getDatabase } from '../host.js';\nimport { applyPostgresRuntimeTimeouts } from '../postgres-timeouts.js';\nimport { runSerializedAgainstSystemTableBootstrap } from '../system/bootstrap.js';\nimport {\n assertPostgresSystemTimestampsCurrent,\n ensureDispatchSubscriptionsSystemTableCompatibility,\n ensureDispatchSystemTableCompatibility,\n getDatabaseEngine,\n} from '../system/compatibility.js';\nimport {\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n CREATE_SMRT_DISPATCH_TABLE,\n getSystemTableDDLForEngine,\n} from '../system/schema.js';\nimport { DispatchCollection } from './collections/Dispatches.js';\nimport { DispatchSubscriptionCollection } from './collections/DispatchSubscriptions.js';\nimport { Dispatch } from './models/Dispatch.js';\nimport { DispatchSubscription } from './models/DispatchSubscription.js';\nimport {\n resolveDispatchTenantId,\n resolveDispatchTenantScope,\n} from './tenant-resolver.js';\nimport type {\n DispatchBusOptions,\n DispatchBusStartupRetry,\n DispatchCleanupOptions,\n DispatchCleanupResult,\n DispatchEmitOptions,\n DispatchHandler,\n DispatchListOptions,\n DispatchMetadata,\n DispatchProcessOptions,\n DispatchRetryOptions,\n DispatchSubscribeOptions,\n} from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Registered in-memory handler\n */\ninterface RegisteredHandler {\n pattern: string;\n handler: DispatchHandler;\n subscription: DispatchSubscription;\n}\n\n/**\n * Central hub for inter-agent messaging with both in-memory and persistent delivery.\n *\n * The `DispatchBus` combines two complementary delivery models:\n *\n * - **In-memory handlers** (`on(pattern, handler)`) — called synchronously (fire-and-forget)\n * when a dispatch is emitted. Fast, but lost on process restart.\n * - **Persistent subscriptions** (`subscribe({ signalType, subscriber })`) — stored in\n * `_smrt_dispatch_subscriptions` and processed later by calling `process(subscriber, handler)`.\n * Survive restarts; suitable for background workers and scheduled agents.\n *\n * Signal types support single-segment wildcards: `'campaign.*'` matches `'campaign.completed'`\n * and `'campaign.failed'`, but not `'campaign.phase.two'`.\n *\n * Dispatch lifecycle: `pending → processing → completed` (or `failed` on handler error).\n * Use `retry()` to reset failed dispatches back to `pending`.\n *\n * Create instances via `createDispatchBus()` — do not use `new DispatchBus()` directly\n * in application code.\n *\n * @example\n * ```typescript\n * const bus = await createDispatchBus({ db: myDb });\n *\n * // In-memory: immediate, fire-and-forget\n * bus.on('invoice.paid', async (payload) => {\n * console.log('Invoice paid:', payload.invoiceId);\n * });\n *\n * // Persistent: processed by 'notifications' agent on next run\n * await bus.subscribe({ signalType: 'invoice.*', subscriber: 'notifications' });\n *\n * // Emit (notifies in-memory handlers immediately, stores persistent dispatch)\n * await bus.emit('invoice.paid', { invoiceId: 'inv-001' }, { source: 'billing' });\n *\n * // Later, in the notifications agent:\n * await bus.process('notifications', async (payload) => {\n * await sendEmail(payload.invoiceId);\n * });\n * ```\n */\nexport class DispatchBus {\n private db: DatabaseInterface;\n private handlers: Map<string, RegisteredHandler[]> = new Map();\n private initialized: boolean = false;\n\n /**\n * Reserved subscriber name used internally for in-memory `on()` handlers.\n * Callers may not register persistent subscriptions under this name, nor\n * assert it as an emit `source`, so they cannot impersonate the in-memory\n * pseudo-subscriber (S5 #1398).\n */\n private static readonly RESERVED_SUBSCRIBER = '_in_memory_';\n\n /** Maximum stored length of a caller-asserted dispatch source label. */\n private static readonly MAX_SOURCE_LENGTH = 256;\n\n /**\n * Normalize a caller-asserted `source` into untrusted metadata.\n *\n * `source` is a declared label, not an authenticated identity, so it must not\n * be trusted for authorization. This caps its length and **rejects** the\n * reserved internal sentinel (`_in_memory_`) by throwing — the contract is\n * that the sentinel is not an accepted source, so a caller cannot quietly\n * impersonate the in-memory subscriber pseudo-source (S5 #1398). Empty/missing\n * values default to `'unknown'` (unchanged behavior).\n *\n * @throws Error if `source` is the reserved in-memory sentinel.\n */\n private static sanitizeSource(source: string | undefined): string {\n const raw = (source ?? '').trim();\n if (!raw) {\n return 'unknown';\n }\n if (raw === DispatchBus.RESERVED_SUBSCRIBER) {\n throw new Error(\n `DispatchBus.emit: \"${DispatchBus.RESERVED_SUBSCRIBER}\" is a reserved source and cannot be asserted`,\n );\n }\n return raw.slice(0, DispatchBus.MAX_SOURCE_LENGTH);\n }\n\n /**\n * Create a new DispatchBus (use createDispatchBus factory instead)\n */\n constructor(db: DatabaseInterface) {\n this.db = db;\n }\n\n /**\n * Initialize the dispatch tables\n */\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n // `_smrt_dispatch`/`_smrt_dispatch_subscriptions` are framework-owned\n // system tables (part of `ALL_SYSTEM_TABLES`), but this standalone\n // initializer provisions/verifies them independently of a full\n // `ensureSystemTables()` bootstrap. Without serialization, concurrent\n // `DispatchBus` instances against the same already-warmed PostgreSQL\n // database (e.g. multiple `Suasor`/`Agent` instantiations, or concurrent\n // test files sharing one database) race on unguarded DDL — proven to\n // throw `duplicate key value violates unique constraint\n // \"pg_type_typname_nsp_index\"` on concurrent `CREATE TABLE IF NOT\n // EXISTS` (#2861). Fresh DDL always creates timezone-aware columns (see\n // `getSystemTableDDLForEngine()`), so this race does not itself produce a\n // legacy-typed column; a stale `assertPostgresSystemTimestampsCurrent()`\n // guard requires a *separate* concurrent actor to have put some\n // `_smrt_*` table into a legacy-typed state (e.g. a test deliberately\n // simulating a pre-migration database), which this lock does not by\n // itself rule out — see the issue for that residual risk. Running this\n // under the same advisory lock `ensureSystemTables()` uses makes every\n // writer of these tables mutually exclusive instead of merely\n // idempotent, which is what the proven race requires.\n //\n // Resolve the engine from `this.db` — the caller-supplied handle — once,\n // up front. `runSerializedAgainstSystemTableBootstrap()` hands the\n // callback a *different* handle on PostgreSQL (the lock-holding\n // transaction it opens), and that handle is not guaranteed to carry the\n // `url`/`type` (or `config.url`/`config.type`) fields engine detection\n // reads (see the `db.config?.url`-style adapters `getDatabaseEngine()`\n // already supports elsewhere in this package). Recomputing the engine\n // on the transaction handle can therefore silently disagree with the\n // outer gate that just decided `postgres` — `detectEngine('', undefined)`\n // falls back to `sqlite`, which would create the dispatch tables with\n // `TIMESTAMP` instead of `TIMESTAMPTZ` and skip the PostgreSQL timestamp\n // guard entirely. Threading the one resolved `engine` through — as\n // `ensureSystemTables()`/`bootstrapSystemTables()` already thread\n // `typeHint` — keeps the inner detection bound to the outer decision.\n const engine = getDatabaseEngine(this.db);\n await runSerializedAgainstSystemTableBootstrap(\n this.db,\n engine,\n async (tx) => {\n // Create dispatch tables if they don't exist\n const dispatchExists = await DispatchCollection.tableExists(tx, engine);\n if (!dispatchExists) {\n // Split the DDL into separate statements and execute each\n const statements = getSystemTableDDLForEngine(\n CREATE_SMRT_DISPATCH_TABLE,\n engine,\n )\n .split(';')\n .filter((s) => s.trim());\n for (const stmt of statements) {\n await tx.query(stmt);\n }\n }\n await assertPostgresSystemTimestampsCurrent(tx, engine);\n await ensureDispatchSystemTableCompatibility(tx, engine);\n\n const subsExists = await DispatchSubscriptionCollection.tableExists(\n tx,\n engine,\n );\n if (!subsExists) {\n const statements = getSystemTableDDLForEngine(\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n engine,\n )\n .split(';')\n .filter((s) => s.trim());\n for (const stmt of statements) {\n await tx.query(stmt);\n }\n }\n await assertPostgresSystemTimestampsCurrent(tx, engine);\n await ensureDispatchSubscriptionsSystemTableCompatibility(tx, engine);\n },\n );\n\n this.initialized = true;\n }\n\n /**\n * Emits a dispatch message.\n *\n * Two things happen atomically (from the caller's perspective):\n * 1. One or more `Dispatch` rows are inserted with `status: 'pending'` into `_smrt_dispatch`.\n * 2. All matching in-memory handlers (registered via `on()`) are invoked\n * fire-and-forget (errors are caught and logged, not thrown back to the caller).\n *\n * Persistent subscriptions registered via `subscribe()` will see this dispatch\n * the next time `process(subscriber, handler)` is called for their subscriber name.\n *\n * **Delivery modes** affect how dispatches are created:\n * - `compete` subscribers share a single dispatch (`target_subscriber = NULL`).\n * First subscriber to `process()` claims it (at-most-once delivery).\n * - `fanout` subscribers each get their own dispatch copy (`target_subscriber` set\n * to the subscriber name), so each processes independently.\n *\n * If no subscriptions exist, one dispatch is created for future processing.\n *\n * **Security (S5 #1398):**\n * - `tenant_id` is derived **server-side** from the active tenant context and\n * is never read from caller options, so it cannot be spoofed. Subscribers in\n * a different tenant cannot see or claim this dispatch (see {@link process}).\n * When there is no active tenant context (system/global), `tenant_id` is\n * `NULL` and the dispatch is visible to all scopes — preserving pre-tenancy\n * behavior.\n * - `options.source` is **caller-asserted, untrusted metadata** — it is a\n * declared label only and must not be relied on as an authenticated emitter\n * identity. It is length-capped and the reserved internal sentinel\n * (`_in_memory_`) is rejected so a caller cannot impersonate the in-memory\n * subscriber pseudo-source.\n *\n * @param type - Signal type string, e.g. `'campaign.completed'` or `'invoice.paid'`\n * @param payload - Any JSON-serializable data to attach to the dispatch\n * @param options.source - Declared (untrusted) name of the emitting agent/component (default `'unknown'`)\n * @param options.sourceId - Optional ID of the specific emitting entity\n * @param options.metadata - Optional additional JSON metadata for the dispatch record\n * @returns A persisted `Dispatch` instance (the compete dispatch, or the first fanout copy if fanout-only)\n *\n * @example\n * ```typescript\n * const dispatch = await bus.emit(\n * 'campaign.completed',\n * { campaignId: 'cmp-001', impressions: 10_000 },\n * { source: 'suasor', sourceId: agentId },\n * );\n * console.log(dispatch.id); // UUID of the created dispatch record\n * ```\n *\n * @see {@link on} for in-memory handlers\n * @see {@link subscribe} for persistent subscriptions\n * @see {@link process} to consume pending dispatches\n */\n async emit(\n type: string,\n payload: unknown,\n options: DispatchEmitOptions = {},\n ): Promise<Dispatch> {\n await this.initialize();\n\n // Derive the tenant scope server-side from the active context. This is the\n // trust anchor for cross-tenant isolation (S5 #1398) — it is never taken\n // from caller options, so it cannot be spoofed. `undefined`/`null` means\n // \"no tenant context\" → tenant_id stays NULL (global dispatch).\n const tenantId = resolveDispatchTenantId() ?? null;\n\n // Treat the caller-asserted source as untrusted metadata.\n const source = DispatchBus.sanitizeSource(options.source);\n\n // Query subscriptions to determine if fan-out copies are needed.\n // This runs on every emit() — uses SQL-level exact matching with\n // in-memory fallback only for wildcard subscriptions to minimize I/O.\n // Scoped to the active tenant so emit only fans out to this tenant's\n // (and, for global emits, global) subscriptions (S5 #1398).\n const matchingSubs = await DispatchSubscriptionCollection.findBySignalType(\n this.db,\n type,\n resolveDispatchTenantScope(),\n );\n\n // Separate into compete and fanout subscribers\n const fanoutSubs = matchingSubs.filter((s) => s.delivery === 'fanout');\n const competeSubs = matchingSubs.filter((s) => s.delivery !== 'fanout');\n\n // Create the base dispatch record\n const dispatch = new Dispatch({\n type,\n source,\n source_id: options.sourceId || null,\n payload: JSON.stringify(payload || {}),\n metadata: JSON.stringify(options.metadata || {}),\n status: 'pending',\n correlation_id: options.correlationId || null,\n tenant_id: tenantId,\n });\n\n // Track first persisted dispatch to return to caller\n let returnDispatch = dispatch;\n\n // For fanout subscribers: create a per-subscriber dispatch copy\n for (const sub of fanoutSubs) {\n const fanoutDispatch = new Dispatch({\n type,\n source,\n source_id: options.sourceId || null,\n payload: JSON.stringify(payload || {}),\n metadata: JSON.stringify(options.metadata || {}),\n status: 'pending',\n target_subscriber: sub.subscriber,\n correlation_id: options.correlationId || null,\n tenant_id: tenantId,\n });\n await DispatchCollection.insert(this.db, fanoutDispatch);\n // If no compete dispatch will be persisted, return the first fanout copy\n if (competeSubs.length === 0 && matchingSubs.length > 0) {\n returnDispatch = fanoutDispatch;\n }\n }\n\n // For compete subscribers (or if no subscriptions found): insert the original\n // dispatch with target_subscriber = NULL so any compete subscriber can claim it\n if (competeSubs.length > 0 || matchingSubs.length === 0) {\n await DispatchCollection.insert(this.db, dispatch);\n }\n\n // Notify in-memory handlers immediately (fire-and-forget)\n this.notifyHandlers(type, payload, dispatch.getMetadata());\n\n return returnDispatch;\n }\n\n /**\n * Registers an in-memory handler for a signal type pattern.\n *\n * In-memory handlers are called immediately (fire-and-forget) during `emit()`.\n * They are stored only in memory and lost on process restart — use `subscribe()`\n * for durable, restart-safe subscriptions.\n *\n * Pattern matching supports a single-segment wildcard `*`:\n * - `'campaign.*'` matches `'campaign.completed'` and `'campaign.failed'`\n * - `'campaign.*'` does **not** match `'campaign.phase.two'`\n *\n * Multiple handlers may be registered for the same pattern — all will be called.\n *\n * @param pattern - Signal type pattern, optionally with a trailing `.*` wildcard\n * @param handler - Async or sync function to call with `(payload, metadata)`\n *\n * @example\n * ```typescript\n * bus.on('invoice.*', async (payload, metadata) => {\n * console.log(`Invoice event from ${metadata.source}:`, payload);\n * });\n * ```\n *\n * @see {@link off} to remove a handler\n * @see {@link subscribe} for persistent (restart-safe) subscriptions\n */\n on(pattern: string, handler: DispatchHandler): void {\n const subscription = new DispatchSubscription({\n signal_type: pattern,\n subscriber: '_in_memory_',\n handler: 'callback',\n });\n\n const registered: RegisteredHandler = { pattern, handler, subscription };\n\n if (!this.handlers.has(pattern)) {\n this.handlers.set(pattern, []);\n }\n this.handlers.get(pattern)?.push(registered);\n }\n\n /**\n * Remove an in-memory handler\n *\n * @param pattern - Signal type pattern\n * @param handler - Handler function to remove\n * @returns True if handler was found and removed\n */\n off(pattern: string, handler: DispatchHandler): boolean {\n const handlers = this.handlers.get(pattern);\n if (!handlers) return false;\n\n const index = handlers.findIndex((h) => h.handler === handler);\n if (index === -1) return false;\n\n handlers.splice(index, 1);\n if (handlers.length === 0) {\n this.handlers.delete(pattern);\n }\n return true;\n }\n\n /**\n * Creates or updates a persistent subscription in the database.\n *\n * Persistent subscriptions survive process restarts. When `process(subscriber, handler)`\n * is called later, all pending dispatches matching this subscriber's signal patterns\n * will be delivered.\n *\n * Calling `subscribe()` with the same `signalType`/`subscriber` pair is idempotent\n * (upsert) — it is safe to call on every agent startup.\n *\n * Set `delivery: 'fanout'` to give each subscriber their own dispatch copy.\n * Default is `'compete'` (at-most-once, first subscriber to process claims it).\n *\n * @param options.signalType - Signal type pattern to subscribe to (wildcards supported)\n * @param options.subscriber - Name that identifies this subscriber (e.g. agent class name)\n * @param options.handler - Optional method name on the subscriber to call (default `'handleDispatch'`)\n * @param options.delivery - `'compete'` (default) for at-most-once, `'fanout'` for per-subscriber copies\n * @param options.enabled - If `false`, subscription is created but disabled (default `true`)\n *\n * @example\n * ```typescript\n * // Subscribe on agent startup (idempotent)\n * await bus.subscribe({ signalType: 'campaign.*', subscriber: 'FiscusAgent' });\n *\n * // Fan-out: each subscriber gets their own dispatch copy\n * await bus.subscribe({\n * signalType: 'campaign.*',\n * subscriber: 'AuditorAgent',\n * delivery: 'fanout',\n * });\n *\n * // Later, process matching pending dispatches\n * await bus.process('FiscusAgent', async (payload, metadata) => { ... });\n * ```\n *\n * @see {@link process} to consume pending dispatches for this subscriber\n * @see {@link unsubscribe} to remove the subscription\n * @see {@link on} for non-persistent in-memory handling\n */\n async subscribe(options: DispatchSubscribeOptions): Promise<void> {\n await this.initialize();\n\n // Gate the subscriber namespace (S5 #1398): require a concrete subscriber\n // name and reject the reserved internal sentinel so callers cannot\n // register a persistent subscription that masquerades as the in-memory\n // pseudo-subscriber.\n const subscriber = (options.subscriber ?? '').trim();\n if (!subscriber) {\n throw new Error('DispatchBus.subscribe requires a non-empty subscriber');\n }\n if (subscriber === DispatchBus.RESERVED_SUBSCRIBER) {\n throw new Error(\n `DispatchBus.subscribe: \"${DispatchBus.RESERVED_SUBSCRIBER}\" is a reserved subscriber name`,\n );\n }\n const signalType = (options.signalType ?? '').trim();\n if (!signalType) {\n throw new Error('DispatchBus.subscribe requires a non-empty signalType');\n }\n\n const subscription = new DispatchSubscription({\n signal_type: signalType,\n subscriber,\n handler: options.handler || 'handleDispatch',\n delivery: options.delivery || 'compete',\n enabled: options.enabled !== false ? 1 : 0,\n // Server-derived tenant scope (never from caller options). NULL when there\n // is no active tenant context (global subscription).\n tenant_id: resolveDispatchTenantId() ?? null,\n });\n\n await DispatchSubscriptionCollection.upsert(this.db, subscription);\n }\n\n /**\n * Remove a persistent subscription\n *\n * @param signalType - Signal type pattern\n * @param subscriber - Subscriber name\n */\n async unsubscribe(signalType: string, subscriber: string): Promise<void> {\n await this.initialize();\n // Scoped to the active tenant so one tenant cannot remove another's\n // subscription (S5 #1398).\n await DispatchSubscriptionCollection.deleteByKey(\n this.db,\n signalType,\n subscriber,\n resolveDispatchTenantScope(),\n );\n }\n\n /**\n * Processes pending dispatches for a named subscriber.\n *\n * For each matching `pending` dispatch:\n * 1. Sets status to `processing`\n * 2. Calls `handler(payload, metadata)`\n * 3. On success: sets status to `completed`\n * 4. On error: sets status to `failed` (stores the error message)\n *\n * Uses a wildcard-aware query strategy: subscriptions with `*` patterns fetch\n * all pending dispatches and filter in memory; exact-match subscriptions use\n * a direct SQL `IN` query for efficiency.\n *\n * @param subscriber - The subscriber name (must match a `subscribe()` call)\n * @param handler - Function to call for each pending dispatch\n * @param options.limit - Maximum dispatches to process in one call (default 100)\n * @param options.signalTypes - Optional filter to process only specific signal types\n * @returns Number of dispatches successfully processed (excludes failed)\n *\n * @example\n * ```typescript\n * const count = await bus.process('FiscusAgent', async (payload, metadata) => {\n * if (metadata.signalType === 'campaign.completed') {\n * await generateInvoice(payload.campaignId);\n * }\n * });\n * console.log(`Processed ${count} dispatches`);\n * ```\n *\n * @see {@link subscribe} to register the persistent subscription first\n * @see {@link retry} to reset failed dispatches back to pending\n */\n async process(\n subscriber: string,\n handler: DispatchHandler,\n options: DispatchProcessOptions = {},\n ): Promise<number> {\n await this.initialize();\n\n // Resolve the active tenant scope server-side (S5 #1398). This distinguishes\n // three states (see resolveDispatchTenantScope):\n // - tenancy off → no filter; system/global + non-tenant deployments behave\n // exactly as before.\n // - active tenant T → only T's dispatches plus global (NULL) dispatches are\n // claimable, so a subscriber in tenant A cannot see/claim tenant B's.\n // - tenancy on but no active tenant → fail-closed to global (NULL) rows\n // only; a missing context never leaks other tenants' dispatches.\n const tenantScope = resolveDispatchTenantScope();\n\n // Get subscriber's subscriptions, scoped to the active tenant so a tenant-A\n // processor only matches tenant-A (and, when global, global) subscriptions.\n const subscriptions = await DispatchSubscriptionCollection.findBySubscriber(\n this.db,\n subscriber,\n true,\n tenantScope,\n );\n\n if (subscriptions.length === 0) {\n return 0;\n }\n\n // Get signal types (including wildcards)\n const signalTypes = subscriptions.map((s) => s.signalType);\n\n // For non-wildcard subscriptions, we can query directly\n // For wildcards, we need to get all pending and filter\n const hasWildcards = signalTypes.some((t) => t.includes('*'));\n\n // Helper: check if a dispatch should be visible to this subscriber\n const isVisibleToSubscriber = (dispatch: Dispatch): boolean => {\n // Tenant isolation (defense in depth — the SQL queries below also filter).\n if (tenantScope.enforced) {\n if (tenantScope.tenantId === null) {\n // Tenancy on, no active tenant → only global (NULL) rows are visible.\n if (dispatch.tenantId !== null) {\n return false;\n }\n } else if (\n dispatch.tenantId !== null &&\n dispatch.tenantId !== tenantScope.tenantId\n ) {\n // Active tenant → that tenant's rows plus global (NULL) rows only.\n return false;\n }\n }\n if (dispatch.targetSubscriber === subscriber) {\n // Targeted at this subscriber (fanout copy) — always visible\n return true;\n }\n if (dispatch.targetSubscriber !== null) {\n // Targeted at a different subscriber — not visible\n return false;\n }\n // Null target (compete dispatch): only visible if this subscriber\n // has a compete subscription matching this dispatch type\n const matchingSub = subscriptions.find((sub) =>\n sub.matches(dispatch.type),\n );\n return matchingSub?.delivery === 'compete';\n };\n\n let pendingDispatches: Dispatch[];\n if (hasWildcards) {\n // Get all pending dispatches and filter by subscription patterns + target\n const allPending = await DispatchCollection.list(this.db, {\n status: 'pending',\n limit: options.limit || 100,\n tenantScope,\n });\n\n pendingDispatches = allPending.filter(\n (dispatch) =>\n subscriptions.some((sub) => sub.matches(dispatch.type)) &&\n isVisibleToSubscriber(dispatch),\n );\n } else {\n // Direct query for exact signal types (with target_subscriber filter)\n const raw = await DispatchCollection.findPending(\n this.db,\n signalTypes,\n options.limit || 100,\n subscriber,\n tenantScope,\n );\n // Post-filter for delivery mode correctness\n pendingDispatches = raw.filter(isVisibleToSubscriber);\n }\n\n // Filter by specific signal types if provided\n if (options.signalTypes && options.signalTypes.length > 0) {\n pendingDispatches = pendingDispatches.filter((d) =>\n options.signalTypes?.includes(d.type),\n );\n }\n\n let processed = 0;\n\n for (const dispatch of pendingDispatches) {\n // Atomically claim the dispatch (S5 #1398). The conditional UPDATE\n // (WHERE id = ? AND status = 'pending' [AND tenant predicate]) closes the\n // TOCTOU window: if a competing worker — or a system-scoped processor —\n // already moved this row out of 'pending', our claim affects no rows and\n // we skip it, so a compete/global dispatch is processed at most once.\n const claimed = await DispatchCollection.claim(\n this.db,\n dispatch,\n tenantScope,\n );\n if (!claimed) {\n continue;\n }\n\n try {\n // Invoke handler\n await handler(dispatch.payload, dispatch.getMetadata());\n\n // Mark as completed\n dispatch.markCompleted(subscriber);\n await DispatchCollection.update(this.db, dispatch);\n processed++;\n } catch (error) {\n // Mark as failed\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n dispatch.markFailed(errorMessage);\n await DispatchCollection.update(this.db, dispatch);\n }\n }\n\n return processed;\n }\n\n /**\n * Retry failed dispatches\n *\n * Resets failed dispatches to pending status so they can be processed again.\n *\n * @param options - Retry options\n * @returns Number of dispatches reset\n */\n async retry(options: DispatchRetryOptions = {}): Promise<number> {\n await this.initialize();\n\n // Tenant isolation (S5 #1398): only the active tenant's failed dispatches\n // (plus global ones) are eligible — a tenant's retry() must not reset\n // another tenant's rows. Scope is derived server-side; callers cannot widen\n // it.\n const retryable = await DispatchCollection.findRetryable(\n this.db,\n options,\n resolveDispatchTenantScope(),\n );\n\n for (const dispatch of retryable) {\n dispatch.resetForRetry();\n await DispatchCollection.update(this.db, dispatch);\n }\n\n return retryable.length;\n }\n\n /**\n * Clean up old dispatches\n *\n * @param options - Cleanup options\n * @returns Number of dispatches deleted\n */\n async cleanup(\n options: DispatchCleanupOptions = {},\n ): Promise<DispatchCleanupResult> {\n await this.initialize();\n // Tenant isolation (S5 #1398): only the active tenant's dispatches (plus\n // global ones) are deleted — a tenant's cleanup() must not delete another\n // tenant's rows. Scope is derived server-side; callers cannot widen it.\n return DispatchCollection.cleanup(\n this.db,\n options,\n resolveDispatchTenantScope(),\n );\n }\n\n /**\n * Lists dispatches, scoped to the active tenant context (S5 #1398).\n *\n * The tenant scope is derived **server-side** from the active context and\n * **overrides any caller-supplied scope** — callers cannot select another\n * tenant or widen visibility to all tenants. Scoping rules:\n *\n * - tenancy off → no tenant filter (pre-tenancy behavior).\n * - active tenant T → that tenant's dispatches only. Global (NULL-tenant)\n * dispatches are system-only on reads (#3301): a global row's payload (for\n * example a delivery emitted outside tenant context) may carry another\n * tenant's recipients/subjects. Processing (`process`) still lets a tenant\n * worker claim global work.\n * - tenancy on but no active tenant → global (NULL) dispatches only\n * (fail-closed; never all tenants).\n *\n * @param options - Non-tenant list filters (status/type/source/etc.). Any\n * `tenantScope` field is ignored and replaced with the server-derived one.\n */\n async list(options: DispatchListOptions = {}): Promise<Dispatch[]> {\n await this.initialize();\n // Strip any caller-supplied scope and inject the server-derived one.\n const { tenantScope: _ignoredCallerScope, ...safeOptions } = options;\n // `includeGlobal: false` — global (NULL-tenant) rows are system-only on\n // this inspection read (#3301).\n return DispatchCollection.list(\n this.db,\n {\n ...safeOptions,\n tenantScope: resolveDispatchTenantScope(),\n },\n false,\n );\n }\n\n /**\n * Gets a dispatch by ID, enforcing the active tenant scope (S5 #1398).\n *\n * Applies the same server-derived tenant predicate as {@link list}: a\n * subscriber in tenant A cannot fetch tenant B's dispatch, or a global\n * (NULL-tenant) dispatch, by id (#3301), and when tenancy is on with no\n * active tenant only global (NULL) dispatches are returned. Returns `null`\n * when the dispatch exists but is out of scope.\n */\n async get(id: string): Promise<Dispatch | null> {\n await this.initialize();\n return DispatchCollection.getScoped(\n this.db,\n id,\n resolveDispatchTenantScope(),\n false,\n );\n }\n\n /**\n * List all subscriptions\n */\n async listSubscriptions(\n subscriber?: string,\n ): Promise<DispatchSubscription[]> {\n await this.initialize();\n\n // Scoped to the active tenant so listing never leaks another tenant's\n // subscriptions (S5 #1398).\n const tenantScope = resolveDispatchTenantScope();\n\n if (subscriber) {\n return DispatchSubscriptionCollection.findBySubscriber(\n this.db,\n subscriber,\n false,\n tenantScope,\n );\n }\n return DispatchSubscriptionCollection.list(this.db, false, tenantScope);\n }\n\n /**\n * Notify in-memory handlers (fire-and-forget)\n */\n private notifyHandlers(\n type: string,\n payload: unknown,\n metadata: DispatchMetadata,\n ): void {\n // Collect all matching handlers\n const matchingHandlers: RegisteredHandler[] = [];\n\n for (const [, handlers] of this.handlers) {\n for (const registered of handlers) {\n if (registered.subscription.matches(type)) {\n matchingHandlers.push(registered);\n }\n }\n }\n\n // Fire-and-forget - don't await\n for (const registered of matchingHandlers) {\n try {\n const result = registered.handler(payload, metadata);\n // Handle both sync and async handlers\n if (result && typeof result.catch === 'function') {\n void result.catch((error: unknown) => {\n logger.error(\n `DispatchBus: Handler for \"${registered.pattern}\" failed`,\n { error },\n );\n });\n }\n } catch (error) {\n logger.error(\n `DispatchBus: Handler for \"${registered.pattern}\" failed`,\n { error },\n );\n }\n }\n }\n}\n\n/**\n * Run one startup step, retrying it with exponential backoff while it fails\n * with a transient connection error (#3657). Anything else, or the last\n * attempt's failure, is thrown as is.\n */\nasync function withStartupRetry<T>(\n step: () => Promise<T>,\n retry: DispatchBusStartupRetry | false | undefined,\n): Promise<T> {\n const attempts =\n retry === false ? 1 : Math.max(1, Math.floor(retry?.attempts ?? 5));\n const maxDelayMs = retry ? (retry.maxDelayMs ?? 5000) : 5000;\n let delayMs = retry ? (retry.initialDelayMs ?? 500) : 500;\n for (let attempt = 1; ; attempt++) {\n try {\n return await step();\n } catch (error) {\n if (attempt >= attempts || !isTransientConnectionError(error)) {\n throw error;\n }\n const wait = Math.min(delayMs, maxDelayMs);\n logger.warn(\n `DispatchBus: transient connection failure during startup (attempt ${attempt} of ${attempts}); retrying in ${wait} ms`,\n { error: error instanceof Error ? error.message : String(error) },\n );\n await new Promise((resolve) => setTimeout(resolve, wait));\n delayMs *= 2;\n }\n }\n}\n\n/**\n * Create a DispatchBus instance\n *\n * Opening the database and `initialize()` are retried on a transient\n * connection failure; see {@link DispatchBusOptions.startupRetry}.\n *\n * @param options - Bus configuration\n * @returns Initialized DispatchBus\n */\nexport async function createDispatchBus(\n options: DispatchBusOptions = {},\n): Promise<DispatchBus> {\n // Resolve database configuration\n const dbConfig = options.db || options.persistence;\n\n if (!dbConfig) {\n throw new Error('DispatchBus requires a database configuration');\n }\n\n // Get or create database interface\n let db: DatabaseInterface;\n if (typeof dbConfig === 'string') {\n // `DispatchBusOptions.db` does not admit a bare string, but this branch is\n // live code and must not be the one unbounded pool a reader finds while\n // auditing this file (#2377).\n db = await withStartupRetry(\n () =>\n getDatabase(\n applyPostgresRuntimeTimeouts({\n url: dbConfig,\n }) as Parameters<typeof getDatabase>[0],\n ),\n options.startupRetry,\n );\n } else if ('query' in dbConfig) {\n // Already a DatabaseInterface (has query method)\n db = dbConfig as DatabaseInterface;\n } else if ('type' in dbConfig && 'url' in dbConfig) {\n // Database config object - use getDatabase with type and url.\n // Bounded like every other runtime pool so a dispatch-bus connection is\n // not the one unbounded PostgreSQL client in the process (#2377).\n db = await withStartupRetry(\n () =>\n getDatabase(\n applyPostgresRuntimeTimeouts({\n type: dbConfig.type as 'sqlite' | 'postgres' | 'duckdb',\n url: dbConfig.url,\n }) as Parameters<typeof getDatabase>[0],\n ),\n options.startupRetry,\n );\n } else {\n throw new Error('Invalid database configuration for DispatchBus');\n }\n\n const bus = new DispatchBus(db);\n // initialize() sets `initialized` only on success and its DDL is idempotent\n // and serialized, so a retry after a dropped connection starts it over.\n await withStartupRetry(() => bus.initialize(), options.startupRetry);\n\n return bus;\n}\n"],"mappings":"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"}
|
package/dist/dispatch/index.d.ts
CHANGED
|
@@ -43,5 +43,5 @@ export { DispatchSubscriptionCollection } from './collections/DispatchSubscripti
|
|
|
43
43
|
export { Dispatch, type DispatchData } from './models/Dispatch.js';
|
|
44
44
|
export { DispatchSubscription, type DispatchSubscriptionData, } from './models/DispatchSubscription.js';
|
|
45
45
|
export { type DispatchTenantResolver, type DispatchTenantScope, isTenantScopedClassResolved, resolveDispatchTenantId, resolveDispatchTenantScope, setDispatchTenantResolver, setTenantScopedClassResolver, } from './tenant-resolver.js';
|
|
46
|
-
export type { DispatchBusOptions, DispatchCleanupOptions, DispatchCleanupResult, DispatchEmitOptions, DispatchHandler, DispatchListOptions, DispatchMetadata, DispatchProcessOptions, DispatchRetryOptions, DispatchStatus, DispatchSubscribeOptions, } from './types.js';
|
|
46
|
+
export type { DispatchBusOptions, DispatchBusStartupRetry, DispatchCleanupOptions, DispatchCleanupResult, DispatchEmitOptions, DispatchHandler, DispatchListOptions, DispatchMetadata, DispatchProcessOptions, DispatchRetryOptions, DispatchStatus, DispatchSubscribeOptions, } from './types.js';
|
|
47
47
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dispatch/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,8BAA8B,EAAE,MAAM,wCAAwC,CAAC;AAExF,OAAO,EAAE,QAAQ,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EACL,oBAAoB,EACpB,KAAK,wBAAwB,GAC9B,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,2BAA2B,EAC3B,uBAAuB,EACvB,0BAA0B,EAC1B,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAG9B,YAAY,EACV,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EACf,mBAAmB,EACnB,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EACpB,cAAc,EACd,wBAAwB,GACzB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dispatch/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,8BAA8B,EAAE,MAAM,wCAAwC,CAAC;AAExF,OAAO,EAAE,QAAQ,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EACL,oBAAoB,EACpB,KAAK,wBAAwB,GAC9B,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,2BAA2B,EAC3B,uBAAuB,EACvB,0BAA0B,EAC1B,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAG9B,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EACf,mBAAmB,EACnB,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EACpB,cAAc,EACd,wBAAwB,GACzB,MAAM,YAAY,CAAC"}
|
package/dist/dispatch/types.d.ts
CHANGED
|
@@ -174,5 +174,22 @@ export interface DispatchBusOptions {
|
|
|
174
174
|
type: 'sql' | 'json' | 'duckdb';
|
|
175
175
|
url: string;
|
|
176
176
|
};
|
|
177
|
+
/**
|
|
178
|
+
* Bounded retry of the startup bootstrap (opening the database and
|
|
179
|
+
* `initialize()`) on a transient connection failure, such as PostgreSQL or
|
|
180
|
+
* its pooler dropping the first connection after a migration (#3657). Any
|
|
181
|
+
* other error fails at once. Defaults to 5 attempts with exponential backoff
|
|
182
|
+
* from 500 ms, capped at 5 s between attempts; `false` disables it.
|
|
183
|
+
*/
|
|
184
|
+
startupRetry?: DispatchBusStartupRetry | false;
|
|
185
|
+
}
|
|
186
|
+
/** Startup retry budget for {@link DispatchBusOptions.startupRetry}. */
|
|
187
|
+
export interface DispatchBusStartupRetry {
|
|
188
|
+
/** Total attempts, including the first (default 5, minimum 1). */
|
|
189
|
+
attempts?: number;
|
|
190
|
+
/** Delay before the second attempt, doubled each time (default 500 ms). */
|
|
191
|
+
initialDelayMs?: number;
|
|
192
|
+
/** Upper bound on any one delay (default 5000 ms). */
|
|
193
|
+
maxDelayMs?: number;
|
|
177
194
|
}
|
|
178
195
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/dispatch/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/dispatch/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** Provider-measured external usage, separate from hosted AI billing. */
|
|
2
|
+
import type { DatabaseInterface } from '@happyvertical/sql';
|
|
3
|
+
export declare const EXTERNAL_AGENT_USAGE_OPERATION = "external-agent";
|
|
4
|
+
export declare const EXTERNAL_AGENT_COUNTERS: readonly ["inputTokens", "outputTokens", "totalTokens", "cacheReadTokens", "cacheWriteTokens", "reasoningTokens"];
|
|
5
|
+
export type ExternalAgentCounter = (typeof EXTERNAL_AGENT_COUNTERS)[number];
|
|
6
|
+
export type ExternalAgentCounters = Partial<Record<ExternalAgentCounter, number | null>>;
|
|
7
|
+
/** Trusted ownership binding supplied by the authenticated application. */
|
|
8
|
+
export interface ExternalAgentUsageBinding {
|
|
9
|
+
tenantId: string;
|
|
10
|
+
workspaceId: string;
|
|
11
|
+
projectId: string;
|
|
12
|
+
taskId: string;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
parentSessionId?: string;
|
|
15
|
+
role: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ExternalAgentUsageReport {
|
|
18
|
+
version: 1;
|
|
19
|
+
binding: ExternalAgentUsageBinding;
|
|
20
|
+
provider: string;
|
|
21
|
+
model: string;
|
|
22
|
+
provenance: {
|
|
23
|
+
harness: string;
|
|
24
|
+
source: string;
|
|
25
|
+
eventId: string;
|
|
26
|
+
};
|
|
27
|
+
observedAt: string;
|
|
28
|
+
sequence: number;
|
|
29
|
+
epoch: number;
|
|
30
|
+
basis: 'delta' | 'cumulative';
|
|
31
|
+
/** First cumulative snapshot of each epoch: known zero origin or unknown baseline. */
|
|
32
|
+
baseline: 'zero' | 'unknown';
|
|
33
|
+
scope: 'exclusive';
|
|
34
|
+
inputIncludesCache: boolean;
|
|
35
|
+
outputIncludesReasoning: boolean;
|
|
36
|
+
counters: ExternalAgentCounters;
|
|
37
|
+
}
|
|
38
|
+
export interface ExternalAgentUsageReporter {
|
|
39
|
+
reporterId: string;
|
|
40
|
+
reporterType: 'agent' | 'user';
|
|
41
|
+
}
|
|
42
|
+
export interface ExternalAgentUsageReceipt {
|
|
43
|
+
reporter?: ExternalAgentUsageReporter;
|
|
44
|
+
id: string;
|
|
45
|
+
report: ExternalAgentUsageReport;
|
|
46
|
+
delta: Record<ExternalAgentCounter, number | null>;
|
|
47
|
+
}
|
|
48
|
+
export interface ExternalAgentUsageSummary {
|
|
49
|
+
binding: ExternalAgentUsageBinding;
|
|
50
|
+
source: 'external-agent';
|
|
51
|
+
reports: number;
|
|
52
|
+
latest: ExternalAgentUsageReport | null;
|
|
53
|
+
counters: Record<ExternalAgentCounter, number | null>;
|
|
54
|
+
complete: boolean;
|
|
55
|
+
coverage: Record<ExternalAgentCounter, {
|
|
56
|
+
knownReports: number;
|
|
57
|
+
unknownReports: number;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
export declare class ExternalAgentUsageError extends Error {
|
|
61
|
+
readonly code: 'invalid_input' | 'conflict';
|
|
62
|
+
constructor(code: 'invalid_input' | 'conflict', message: string);
|
|
63
|
+
}
|
|
64
|
+
/** Validate and copy only the public structured contract; arbitrary tags are refused. */
|
|
65
|
+
export declare function normalizeExternalAgentUsageReport(value: ExternalAgentUsageReport): ExternalAgentUsageReport;
|
|
66
|
+
/**
|
|
67
|
+
* Recorder accepts a trusted scope separately from an untrusted report. Caller
|
|
68
|
+
* authorizes scope ownership before construction. No ambient tenant inference.
|
|
69
|
+
* Existing system tables must be bootstrapped before opening a transaction.
|
|
70
|
+
*/
|
|
71
|
+
export declare class ExternalAgentUsageRecorder {
|
|
72
|
+
private readonly db;
|
|
73
|
+
private readonly binding;
|
|
74
|
+
private readonly reporter?;
|
|
75
|
+
constructor(db: DatabaseInterface, trustedBinding: ExternalAgentUsageBinding, reporter?: ExternalAgentUsageReporter);
|
|
76
|
+
record(input: ExternalAgentUsageReport): Promise<ExternalAgentUsageReceipt>;
|
|
77
|
+
/** Safe per-session projection; never estimates cost or coalesces unknown to zero. */
|
|
78
|
+
summarize(): Promise<ExternalAgentUsageSummary>;
|
|
79
|
+
}
|
|
80
|
+
/** Trusted project ownership scope, authorized by the application before reading. */
|
|
81
|
+
export interface ExternalAgentUsageQuery {
|
|
82
|
+
tenantId: string;
|
|
83
|
+
workspaceId: string;
|
|
84
|
+
projectId: string;
|
|
85
|
+
taskId?: string;
|
|
86
|
+
sessionId?: string;
|
|
87
|
+
}
|
|
88
|
+
/** Per-session summaries only: inclusive parent totals are never added to children. */
|
|
89
|
+
export declare function summarizeExternalAgentUsage(db: DatabaseInterface, scope: ExternalAgentUsageQuery): Promise<ExternalAgentUsageSummary[]>;
|
|
90
|
+
//# sourceMappingURL=external-agent-usage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external-agent-usage.d.ts","sourceRoot":"","sources":["../src/external-agent-usage.ts"],"names":[],"mappings":"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"}
|