@happyvertical/smrt-core 0.40.69 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/AGENTS.md +29 -4
  2. package/README.md +20 -1
  3. package/agents/change-feed.md +1 -1
  4. package/agents/query-bounds.md +45 -0
  5. package/agents/schema-paths.md +786 -0
  6. package/dist/browser.d.ts +1 -0
  7. package/dist/browser.d.ts.map +1 -1
  8. package/dist/browser.js +5 -3
  9. package/dist/cascade.d.ts +120 -0
  10. package/dist/cascade.d.ts.map +1 -0
  11. package/dist/cascade.js +430 -0
  12. package/dist/cascade.js.map +1 -0
  13. package/dist/change-feed.d.ts +34 -2
  14. package/dist/change-feed.d.ts.map +1 -1
  15. package/dist/change-feed.js +52 -11
  16. package/dist/change-feed.js.map +1 -1
  17. package/dist/class.d.ts +36 -3
  18. package/dist/class.d.ts.map +1 -1
  19. package/dist/class.js +87 -9
  20. package/dist/class.js.map +1 -1
  21. package/dist/collection-cache.js +0 -0
  22. package/dist/collection-cache.js.map +1 -1
  23. package/dist/collection.d.ts +130 -2
  24. package/dist/collection.d.ts.map +1 -1
  25. package/dist/collection.js +290 -57
  26. package/dist/collection.js.map +1 -1
  27. package/dist/config.d.ts +10 -0
  28. package/dist/config.d.ts.map +1 -1
  29. package/dist/config.js.map +1 -1
  30. package/dist/database.d.ts +8 -0
  31. package/dist/database.d.ts.map +1 -1
  32. package/dist/database.js +16 -8
  33. package/dist/database.js.map +1 -1
  34. package/dist/db-errors.d.ts +105 -0
  35. package/dist/db-errors.d.ts.map +1 -0
  36. package/dist/db-errors.js +382 -0
  37. package/dist/db-errors.js.map +1 -0
  38. package/dist/decorators/index.d.ts +80 -6
  39. package/dist/decorators/index.d.ts.map +1 -1
  40. package/dist/decorators/index.js +102 -12
  41. package/dist/decorators/index.js.map +1 -1
  42. package/dist/dispatch/bus.d.ts.map +1 -1
  43. package/dist/dispatch/bus.js +4 -3
  44. package/dist/dispatch/bus.js.map +1 -1
  45. package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
  46. package/dist/dispatch/collections/Dispatches.js +19 -4
  47. package/dist/dispatch/collections/Dispatches.js.map +1 -1
  48. package/dist/dispatch/types.d.ts +5 -0
  49. package/dist/dispatch/types.d.ts.map +1 -1
  50. package/dist/embedded-write-queue.d.ts +46 -0
  51. package/dist/embedded-write-queue.d.ts.map +1 -0
  52. package/dist/embedded-write-queue.js +66 -0
  53. package/dist/embedded-write-queue.js.map +1 -0
  54. package/dist/embeddings/storage.d.ts +7 -0
  55. package/dist/embeddings/storage.d.ts.map +1 -1
  56. package/dist/embeddings/storage.js +29 -12
  57. package/dist/embeddings/storage.js.map +1 -1
  58. package/dist/errors.d.ts +31 -3
  59. package/dist/errors.d.ts.map +1 -1
  60. package/dist/errors.js +34 -2
  61. package/dist/errors.js.map +1 -1
  62. package/dist/generators/changes-route.d.ts.map +1 -1
  63. package/dist/generators/changes-route.js +6 -3
  64. package/dist/generators/changes-route.js.map +1 -1
  65. package/dist/generators/mcp-runtime-template.d.ts +8 -0
  66. package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
  67. package/dist/generators/mcp-runtime-template.js +38 -4
  68. package/dist/generators/mcp-runtime-template.js.map +1 -1
  69. package/dist/generators/mcp.d.ts +16 -0
  70. package/dist/generators/mcp.d.ts.map +1 -1
  71. package/dist/generators/mcp.js +41 -3
  72. package/dist/generators/mcp.js.map +1 -1
  73. package/dist/generators/rest.d.ts +22 -0
  74. package/dist/generators/rest.d.ts.map +1 -1
  75. package/dist/generators/rest.js +34 -3
  76. package/dist/generators/rest.js.map +1 -1
  77. package/dist/hierarchical.js +1 -1
  78. package/dist/index.d.ts +7 -1
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +12 -5
  81. package/dist/interceptors.d.ts +21 -0
  82. package/dist/interceptors.d.ts.map +1 -1
  83. package/dist/interceptors.js +27 -1
  84. package/dist/interceptors.js.map +1 -1
  85. package/dist/manifest/generator.d.ts.map +1 -1
  86. package/dist/manifest/generator.js +4 -7
  87. package/dist/manifest/generator.js.map +1 -1
  88. package/dist/manifest/static-manifest.js +10 -10
  89. package/dist/manifest/static-manifest.js.map +1 -1
  90. package/dist/manifest/store.js +1 -1
  91. package/dist/manifest/store.js.map +1 -1
  92. package/dist/manifest.json +19 -19
  93. package/dist/migrations/differ.d.ts +211 -9
  94. package/dist/migrations/differ.d.ts.map +1 -1
  95. package/dist/migrations/differ.js +613 -50
  96. package/dist/migrations/differ.js.map +1 -1
  97. package/dist/migrations/generator.d.ts +31 -4
  98. package/dist/migrations/generator.d.ts.map +1 -1
  99. package/dist/migrations/generator.js +49 -5
  100. package/dist/migrations/generator.js.map +1 -1
  101. package/dist/migrations/index.d.ts +4 -2
  102. package/dist/migrations/index.d.ts.map +1 -1
  103. package/dist/migrations/index.js +6 -3
  104. package/dist/migrations/minor-units.d.ts +162 -0
  105. package/dist/migrations/minor-units.d.ts.map +1 -0
  106. package/dist/migrations/minor-units.js +381 -0
  107. package/dist/migrations/minor-units.js.map +1 -0
  108. package/dist/migrations/orchestrate.js +35 -6
  109. package/dist/migrations/orchestrate.js.map +1 -1
  110. package/dist/migrations/sqlite-rebuild.d.ts +142 -0
  111. package/dist/migrations/sqlite-rebuild.d.ts.map +1 -0
  112. package/dist/migrations/sqlite-rebuild.js +514 -0
  113. package/dist/migrations/sqlite-rebuild.js.map +1 -0
  114. package/dist/migrations/tracker.d.ts +114 -1
  115. package/dist/migrations/tracker.d.ts.map +1 -1
  116. package/dist/migrations/tracker.js +331 -16
  117. package/dist/migrations/tracker.js.map +1 -1
  118. package/dist/migrations/types.d.ts +19 -4
  119. package/dist/migrations/types.d.ts.map +1 -1
  120. package/dist/migrations.js +6 -3
  121. package/dist/object.d.ts +142 -10
  122. package/dist/object.d.ts.map +1 -1
  123. package/dist/object.js +196 -41
  124. package/dist/object.js.map +1 -1
  125. package/dist/postgres-timeouts.d.ts +240 -0
  126. package/dist/postgres-timeouts.d.ts.map +1 -0
  127. package/dist/postgres-timeouts.js +204 -0
  128. package/dist/postgres-timeouts.js.map +1 -0
  129. package/dist/query-bounds.d.ts +101 -0
  130. package/dist/query-bounds.d.ts.map +1 -0
  131. package/dist/query-bounds.js +177 -0
  132. package/dist/query-bounds.js.map +1 -0
  133. package/dist/registry/class-registration.d.ts.map +1 -1
  134. package/dist/registry/class-registration.js +3 -1
  135. package/dist/registry/class-registration.js.map +1 -1
  136. package/dist/registry/manifest-field-merge.d.ts +12 -0
  137. package/dist/registry/manifest-field-merge.d.ts.map +1 -1
  138. package/dist/registry/manifest-field-merge.js +14 -2
  139. package/dist/registry/manifest-field-merge.js.map +1 -1
  140. package/dist/registry/schema-builder.d.ts +22 -1
  141. package/dist/registry/schema-builder.d.ts.map +1 -1
  142. package/dist/registry/schema-builder.js +205 -165
  143. package/dist/registry/schema-builder.js.map +1 -1
  144. package/dist/registry/types.d.ts +35 -3
  145. package/dist/registry/types.d.ts.map +1 -1
  146. package/dist/registry.d.ts +41 -46
  147. package/dist/registry.d.ts.map +1 -1
  148. package/dist/registry.js +61 -83
  149. package/dist/registry.js.map +1 -1
  150. package/dist/scanner/manifest-generator.d.ts +45 -0
  151. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  152. package/dist/scanner/manifest-generator.js +92 -28
  153. package/dist/scanner/manifest-generator.js.map +1 -1
  154. package/dist/scanner/types.d.ts +5 -0
  155. package/dist/scanner/types.d.ts.map +1 -1
  156. package/dist/scanner/types.js.map +1 -1
  157. package/dist/schema/conflict-target.d.ts +104 -0
  158. package/dist/schema/conflict-target.d.ts.map +1 -0
  159. package/dist/schema/conflict-target.js +129 -0
  160. package/dist/schema/conflict-target.js.map +1 -0
  161. package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
  162. package/dist/schema/ddl/base-strategy.js +2 -2
  163. package/dist/schema/ddl/base-strategy.js.map +1 -1
  164. package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
  165. package/dist/schema/ddl/duckdb-strategy.js +2 -1
  166. package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
  167. package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
  168. package/dist/schema/ddl/postgres-strategy.js +12 -1
  169. package/dist/schema/ddl/postgres-strategy.js.map +1 -1
  170. package/dist/schema/generator.d.ts +307 -6
  171. package/dist/schema/generator.d.ts.map +1 -1
  172. package/dist/schema/generator.js +510 -87
  173. package/dist/schema/generator.js.map +1 -1
  174. package/dist/schema/index-utils.d.ts +120 -0
  175. package/dist/schema/index-utils.d.ts.map +1 -1
  176. package/dist/schema/index-utils.js +242 -1
  177. package/dist/schema/index-utils.js.map +1 -1
  178. package/dist/schema/index.d.ts +3 -0
  179. package/dist/schema/index.d.ts.map +1 -1
  180. package/dist/schema/index.js +4 -1
  181. package/dist/schema/live-parity.d.ts +90 -0
  182. package/dist/schema/live-parity.d.ts.map +1 -0
  183. package/dist/schema/live-parity.js +602 -0
  184. package/dist/schema/live-parity.js.map +1 -0
  185. package/dist/schema/manifest-schema.d.ts +121 -0
  186. package/dist/schema/manifest-schema.d.ts.map +1 -0
  187. package/dist/schema/manifest-schema.js +267 -0
  188. package/dist/schema/manifest-schema.js.map +1 -0
  189. package/dist/schema/schema-aggregator.d.ts +24 -10
  190. package/dist/schema/schema-aggregator.d.ts.map +1 -1
  191. package/dist/schema/schema-aggregator.js +35 -90
  192. package/dist/schema/schema-aggregator.js.map +1 -1
  193. package/dist/schema/system-table-shapes.d.ts +65 -0
  194. package/dist/schema/system-table-shapes.d.ts.map +1 -0
  195. package/dist/schema/system-table-shapes.js +187 -0
  196. package/dist/schema/system-table-shapes.js.map +1 -0
  197. package/dist/schema/types.d.ts +103 -4
  198. package/dist/schema/types.d.ts.map +1 -1
  199. package/dist/schema/utils.d.ts +2 -1
  200. package/dist/schema/utils.d.ts.map +1 -1
  201. package/dist/schema/utils.js +5 -3
  202. package/dist/schema/utils.js.map +1 -1
  203. package/dist/schema.js +4 -1
  204. package/dist/smrt-knowledge.json +20 -8
  205. package/dist/sync/apply.d.ts.map +1 -1
  206. package/dist/sync/apply.js +9 -16
  207. package/dist/sync/apply.js.map +1 -1
  208. package/dist/system/compatibility.d.ts +42 -0
  209. package/dist/system/compatibility.d.ts.map +1 -1
  210. package/dist/system/compatibility.js +182 -9
  211. package/dist/system/compatibility.js.map +1 -1
  212. package/dist/system/index.d.ts +1 -0
  213. package/dist/system/index.d.ts.map +1 -1
  214. package/dist/system/index.js +3 -2
  215. package/dist/system/retention.d.ts +237 -0
  216. package/dist/system/retention.d.ts.map +1 -0
  217. package/dist/system/retention.js +497 -0
  218. package/dist/system/retention.js.map +1 -0
  219. package/dist/system/schema.d.ts +100 -15
  220. package/dist/system/schema.d.ts.map +1 -1
  221. package/dist/system/schema.js +81 -45
  222. package/dist/system/schema.js.map +1 -1
  223. package/dist/system/types.d.ts +0 -2
  224. package/dist/system/types.d.ts.map +1 -1
  225. package/dist/testing/database.d.ts.map +1 -1
  226. package/dist/testing/database.js +1 -0
  227. package/dist/testing/database.js.map +1 -1
  228. package/dist/vite-plugin/index.d.ts.map +1 -1
  229. package/dist/vite-plugin/index.js +4 -9
  230. package/dist/vite-plugin/index.js.map +1 -1
  231. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  232. package/dist/vite-plugin/sveltekit-generator.js +71 -5
  233. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  234. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  235. package/dist/vite-plugin/web-collections.js +6 -4
  236. package/dist/vite-plugin/web-collections.js.map +1 -1
  237. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"Dispatches.js","names":[],"sources":["../../../src/dispatch/collections/Dispatches.ts"],"sourcesContent":["/**\n * DispatchCollection - Storage operations for dispatch messages\n *\n * CRUD operations for the _smrt_dispatch system table.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { Dispatch, type DispatchData } from '../models/Dispatch.js';\nimport type { DispatchTenantScope } from '../tenant-resolver.js';\nimport type {\n DispatchCleanupOptions,\n DispatchCleanupResult,\n DispatchListOptions,\n DispatchRetryOptions,\n DispatchStatus,\n} from '../types.js';\n\n/**\n * Build the SQL tenant predicate for the active scope (S5 #1398).\n *\n * Appends a parameterized condition to `conditions` (and the corresponding\n * value to `params`) based on the resolved {@link DispatchTenantScope}:\n *\n * - tenancy off / no scope → no predicate (pre-tenancy behavior)\n * - `enforced` + active tenant T → `(tenant_id = ? OR tenant_id IS NULL)`\n * - `enforced` + no active tenant → `tenant_id IS NULL` (fail-closed global)\n *\n * @param placeholder - Engine-specific positional placeholder (e.g. `$3`) used\n * when a tenant id value is bound. Ignored for the no-value branches.\n * @returns `true` when a value placeholder was consumed (so the caller can\n * advance its param index), `false` otherwise.\n */\nfunction pushTenantPredicate(\n conditions: string[],\n params: unknown[],\n scope: DispatchTenantScope | undefined,\n placeholder: string,\n): boolean {\n if (!scope?.enforced) {\n return false;\n }\n if (scope.tenantId !== null) {\n conditions.push(`(tenant_id = ${placeholder} OR tenant_id IS NULL)`);\n params.push(scope.tenantId);\n return true;\n }\n conditions.push('tenant_id IS NULL');\n return false;\n}\n\n/**\n * Storage operations for dispatches in _smrt_dispatch table\n */\nexport class DispatchCollection {\n /**\n * Insert a new dispatch\n */\n static async insert(\n db: DatabaseInterface,\n dispatch: Dispatch,\n ): Promise<void> {\n const row = dispatch.toRow();\n await db.query(\n `INSERT INTO _smrt_dispatch\n (id, type, source, source_id, payload, status, attempts, last_error,\n processed_at, processed_by, target_subscriber, correlation_id, tenant_id, metadata, created_at, updated_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)`,\n row.id,\n row.type,\n row.source,\n row.source_id,\n row.payload,\n row.status,\n row.attempts,\n row.last_error,\n row.processed_at,\n row.processed_by,\n row.target_subscriber,\n row.correlation_id,\n row.tenant_id,\n row.metadata,\n row.created_at,\n row.updated_at,\n );\n }\n\n /**\n * Update an existing dispatch\n */\n static async update(\n db: DatabaseInterface,\n dispatch: Dispatch,\n ): Promise<void> {\n const row = dispatch.toRow();\n await db.query(\n `UPDATE _smrt_dispatch SET\n type = $1, source = $2, source_id = $3, payload = $4, status = $5,\n attempts = $6, last_error = $7, processed_at = $8, processed_by = $9,\n target_subscriber = $10, correlation_id = $11, metadata = $12, updated_at = $13\n WHERE id = $14`,\n row.type,\n row.source,\n row.source_id,\n row.payload,\n row.status,\n row.attempts,\n row.last_error,\n row.processed_at,\n row.processed_by,\n row.target_subscriber,\n row.correlation_id,\n row.metadata,\n row.updated_at,\n row.id,\n );\n }\n\n /**\n * Get a dispatch by ID (unscoped).\n *\n * Internal/system use only — applies no tenant filter. Public reads go\n * through the DispatchBus, which uses {@link getScoped} so a caller cannot\n * fetch another tenant's dispatch by id (S5 #1398).\n */\n static async get(\n db: DatabaseInterface,\n id: string,\n ): Promise<Dispatch | null> {\n const result = await db.single`\n SELECT * FROM _smrt_dispatch WHERE id = ${id}\n `;\n\n if (!result) return null;\n return Dispatch.fromRow(result as DispatchData);\n }\n\n /**\n * Get a dispatch by ID, enforcing the active tenant scope (S5 #1398).\n *\n * Applies the same tenant predicate as {@link list}/{@link findPending}, so a\n * cross-tenant or out-of-scope id lookup returns `null` rather than leaking\n * the row. The scope is derived server-side by the DispatchBus.\n */\n static async getScoped(\n db: DatabaseInterface,\n id: string,\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch | null> {\n const conditions: string[] = ['id = $1'];\n const params: unknown[] = [id];\n\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n const { rows } = await db.query(\n `SELECT * FROM _smrt_dispatch WHERE ${conditions.join(' AND ')} LIMIT 1`,\n ...params,\n );\n\n if (rows.length === 0) return null;\n return Dispatch.fromRow(rows[0] as unknown as DispatchData);\n }\n\n /**\n * List dispatches with filtering\n */\n static async list(\n db: DatabaseInterface,\n options: DispatchListOptions = {},\n ): Promise<Dispatch[]> {\n const conditions: string[] = [];\n const params: unknown[] = [];\n let paramIndex = 1;\n\n if (options.status) {\n conditions.push(`status = $${paramIndex++}`);\n params.push(options.status);\n }\n\n if (options.source) {\n conditions.push(`source = $${paramIndex++}`);\n params.push(options.source);\n }\n\n if (options.type) {\n conditions.push(`type = $${paramIndex++}`);\n params.push(options.type);\n }\n\n if (options.targetSubscriber) {\n conditions.push(`target_subscriber = $${paramIndex++}`);\n params.push(options.targetSubscriber);\n }\n\n if (options.correlationId !== undefined) {\n conditions.push(`correlation_id = $${paramIndex++}`);\n params.push(options.correlationId);\n }\n\n // Tenant isolation (S5 #1398). Scope is derived server-side by the bus;\n // callers cannot widen it. See DispatchListOptions.tenantScope.\n if (\n pushTenantPredicate(\n conditions,\n params,\n options.tenantScope,\n `$${paramIndex}`,\n )\n ) {\n paramIndex++;\n }\n\n let sql = 'SELECT * FROM _smrt_dispatch';\n if (conditions.length > 0) {\n sql += ` WHERE ${conditions.join(' AND ')}`;\n }\n\n // Whitelist allowed orderBy values to prevent SQL injection\n const allowedOrderBy = [\n 'created_at DESC',\n 'created_at ASC',\n 'updated_at DESC',\n 'updated_at ASC',\n 'type',\n 'type DESC',\n 'type ASC',\n 'status',\n 'status DESC',\n 'status ASC',\n 'source',\n 'source DESC',\n 'source ASC',\n ];\n const safeOrderBy =\n options.orderBy && allowedOrderBy.includes(options.orderBy)\n ? options.orderBy\n : 'created_at DESC';\n sql += ` ORDER BY ${safeOrderBy}`;\n\n // Use parameterized queries for limit/offset\n if (options.limit !== undefined) {\n sql += ` LIMIT $${paramIndex++}`;\n params.push(options.limit);\n }\n\n if (options.offset !== undefined) {\n sql += ` OFFSET $${paramIndex++}`;\n params.push(options.offset);\n }\n\n const { rows } = await db.query(sql, ...params);\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Find pending dispatches for a set of signal types\n *\n * @param subscriber - When provided, filters to dispatches with no target\n * or targeted specifically at this subscriber (for fan-out delivery)\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts results so a subscriber in one tenant cannot see\n * another tenant's dispatch. See {@link pushTenantPredicate} for the exact\n * semantics (off → no filter; active tenant → that tenant + global; on but\n * no tenant → global only).\n */\n static async findPending(\n db: DatabaseInterface,\n signalTypes: string[],\n limit: number = 100,\n subscriber?: string,\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch[]> {\n if (signalTypes.length === 0) {\n return [];\n }\n\n let paramIndex = 1;\n const placeholders = signalTypes.map(() => `$${paramIndex++}`).join(', ');\n const conditions: string[] = [\n \"status = 'pending'\",\n `type IN (${placeholders})`,\n ];\n const params: unknown[] = [...signalTypes];\n\n if (subscriber) {\n conditions.push(\n `(target_subscriber IS NULL OR target_subscriber = $${paramIndex++})`,\n );\n params.push(subscriber);\n }\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const sql = `SELECT * FROM _smrt_dispatch\n WHERE ${conditions.join(' AND ')}\n ORDER BY created_at ASC LIMIT $${paramIndex}`;\n params.push(limit);\n\n const { rows } = await db.query(sql, ...params);\n\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Atomically claim a pending dispatch for processing (S5 #1398).\n *\n * Performs a conditional `UPDATE ... WHERE id = ? AND status = 'pending'`\n * (plus the tenant predicate for the active scope) and reports whether the\n * row was claimed by THIS call. Only the caller whose update affected a row\n * may run the handler, which closes the claim TOCTOU window where a `process`\n * filter-then-update could let two workers (or a tenant-scoped and a\n * system-scoped processor) both pick up the same compete/global dispatch.\n *\n * On a successful claim the dispatch's status/attempts/updated_at are\n * advanced in the DB; the in-memory `dispatch` instance is updated to match\n * so the caller can proceed to `update()` it to completed/failed.\n *\n * @returns `true` if this call claimed the dispatch, `false` if another\n * worker already moved it out of `pending` (or it is not visible in scope).\n */\n static async claim(\n db: DatabaseInterface,\n dispatch: Dispatch,\n tenantScope?: DispatchTenantScope,\n ): Promise<boolean> {\n dispatch.markProcessing();\n const row = dispatch.toRow();\n\n const conditions: string[] = ['id = $4', \"status = 'pending'\"];\n const params: unknown[] = [\n row.status,\n row.attempts,\n row.updated_at,\n row.id,\n ];\n let paramIndex = 5;\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const { rowCount } = await db.query(\n `UPDATE _smrt_dispatch\n SET status = $1, attempts = $2, updated_at = $3\n WHERE ${conditions.join(' AND ')}`,\n ...params,\n );\n\n return (rowCount ?? 0) > 0;\n }\n\n /**\n * Find failed dispatches eligible for retry.\n *\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts retryable rows to the active tenant (plus global) so a\n * tenant's `retry()` cannot reset another tenant's failed dispatches. See\n * {@link pushTenantPredicate} for the exact semantics.\n */\n static async findRetryable(\n db: DatabaseInterface,\n options: DispatchRetryOptions = {},\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch[]> {\n const conditions: string[] = [\"status = 'failed'\"];\n const params: unknown[] = [];\n let paramIndex = 1;\n\n if (options.maxAttempts) {\n conditions.push(`attempts < $${paramIndex++}`);\n params.push(options.maxAttempts);\n }\n\n if (options.olderThan) {\n conditions.push(`created_at < $${paramIndex++}`);\n params.push(options.olderThan.toISOString());\n }\n\n if (options.signalTypes && options.signalTypes.length > 0) {\n const placeholders = options.signalTypes\n .map(() => `$${paramIndex++}`)\n .join(', ');\n conditions.push(`type IN (${placeholders})`);\n params.push(...options.signalTypes);\n }\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const { rows } = await db.query(\n `SELECT * FROM _smrt_dispatch WHERE ${conditions.join(' AND ')} ORDER BY created_at ASC`,\n ...params,\n );\n\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Count dispatches by status\n */\n static async countByStatus(\n db: DatabaseInterface,\n status: DispatchStatus,\n ): Promise<number> {\n const result = await db.single`\n SELECT COUNT(*) as count FROM _smrt_dispatch WHERE status = ${status}\n `;\n return (result as { count: number })?.count || 0;\n }\n\n /**\n * Delete a dispatch\n */\n static async delete(db: DatabaseInterface, id: string): Promise<void> {\n await db.query(`DELETE FROM _smrt_dispatch WHERE id = $1`, id);\n }\n\n /**\n * Cleanup old dispatches.\n *\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts deletions to the active tenant (plus global) so a\n * tenant's `cleanup()` cannot delete another tenant's dispatches. See\n * {@link pushTenantPredicate} for the exact semantics.\n */\n static async cleanup(\n db: DatabaseInterface,\n options: DispatchCleanupOptions = {},\n tenantScope?: DispatchTenantScope,\n ): Promise<DispatchCleanupResult> {\n const result: DispatchCleanupResult = {\n completedDeleted: 0,\n failedDeleted: 0,\n };\n\n if (options.completedOlderThanDays) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - options.completedOlderThanDays);\n\n const conditions: string[] = [\n \"status = 'completed'\",\n 'processed_at < $1',\n ];\n const params: unknown[] = [cutoff.toISOString()];\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n const { rowCount } = await db.query(\n `DELETE FROM _smrt_dispatch WHERE ${conditions.join(' AND ')}`,\n ...params,\n );\n result.completedDeleted = rowCount || 0;\n }\n\n if (options.failedOlderThanDays) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - options.failedOlderThanDays);\n\n const conditions: string[] = [\"status = 'failed'\", 'updated_at < $1'];\n const params: unknown[] = [cutoff.toISOString()];\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n const { rowCount } = await db.query(\n `DELETE FROM _smrt_dispatch WHERE ${conditions.join(' AND ')}`,\n ...params,\n );\n result.failedDeleted = rowCount || 0;\n }\n\n return result;\n }\n\n /**\n * Check if dispatch table exists\n */\n static async tableExists(db: DatabaseInterface): Promise<boolean> {\n try {\n await db.query(`SELECT 1 FROM _smrt_dispatch LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Dispatches.js","names":[],"sources":["../../../src/dispatch/collections/Dispatches.ts"],"sourcesContent":["/**\n * DispatchCollection - Storage operations for dispatch messages\n *\n * CRUD operations for the _smrt_dispatch system table.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { Dispatch, type DispatchData } from '../models/Dispatch.js';\nimport type { DispatchTenantScope } from '../tenant-resolver.js';\nimport type {\n DispatchCleanupOptions,\n DispatchCleanupResult,\n DispatchListOptions,\n DispatchRetryOptions,\n DispatchStatus,\n} from '../types.js';\n\n/**\n * Build the SQL tenant predicate for the active scope (S5 #1398).\n *\n * Appends a parameterized condition to `conditions` (and the corresponding\n * value to `params`) based on the resolved {@link DispatchTenantScope}:\n *\n * - tenancy off / no scope → no predicate (pre-tenancy behavior)\n * - `enforced` + active tenant T → `(tenant_id = ? OR tenant_id IS NULL)`\n * - `enforced` + no active tenant → `tenant_id IS NULL` (fail-closed global)\n *\n * @param placeholder - Engine-specific positional placeholder (e.g. `$3`) used\n * when a tenant id value is bound. Ignored for the no-value branches.\n * @returns `true` when a value placeholder was consumed (so the caller can\n * advance its param index), `false` otherwise.\n */\nfunction pushTenantPredicate(\n conditions: string[],\n params: unknown[],\n scope: DispatchTenantScope | undefined,\n placeholder: string,\n): boolean {\n if (!scope?.enforced) {\n return false;\n }\n if (scope.tenantId !== null) {\n conditions.push(`(tenant_id = ${placeholder} OR tenant_id IS NULL)`);\n params.push(scope.tenantId);\n return true;\n }\n conditions.push('tenant_id IS NULL');\n return false;\n}\n\n/**\n * Count the rows a cleanup predicate selects, then delete them (#2375).\n *\n * Counting first gives a usable figure on every adapter — `rowCount` is not\n * reliably populated across the engines SMRT supports — and is what lets\n * `dryRun` preview the very same predicate the real cleanup runs. The two\n * statements are not transactional, so the figure is approximate under\n * concurrent writers.\n */\nasync function deleteOrCount(\n db: DatabaseInterface,\n conditions: string[],\n params: unknown[],\n dryRun: boolean,\n): Promise<number> {\n const where = conditions.join(' AND ');\n const countResult = await db.query(\n `SELECT COUNT(*) AS total FROM _smrt_dispatch WHERE ${where}`,\n ...params,\n );\n const total = Number(countResult.rows?.[0]?.total ?? 0);\n if (!Number.isFinite(total) || total <= 0) return 0;\n\n if (!dryRun) {\n await db.query(`DELETE FROM _smrt_dispatch WHERE ${where}`, ...params);\n }\n\n return total;\n}\n\n/**\n * Storage operations for dispatches in _smrt_dispatch table\n */\nexport class DispatchCollection {\n /**\n * Insert a new dispatch\n */\n static async insert(\n db: DatabaseInterface,\n dispatch: Dispatch,\n ): Promise<void> {\n const row = dispatch.toRow();\n await db.query(\n `INSERT INTO _smrt_dispatch\n (id, type, source, source_id, payload, status, attempts, last_error,\n processed_at, processed_by, target_subscriber, correlation_id, tenant_id, metadata, created_at, updated_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)`,\n row.id,\n row.type,\n row.source,\n row.source_id,\n row.payload,\n row.status,\n row.attempts,\n row.last_error,\n row.processed_at,\n row.processed_by,\n row.target_subscriber,\n row.correlation_id,\n row.tenant_id,\n row.metadata,\n row.created_at,\n row.updated_at,\n );\n }\n\n /**\n * Update an existing dispatch\n */\n static async update(\n db: DatabaseInterface,\n dispatch: Dispatch,\n ): Promise<void> {\n const row = dispatch.toRow();\n await db.query(\n `UPDATE _smrt_dispatch SET\n type = $1, source = $2, source_id = $3, payload = $4, status = $5,\n attempts = $6, last_error = $7, processed_at = $8, processed_by = $9,\n target_subscriber = $10, correlation_id = $11, metadata = $12, updated_at = $13\n WHERE id = $14`,\n row.type,\n row.source,\n row.source_id,\n row.payload,\n row.status,\n row.attempts,\n row.last_error,\n row.processed_at,\n row.processed_by,\n row.target_subscriber,\n row.correlation_id,\n row.metadata,\n row.updated_at,\n row.id,\n );\n }\n\n /**\n * Get a dispatch by ID (unscoped).\n *\n * Internal/system use only — applies no tenant filter. Public reads go\n * through the DispatchBus, which uses {@link getScoped} so a caller cannot\n * fetch another tenant's dispatch by id (S5 #1398).\n */\n static async get(\n db: DatabaseInterface,\n id: string,\n ): Promise<Dispatch | null> {\n const result = await db.single`\n SELECT * FROM _smrt_dispatch WHERE id = ${id}\n `;\n\n if (!result) return null;\n return Dispatch.fromRow(result as DispatchData);\n }\n\n /**\n * Get a dispatch by ID, enforcing the active tenant scope (S5 #1398).\n *\n * Applies the same tenant predicate as {@link list}/{@link findPending}, so a\n * cross-tenant or out-of-scope id lookup returns `null` rather than leaking\n * the row. The scope is derived server-side by the DispatchBus.\n */\n static async getScoped(\n db: DatabaseInterface,\n id: string,\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch | null> {\n const conditions: string[] = ['id = $1'];\n const params: unknown[] = [id];\n\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n const { rows } = await db.query(\n `SELECT * FROM _smrt_dispatch WHERE ${conditions.join(' AND ')} LIMIT 1`,\n ...params,\n );\n\n if (rows.length === 0) return null;\n return Dispatch.fromRow(rows[0] as unknown as DispatchData);\n }\n\n /**\n * List dispatches with filtering\n */\n static async list(\n db: DatabaseInterface,\n options: DispatchListOptions = {},\n ): Promise<Dispatch[]> {\n const conditions: string[] = [];\n const params: unknown[] = [];\n let paramIndex = 1;\n\n if (options.status) {\n conditions.push(`status = $${paramIndex++}`);\n params.push(options.status);\n }\n\n if (options.source) {\n conditions.push(`source = $${paramIndex++}`);\n params.push(options.source);\n }\n\n if (options.type) {\n conditions.push(`type = $${paramIndex++}`);\n params.push(options.type);\n }\n\n if (options.targetSubscriber) {\n conditions.push(`target_subscriber = $${paramIndex++}`);\n params.push(options.targetSubscriber);\n }\n\n if (options.correlationId !== undefined) {\n conditions.push(`correlation_id = $${paramIndex++}`);\n params.push(options.correlationId);\n }\n\n // Tenant isolation (S5 #1398). Scope is derived server-side by the bus;\n // callers cannot widen it. See DispatchListOptions.tenantScope.\n if (\n pushTenantPredicate(\n conditions,\n params,\n options.tenantScope,\n `$${paramIndex}`,\n )\n ) {\n paramIndex++;\n }\n\n let sql = 'SELECT * FROM _smrt_dispatch';\n if (conditions.length > 0) {\n sql += ` WHERE ${conditions.join(' AND ')}`;\n }\n\n // Whitelist allowed orderBy values to prevent SQL injection\n const allowedOrderBy = [\n 'created_at DESC',\n 'created_at ASC',\n 'updated_at DESC',\n 'updated_at ASC',\n 'type',\n 'type DESC',\n 'type ASC',\n 'status',\n 'status DESC',\n 'status ASC',\n 'source',\n 'source DESC',\n 'source ASC',\n ];\n const safeOrderBy =\n options.orderBy && allowedOrderBy.includes(options.orderBy)\n ? options.orderBy\n : 'created_at DESC';\n sql += ` ORDER BY ${safeOrderBy}`;\n\n // Use parameterized queries for limit/offset\n if (options.limit !== undefined) {\n sql += ` LIMIT $${paramIndex++}`;\n params.push(options.limit);\n }\n\n if (options.offset !== undefined) {\n sql += ` OFFSET $${paramIndex++}`;\n params.push(options.offset);\n }\n\n const { rows } = await db.query(sql, ...params);\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Find pending dispatches for a set of signal types\n *\n * @param subscriber - When provided, filters to dispatches with no target\n * or targeted specifically at this subscriber (for fan-out delivery)\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts results so a subscriber in one tenant cannot see\n * another tenant's dispatch. See {@link pushTenantPredicate} for the exact\n * semantics (off → no filter; active tenant → that tenant + global; on but\n * no tenant → global only).\n */\n static async findPending(\n db: DatabaseInterface,\n signalTypes: string[],\n limit: number = 100,\n subscriber?: string,\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch[]> {\n if (signalTypes.length === 0) {\n return [];\n }\n\n let paramIndex = 1;\n const placeholders = signalTypes.map(() => `$${paramIndex++}`).join(', ');\n const conditions: string[] = [\n \"status = 'pending'\",\n `type IN (${placeholders})`,\n ];\n const params: unknown[] = [...signalTypes];\n\n if (subscriber) {\n conditions.push(\n `(target_subscriber IS NULL OR target_subscriber = $${paramIndex++})`,\n );\n params.push(subscriber);\n }\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const sql = `SELECT * FROM _smrt_dispatch\n WHERE ${conditions.join(' AND ')}\n ORDER BY created_at ASC LIMIT $${paramIndex}`;\n params.push(limit);\n\n const { rows } = await db.query(sql, ...params);\n\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Atomically claim a pending dispatch for processing (S5 #1398).\n *\n * Performs a conditional `UPDATE ... WHERE id = ? AND status = 'pending'`\n * (plus the tenant predicate for the active scope) and reports whether the\n * row was claimed by THIS call. Only the caller whose update affected a row\n * may run the handler, which closes the claim TOCTOU window where a `process`\n * filter-then-update could let two workers (or a tenant-scoped and a\n * system-scoped processor) both pick up the same compete/global dispatch.\n *\n * On a successful claim the dispatch's status/attempts/updated_at are\n * advanced in the DB; the in-memory `dispatch` instance is updated to match\n * so the caller can proceed to `update()` it to completed/failed.\n *\n * @returns `true` if this call claimed the dispatch, `false` if another\n * worker already moved it out of `pending` (or it is not visible in scope).\n */\n static async claim(\n db: DatabaseInterface,\n dispatch: Dispatch,\n tenantScope?: DispatchTenantScope,\n ): Promise<boolean> {\n dispatch.markProcessing();\n const row = dispatch.toRow();\n\n const conditions: string[] = ['id = $4', \"status = 'pending'\"];\n const params: unknown[] = [\n row.status,\n row.attempts,\n row.updated_at,\n row.id,\n ];\n let paramIndex = 5;\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const { rowCount } = await db.query(\n `UPDATE _smrt_dispatch\n SET status = $1, attempts = $2, updated_at = $3\n WHERE ${conditions.join(' AND ')}`,\n ...params,\n );\n\n return (rowCount ?? 0) > 0;\n }\n\n /**\n * Find failed dispatches eligible for retry.\n *\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts retryable rows to the active tenant (plus global) so a\n * tenant's `retry()` cannot reset another tenant's failed dispatches. See\n * {@link pushTenantPredicate} for the exact semantics.\n */\n static async findRetryable(\n db: DatabaseInterface,\n options: DispatchRetryOptions = {},\n tenantScope?: DispatchTenantScope,\n ): Promise<Dispatch[]> {\n const conditions: string[] = [\"status = 'failed'\"];\n const params: unknown[] = [];\n let paramIndex = 1;\n\n if (options.maxAttempts) {\n conditions.push(`attempts < $${paramIndex++}`);\n params.push(options.maxAttempts);\n }\n\n if (options.olderThan) {\n conditions.push(`created_at < $${paramIndex++}`);\n params.push(options.olderThan.toISOString());\n }\n\n if (options.signalTypes && options.signalTypes.length > 0) {\n const placeholders = options.signalTypes\n .map(() => `$${paramIndex++}`)\n .join(', ');\n conditions.push(`type IN (${placeholders})`);\n params.push(...options.signalTypes);\n }\n\n if (\n pushTenantPredicate(conditions, params, tenantScope, `$${paramIndex}`)\n ) {\n paramIndex++;\n }\n\n const { rows } = await db.query(\n `SELECT * FROM _smrt_dispatch WHERE ${conditions.join(' AND ')} ORDER BY created_at ASC`,\n ...params,\n );\n\n return rows.map((row: Record<string, unknown>) =>\n Dispatch.fromRow(row as unknown as DispatchData),\n );\n }\n\n /**\n * Count dispatches by status\n */\n static async countByStatus(\n db: DatabaseInterface,\n status: DispatchStatus,\n ): Promise<number> {\n const result = await db.single`\n SELECT COUNT(*) as count FROM _smrt_dispatch WHERE status = ${status}\n `;\n return (result as { count: number })?.count || 0;\n }\n\n /**\n * Delete a dispatch\n */\n static async delete(db: DatabaseInterface, id: string): Promise<void> {\n await db.query(`DELETE FROM _smrt_dispatch WHERE id = $1`, id);\n }\n\n /**\n * Cleanup old dispatches.\n *\n * @param tenantScope - Active tenant scope (S5 #1398). Derived server-side by\n * the bus; restricts deletions to the active tenant (plus global) so a\n * tenant's `cleanup()` cannot delete another tenant's dispatches. See\n * {@link pushTenantPredicate} for the exact semantics.\n */\n static async cleanup(\n db: DatabaseInterface,\n options: DispatchCleanupOptions = {},\n tenantScope?: DispatchTenantScope,\n ): Promise<DispatchCleanupResult> {\n const result: DispatchCleanupResult = {\n completedDeleted: 0,\n failedDeleted: 0,\n };\n\n if (options.completedOlderThanDays) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - options.completedOlderThanDays);\n\n const conditions: string[] = [\n \"status = 'completed'\",\n 'processed_at < $1',\n ];\n const params: unknown[] = [cutoff.toISOString()];\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n result.completedDeleted = await deleteOrCount(\n db,\n conditions,\n params,\n options.dryRun ?? false,\n );\n }\n\n if (options.failedOlderThanDays) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - options.failedOlderThanDays);\n\n const conditions: string[] = [\"status = 'failed'\", 'updated_at < $1'];\n const params: unknown[] = [cutoff.toISOString()];\n pushTenantPredicate(conditions, params, tenantScope, '$2');\n\n result.failedDeleted = await deleteOrCount(\n db,\n conditions,\n params,\n options.dryRun ?? false,\n );\n }\n\n return result;\n }\n\n /**\n * Check if dispatch table exists\n */\n static async tableExists(db: DatabaseInterface): Promise<boolean> {\n try {\n await db.query(`SELECT 1 FROM _smrt_dispatch LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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"}
@@ -121,6 +121,11 @@ export interface DispatchCleanupOptions {
121
121
  completedOlderThanDays?: number;
122
122
  /** Delete failed dispatches older than this many days */
123
123
  failedOlderThanDays?: number;
124
+ /**
125
+ * Count the dispatches the cutoffs select without deleting them (#2375).
126
+ * The result reports what a real cleanup with the same options would remove.
127
+ */
128
+ dryRun?: boolean;
124
129
  }
125
130
  /**
126
131
  * Result of dispatch cleanup
@@ -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,46 @@
1
+ /**
2
+ * In-process write serialization for embedded database engines (#2360).
3
+ *
4
+ * File-backed SQLite (libsql) allows one writer at a time and the SDK opens a
5
+ * NEW connection for every null-aware upsert attempt (`transaction("write")`),
6
+ * with no busy timeout on either connection. Any other write on the root
7
+ * connection while such a transaction is open — most commonly the change-feed
8
+ * append that follows every model save — collides as `SQLITE_BUSY`, and under
9
+ * sustained concurrency (a `Promise.all` of creates) the two sides livelock
10
+ * through every retry tier. Before the tenant-led conflict target (#2360) the
11
+ * null-aware path was rare (custom conflict keys with NULL values, #1246);
12
+ * with `tenant_id` leading every tenant-scoped default key, EVERY NULL-tenant
13
+ * (global) create takes it, so the collision became the steady state.
14
+ *
15
+ * The queue makes the process a single writer per database for exactly the
16
+ * two participants of that collision:
17
+ *
18
+ * - a `save()` whose upsert conflict values contain NULL (the only shape the
19
+ * SDK routes through a second-connection transaction), and
20
+ * - the change-feed append (a root-connection write that follows every save).
21
+ *
22
+ * Each hold is a LEAF database call — never an interceptor or hook — so the
23
+ * queue cannot deadlock on nested saves. PostgreSQL is never serialized here:
24
+ * its null-aware upsert already arbitrates through advisory locks
25
+ * server-side, and cross-process writers exist that an in-process queue could
26
+ * not order anyway (which is also why this queue is a mitigation for embedded
27
+ * engines' in-process writers, not a cross-process guarantee — embedded
28
+ * engines have no cross-process writers in supported deployments).
29
+ */
30
+ interface QueueableDatabase {
31
+ url?: string;
32
+ }
33
+ /**
34
+ * Whether `db` is an embedded engine (SQLite/DuckDB/JSON — anything that is
35
+ * not PostgreSQL). Handles without a URL are treated as embedded: serializing
36
+ * them is at worst a throughput cost, while not serializing an embedded
37
+ * handle reintroduces the livelock.
38
+ */
39
+ export declare function isEmbeddedDatabase(db: QueueableDatabase): boolean;
40
+ /**
41
+ * Run `operation` after every previously queued write on the same database
42
+ * has settled. When `serialize` is false the operation runs immediately.
43
+ */
44
+ export declare function withEmbeddedWriteQueue<T>(db: QueueableDatabase, serialize: boolean, operation: () => Promise<T>): Promise<T>;
45
+ export {};
46
+ //# sourceMappingURL=embedded-write-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedded-write-queue.d.ts","sourceRoot":"","sources":["../src/embedded-write-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAMH,UAAU,iBAAiB;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAEjE;AAMD;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,OAAO,EAClB,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAC1B,OAAO,CAAC,CAAC,CAAC,CAoBZ"}
@@ -0,0 +1,66 @@
1
+ //#region src/embedded-write-queue.ts
2
+ /**
3
+ * In-process write serialization for embedded database engines (#2360).
4
+ *
5
+ * File-backed SQLite (libsql) allows one writer at a time and the SDK opens a
6
+ * NEW connection for every null-aware upsert attempt (`transaction("write")`),
7
+ * with no busy timeout on either connection. Any other write on the root
8
+ * connection while such a transaction is open — most commonly the change-feed
9
+ * append that follows every model save — collides as `SQLITE_BUSY`, and under
10
+ * sustained concurrency (a `Promise.all` of creates) the two sides livelock
11
+ * through every retry tier. Before the tenant-led conflict target (#2360) the
12
+ * null-aware path was rare (custom conflict keys with NULL values, #1246);
13
+ * with `tenant_id` leading every tenant-scoped default key, EVERY NULL-tenant
14
+ * (global) create takes it, so the collision became the steady state.
15
+ *
16
+ * The queue makes the process a single writer per database for exactly the
17
+ * two participants of that collision:
18
+ *
19
+ * - a `save()` whose upsert conflict values contain NULL (the only shape the
20
+ * SDK routes through a second-connection transaction), and
21
+ * - the change-feed append (a root-connection write that follows every save).
22
+ *
23
+ * Each hold is a LEAF database call — never an interceptor or hook — so the
24
+ * queue cannot deadlock on nested saves. PostgreSQL is never serialized here:
25
+ * its null-aware upsert already arbitrates through advisory locks
26
+ * server-side, and cross-process writers exist that an in-process queue could
27
+ * not order anyway (which is also why this queue is a mitigation for embedded
28
+ * engines' in-process writers, not a cross-process guarantee — embedded
29
+ * engines have no cross-process writers in supported deployments).
30
+ */
31
+ /** Queue tail per database identity (URL string, or the handle itself). */
32
+ var urlQueues = /* @__PURE__ */ new Map();
33
+ var handleQueues = /* @__PURE__ */ new WeakMap();
34
+ /**
35
+ * Whether `db` is an embedded engine (SQLite/DuckDB/JSON — anything that is
36
+ * not PostgreSQL). Handles without a URL are treated as embedded: serializing
37
+ * them is at worst a throughput cost, while not serializing an embedded
38
+ * handle reintroduces the livelock.
39
+ */
40
+ function isEmbeddedDatabase(db) {
41
+ return !/^postgres(?:ql)?:/iu.test(db?.url ?? "");
42
+ }
43
+ function queueKey(db) {
44
+ return typeof db?.url === "string" && db.url.length > 0 ? db.url : db;
45
+ }
46
+ /**
47
+ * Run `operation` after every previously queued write on the same database
48
+ * has settled. When `serialize` is false the operation runs immediately.
49
+ */
50
+ async function withEmbeddedWriteQueue(db, serialize, operation) {
51
+ if (!serialize) return operation();
52
+ const key = queueKey(db);
53
+ const next = ((typeof key === "string" ? urlQueues.get(key) : handleQueues.get(key)) ?? Promise.resolve()).then(operation, operation);
54
+ const tail = next.catch(() => void 0);
55
+ if (typeof key === "string") {
56
+ urlQueues.set(key, tail);
57
+ tail.finally(() => {
58
+ if (urlQueues.get(key) === tail) urlQueues.delete(key);
59
+ });
60
+ } else handleQueues.set(key, tail);
61
+ return next;
62
+ }
63
+ //#endregion
64
+ export { isEmbeddedDatabase, withEmbeddedWriteQueue };
65
+
66
+ //# sourceMappingURL=embedded-write-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedded-write-queue.js","names":[],"sources":["../src/embedded-write-queue.ts"],"sourcesContent":["/**\n * In-process write serialization for embedded database engines (#2360).\n *\n * File-backed SQLite (libsql) allows one writer at a time and the SDK opens a\n * NEW connection for every null-aware upsert attempt (`transaction(\"write\")`),\n * with no busy timeout on either connection. Any other write on the root\n * connection while such a transaction is open — most commonly the change-feed\n * append that follows every model save — collides as `SQLITE_BUSY`, and under\n * sustained concurrency (a `Promise.all` of creates) the two sides livelock\n * through every retry tier. Before the tenant-led conflict target (#2360) the\n * null-aware path was rare (custom conflict keys with NULL values, #1246);\n * with `tenant_id` leading every tenant-scoped default key, EVERY NULL-tenant\n * (global) create takes it, so the collision became the steady state.\n *\n * The queue makes the process a single writer per database for exactly the\n * two participants of that collision:\n *\n * - a `save()` whose upsert conflict values contain NULL (the only shape the\n * SDK routes through a second-connection transaction), and\n * - the change-feed append (a root-connection write that follows every save).\n *\n * Each hold is a LEAF database call — never an interceptor or hook — so the\n * queue cannot deadlock on nested saves. PostgreSQL is never serialized here:\n * its null-aware upsert already arbitrates through advisory locks\n * server-side, and cross-process writers exist that an in-process queue could\n * not order anyway (which is also why this queue is a mitigation for embedded\n * engines' in-process writers, not a cross-process guarantee — embedded\n * engines have no cross-process writers in supported deployments).\n */\n\n/** Queue tail per database identity (URL string, or the handle itself). */\nconst urlQueues = new Map<string, Promise<unknown>>();\nconst handleQueues = new WeakMap<object, Promise<unknown>>();\n\ninterface QueueableDatabase {\n url?: string;\n}\n\n/**\n * Whether `db` is an embedded engine (SQLite/DuckDB/JSON — anything that is\n * not PostgreSQL). Handles without a URL are treated as embedded: serializing\n * them is at worst a throughput cost, while not serializing an embedded\n * handle reintroduces the livelock.\n */\nexport function isEmbeddedDatabase(db: QueueableDatabase): boolean {\n return !/^postgres(?:ql)?:/iu.test(db?.url ?? '');\n}\n\nfunction queueKey(db: QueueableDatabase): string | object {\n return typeof db?.url === 'string' && db.url.length > 0 ? db.url : db;\n}\n\n/**\n * Run `operation` after every previously queued write on the same database\n * has settled. When `serialize` is false the operation runs immediately.\n */\nexport async function withEmbeddedWriteQueue<T>(\n db: QueueableDatabase,\n serialize: boolean,\n operation: () => Promise<T>,\n): Promise<T> {\n if (!serialize) return operation();\n\n const key = queueKey(db);\n const previous =\n (typeof key === 'string' ? urlQueues.get(key) : handleQueues.get(key)) ??\n Promise.resolve();\n const next = previous.then(operation, operation);\n // The stored tail must never reject, or one failed write would poison the\n // queue for every later writer.\n const tail = next.catch(() => undefined);\n if (typeof key === 'string') {\n urlQueues.set(key, tail);\n void tail.finally(() => {\n if (urlQueues.get(key) === tail) urlQueues.delete(key);\n });\n } else {\n handleQueues.set(key, tail);\n }\n return next;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,IAAM,4BAAY,IAAI,IAA8B;AACpD,IAAM,+BAAe,IAAI,QAAkC;;;;;;;AAY3D,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,CAAC,sBAAsB,KAAK,IAAI,OAAO,EAAE;AAClD;AAEA,SAAS,SAAS,IAAwC;CACxD,OAAO,OAAO,IAAI,QAAQ,YAAY,GAAG,IAAI,SAAS,IAAI,GAAG,MAAM;AACrE;;;;;AAMA,eAAsB,uBACpB,IACA,WACA,WACY;CACZ,IAAI,CAAC,WAAW,OAAO,UAAU;CAEjC,MAAM,MAAM,SAAS,EAAE;CAIvB,MAAM,SAFH,OAAO,QAAQ,WAAW,UAAU,IAAI,GAAG,IAAI,aAAa,IAAI,GAAG,MACpE,QAAQ,QAAQ,EAAA,CACI,KAAK,WAAW,SAAS;CAG/C,MAAM,OAAO,KAAK,YAAY,KAAA,CAAS;CACvC,IAAI,OAAO,QAAQ,UAAU;EAC3B,UAAU,IAAI,KAAK,IAAI;EACvB,KAAU,cAAc;GACtB,IAAI,UAAU,IAAI,GAAG,MAAM,MAAM,UAAU,OAAO,GAAG;EACvD,CAAC;CACH,OACE,aAAa,IAAI,KAAK,IAAI;CAE5B,OAAO;AACT"}
@@ -40,6 +40,13 @@ export declare class EmbeddingStorage {
40
40
  model?: string;
41
41
  limit?: number;
42
42
  minSimilarity?: number;
43
+ /**
44
+ * Restrict candidates to these object ids BEFORE ranking (#2365).
45
+ * `undefined` means no restriction; an empty array means no candidates
46
+ * (the search returns no results). Used by tenant-scoped collections so
47
+ * top-K never ranks rows the caller is not allowed to see.
48
+ */
49
+ objectIds?: string[];
43
50
  }, vector?: VectorCapabilities): Promise<Array<{
44
51
  objectId: string;
45
52
  similarity: number;
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/embeddings/storage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAO/C;;GAEG;AACH,qBAAa,gBAAgB;IAC3B;;;;;;OAMG;WACU,MAAM,CACjB,EAAE,EAAE,iBAAiB,EACrB,IAAI,EAAE;QACJ,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,EACD,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,IAAI,CAAC;IA6ChB;;;;;;;;;;;;;OAaG;WACU,aAAa,CACxB,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EAAE,EACnB,OAAO,GAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;KACnB,EACN,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAuE3D;;;;;;;OAOG;WACU,mBAAmB,CAC9B,GAAG,EAAE,iBAAiB,EACtB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAShB;;;;;;;;;OASG;WACU,GAAG,CACd,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAclC;;;;;;;OAOG;WACU,YAAY,CACvB,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,eAAe,EAAE,CAAC;IAY7B;;;;;;;;OAQG;WACU,YAAY,CACvB,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,EAClB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,EAAE,CAAC;IAqB7B;;;;;;;OAOG;WACU,0BAA0B,CACrC,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,EAAE,CAAC;IAWpB;;;;;;OAMG;WACU,eAAe,CAC1B,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC;IAQhB;;;;;;;;OAQG;WACU,MAAM,CACjB,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC;IAWhB;;;;;OAKG;WACU,WAAW,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;IASjE;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ;CAexB"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/embeddings/storage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAO/C;;GAEG;AACH,qBAAa,gBAAgB;IAC3B;;;;;;OAMG;WACU,MAAM,CACjB,EAAE,EAAE,iBAAiB,EACrB,IAAI,EAAE;QACJ,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,EACD,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,IAAI,CAAC;IA6ChB;;;;;;;;;;;;;OAaG;WACU,aAAa,CACxB,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EAAE,EACnB,OAAO,GAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB;;;;;WAKG;QACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;KACjB,EACN,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IA+G3D;;;;;;;OAOG;WACU,mBAAmB,CAC9B,GAAG,EAAE,iBAAiB,EACtB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAShB;;;;;;;;;OASG;WACU,GAAG,CACd,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAclC;;;;;;;OAOG;WACU,YAAY,CACvB,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,eAAe,EAAE,CAAC;IAY7B;;;;;;;;OAQG;WACU,YAAY,CACvB,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,EAClB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,EAAE,CAAC;IAqB7B;;;;;;;OAOG;WACU,0BAA0B,CACrC,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,EAAE,CAAC;IAWpB;;;;;;OAMG;WACU,eAAe,CAC1B,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC;IAQhB;;;;;;;;OAQG;WACU,MAAM,CACjB,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC;IAWhB;;;;;OAKG;WACU,WAAW,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;IASjE;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ;CAexB"}
@@ -1,3 +1,4 @@
1
+ import { chunkArray } from "../utils/chunk.js";
1
2
  import { CosineSimilarity } from "./similarity.js";
2
3
  import { createLogger } from "@happyvertical/logger";
3
4
  import { randomUUID } from "node:crypto";
@@ -69,7 +70,8 @@ var EmbeddingStorage = class EmbeddingStorage {
69
70
  * @returns Array of { objectId, similarity } ranked by similarity (highest first)
70
71
  */
71
72
  static async searchSimilar(db, objectClass, embedding, options = {}, vector) {
72
- const { field, model, limit = 10, minSimilarity = 0 } = options;
73
+ const { field, model, limit = 10, minSimilarity = 0, objectIds } = options;
74
+ if (objectIds !== void 0 && objectIds.length === 0) return [];
73
75
  if (vector) try {
74
76
  const conditions = ["object_class = $2"];
75
77
  const params = [objectClass];
@@ -81,21 +83,36 @@ var EmbeddingStorage = class EmbeddingStorage {
81
83
  conditions.push(`model = $${params.length + 2}`);
82
84
  params.push(model);
83
85
  }
84
- return (await vector.search("_smrt_embeddings", VECTOR_COLUMN, embedding, {
85
- limit,
86
- metric: "cosine",
87
- where: conditions.join(" AND "),
88
- params
89
- })).map((r) => ({
90
- objectId: r.object_id,
91
- similarity: 1 - r.distance
92
- })).filter((r) => r.similarity >= minSimilarity);
86
+ const idChunks = objectIds ? chunkArray(objectIds, 900) : [void 0];
87
+ const merged = [];
88
+ for (const chunk of idChunks) {
89
+ const chunkConditions = [...conditions];
90
+ const chunkParams = [...params];
91
+ if (chunk) {
92
+ const placeholders = chunk.map((_, index) => `$${chunkParams.length + 2 + index}`);
93
+ chunkConditions.push(`object_id IN (${placeholders.join(", ")})`);
94
+ chunkParams.push(...chunk);
95
+ }
96
+ const results = await vector.search("_smrt_embeddings", VECTOR_COLUMN, embedding, {
97
+ limit,
98
+ metric: "cosine",
99
+ where: chunkConditions.join(" AND "),
100
+ params: chunkParams
101
+ });
102
+ merged.push(...results.map((r) => ({
103
+ objectId: r.object_id,
104
+ similarity: 1 - r.distance
105
+ })));
106
+ }
107
+ return merged.filter((r) => r.similarity >= minSimilarity).sort((a, b) => b.similarity - a.similarity).slice(0, limit);
93
108
  } catch (error) {
94
109
  logger.warn(`[embeddings] Native vector search failed, falling back to in-memory: ${error instanceof Error ? error.message : String(error)}`);
95
110
  }
96
111
  const storedEmbeddings = await EmbeddingStorage.listForClass(db, objectClass, field, model);
97
- if (storedEmbeddings.length === 0) return [];
98
- return storedEmbeddings.map((stored) => ({
112
+ const allowedIds = objectIds ? new Set(objectIds) : void 0;
113
+ const candidates = allowedIds ? storedEmbeddings.filter((stored) => allowedIds.has(stored.object_id)) : storedEmbeddings;
114
+ if (candidates.length === 0) return [];
115
+ return candidates.map((stored) => ({
99
116
  objectId: stored.object_id,
100
117
  similarity: CosineSimilarity.calculate(embedding, stored.embedding)
101
118
  })).filter((item) => item.similarity >= minSimilarity).sort((a, b) => b.similarity - a.similarity).slice(0, limit);
@@ -1 +1 @@
1
- {"version":3,"file":"storage.js","names":[],"sources":["../../src/embeddings/storage.ts"],"sourcesContent":["/**\n * Embedding Storage Operations\n *\n * CRUD operations for the _smrt_embeddings system table.\n * Supports both JSON-based and native vector storage strategies.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface, VectorCapabilities } from '@happyvertical/sql';\nimport { CosineSimilarity } from './similarity';\nimport type { StoredEmbedding } from './types';\n\nconst logger = createLogger({ level: 'info' });\n\n/** Column name for native vector storage */\nconst VECTOR_COLUMN = 'embedding_vector';\n\n/**\n * Storage operations for embeddings in _smrt_embeddings table\n */\nexport class EmbeddingStorage {\n /**\n * Insert or update an embedding\n *\n * @param db - Database interface\n * @param data - Embedding data to store\n * @param vector - Optional vector capabilities for native storage\n */\n static async upsert(\n db: DatabaseInterface,\n data: {\n objectClass: string;\n objectId: string;\n fieldName: string;\n contentHash: string;\n embedding: number[];\n model: string;\n dimensions: number;\n provider?: string;\n },\n vector?: VectorCapabilities,\n ): Promise<void> {\n const now = new Date().toISOString();\n const id = randomUUID();\n\n await db.upsert(\n '_smrt_embeddings',\n ['object_class', 'object_id', 'field_name', 'model'],\n {\n id,\n object_class: data.objectClass,\n object_id: data.objectId,\n field_name: data.fieldName,\n content_hash: data.contentHash,\n embedding: JSON.stringify(data.embedding),\n model: data.model,\n dimensions: data.dimensions,\n provider: data.provider || null,\n created_at: now,\n updated_at: now,\n },\n );\n\n // Also store native vector if capability is available\n if (vector) {\n try {\n await vector.upsertVector(\n '_smrt_embeddings',\n {\n object_class: data.objectClass,\n object_id: data.objectId,\n field_name: data.fieldName,\n model: data.model,\n },\n VECTOR_COLUMN,\n data.embedding,\n );\n } catch (error) {\n // Log but don't fail — JSON fallback is always available\n logger.warn(\n `[embeddings] Failed to store native vector: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n\n /**\n * Search for similar embeddings using the best available strategy.\n *\n * When vector capabilities are provided, delegates to database-level\n * vector search. Otherwise falls back to loading all embeddings and\n * computing cosine similarity in memory.\n *\n * @param db - Database interface\n * @param objectClass - Class name to search within\n * @param embedding - Query embedding vector\n * @param options - Search options\n * @param vector - Optional vector capabilities for native search\n * @returns Array of { objectId, similarity } ranked by similarity (highest first)\n */\n static async searchSimilar(\n db: DatabaseInterface,\n objectClass: string,\n embedding: number[],\n options: {\n field?: string;\n model?: string;\n limit?: number;\n minSimilarity?: number;\n } = {},\n vector?: VectorCapabilities,\n ): Promise<Array<{ objectId: string; similarity: number }>> {\n const { field, model, limit = 10, minSimilarity = 0 } = options;\n\n // Native vector search path\n if (vector) {\n try {\n // Build WHERE clause for class/field/model filtering.\n // Parameter numbering starts at $2 because vector.search() reserves\n // $1 for the query vector embedding in its internal SQL query.\n const conditions: string[] = ['object_class = $2'];\n const params: unknown[] = [objectClass];\n\n if (field) {\n conditions.push(`field_name = $${params.length + 2}`);\n params.push(field);\n }\n\n if (model) {\n conditions.push(`model = $${params.length + 2}`);\n params.push(model);\n }\n\n const results = await vector.search(\n '_smrt_embeddings',\n VECTOR_COLUMN,\n embedding,\n {\n limit,\n metric: 'cosine',\n where: conditions.join(' AND '),\n params,\n },\n );\n\n // Convert cosine distance (0=identical, 2=opposite) to similarity (1=identical, -1=opposite)\n return results\n .map((r) => ({\n objectId: r.object_id as string,\n similarity: 1 - r.distance,\n }))\n .filter((r) => r.similarity >= minSimilarity);\n } catch (error) {\n // Fall back to in-memory search on vector search failure\n logger.warn(\n `[embeddings] Native vector search failed, falling back to in-memory: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n // In-memory fallback: load all embeddings and compute cosine similarity\n const storedEmbeddings = await EmbeddingStorage.listForClass(\n db,\n objectClass,\n field,\n model,\n );\n\n if (storedEmbeddings.length === 0) {\n return [];\n }\n\n return storedEmbeddings\n .map((stored) => ({\n objectId: stored.object_id,\n similarity: CosineSimilarity.calculate(embedding, stored.embedding),\n }))\n .filter((item) => item.similarity >= minSimilarity)\n .sort((a, b) => b.similarity - a.similarity)\n .slice(0, limit);\n }\n\n /**\n * Ensure the native vector column and index exist on _smrt_embeddings.\n * Called once during initialization when storage: 'native' is configured.\n *\n * @param db - Database interface\n * @param dimensions - Vector dimensions\n * @param vector - Vector capabilities\n */\n static async ensureVectorStorage(\n _db: DatabaseInterface,\n dimensions: number,\n vector: VectorCapabilities,\n ): Promise<void> {\n await vector.ensureColumn('_smrt_embeddings', VECTOR_COLUMN, dimensions);\n await vector.ensureIndex('_smrt_embeddings', VECTOR_COLUMN, {\n dimensions,\n metric: 'cosine',\n type: 'hnsw',\n });\n }\n\n /**\n * Get a specific embedding\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n * @param fieldName - Field name\n * @param model - Model used\n * @returns Stored embedding or null\n */\n static async get(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n fieldName: string,\n model: string,\n ): Promise<StoredEmbedding | null> {\n const result = await db.single`\n SELECT * FROM _smrt_embeddings\n WHERE object_class = ${objectClass}\n AND object_id = ${objectId}\n AND field_name = ${fieldName}\n AND model = ${model}\n `;\n\n if (!result) return null;\n\n return EmbeddingStorage.parseRow(result);\n }\n\n /**\n * Get all embeddings for an object\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n * @returns Array of stored embeddings\n */\n static async getForObject(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n ): Promise<StoredEmbedding[]> {\n const { rows } = await db.query(\n `SELECT * FROM _smrt_embeddings WHERE object_class = ? AND object_id = ?`,\n objectClass,\n objectId,\n );\n\n return rows.map((row: Record<string, unknown>) =>\n EmbeddingStorage.parseRow(row),\n );\n }\n\n /**\n * List embeddings for a class (optionally filtered by field and model)\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param fieldName - Optional field filter\n * @param model - Optional model filter\n * @returns Array of stored embeddings\n */\n static async listForClass(\n db: DatabaseInterface,\n objectClass: string,\n fieldName?: string,\n model?: string,\n ): Promise<StoredEmbedding[]> {\n let sql = `SELECT * FROM _smrt_embeddings WHERE object_class = ?`;\n const params: unknown[] = [objectClass];\n\n if (fieldName) {\n sql += ` AND field_name = ?`;\n params.push(fieldName);\n }\n\n if (model) {\n sql += ` AND model = ?`;\n params.push(model);\n }\n\n const { rows } = await db.query(sql, ...params);\n\n return rows.map((row: Record<string, unknown>) =>\n EmbeddingStorage.parseRow(row),\n );\n }\n\n /**\n * Get object IDs that have embeddings for a specific field\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param fieldName - Field name\n * @returns Array of object IDs\n */\n static async getObjectIdsWithEmbeddings(\n db: DatabaseInterface,\n objectClass: string,\n fieldName: string,\n ): Promise<string[]> {\n const { rows } = await db.query(\n `SELECT DISTINCT object_id FROM _smrt_embeddings\n WHERE object_class = ? AND field_name = ?`,\n objectClass,\n fieldName,\n );\n\n return rows.map((row: Record<string, unknown>) => row.object_id as string);\n }\n\n /**\n * Delete all embeddings for an object\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n */\n static async deleteForObject(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n ): Promise<void> {\n await db.query(\n `DELETE FROM _smrt_embeddings WHERE object_class = ? AND object_id = ?`,\n objectClass,\n objectId,\n );\n }\n\n /**\n * Delete a specific embedding\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n * @param fieldName - Field name\n * @param model - Model used\n */\n static async delete(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n fieldName: string,\n model: string,\n ): Promise<void> {\n await db.query(\n `DELETE FROM _smrt_embeddings\n WHERE object_class = ? AND object_id = ? AND field_name = ? AND model = ?`,\n objectClass,\n objectId,\n fieldName,\n model,\n );\n }\n\n /**\n * Check if embedding table exists\n *\n * @param db - Database interface\n * @returns True if table exists\n */\n static async tableExists(db: DatabaseInterface): Promise<boolean> {\n try {\n await db.query(`SELECT 1 FROM _smrt_embeddings LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Parse a database row into StoredEmbedding\n */\n private static parseRow(row: Record<string, unknown>): StoredEmbedding {\n return {\n id: row.id as string,\n object_class: row.object_class as string,\n object_id: row.object_id as string,\n field_name: row.field_name as string,\n content_hash: row.content_hash as string,\n embedding: JSON.parse(row.embedding as string) as number[],\n model: row.model as string,\n dimensions: row.dimensions as number,\n provider: row.provider as string | undefined,\n created_at: new Date(row.created_at as string),\n updated_at: new Date(row.updated_at as string),\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"storage.js","names":[],"sources":["../../src/embeddings/storage.ts"],"sourcesContent":["/**\n * Embedding Storage Operations\n *\n * CRUD operations for the _smrt_embeddings system table.\n * Supports both JSON-based and native vector storage strategies.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface, VectorCapabilities } from '@happyvertical/sql';\nimport { chunkArray, IN_LIST_CHUNK_SIZE } from '../utils/chunk';\nimport { CosineSimilarity } from './similarity';\nimport type { StoredEmbedding } from './types';\n\nconst logger = createLogger({ level: 'info' });\n\n/** Column name for native vector storage */\nconst VECTOR_COLUMN = 'embedding_vector';\n\n/**\n * Storage operations for embeddings in _smrt_embeddings table\n */\nexport class EmbeddingStorage {\n /**\n * Insert or update an embedding\n *\n * @param db - Database interface\n * @param data - Embedding data to store\n * @param vector - Optional vector capabilities for native storage\n */\n static async upsert(\n db: DatabaseInterface,\n data: {\n objectClass: string;\n objectId: string;\n fieldName: string;\n contentHash: string;\n embedding: number[];\n model: string;\n dimensions: number;\n provider?: string;\n },\n vector?: VectorCapabilities,\n ): Promise<void> {\n const now = new Date().toISOString();\n const id = randomUUID();\n\n await db.upsert(\n '_smrt_embeddings',\n ['object_class', 'object_id', 'field_name', 'model'],\n {\n id,\n object_class: data.objectClass,\n object_id: data.objectId,\n field_name: data.fieldName,\n content_hash: data.contentHash,\n embedding: JSON.stringify(data.embedding),\n model: data.model,\n dimensions: data.dimensions,\n provider: data.provider || null,\n created_at: now,\n updated_at: now,\n },\n );\n\n // Also store native vector if capability is available\n if (vector) {\n try {\n await vector.upsertVector(\n '_smrt_embeddings',\n {\n object_class: data.objectClass,\n object_id: data.objectId,\n field_name: data.fieldName,\n model: data.model,\n },\n VECTOR_COLUMN,\n data.embedding,\n );\n } catch (error) {\n // Log but don't fail — JSON fallback is always available\n logger.warn(\n `[embeddings] Failed to store native vector: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n\n /**\n * Search for similar embeddings using the best available strategy.\n *\n * When vector capabilities are provided, delegates to database-level\n * vector search. Otherwise falls back to loading all embeddings and\n * computing cosine similarity in memory.\n *\n * @param db - Database interface\n * @param objectClass - Class name to search within\n * @param embedding - Query embedding vector\n * @param options - Search options\n * @param vector - Optional vector capabilities for native search\n * @returns Array of { objectId, similarity } ranked by similarity (highest first)\n */\n static async searchSimilar(\n db: DatabaseInterface,\n objectClass: string,\n embedding: number[],\n options: {\n field?: string;\n model?: string;\n limit?: number;\n minSimilarity?: number;\n /**\n * Restrict candidates to these object ids BEFORE ranking (#2365).\n * `undefined` means no restriction; an empty array means no candidates\n * (the search returns no results). Used by tenant-scoped collections so\n * top-K never ranks rows the caller is not allowed to see.\n */\n objectIds?: string[];\n } = {},\n vector?: VectorCapabilities,\n ): Promise<Array<{ objectId: string; similarity: number }>> {\n const { field, model, limit = 10, minSimilarity = 0, objectIds } = options;\n\n // An explicit empty candidate set means \"nothing is visible\" — never fall\n // through to the unrestricted search.\n if (objectIds !== undefined && objectIds.length === 0) {\n return [];\n }\n\n // Native vector search path\n if (vector) {\n try {\n // Build WHERE clause for class/field/model filtering.\n // Parameter numbering starts at $2 because vector.search() reserves\n // $1 for the query vector embedding in its internal SQL query.\n const conditions: string[] = ['object_class = $2'];\n const params: unknown[] = [objectClass];\n\n if (field) {\n conditions.push(`field_name = $${params.length + 2}`);\n params.push(field);\n }\n\n if (model) {\n conditions.push(`model = $${params.length + 2}`);\n params.push(model);\n }\n\n // Candidate restriction (#2365). The id list is chunked so one query\n // never exceeds the backend's bind-variable limit; per-chunk top-K\n // results merge into an exact global top-K because every chunk\n // returns its own best `limit` candidates.\n const idChunks = objectIds\n ? chunkArray(objectIds, IN_LIST_CHUNK_SIZE)\n : [undefined];\n\n const merged: Array<{ objectId: string; similarity: number }> = [];\n for (const chunk of idChunks) {\n const chunkConditions = [...conditions];\n const chunkParams = [...params];\n if (chunk) {\n const placeholders = chunk.map(\n (_, index) => `$${chunkParams.length + 2 + index}`,\n );\n chunkConditions.push(`object_id IN (${placeholders.join(', ')})`);\n chunkParams.push(...chunk);\n }\n\n const results = await vector.search(\n '_smrt_embeddings',\n VECTOR_COLUMN,\n embedding,\n {\n limit,\n metric: 'cosine',\n where: chunkConditions.join(' AND '),\n params: chunkParams,\n },\n );\n\n // Convert cosine distance (0=identical, 2=opposite) to similarity\n // (1=identical, -1=opposite)\n merged.push(\n ...results.map((r) => ({\n objectId: r.object_id as string,\n similarity: 1 - r.distance,\n })),\n );\n }\n\n return merged\n .filter((r) => r.similarity >= minSimilarity)\n .sort((a, b) => b.similarity - a.similarity)\n .slice(0, limit);\n } catch (error) {\n // Fall back to in-memory search on vector search failure\n logger.warn(\n `[embeddings] Native vector search failed, falling back to in-memory: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n // In-memory fallback: load all embeddings and compute cosine similarity\n const storedEmbeddings = await EmbeddingStorage.listForClass(\n db,\n objectClass,\n field,\n model,\n );\n\n // Candidate restriction (#2365): drop invisible rows BEFORE ranking so\n // top-K is computed only over what the caller may see.\n const allowedIds = objectIds ? new Set(objectIds) : undefined;\n const candidates = allowedIds\n ? storedEmbeddings.filter((stored) => allowedIds.has(stored.object_id))\n : storedEmbeddings;\n\n if (candidates.length === 0) {\n return [];\n }\n\n return candidates\n .map((stored) => ({\n objectId: stored.object_id,\n similarity: CosineSimilarity.calculate(embedding, stored.embedding),\n }))\n .filter((item) => item.similarity >= minSimilarity)\n .sort((a, b) => b.similarity - a.similarity)\n .slice(0, limit);\n }\n\n /**\n * Ensure the native vector column and index exist on _smrt_embeddings.\n * Called once during initialization when storage: 'native' is configured.\n *\n * @param db - Database interface\n * @param dimensions - Vector dimensions\n * @param vector - Vector capabilities\n */\n static async ensureVectorStorage(\n _db: DatabaseInterface,\n dimensions: number,\n vector: VectorCapabilities,\n ): Promise<void> {\n await vector.ensureColumn('_smrt_embeddings', VECTOR_COLUMN, dimensions);\n await vector.ensureIndex('_smrt_embeddings', VECTOR_COLUMN, {\n dimensions,\n metric: 'cosine',\n type: 'hnsw',\n });\n }\n\n /**\n * Get a specific embedding\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n * @param fieldName - Field name\n * @param model - Model used\n * @returns Stored embedding or null\n */\n static async get(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n fieldName: string,\n model: string,\n ): Promise<StoredEmbedding | null> {\n const result = await db.single`\n SELECT * FROM _smrt_embeddings\n WHERE object_class = ${objectClass}\n AND object_id = ${objectId}\n AND field_name = ${fieldName}\n AND model = ${model}\n `;\n\n if (!result) return null;\n\n return EmbeddingStorage.parseRow(result);\n }\n\n /**\n * Get all embeddings for an object\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n * @returns Array of stored embeddings\n */\n static async getForObject(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n ): Promise<StoredEmbedding[]> {\n const { rows } = await db.query(\n `SELECT * FROM _smrt_embeddings WHERE object_class = ? AND object_id = ?`,\n objectClass,\n objectId,\n );\n\n return rows.map((row: Record<string, unknown>) =>\n EmbeddingStorage.parseRow(row),\n );\n }\n\n /**\n * List embeddings for a class (optionally filtered by field and model)\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param fieldName - Optional field filter\n * @param model - Optional model filter\n * @returns Array of stored embeddings\n */\n static async listForClass(\n db: DatabaseInterface,\n objectClass: string,\n fieldName?: string,\n model?: string,\n ): Promise<StoredEmbedding[]> {\n let sql = `SELECT * FROM _smrt_embeddings WHERE object_class = ?`;\n const params: unknown[] = [objectClass];\n\n if (fieldName) {\n sql += ` AND field_name = ?`;\n params.push(fieldName);\n }\n\n if (model) {\n sql += ` AND model = ?`;\n params.push(model);\n }\n\n const { rows } = await db.query(sql, ...params);\n\n return rows.map((row: Record<string, unknown>) =>\n EmbeddingStorage.parseRow(row),\n );\n }\n\n /**\n * Get object IDs that have embeddings for a specific field\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param fieldName - Field name\n * @returns Array of object IDs\n */\n static async getObjectIdsWithEmbeddings(\n db: DatabaseInterface,\n objectClass: string,\n fieldName: string,\n ): Promise<string[]> {\n const { rows } = await db.query(\n `SELECT DISTINCT object_id FROM _smrt_embeddings\n WHERE object_class = ? AND field_name = ?`,\n objectClass,\n fieldName,\n );\n\n return rows.map((row: Record<string, unknown>) => row.object_id as string);\n }\n\n /**\n * Delete all embeddings for an object\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n */\n static async deleteForObject(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n ): Promise<void> {\n await db.query(\n `DELETE FROM _smrt_embeddings WHERE object_class = ? AND object_id = ?`,\n objectClass,\n objectId,\n );\n }\n\n /**\n * Delete a specific embedding\n *\n * @param db - Database interface\n * @param objectClass - Class name\n * @param objectId - Object ID\n * @param fieldName - Field name\n * @param model - Model used\n */\n static async delete(\n db: DatabaseInterface,\n objectClass: string,\n objectId: string,\n fieldName: string,\n model: string,\n ): Promise<void> {\n await db.query(\n `DELETE FROM _smrt_embeddings\n WHERE object_class = ? AND object_id = ? AND field_name = ? AND model = ?`,\n objectClass,\n objectId,\n fieldName,\n model,\n );\n }\n\n /**\n * Check if embedding table exists\n *\n * @param db - Database interface\n * @returns True if table exists\n */\n static async tableExists(db: DatabaseInterface): Promise<boolean> {\n try {\n await db.query(`SELECT 1 FROM _smrt_embeddings LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Parse a database row into StoredEmbedding\n */\n private static parseRow(row: Record<string, unknown>): StoredEmbedding {\n return {\n id: row.id as string,\n object_class: row.object_class as string,\n object_id: row.object_id as string,\n field_name: row.field_name as string,\n content_hash: row.content_hash as string,\n embedding: JSON.parse(row.embedding as string) as number[],\n model: row.model as string,\n dimensions: row.dimensions as number,\n provider: row.provider as string | undefined,\n created_at: new Date(row.created_at as string),\n updated_at: new Date(row.updated_at as string),\n };\n }\n}\n"],"mappings":";;;;;;;;;;;AAcA,IAAM,SAAS,aAAa,EAAE,OAAO,OAAO,CAAC;;AAG7C,IAAM,gBAAgB;;;;AAKtB,IAAa,mBAAb,MAAa,iBAAiB;;;;;;;;CAQ5B,aAAa,OACX,IACA,MAUA,QACe;EACf,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,MAAM,KAAK,WAAW;EAEtB,MAAM,GAAG,OACP,oBACA;GAAC;GAAgB;GAAa;GAAc;EAAO,GACnD;GACE;GACA,cAAc,KAAK;GACnB,WAAW,KAAK;GAChB,YAAY,KAAK;GACjB,cAAc,KAAK;GACnB,WAAW,KAAK,UAAU,KAAK,SAAS;GACxC,OAAO,KAAK;GACZ,YAAY,KAAK;GACjB,UAAU,KAAK,YAAY;GAC3B,YAAY;GACZ,YAAY;EACd,CACF;EAGA,IAAI,QACF,IAAI;GACF,MAAM,OAAO,aACX,oBACA;IACE,cAAc,KAAK;IACnB,WAAW,KAAK;IAChB,YAAY,KAAK;IACjB,OAAO,KAAK;GACd,GACA,eACA,KAAK,SACP;EACF,SAAS,OAAO;GAEd,OAAO,KACL,+CAA+C,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACtG;EACF;CAEJ;;;;;;;;;;;;;;;CAgBA,aAAa,cACX,IACA,aACA,WACA,UAYI,CAAC,GACL,QAC0D;EAC1D,MAAM,EAAE,OAAO,OAAO,QAAQ,IAAI,gBAAgB,GAAG,cAAc;EAInE,IAAI,cAAc,KAAA,KAAa,UAAU,WAAW,GAClD,OAAO,CAAC;EAIV,IAAI,QACF,IAAI;GAIF,MAAM,aAAuB,CAAC,mBAAmB;GACjD,MAAM,SAAoB,CAAC,WAAW;GAEtC,IAAI,OAAO;IACT,WAAW,KAAK,iBAAiB,OAAO,SAAS,GAAG;IACpD,OAAO,KAAK,KAAK;GACnB;GAEA,IAAI,OAAO;IACT,WAAW,KAAK,YAAY,OAAO,SAAS,GAAG;IAC/C,OAAO,KAAK,KAAK;GACnB;GAMA,MAAM,WAAW,YACb,WAAW,WAAA,GAA6B,IACxC,CAAC,KAAA,CAAS;GAEd,MAAM,SAA0D,CAAC;GACjE,KAAK,MAAM,SAAS,UAAU;IAC5B,MAAM,kBAAkB,CAAC,GAAG,UAAU;IACtC,MAAM,cAAc,CAAC,GAAG,MAAM;IAC9B,IAAI,OAAO;KACT,MAAM,eAAe,MAAM,KACxB,GAAG,UAAU,IAAI,YAAY,SAAS,IAAI,OAC7C;KACA,gBAAgB,KAAK,iBAAiB,aAAa,KAAK,IAAI,EAAE,EAAE;KAChE,YAAY,KAAK,GAAG,KAAK;IAC3B;IAEA,MAAM,UAAU,MAAM,OAAO,OAC3B,oBACA,eACA,WACA;KACE;KACA,QAAQ;KACR,OAAO,gBAAgB,KAAK,OAAO;KACnC,QAAQ;IACV,CACF;IAIA,OAAO,KACL,GAAG,QAAQ,KAAK,OAAO;KACrB,UAAU,EAAE;KACZ,YAAY,IAAI,EAAE;IACpB,EAAE,CACJ;GACF;GAEA,OAAO,OACJ,QAAQ,MAAM,EAAE,cAAc,aAAa,CAAC,CAC5C,MAAM,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU,CAAC,CAC3C,MAAM,GAAG,KAAK;EACnB,SAAS,OAAO;GAEd,OAAO,KACL,wEAAwE,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAC/H;EACF;EAIF,MAAM,mBAAmB,MAAM,iBAAiB,aAC9C,IACA,aACA,OACA,KACF;EAIA,MAAM,aAAa,YAAY,IAAI,IAAI,SAAS,IAAI,KAAA;EACpD,MAAM,aAAa,aACf,iBAAiB,QAAQ,WAAW,WAAW,IAAI,OAAO,SAAS,CAAC,IACpE;EAEJ,IAAI,WAAW,WAAW,GACxB,OAAO,CAAC;EAGV,OAAO,WACJ,KAAK,YAAY;GAChB,UAAU,OAAO;GACjB,YAAY,iBAAiB,UAAU,WAAW,OAAO,SAAS;EACpE,EAAE,CAAC,CACF,QAAQ,SAAS,KAAK,cAAc,aAAa,CAAC,CAClD,MAAM,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU,CAAC,CAC3C,MAAM,GAAG,KAAK;CACnB;;;;;;;;;CAUA,aAAa,oBACX,KACA,YACA,QACe;EACf,MAAM,OAAO,aAAa,oBAAoB,eAAe,UAAU;EACvE,MAAM,OAAO,YAAY,oBAAoB,eAAe;GAC1D;GACA,QAAQ;GACR,MAAM;EACR,CAAC;CACH;;;;;;;;;;;CAYA,aAAa,IACX,IACA,aACA,UACA,WACA,OACiC;EACjC,MAAM,SAAS,MAAM,GAAG,MAAM;;6BAEL,YAAY;0BACf,SAAS;2BACR,UAAU;sBACf,MAAM;;EAGxB,IAAI,CAAC,QAAQ,OAAO;EAEpB,OAAO,iBAAiB,SAAS,MAAM;CACzC;;;;;;;;;CAUA,aAAa,aACX,IACA,aACA,UAC4B;EAC5B,MAAM,EAAE,SAAS,MAAM,GAAG,MACxB,2EACA,aACA,QACF;EAEA,OAAO,KAAK,KAAK,QACf,iBAAiB,SAAS,GAAG,CAC/B;CACF;;;;;;;;;;CAWA,aAAa,aACX,IACA,aACA,WACA,OAC4B;EAC5B,IAAI,MAAM;EACV,MAAM,SAAoB,CAAC,WAAW;EAEtC,IAAI,WAAW;GACb,OAAO;GACP,OAAO,KAAK,SAAS;EACvB;EAEA,IAAI,OAAO;GACT,OAAO;GACP,OAAO,KAAK,KAAK;EACnB;EAEA,MAAM,EAAE,SAAS,MAAM,GAAG,MAAM,KAAK,GAAG,MAAM;EAE9C,OAAO,KAAK,KAAK,QACf,iBAAiB,SAAS,GAAG,CAC/B;CACF;;;;;;;;;CAUA,aAAa,2BACX,IACA,aACA,WACmB;EACnB,MAAM,EAAE,SAAS,MAAM,GAAG,MACxB;mDAEA,aACA,SACF;EAEA,OAAO,KAAK,KAAK,QAAiC,IAAI,SAAmB;CAC3E;;;;;;;;CASA,aAAa,gBACX,IACA,aACA,UACe;EACf,MAAM,GAAG,MACP,yEACA,aACA,QACF;CACF;;;;;;;;;;CAWA,aAAa,OACX,IACA,aACA,UACA,WACA,OACe;EACf,MAAM,GAAG,MACP;mFAEA,aACA,UACA,WACA,KACF;CACF;;;;;;;CAQA,aAAa,YAAY,IAAyC;EAChE,IAAI;GACF,MAAM,GAAG,MAAM,wCAAwC;GACvD,OAAO;EACT,QAAQ;GACN,OAAO;EACT;CACF;;;;CAKA,OAAe,SAAS,KAA+C;EACrE,OAAO;GACL,IAAI,IAAI;GACR,cAAc,IAAI;GAClB,WAAW,IAAI;GACf,YAAY,IAAI;GAChB,cAAc,IAAI;GAClB,WAAW,KAAK,MAAM,IAAI,SAAmB;GAC7C,OAAO,IAAI;GACX,YAAY,IAAI;GAChB,UAAU,IAAI;GACd,YAAY,IAAI,KAAK,IAAI,UAAoB;GAC7C,YAAY,IAAI,KAAK,IAAI,UAAoB;EAC/C;CACF;AACF"}
package/dist/errors.d.ts CHANGED
@@ -17,7 +17,7 @@ export declare abstract class SmrtError extends Error {
17
17
  name: string;
18
18
  message: string;
19
19
  code: string;
20
- category: "ai" | "database" | "filesystem" | "validation" | "network" | "configuration" | "runtime";
20
+ category: "database" | "ai" | "filesystem" | "validation" | "network" | "configuration" | "runtime";
21
21
  details: Record<string, unknown> | undefined;
22
22
  stack: string | undefined;
23
23
  cause: {
@@ -250,11 +250,39 @@ export declare class TenantIsolationError extends SmrtError {
250
250
  */
251
251
  export declare class ErrorUtils {
252
252
  /**
253
- * Wraps a function with error handling and automatic retry logic
253
+ * Wraps a function with error handling and automatic retry logic.
254
+ *
255
+ * Retries are for failures a later attempt might survive. An error is
256
+ * rethrown immediately, with no backoff, when it is:
257
+ *
258
+ * - a {@link ValidationError} or {@link ConfigurationError} — the input is
259
+ * wrong, and the same input will be wrong next time;
260
+ * - a {@link TenantIsolationError} — a security boundary, and deterministic;
261
+ * - a **deterministic database failure** (#2366), classified through the
262
+ * driver-error cause chain by {@link classifyDatabaseError}: constraint
263
+ * violations, invalid input syntax, missing tables/columns, and statements
264
+ * issued inside an already-aborted PostgreSQL transaction (`25P02`).
265
+ *
266
+ * The database check matters because `@happyvertical/sql` wraps every driver
267
+ * error as `DatabaseError('Failed to upsert record into table', …)`, hiding
268
+ * the constraint wording from `error.message`. Without it, a unique-key
269
+ * collision burned the full 500 + 1000 + 2000 ms backoff and — inside a
270
+ * caller-managed transaction — replaced the real cause with
271
+ * `25P02 current transaction is aborted` on the way out.
272
+ *
273
+ * Errors the classifier has no opinion about (`kind: 'unknown'`) keep the
274
+ * original permissive behavior and are retried.
254
275
  */
255
276
  static withRetry<T>(operation: () => Promise<T>, maxRetries?: number, delay?: number, backoffMultiplier?: number): Promise<T>;
256
277
  /**
257
- * Checks if an error is retryable
278
+ * Checks if an error is retryable.
279
+ *
280
+ * A database failure is classified through its driver-error cause chain
281
+ * first (#2366), so a wrapped `40001 serialization_failure` or `SQLITE_BUSY`
282
+ * reports retryable while a wrapped `23505 unique_violation` does not —
283
+ * neither of which the `category === 'database'` test alone could tell
284
+ * apart. Only when the chain yields no opinion do the original
285
+ * category/message heuristics apply.
258
286
  */
259
287
  static isRetryable(error: Error): boolean;
260
288
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
package/dist/errors.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { classifyDatabaseError } from "./db-errors.js";
1
2
  import { createLogger } from "@happyvertical/logger";
2
3
  //#region src/errors.ts
3
4
  /**
@@ -472,7 +473,28 @@ var TenantIsolationError = class TenantIsolationError extends SmrtError {
472
473
  */
473
474
  var ErrorUtils = class {
474
475
  /**
475
- * Wraps a function with error handling and automatic retry logic
476
+ * Wraps a function with error handling and automatic retry logic.
477
+ *
478
+ * Retries are for failures a later attempt might survive. An error is
479
+ * rethrown immediately, with no backoff, when it is:
480
+ *
481
+ * - a {@link ValidationError} or {@link ConfigurationError} — the input is
482
+ * wrong, and the same input will be wrong next time;
483
+ * - a {@link TenantIsolationError} — a security boundary, and deterministic;
484
+ * - a **deterministic database failure** (#2366), classified through the
485
+ * driver-error cause chain by {@link classifyDatabaseError}: constraint
486
+ * violations, invalid input syntax, missing tables/columns, and statements
487
+ * issued inside an already-aborted PostgreSQL transaction (`25P02`).
488
+ *
489
+ * The database check matters because `@happyvertical/sql` wraps every driver
490
+ * error as `DatabaseError('Failed to upsert record into table', …)`, hiding
491
+ * the constraint wording from `error.message`. Without it, a unique-key
492
+ * collision burned the full 500 + 1000 + 2000 ms backoff and — inside a
493
+ * caller-managed transaction — replaced the real cause with
494
+ * `25P02 current transaction is aborted` on the way out.
495
+ *
496
+ * Errors the classifier has no opinion about (`kind: 'unknown'`) keep the
497
+ * original permissive behavior and are retried.
476
498
  */
477
499
  static async withRetry(operation, maxRetries = 3, delay = 1e3, backoffMultiplier = 2) {
478
500
  let lastError = /* @__PURE__ */ new Error("Operation failed without error details");
@@ -482,6 +504,7 @@ var ErrorUtils = class {
482
504
  lastError = error instanceof Error ? error : new Error(String(error));
483
505
  if (attempt === maxRetries) throw lastError;
484
506
  if (error instanceof ValidationError || error instanceof ConfigurationError || error instanceof TenantIsolationError) throw error;
507
+ if (classifyDatabaseError(error).deterministic) throw lastError;
485
508
  try {
486
509
  await new Promise((resolve) => {
487
510
  setTimeout(() => resolve(), delay * backoffMultiplier ** attempt);
@@ -493,9 +516,18 @@ var ErrorUtils = class {
493
516
  throw lastError;
494
517
  }
495
518
  /**
496
- * Checks if an error is retryable
519
+ * Checks if an error is retryable.
520
+ *
521
+ * A database failure is classified through its driver-error cause chain
522
+ * first (#2366), so a wrapped `40001 serialization_failure` or `SQLITE_BUSY`
523
+ * reports retryable while a wrapped `23505 unique_violation` does not —
524
+ * neither of which the `category === 'database'` test alone could tell
525
+ * apart. Only when the chain yields no opinion do the original
526
+ * category/message heuristics apply.
497
527
  */
498
528
  static isRetryable(error) {
529
+ const databaseClassification = classifyDatabaseError(error);
530
+ if (databaseClassification.kind !== "unknown") return databaseClassification.retryable;
499
531
  if (error instanceof SmrtError) return error.category === "network" || error.category === "ai";
500
532
  return [
501
533
  /ECONNRESET/,