@happyvertical/smrt-core 0.36.8 → 0.37.1

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 (105) hide show
  1. package/dist/child-accessors.d.ts +1 -1
  2. package/dist/child-accessors.d.ts.map +1 -1
  3. package/dist/child-accessors.js +1 -1
  4. package/dist/child-accessors.js.map +1 -1
  5. package/dist/class.d.ts.map +1 -1
  6. package/dist/class.js +3 -1
  7. package/dist/class.js.map +1 -1
  8. package/dist/collection-cache.d.ts.map +1 -1
  9. package/dist/collection-cache.js +5 -3
  10. package/dist/collection-cache.js.map +1 -1
  11. package/dist/collection.d.ts +39 -16
  12. package/dist/collection.d.ts.map +1 -1
  13. package/dist/collection.js +40 -19
  14. package/dist/collection.js.map +1 -1
  15. package/dist/consumer-plugin/index.js.map +1 -1
  16. package/dist/decorators/compatibility.d.ts +1 -1
  17. package/dist/decorators/compatibility.d.ts.map +1 -1
  18. package/dist/decorators/compatibility.js.map +1 -1
  19. package/dist/decorators/index.d.ts +4 -4
  20. package/dist/decorators/index.d.ts.map +1 -1
  21. package/dist/decorators/index.js.map +1 -1
  22. package/dist/hierarchical.d.ts.map +1 -1
  23. package/dist/hierarchical.js.map +1 -1
  24. package/dist/junction.d.ts.map +1 -1
  25. package/dist/junction.js.map +1 -1
  26. package/dist/manifest/discover-smrt-packages.d.ts +10 -0
  27. package/dist/manifest/discover-smrt-packages.d.ts.map +1 -1
  28. package/dist/manifest/discover-smrt-packages.js.map +1 -1
  29. package/dist/manifest/generator.d.ts.map +1 -1
  30. package/dist/manifest/generator.js +34 -37
  31. package/dist/manifest/generator.js.map +1 -1
  32. package/dist/manifest/index.js +2 -2
  33. package/dist/manifest/index.js.map +1 -1
  34. package/dist/manifest/manifest-loader.d.ts +10 -0
  35. package/dist/manifest/manifest-loader.d.ts.map +1 -1
  36. package/dist/manifest/manifest-loader.js.map +1 -1
  37. package/dist/manifest/static-manifest.d.ts.map +1 -1
  38. package/dist/manifest/static-manifest.js +39 -20
  39. package/dist/manifest/static-manifest.js.map +1 -1
  40. package/dist/manifest/store.js +2 -2
  41. package/dist/manifest/store.js.map +1 -1
  42. package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
  43. package/dist/manifest/test-manifest-stub.js +2301 -629
  44. package/dist/manifest/test-manifest-stub.js.map +1 -1
  45. package/dist/manifest.json +39 -20
  46. package/dist/migrations/differ.d.ts +104 -13
  47. package/dist/migrations/differ.d.ts.map +1 -1
  48. package/dist/migrations/differ.js +199 -26
  49. package/dist/migrations/differ.js.map +1 -1
  50. package/dist/node_modules/.pnpm/{minimatch@10.2.3 → minimatch@10.2.5}/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -1
  51. package/dist/node_modules/.pnpm/{minimatch@10.2.3 → minimatch@10.2.5}/node_modules/minimatch/dist/esm/ast.js +1 -7
  52. package/dist/node_modules/.pnpm/minimatch@10.2.5/node_modules/minimatch/dist/esm/ast.js.map +1 -0
  53. package/dist/node_modules/.pnpm/{minimatch@10.2.3 → minimatch@10.2.5}/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -1
  54. package/dist/node_modules/.pnpm/{minimatch@10.2.3 → minimatch@10.2.5}/node_modules/minimatch/dist/esm/escape.js.map +1 -1
  55. package/dist/node_modules/.pnpm/{minimatch@10.2.3 → minimatch@10.2.5}/node_modules/minimatch/dist/esm/index.js +17 -14
  56. package/dist/node_modules/.pnpm/minimatch@10.2.5/node_modules/minimatch/dist/esm/index.js.map +1 -0
  57. package/dist/node_modules/.pnpm/minimatch@10.2.5/node_modules/minimatch/dist/esm/unescape.js +10 -0
  58. package/dist/node_modules/.pnpm/{minimatch@10.2.3 → minimatch@10.2.5}/node_modules/minimatch/dist/esm/unescape.js.map +1 -1
  59. package/dist/object.d.ts +64 -17
  60. package/dist/object.d.ts.map +1 -1
  61. package/dist/object.js +76 -30
  62. package/dist/object.js.map +1 -1
  63. package/dist/registry/class-registration.d.ts +3 -3
  64. package/dist/registry/class-registration.d.ts.map +1 -1
  65. package/dist/registry/class-registration.js +39 -42
  66. package/dist/registry/class-registration.js.map +1 -1
  67. package/dist/registry/inheritance-resolver.d.ts +17 -3
  68. package/dist/registry/inheritance-resolver.d.ts.map +1 -1
  69. package/dist/registry/inheritance-resolver.js +1 -1
  70. package/dist/registry/inheritance-resolver.js.map +1 -1
  71. package/dist/registry/manifest-field-merge.d.ts +17 -3
  72. package/dist/registry/manifest-field-merge.d.ts.map +1 -1
  73. package/dist/registry/manifest-field-merge.js +8 -6
  74. package/dist/registry/manifest-field-merge.js.map +1 -1
  75. package/dist/registry/schema-builder.d.ts +1 -1
  76. package/dist/registry/schema-builder.d.ts.map +1 -1
  77. package/dist/registry/schema-builder.js.map +1 -1
  78. package/dist/registry/shared-state.d.ts +3 -3
  79. package/dist/registry/shared-state.d.ts.map +1 -1
  80. package/dist/registry/shared-state.js.map +1 -1
  81. package/dist/registry/types.d.ts +78 -19
  82. package/dist/registry/types.d.ts.map +1 -1
  83. package/dist/registry/validator.d.ts +2 -1
  84. package/dist/registry/validator.d.ts.map +1 -1
  85. package/dist/registry/validator.js +38 -39
  86. package/dist/registry/validator.js.map +1 -1
  87. package/dist/registry.d.ts +84 -57
  88. package/dist/registry.d.ts.map +1 -1
  89. package/dist/registry.js +31 -25
  90. package/dist/registry.js.map +1 -1
  91. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  92. package/dist/scanner/manifest-generator.js +22 -20
  93. package/dist/scanner/manifest-generator.js.map +1 -1
  94. package/dist/smrt-knowledge.json +8 -7
  95. package/dist/system-fields.d.ts +1 -1
  96. package/dist/system-fields.d.ts.map +1 -1
  97. package/dist/system-fields.js.map +1 -1
  98. package/dist/vite-plugin/index.js +1 -1
  99. package/package.json +7 -7
  100. package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/ast.js.map +0 -1
  101. package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/index.js.map +0 -1
  102. package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/unescape.js +0 -10
  103. /package/dist/node_modules/.pnpm/{minimatch@10.2.3 → minimatch@10.2.5}/node_modules/minimatch/dist/esm/assert-valid-pattern.js +0 -0
  104. /package/dist/node_modules/.pnpm/{minimatch@10.2.3 → minimatch@10.2.5}/node_modules/minimatch/dist/esm/brace-expressions.js +0 -0
  105. /package/dist/node_modules/.pnpm/{minimatch@10.2.3 → minimatch@10.2.5}/node_modules/minimatch/dist/esm/escape.js +0 -0
@@ -22,6 +22,6 @@ export declare function childAccessorName(fieldName: string): string;
22
22
  * `ObjectRegistry.getRelationships`)
23
23
  */
24
24
  export declare function applyOneToManyChildAccessors(ctor: {
25
- prototype?: any;
25
+ prototype?: unknown;
26
26
  } | undefined, relationships: RelationshipMetadata[] | undefined): void;
27
27
  //# sourceMappingURL=child-accessors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"child-accessors.d.ts","sourceRoot":"","sources":["../src/child-accessors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7D;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE;IAAE,SAAS,CAAC,EAAE,GAAG,CAAA;CAAE,GAAG,SAAS,EACrC,aAAa,EAAE,oBAAoB,EAAE,GAAG,SAAS,GAChD,IAAI,CAoCN"}
1
+ {"version":3,"file":"child-accessors.d.ts","sourceRoot":"","sources":["../src/child-accessors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7D;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,EACzC,aAAa,EAAE,oBAAoB,EAAE,GAAG,SAAS,GAChD,IAAI,CAsCN"}
@@ -3,7 +3,7 @@ function childAccessorName(fieldName) {
3
3
  }
4
4
  function applyOneToManyChildAccessors(ctor, relationships) {
5
5
  const prototype = ctor?.prototype;
6
- if (!prototype || !relationships) {
6
+ if (!prototype || typeof prototype !== "object" || !relationships) {
7
7
  return;
8
8
  }
9
9
  for (const relationship of relationships) {
@@ -1 +1 @@
1
- {"version":3,"file":"child-accessors.js","sources":["../src/child-accessors.ts"],"sourcesContent":["/**\n * R10 — consolidated child-accessor API for `@oneToMany` relationships.\n *\n * Before R10 every model hand-rolled its own child accessor (`getTerms`,\n * `getChildren`, `getForProfile`, …) with inconsistent names and shapes. This\n * module installs ONE canonical accessor per `@oneToMany` field at class\n * registration time: a `@oneToMany('Child') items` field gains a `getItems()`\n * instance method that delegates to the existing `loadRelatedMany('items')`\n * resolver — the same code path used for lazy loading and `include:` eager\n * loading, which itself queries through the child collection (the queryable\n * primitive).\n *\n * Two invariants make this safe to apply to every registered class:\n *\n * - **Additive.** If a method of the computed name already exists anywhere on\n * the prototype chain — a hand-rolled accessor like\n * `ProfileRelationship.getTerms()`, a differently-shaped `Profile.getMetadata()`,\n * or a `SmrtObject` base method — it is left untouched. The generated\n * accessor never renames or shadows an existing public signature.\n * - **Runtime-only.** Accessors are attached to the prototype at registration;\n * they are invisible to the build-time manifest, so they never leak into the\n * generated REST/CLI/MCP surface (those read `classInfo.tools`/scanned\n * methods, not the runtime prototype). They are a developer-ergonomics\n * convenience layered on top of `loadRelatedMany`.\n *\n * Multiplicity note: when the child class declares more than one foreign key\n * back to the parent (e.g. `ProfileRelationship.fromProfileId` /\n * `toProfileId`), annotate the `@oneToMany` with `{ foreignKey: 'fromProfileId' }`\n * so `loadRelatedMany` resolves the correct inverse side. Without it the\n * resolver falls back to the first matching foreign key (legacy behavior). An\n * explicit `foreignKey` that matches no inverse FK is treated as an error.\n *\n * STI note: a child class inherits the base's generated accessor through the\n * prototype chain. `loadRelatedMany` resolves the inverse FK against the\n * instance's class AND its registered ancestors (see\n * `ObjectRegistry.getInverseRelationshipsForSelf`), so calling the accessor on\n * an STI subclass instance resolves a `@oneToMany` declared on its base.\n */\n\nimport type { RelationshipMetadata } from './registry/types';\n\n/**\n * Compute the canonical child-accessor method name for a `@oneToMany` field.\n *\n * `terms` → `getTerms`, `relationshipsFrom` → `getRelationshipsFrom`. Exported\n * so tests and tooling can derive the name without re-implementing the rule.\n *\n * @param fieldName - The `@oneToMany` property name on the parent class\n * @returns The generated accessor method name\n */\nexport function childAccessorName(fieldName: string): string {\n return `get${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;\n}\n\n/**\n * Install generated child accessors for a class's `@oneToMany` relationships.\n *\n * Called from the `@smrt()` decorator immediately after the class is\n * registered, so `relationships` reflects the class's own (decorator- or\n * manifest-derived) relationship metadata. Non-`oneToMany` entries are ignored,\n * so callers may pass the unfiltered relationship list.\n *\n * @param ctor - The registered class constructor (its prototype is augmented)\n * @param relationships - Relationship metadata for the class (from\n * `ObjectRegistry.getRelationships`)\n */\nexport function applyOneToManyChildAccessors(\n ctor: { prototype?: any } | undefined,\n relationships: RelationshipMetadata[] | undefined,\n): void {\n const prototype = ctor?.prototype;\n if (!prototype || !relationships) {\n return;\n }\n\n for (const relationship of relationships) {\n if (relationship.type !== 'oneToMany') {\n continue;\n }\n\n const fieldName = relationship.fieldName;\n if (!fieldName) {\n continue;\n }\n\n const accessorName = childAccessorName(fieldName);\n\n // Additive only: never shadow a hand-rolled accessor (or any inherited\n // method, including SmrtObject base methods) of the same name. `in`\n // walks the full prototype chain.\n if (accessorName in prototype) {\n continue;\n }\n\n Object.defineProperty(prototype, accessorName, {\n value: function generatedChildAccessor(this: {\n loadRelatedMany(field: string): Promise<any[]>;\n }): Promise<any[]> {\n return this.loadRelatedMany(fieldName);\n },\n writable: true,\n enumerable: false,\n configurable: true,\n });\n }\n}\n"],"names":[],"mappings":"AAkDO,SAAS,kBAAkB,WAA2B;AAC3D,SAAO,MAAM,UAAU,OAAO,CAAC,EAAE,YAAA,CAAa,GAAG,UAAU,MAAM,CAAC,CAAC;AACrE;AAcO,SAAS,6BACd,MACA,eACM;AACN,QAAM,YAAY,MAAM;AACxB,MAAI,CAAC,aAAa,CAAC,eAAe;AAChC;AAAA,EACF;AAEA,aAAW,gBAAgB,eAAe;AACxC,QAAI,aAAa,SAAS,aAAa;AACrC;AAAA,IACF;AAEA,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,WAAW;AACd;AAAA,IACF;AAEA,UAAM,eAAe,kBAAkB,SAAS;AAKhD,QAAI,gBAAgB,WAAW;AAC7B;AAAA,IACF;AAEA,WAAO,eAAe,WAAW,cAAc;AAAA,MAC7C,OAAO,SAAS,yBAEG;AACjB,eAAO,KAAK,gBAAgB,SAAS;AAAA,MACvC;AAAA,MACA,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,cAAc;AAAA,IAAA,CACf;AAAA,EACH;AACF;"}
1
+ {"version":3,"file":"child-accessors.js","sources":["../src/child-accessors.ts"],"sourcesContent":["/**\n * R10 — consolidated child-accessor API for `@oneToMany` relationships.\n *\n * Before R10 every model hand-rolled its own child accessor (`getTerms`,\n * `getChildren`, `getForProfile`, …) with inconsistent names and shapes. This\n * module installs ONE canonical accessor per `@oneToMany` field at class\n * registration time: a `@oneToMany('Child') items` field gains a `getItems()`\n * instance method that delegates to the existing `loadRelatedMany('items')`\n * resolver — the same code path used for lazy loading and `include:` eager\n * loading, which itself queries through the child collection (the queryable\n * primitive).\n *\n * Two invariants make this safe to apply to every registered class:\n *\n * - **Additive.** If a method of the computed name already exists anywhere on\n * the prototype chain — a hand-rolled accessor like\n * `ProfileRelationship.getTerms()`, a differently-shaped `Profile.getMetadata()`,\n * or a `SmrtObject` base method — it is left untouched. The generated\n * accessor never renames or shadows an existing public signature.\n * - **Runtime-only.** Accessors are attached to the prototype at registration;\n * they are invisible to the build-time manifest, so they never leak into the\n * generated REST/CLI/MCP surface (those read `classInfo.tools`/scanned\n * methods, not the runtime prototype). They are a developer-ergonomics\n * convenience layered on top of `loadRelatedMany`.\n *\n * Multiplicity note: when the child class declares more than one foreign key\n * back to the parent (e.g. `ProfileRelationship.fromProfileId` /\n * `toProfileId`), annotate the `@oneToMany` with `{ foreignKey: 'fromProfileId' }`\n * so `loadRelatedMany` resolves the correct inverse side. Without it the\n * resolver falls back to the first matching foreign key (legacy behavior). An\n * explicit `foreignKey` that matches no inverse FK is treated as an error.\n *\n * STI note: a child class inherits the base's generated accessor through the\n * prototype chain. `loadRelatedMany` resolves the inverse FK against the\n * instance's class AND its registered ancestors (see\n * `ObjectRegistry.getInverseRelationshipsForSelf`), so calling the accessor on\n * an STI subclass instance resolves a `@oneToMany` declared on its base.\n */\n\nimport type { RelationshipMetadata } from './registry/types';\n\n/**\n * Compute the canonical child-accessor method name for a `@oneToMany` field.\n *\n * `terms` → `getTerms`, `relationshipsFrom` → `getRelationshipsFrom`. Exported\n * so tests and tooling can derive the name without re-implementing the rule.\n *\n * @param fieldName - The `@oneToMany` property name on the parent class\n * @returns The generated accessor method name\n */\nexport function childAccessorName(fieldName: string): string {\n return `get${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;\n}\n\n/**\n * Install generated child accessors for a class's `@oneToMany` relationships.\n *\n * Called from the `@smrt()` decorator immediately after the class is\n * registered, so `relationships` reflects the class's own (decorator- or\n * manifest-derived) relationship metadata. Non-`oneToMany` entries are ignored,\n * so callers may pass the unfiltered relationship list.\n *\n * @param ctor - The registered class constructor (its prototype is augmented)\n * @param relationships - Relationship metadata for the class (from\n * `ObjectRegistry.getRelationships`)\n */\nexport function applyOneToManyChildAccessors(\n ctor: { prototype?: unknown } | undefined,\n relationships: RelationshipMetadata[] | undefined,\n): void {\n const prototype = ctor?.prototype;\n // Narrow the `unknown` prototype view to an object before augmenting it; a\n // registered constructor always carries an object prototype when present.\n if (!prototype || typeof prototype !== 'object' || !relationships) {\n return;\n }\n\n for (const relationship of relationships) {\n if (relationship.type !== 'oneToMany') {\n continue;\n }\n\n const fieldName = relationship.fieldName;\n if (!fieldName) {\n continue;\n }\n\n const accessorName = childAccessorName(fieldName);\n\n // Additive only: never shadow a hand-rolled accessor (or any inherited\n // method, including SmrtObject base methods) of the same name. `in`\n // walks the full prototype chain.\n if (accessorName in prototype) {\n continue;\n }\n\n Object.defineProperty(prototype, accessorName, {\n value: function generatedChildAccessor(this: {\n loadRelatedMany(field: string): Promise<unknown[]>;\n }): Promise<unknown[]> {\n return this.loadRelatedMany(fieldName);\n },\n writable: true,\n enumerable: false,\n configurable: true,\n });\n }\n}\n"],"names":[],"mappings":"AAkDO,SAAS,kBAAkB,WAA2B;AAC3D,SAAO,MAAM,UAAU,OAAO,CAAC,EAAE,YAAA,CAAa,GAAG,UAAU,MAAM,CAAC,CAAC;AACrE;AAcO,SAAS,6BACd,MACA,eACM;AACN,QAAM,YAAY,MAAM;AAGxB,MAAI,CAAC,aAAa,OAAO,cAAc,YAAY,CAAC,eAAe;AACjE;AAAA,EACF;AAEA,aAAW,gBAAgB,eAAe;AACxC,QAAI,aAAa,SAAS,aAAa;AACrC;AAAA,IACF;AAEA,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,WAAW;AACd;AAAA,IACF;AAEA,UAAM,eAAe,kBAAkB,SAAS;AAKhD,QAAI,gBAAgB,WAAW;AAC7B;AAAA,IACF;AAEA,WAAO,eAAe,WAAW,cAAc;AAAA,MAC7C,OAAO,SAAS,yBAEO;AACrB,eAAO,KAAK,gBAAgB,SAAS;AAAA,MACvC;AAAA,MACA,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,cAAc;AAAA,IAAA,CACf;AAAA,EACH;AACF;"}
@@ -1 +1 @@
1
- {"version":3,"file":"class.d.ts","sourceRoot":"","sources":["../src/class.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"class.d.ts","sourceRoot":"","sources":["../src/class.ts"],"names":[],"mappings":"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"}
package/dist/class.js CHANGED
@@ -344,7 +344,9 @@ class SmrtClass {
344
344
  await this.handleAiUsageCallback(event, aiConfig, usageConfig);
345
345
  };
346
346
  if (aiConfig.provider || aiConfig.type || aiConfig.apiKey) {
347
- this._ai = await getAI(aiConfig);
347
+ this._ai = await getAI(
348
+ aiConfig
349
+ );
348
350
  }
349
351
  }
350
352
  }
package/dist/class.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"class.js","sources":["../src/class.ts"],"sourcesContent":["import type { AIClientOptions } from '@happyvertical/ai';\nimport { type AIClient, getAI } from '@happyvertical/ai';\nimport type { FilesystemAdapterOptions } from '@happyvertical/files';\nimport { FilesystemAdapter } from '@happyvertical/files';\nimport { createLogger, type LoggerConfig } from '@happyvertical/logger';\nimport type {\n AiTokenUsage,\n AiUsageHandler,\n AiUsageListOptions,\n AiUsageSnapshot,\n AiUsageStats,\n AiUsageSummaryOptions,\n SignalAdapter,\n SmrtAiUsageEvent,\n SmrtAiUsageRecord,\n} from '@happyvertical/smrt-types';\nimport {\n type DatabaseInterface,\n getDatabase,\n type TransactionHandle,\n} from '@happyvertical/sql';\nimport {\n AiUsageCollector,\n AiUsagePersistenceHandler,\n} from './adapters/ai-usage.js';\nimport { estimateAiUsageCost } from './adapters/cost-rates.js';\nimport type {\n AiUsageConfig,\n GlobalSignalConfig,\n MetricsConfig,\n PubSubConfig,\n} from './config.js';\nimport { config } from './config.js';\nimport type { DatabaseConfig } from './database.js';\nimport { detectEngine } from './schema/ddl/index.js';\nimport { SignalBus } from './signals/bus.js';\nimport {\n ensureLegacySystemTableCompatibility,\n tableExists,\n} from './system/compatibility.js';\nimport { ALL_SYSTEM_TABLES, SMRT_SCHEMA_VERSION } from './system/schema.js';\n\nconst SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL =\n \"SELECT pg_advisory_xact_lock(hashtext('smrt'), hashtext('system-tables'))\";\n\nconst logger = createLogger({ level: 'info' });\n\ntype DatabaseWithConfig = DatabaseInterface & {\n config?: {\n type?: string;\n url?: string;\n };\n type?: string;\n};\n\ntype TransactionCapableDatabase = DatabaseInterface & {\n transaction?: <T>(\n this: DatabaseInterface,\n callback: (tx: DatabaseInterface) => Promise<T>,\n ) => Promise<T>;\n};\n\ninterface ResolvedAiUsageConfig {\n enabled: boolean;\n persist: boolean;\n estimateCosts: boolean;\n costRates?: Record<string, { input: number; output: number }>;\n handlers: AiUsageHandler[];\n}\n\ntype AiUsageFilterOptions = Pick<\n AiUsageListOptions,\n | 'since'\n | 'until'\n | 'provider'\n | 'model'\n | 'operation'\n | 'className'\n | 'tenantId'\n>;\n\ninterface AiUsageWhereClause {\n conditions: string[];\n params: unknown[];\n nextParamIndex: number;\n}\n\nfunction firstString(...candidates: unknown[]): string | undefined {\n return candidates.find((candidate): candidate is string => {\n return typeof candidate === 'string';\n });\n}\n\nfunction firstNumber(...candidates: unknown[]): number | undefined {\n return candidates.find((candidate): candidate is number => {\n return typeof candidate === 'number';\n });\n}\n\nfunction getDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseWithConfig;\n return db.url || dbWithConfig.config?.url || '';\n}\n\nfunction getDatabaseTypeHint(\n config: DatabaseConfig | undefined,\n): string | undefined {\n if (!config || typeof config === 'string') {\n return undefined;\n }\n\n const configWithType = config as DatabaseWithConfig & {\n client?: unknown;\n };\n\n if (typeof configWithType.type === 'string') {\n return configWithType.type;\n }\n\n if (typeof configWithType.config?.type === 'string') {\n return configWithType.config.type;\n }\n\n if ('query' in configWithType && typeof configWithType.query === 'function') {\n return undefined;\n }\n\n if ('client' in configWithType && configWithType.client) {\n return 'postgres';\n }\n\n return undefined;\n}\n\nfunction normalizeIncomingAiUsageTokens(\n value: unknown,\n): AiTokenUsage | undefined {\n if (!value || typeof value !== 'object') {\n return undefined;\n }\n\n const usage = value as Record<string, unknown>;\n const promptTokens =\n typeof usage.promptTokens === 'number'\n ? usage.promptTokens\n : typeof usage.inputTokens === 'number'\n ? usage.inputTokens\n : undefined;\n const completionTokens =\n typeof usage.completionTokens === 'number'\n ? usage.completionTokens\n : typeof usage.outputTokens === 'number'\n ? usage.outputTokens\n : undefined;\n const totalTokens =\n typeof usage.totalTokens === 'number'\n ? usage.totalTokens\n : promptTokens !== undefined || completionTokens !== undefined\n ? (promptTokens ?? 0) + (completionTokens ?? 0)\n : undefined;\n\n if (\n promptTokens === undefined &&\n completionTokens === undefined &&\n totalTokens === undefined\n ) {\n return undefined;\n }\n\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n}\n\nfunction hydratePersistedAiUsageTokens(row: {\n prompt_tokens?: unknown;\n completion_tokens?: unknown;\n total_tokens?: unknown;\n}): AiTokenUsage | undefined {\n const promptTokens =\n typeof row.prompt_tokens === 'number'\n ? row.prompt_tokens\n : row.prompt_tokens === null || row.prompt_tokens === undefined\n ? undefined\n : Number(row.prompt_tokens);\n const completionTokens =\n typeof row.completion_tokens === 'number'\n ? row.completion_tokens\n : row.completion_tokens === null || row.completion_tokens === undefined\n ? undefined\n : Number(row.completion_tokens);\n const totalTokens =\n typeof row.total_tokens === 'number'\n ? row.total_tokens\n : row.total_tokens === null || row.total_tokens === undefined\n ? undefined\n : Number(row.total_tokens);\n\n if (\n promptTokens === undefined &&\n completionTokens === undefined &&\n totalTokens === undefined\n ) {\n return undefined;\n }\n\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n}\n\nfunction normalizeAiUsageTags(\n value: unknown,\n): Record<string, string> | undefined {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return undefined;\n }\n\n const tags: Record<string, string> = {};\n for (const [key, tagValue] of Object.entries(value)) {\n if (tagValue === undefined || tagValue === null) continue;\n tags[key] = String(tagValue);\n }\n\n return Object.keys(tags).length > 0 ? tags : undefined;\n}\n\nfunction parseAiUsageTags(value: unknown): Record<string, string> | undefined {\n if (typeof value !== 'string') {\n return undefined;\n }\n\n try {\n return normalizeAiUsageTags(JSON.parse(value));\n } catch {\n return undefined;\n }\n}\n\nfunction normalizeAiUsageTimestamp(value: unknown): Date {\n if (value instanceof Date) {\n return value;\n }\n\n if (typeof value === 'string' || typeof value === 'number') {\n const date = new Date(value);\n if (!Number.isNaN(date.getTime())) {\n return date;\n }\n }\n\n return new Date();\n}\n\nfunction getQueryRows(\n result: Awaited<ReturnType<DatabaseInterface['query']>>,\n): Record<string, unknown>[] {\n return Array.isArray(result)\n ? (result as Record<string, unknown>[])\n : ((result as { rows?: Record<string, unknown>[] }).rows ?? []);\n}\n\nfunction buildAiUsageWhereClause(\n options: AiUsageFilterOptions,\n): AiUsageWhereClause {\n const conditions: string[] = [];\n const params: unknown[] = [];\n let nextParamIndex = 1;\n\n if (options.since) {\n conditions.push(`created_at >= $${nextParamIndex++}`);\n params.push(options.since.toISOString());\n }\n\n if (options.until) {\n conditions.push(`created_at <= $${nextParamIndex++}`);\n params.push(options.until.toISOString());\n }\n\n if (options.provider) {\n conditions.push(`provider = $${nextParamIndex++}`);\n params.push(options.provider);\n }\n\n if (options.model) {\n conditions.push(`model = $${nextParamIndex++}`);\n params.push(options.model);\n }\n\n if (options.operation) {\n conditions.push(`operation = $${nextParamIndex++}`);\n params.push(options.operation);\n }\n\n if (options.className) {\n conditions.push(`class_name = $${nextParamIndex++}`);\n params.push(options.className);\n }\n\n if (options.tenantId === null) {\n conditions.push(`tenant_id IS NULL`);\n } else if (options.tenantId) {\n conditions.push(`tenant_id = $${nextParamIndex++}`);\n params.push(options.tenantId);\n }\n\n return {\n conditions,\n params,\n nextParamIndex,\n };\n}\n\n/**\n * Configuration options for the SmrtClass\n */\nexport interface SmrtClassOptions {\n /**\n * Optional custom class name override\n */\n _className?: string;\n\n /**\n * Database configuration - unified approach matching @happyvertical/sql\n *\n * Supports three formats:\n * - String shortcut: 'products.db' (auto-detects database type)\n * - Config object: { type: 'sqlite', url: 'products.db' }\n * - DatabaseInterface instance: await getDatabase(...)\n *\n * @see DatabaseConfig for type definition\n */\n db?: DatabaseConfig;\n\n /**\n * Alias for db option - for backward compatibility with documentation\n *\n * @deprecated Use 'db' instead. This alias exists for backward compatibility.\n */\n persistence?: DatabaseConfig;\n\n /**\n * Filesystem adapter configuration options\n */\n fs?: FilesystemAdapterOptions;\n\n /**\n * AI client configuration options or instance\n */\n ai?: AIClientOptions | AIClient;\n\n /**\n * AI usage tracking configuration (overrides global defaults)\n */\n usage?: AiUsageConfig;\n\n /**\n * Logging configuration (overrides global default)\n */\n logging?: LoggerConfig;\n\n /**\n * Metrics configuration (overrides global default)\n */\n metrics?: MetricsConfig;\n\n /**\n * Pub/Sub configuration (overrides global default)\n */\n pubsub?: PubSubConfig;\n\n /**\n * Sanitization configuration (overrides global default)\n */\n sanitization?: import('./config.js').GlobalSignalConfig['sanitization'];\n\n /**\n * Custom signal configuration (overrides global default)\n */\n signals?: {\n /** Shared signal bus instance */\n bus?: SignalBus;\n /** Additional custom adapters */\n adapters?: SignalAdapter[];\n };\n\n /**\n * Internal flag to reuse an already initialized DatabaseInterface instance.\n *\n * Skips database resolution and system-table setup during lightweight hydration.\n * @internal\n */\n _reuseInitializedDb?: boolean;\n\n /**\n * Internal flag to defer runtime-only services such as signals and AI setup.\n *\n * Lightweight hydration paths set this so plain query reads avoid per-row\n * runtime bootstrap costs.\n * @internal\n */\n _deferRuntimeInitialization?: boolean;\n}\n\n/**\n * Foundation class providing core functionality for the SMRT framework\n *\n * SmrtClass provides unified access to database, filesystem, and AI client\n * interfaces. It serves as the foundation for all other classes in the\n * SMRT framework.\n */\nexport class SmrtClass {\n /**\n * AI client instance for interacting with AI models\n */\n protected _ai!: AIClient;\n\n /**\n * Filesystem adapter for file operations\n */\n protected _fs!: FilesystemAdapter;\n\n /**\n * Database interface for data persistence\n */\n protected _db!: DatabaseInterface;\n private _dbEngineHint?: string;\n\n /**\n * Class name used for identification\n */\n protected _className!: string;\n\n /**\n * Signal bus for method execution tracking\n */\n protected _signalBus?: SignalBus;\n\n /**\n * Adapters registered by this instance (for cleanup)\n */\n private _registeredAdapters: SignalAdapter[] = [];\n\n /**\n * In-memory AI usage collector for quick inspection.\n */\n private _aiUsageCollector?: AiUsageCollector;\n\n /**\n * Registered AI usage handlers for this instance.\n */\n private _aiUsageHandlers: AiUsageHandler[] = [];\n\n /**\n * Tracks whether optional runtime services (signals, AI, fs) are ready.\n */\n private _runtimeServicesInitialized = false;\n\n /**\n * Shared in-flight runtime initialization promise for single-flight setup.\n */\n private _runtimeServicesInitPromise?: Promise<void>;\n\n /**\n * Configuration options provided to the class\n */\n public options: SmrtClassOptions;\n\n /**\n * Track which databases have had system tables initialized\n * - WeakSet for :memory: databases (URL not unique, track by instance)\n * - Set<string> for all others (URL is unique identifier)\n */\n private static _systemTablesInitialized = new WeakSet<DatabaseInterface>();\n private static _systemTablesInitializedByUrl = new Set<string>();\n\n /**\n * Creates a new SmrtClass instance\n *\n * @param options - Configuration options for database, filesystem, and AI clients\n */\n constructor(options: SmrtClassOptions = {}) {\n this.options = options;\n this._className = this.constructor.name;\n }\n\n /**\n * Determines whether this class requires a database to function\n *\n * Override this method in subclasses that require database access\n * to enable early validation during initialization.\n *\n * @returns True if database is required, false otherwise\n * @example\n * ```typescript\n * class MyDataModel extends SmrtClass {\n * protected requiresDatabase(): boolean {\n * return true; // This class needs database access\n * }\n * }\n * ```\n */\n protected requiresDatabase(): boolean {\n return false; // Base class doesn't require database by default\n }\n\n /**\n * Initializes database, filesystem, and AI client connections\n *\n * This method sets up all required services based on the provided options.\n * It should be called before using any of the service interfaces.\n *\n * @returns Promise that resolves to this instance for chaining\n * @throws {Error} If database is required but not provided in options\n */\n protected async initialize(): Promise<this> {\n await this.initializeCoreResources();\n\n if (!this.options._deferRuntimeInitialization) {\n await this.initializeRuntimeServices();\n }\n\n return this;\n }\n\n /**\n * Initialize core resources required for ORM behavior.\n *\n * This setup is shared by both full runtime initialization and lightweight\n * query hydration. Hydrated objects reuse an existing DB connection and skip\n * repeated system-table checks.\n */\n protected async initializeCoreResources(): Promise<void> {\n // Map persistence to db for backward compatibility\n if (this.options.persistence && !this.options.db) {\n this.options.db = this.options.persistence;\n }\n\n // Validate database configuration if required\n if (this.requiresDatabase() && !this.options.db) {\n throw new Error(\n `${this._className} requires a database configuration. ` +\n `Please provide 'db' in options: { db: { url: '...' } } or { db: 'database.db' }`,\n );\n }\n\n if (this.options.db) {\n this._dbEngineHint = getDatabaseTypeHint(this.options.db);\n\n if (\n this.options._reuseInitializedDb &&\n typeof this.options.db === 'object' &&\n 'query' in this.options.db\n ) {\n this._db = this.options.db as DatabaseInterface;\n this.options.db = this._db;\n } else {\n // Handle four db config formats (in implementation order):\n // 1. String URL: 'products.db' (shortcut)\n // 2. DatabaseInterface instance: already initialized db (has 'query' method)\n // 3. Config with client: { type: 'postgres', client: pgPool } (SvelteKit pattern)\n // 4. Config object: { type: 'sqlite', url: 'products.db' }\n if (typeof this.options.db === 'string') {\n // Format 1: String shortcut - let getDatabase auto-detect type from URL\n // Preserve connection sharing for file-backed databases while leaving\n // true in-memory databases isolated per instance.\n const isMemoryDb = this.options.db === ':memory:';\n this._db = await getDatabase({\n url: this.options.db,\n ...(isMemoryDb ? {} : { dbid: `smrt:${this.options.db}` }),\n });\n } else if ('query' in this.options.db) {\n // Format 2: Already a DatabaseInterface instance - return as-is\n this._db = this.options.db as DatabaseInterface;\n } else if ('client' in this.options.db && this.options.db.client) {\n // Format 3: Config with pre-created client (e.g., from SvelteKit's $env-based connection)\n // Pass the client to getDatabase which will use it instead of creating a new connection\n const dbConfig = this.options.db as {\n type?: string;\n client: unknown;\n url?: string;\n };\n this._db = await getDatabase({\n type: dbConfig.type || 'postgres',\n client: dbConfig.client,\n url: dbConfig.url,\n } as any);\n } else {\n // Format 4: Config object - pass to getDatabase (handles all types uniformly)\n // Preserve connection sharing for file-backed databases while leaving\n // true in-memory databases isolated per instance.\n const dbConfig = this.options.db as { url?: string; type?: string };\n const dbUrl = dbConfig.url || 'memory';\n const isMemoryDb = dbUrl === ':memory:' || dbUrl === 'memory';\n this._db = await getDatabase({\n ...this.options.db,\n ...(isMemoryDb ? {} : { dbid: `smrt:${dbUrl}` }),\n } as any);\n }\n\n /**\n * INTENTIONAL MUTATION: After resolving the database config,\n * we replace options.db with the actual DatabaseInterface instance.\n * This enables child objects to share the same connection via:\n *\n * const child = new ChildObject({ db: parent.options.db });\n *\n * Without this, passing this.options to getCollection() would use the config object\n * which causes a NEW db instance to be created, losing data isolation.\n *\n * See issue #567 for context on why this pattern is necessary.\n */\n this.options.db = this._db;\n\n await this.ensureSystemTables();\n }\n }\n }\n\n /**\n * Initialize optional runtime services that are not required for plain ORM reads.\n */\n protected async initializeRuntimeServices(): Promise<void> {\n if (this._runtimeServicesInitialized) {\n return;\n }\n\n if (!this._runtimeServicesInitPromise) {\n this._runtimeServicesInitPromise = (async () => {\n if (this.options.fs && !this._fs) {\n this._fs = await FilesystemAdapter.create(this.options.fs);\n }\n\n // Initialize AI client with environment variable support\n // Priority: instance options > env vars > global config > defaults\n const globalConfig = config.toJSON();\n const usageConfig = this.mergeAiUsageConfig(globalConfig);\n this.initializeAiUsageHandlers(usageConfig);\n\n if (\n !this._ai &&\n (this.options.ai || globalConfig.ai || process.env.SMRT_AI_PROVIDER)\n ) {\n // Check if options.ai is already a client-like object with embed method\n // This allows passing mock AI clients for testing\n const aiOption = this.options.ai as\n | Record<string, unknown>\n | undefined;\n if (\n aiOption &&\n typeof aiOption === 'object' &&\n typeof aiOption.embed === 'function' &&\n !aiOption.provider\n ) {\n this._ai = aiOption as unknown as AIClient;\n } else {\n // CC-8 follow-up: ideally this would route through\n // `@happyvertical/smrt-config` for sanitization parity (e.g. via\n // `getPackageConfig('ai', ...)`), but smrt-config currently only\n // merges file-based config + runtime overrides — it does NOT\n // read from `process.env` with a typed prefix/schema. Until\n // smrt-config grows an env-loader (or wraps `loadEnvConfig`),\n // we continue to use the underlying utility directly. Tracked\n // alongside the CC-8 audit on issue #1199.\n const { loadEnvConfig } = await import('@happyvertical/utils');\n\n // Start with global defaults\n const baseConfig = globalConfig.ai || {};\n\n // Merge with instance options (takes priority over global)\n const userConfig = { ...baseConfig, ...this.options.ai };\n\n // Load environment variables and merge (user options take priority)\n const aiConfig = loadEnvConfig<any>(userConfig, {\n packageName: 'ai',\n prefix: 'SMRT',\n schema: {\n provider: 'string',\n model: 'string',\n apiKey: 'string',\n timeout: 'number',\n maxRetries: 'number',\n temperature: 'number',\n maxTokens: 'number',\n },\n });\n\n const existingOnUsage =\n aiConfig.onUsage ??\n (userConfig as Record<string, unknown>).onUsage ??\n undefined;\n aiConfig.onUsage = async (event: unknown) => {\n if (typeof existingOnUsage === 'function') {\n await (existingOnUsage as (usageEvent: unknown) => unknown)(\n event,\n );\n }\n await this.handleAiUsageCallback(event, aiConfig, usageConfig);\n };\n\n // Only initialize if we have a provider configured\n if (aiConfig.provider || aiConfig.type || aiConfig.apiKey) {\n // Use getAI() factory to support all AI providers (OpenAI, Anthropic, Gemini, etc.)\n // getAI() returns AIInterface, which we cast to AIClient for backward compatibility\n this._ai = (await getAI(aiConfig as any)) as any as AIClient;\n }\n }\n }\n\n await this.initializeSignals();\n this._runtimeServicesInitialized = true;\n })();\n }\n\n try {\n await this._runtimeServicesInitPromise;\n } finally {\n this._runtimeServicesInitPromise = undefined;\n }\n }\n\n /**\n * Ensure deferred runtime services are ready before using them.\n */\n protected async ensureRuntimeServicesInitialized(): Promise<void> {\n if (!this._runtimeServicesInitialized) {\n await this.initializeRuntimeServices();\n }\n }\n\n /**\n * Resolve the AI client, initializing deferred runtime services on demand.\n */\n protected async getAiClient(): Promise<AIClient> {\n await this.ensureRuntimeServicesInitialized();\n\n if (!this._ai) {\n throw new Error(\n `${this._className} does not have an AI client configured. ` +\n `Provide 'ai' in options or configure a global SMRT AI provider.`,\n );\n }\n\n return this._ai;\n }\n\n /**\n * Resolve the AI client if one is configured, otherwise return undefined.\n */\n protected async getOptionalAiClient(): Promise<AIClient | undefined> {\n await this.ensureRuntimeServicesInitialized();\n return this._ai;\n }\n\n private async isSystemSchemaVersionApplied(\n db: DatabaseInterface,\n version: string,\n ): Promise<boolean> {\n const engine = detectEngine(getDatabaseUrl(db), this._dbEngineHint);\n\n if (\n engine === 'postgres' &&\n !(await tableExists(db, '_smrt_migrations', this._dbEngineHint))\n ) {\n return false;\n }\n\n try {\n const versionParam = engine === 'postgres' ? '$1' : '?';\n const rows = await db.query(\n `SELECT 1 FROM _smrt_migrations WHERE version = ${versionParam} LIMIT 1`,\n version,\n );\n return getQueryRows(rows).length > 0;\n } catch (error) {\n if (engine === 'postgres') {\n throw error;\n }\n\n return false;\n }\n }\n\n /**\n * Ensure SMRT system tables exist in the database\n *\n * System tables use _smrt_ prefix and store framework metadata:\n * - _smrt_contexts: Context memory storage for remembered patterns\n * - _smrt_migrations: Schema version tracking\n * - _smrt_registry: Object registry persistence\n * - _smrt_signals: Signal history/audit log\n *\n * This method is idempotent and safe to call multiple times.\n * Tables are only created once per database connection.\n */\n private async ensureSystemTables(): Promise<void> {\n if (!this._db) return;\n\n const dbUrl = getDatabaseUrl(this._db);\n\n // Some databases share URLs but are different instances:\n // - :memory: databases (SQLite/DuckDB in-memory)\n // - JSON databases (may have undefined or shared URLs)\n // - Any database with undefined URL\n // Use WeakSet for instance tracking, Set<string> for URL tracking\n const dbConstructorName = this._db.constructor?.name || '';\n const isMemoryDb = dbUrl === ':memory:';\n const isJsonDb = dbConstructorName.toLowerCase().includes('json');\n const hasUndefinedUrl = !dbUrl;\n const useInstanceTracking = isMemoryDb || isJsonDb || hasUndefinedUrl;\n\n if (useInstanceTracking) {\n // Check WeakSet for databases that may share URLs (track by instance)\n if (SmrtClass._systemTablesInitialized.has(this._db)) {\n return;\n }\n } else {\n // Check Set<string> for URL-based databases (track by URL)\n if (SmrtClass._systemTablesInitializedByUrl.has(dbUrl)) {\n return;\n }\n }\n\n try {\n await this.withSystemTableBootstrapLock((db) =>\n this.bootstrapSystemTables(db),\n );\n await this.ensureNativeVectorStorage();\n\n // Mark as initialized using appropriate tracking mechanism\n if (useInstanceTracking) {\n SmrtClass._systemTablesInitialized.add(this._db);\n } else {\n SmrtClass._systemTablesInitializedByUrl.add(dbUrl);\n }\n } catch (error) {\n // DO NOT SWALLOW ERRORS - fail loudly so we know what's wrong\n const dbInfo = this._db.constructor?.name || 'unknown database';\n throw new Error(\n `Failed to create system tables for ${dbInfo}: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n }\n\n private async withSystemTableBootstrapLock<T>(\n callback: (db: DatabaseInterface) => Promise<T>,\n ): Promise<T> {\n if (!this._db) {\n throw new Error('Database is not initialized');\n }\n\n const beginTransaction = this._db.beginTransaction;\n const transaction = (this._db as TransactionCapableDatabase).transaction;\n const shouldUsePostgresLock =\n detectEngine(getDatabaseUrl(this._db), this._dbEngineHint) === 'postgres';\n\n if (!shouldUsePostgresLock) {\n return callback(this._db);\n }\n\n if (typeof beginTransaction === 'function') {\n const tx = await beginTransaction.call(this._db);\n if (!tx) {\n throw new Error('Database transaction could not be started');\n }\n\n try {\n await tx.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);\n const result = await callback(tx);\n await tx.commit();\n return result;\n } catch (error) {\n await this.rollbackSystemTableBootstrap(tx);\n throw error;\n }\n }\n\n if (typeof transaction === 'function') {\n const runInTransaction = transaction.bind(this._db) as <R>(\n callback: (tx: DatabaseInterface) => Promise<R>,\n ) => Promise<R>;\n\n return runInTransaction(async (tx) => {\n await tx.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);\n return callback(tx);\n });\n }\n\n throw new Error(\n 'Postgres system table bootstrap requires a transaction-capable database adapter',\n );\n }\n\n private async rollbackSystemTableBootstrap(\n tx: TransactionHandle,\n ): Promise<void> {\n try {\n if (typeof tx.isActive !== 'function' || tx.isActive()) {\n await tx.rollback();\n }\n } catch (rollbackError) {\n logger.warn(\n `[smrt] Failed to rollback system table bootstrap transaction: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`,\n );\n }\n }\n\n private async bootstrapSystemTables(db: DatabaseInterface): Promise<void> {\n // Fast path: check if system tables already exist by querying _smrt_migrations.\n // This avoids 29 sequential DDL round-trips on high-latency connections\n // (e.g. remote Postgres over Tailscale where each round-trip is ~650ms).\n // For Postgres, this runs after acquiring the advisory lock so concurrent\n // initializers can observe a completed bootstrap and skip replaying DDL.\n const version = SMRT_SCHEMA_VERSION;\n if (await this.isSystemSchemaVersionApplied(db, version)) {\n return;\n }\n\n // Older installs can have a subset of system columns already created.\n // Upgrade those tables before replaying idempotent DDL so index creation\n // does not fail on missing legacy columns.\n await ensureLegacySystemTableCompatibility(db, this._dbEngineHint);\n\n // Create all system tables\n // Split multi-statement SQL into individual statements to avoid race conditions\n // Each ALL_SYSTEM_TABLES entry contains CREATE TABLE + CREATE INDEX statements\n const allStatements: string[] = [];\n for (const multiStatementSQL of ALL_SYSTEM_TABLES) {\n // Split on semicolon, filter out empty statements\n const statements = multiStatementSQL\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n allStatements.push(...statements);\n }\n\n // Use db.query() for system tables — they use CREATE TABLE/INDEX IF NOT EXISTS\n // which databases handle natively in a single round-trip. The syncSchema()\n // approach does per-column existence checks (multiple round-trips per table)\n // which is unnecessary for framework-owned system tables and extremely slow\n // on high-latency connections (e.g. remote postgres over Tailscale).\n for (const statement of allStatements) {\n await db.query(statement);\n }\n\n // Record current schema version\n // Use ON CONFLICT for DuckDB compatibility (not INSERT OR IGNORE)\n const id = crypto.randomUUID();\n const description = 'Initial SMRT system tables';\n await db.execute`\n INSERT INTO _smrt_migrations (id, version, description)\n VALUES (${id}, ${version}, ${description})\n ON CONFLICT(version) DO NOTHING\n `;\n }\n\n private async ensureNativeVectorStorage(): Promise<void> {\n if (!this._db) {\n return;\n }\n\n try {\n const { ObjectRegistry } = await import('./registry.js');\n const embeddingConfig = ObjectRegistry.getProjectEmbeddingConfig();\n if (embeddingConfig?.storage === 'native') {\n const { EmbeddingStorage } = await import('./embeddings/storage.js');\n const vector = this._db.vector;\n if (vector) {\n const dimensions = embeddingConfig.dimensions || 768;\n await EmbeddingStorage.ensureVectorStorage(\n this._db,\n dimensions,\n vector,\n );\n } else {\n logger.warn(\n '[smrt] Embedding storage set to \"native\" but database has no vector capability. Falling back to JSON storage.',\n );\n }\n }\n } catch (error) {\n // Don't fail system table initialization for vector setup errors\n logger.warn(\n `[smrt] Failed to initialize vector storage: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n /**\n * Access system tables through standard database interface\n * System tables use _smrt_ prefix to avoid conflicts with user tables\n */\n protected get systemDb(): DatabaseInterface {\n return this._db;\n }\n\n /**\n * Initialize signal bus and adapters\n *\n * Merges global configuration with instance-specific overrides.\n * Registers built-in and custom adapters based on configuration.\n */\n private async initializeSignals(): Promise<void> {\n const globalConfig = config.toJSON();\n const effectiveConfig = this.mergeSignalConfig(globalConfig);\n\n // If a shared bus is provided, always use it (don't create new adapters)\n if (this.options.signals?.bus) {\n this._signalBus = this.options.signals.bus;\n return;\n }\n\n // Otherwise, check if we should initialize signals based on config\n if (!this.shouldInitializeSignals(effectiveConfig)) {\n return;\n }\n\n this._signalBus = new SignalBus({\n sanitization: effectiveConfig.sanitization,\n });\n await this.registerAdapters(effectiveConfig);\n }\n\n /**\n * Merge global and instance signal configuration\n *\n * Instance configuration takes priority over global defaults.\n *\n * @param globalConfig - Global configuration from smrt.configure()\n * @returns Merged configuration\n */\n private mergeSignalConfig(\n globalConfig: GlobalSignalConfig,\n ): GlobalSignalConfig {\n return {\n logging: this.options.logging ?? globalConfig.logging,\n metrics: this.options.metrics ?? globalConfig.metrics,\n pubsub: this.options.pubsub ?? globalConfig.pubsub,\n usage: this.mergeAiUsageConfig(globalConfig),\n sanitization: this.options.sanitization ?? globalConfig.sanitization,\n signals: {\n bus: this.options.signals?.bus ?? globalConfig.signals?.bus,\n adapters: [\n ...(globalConfig.signals?.adapters ?? []),\n ...(this.options.signals?.adapters ?? []),\n ],\n },\n };\n }\n\n /**\n * Check if signals should be initialized\n *\n * Signals are initialized if any adapter is configured.\n *\n * @param config - Effective signal configuration\n * @returns True if signals should be initialized\n */\n private shouldInitializeSignals(config: GlobalSignalConfig): boolean {\n return !!(\n config.logging !== false ||\n config.metrics?.enabled ||\n config.pubsub?.enabled ||\n config.signals?.adapters?.length\n );\n }\n\n /**\n * Register signal adapters based on configuration\n *\n * @param config - Effective signal configuration\n */\n private async registerAdapters(config: GlobalSignalConfig): Promise<void> {\n if (!this._signalBus) return;\n\n // Logging adapter (default: enabled with console)\n if (config.logging !== false) {\n const { createLogger, LoggerAdapter } = await import(\n '@happyvertical/logger'\n );\n const logger = createLogger(config.logging ?? true);\n const adapter = new LoggerAdapter(logger);\n this._signalBus.register(adapter);\n this._registeredAdapters.push(adapter);\n }\n\n // Metrics adapter (default: disabled)\n if (config.metrics?.enabled) {\n const { MetricsAdapter } = await import('./adapters/metrics.js');\n const adapter = new MetricsAdapter();\n this._signalBus.register(adapter);\n this._registeredAdapters.push(adapter);\n }\n\n // Pub/Sub adapter (default: disabled)\n if (config.pubsub?.enabled) {\n const { PubSubAdapter } = await import('./adapters/pubsub.js');\n const adapter = new PubSubAdapter();\n this._signalBus.register(adapter);\n this._registeredAdapters.push(adapter);\n }\n\n // Custom adapters\n if (config.signals?.adapters) {\n for (const adapter of config.signals.adapters) {\n this._signalBus.register(adapter);\n this._registeredAdapters.push(adapter);\n }\n }\n }\n\n /**\n * Gets the filesystem adapter instance\n */\n get fs() {\n return this._fs;\n }\n\n /**\n * Gets the database interface instance\n */\n get db() {\n // Throw helpful error if database is accessed before initialization\n if (!this._db) {\n throw new Error(\n `Database accessed before initialization. ` +\n `Please call await instance.initialize() before accessing the database.`,\n );\n }\n return this._db;\n }\n\n /**\n * Gets the AI client instance\n */\n get ai() {\n return this._ai;\n }\n\n /**\n * Get the in-memory AI usage snapshot for this instance.\n */\n getAiUsageSnapshot(): AiUsageSnapshot | undefined {\n return this._aiUsageCollector?.getSnapshot();\n }\n\n /**\n * Reset the in-memory AI usage collector.\n */\n resetAiUsage(): void {\n this._aiUsageCollector?.reset();\n }\n\n /**\n * List persisted AI usage records.\n */\n async listAiUsage(\n options: AiUsageListOptions = {},\n ): Promise<SmrtAiUsageRecord[]> {\n if (!this._db) {\n throw new Error(\n `AI usage requires a database configuration. ` +\n `Please call initialize() with a db option before querying usage.`,\n );\n }\n\n const { conditions, params } = buildAiUsageWhereClause(options);\n let paramIndex = params.length + 1;\n\n let sql = 'SELECT * FROM _smrt_ai_usage';\n if (conditions.length > 0) {\n sql += ` WHERE ${conditions.join(' AND ')}`;\n }\n\n sql += ` ORDER BY ${\n options.orderBy === 'timestamp ASC' ? 'created_at ASC' : 'created_at DESC'\n }`;\n\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 = getQueryRows(await this._db.query(sql, ...params));\n\n return rows.map((row) => ({\n id: String(row.id),\n provider: String(row.provider),\n model: String(row.model),\n operation: String(row.operation),\n usage: hydratePersistedAiUsageTokens(row),\n estimatedCost:\n row.estimated_cost === null || row.estimated_cost === undefined\n ? undefined\n : Number(row.estimated_cost),\n duration: Number(row.duration ?? 0),\n className:\n row.class_name === null || row.class_name === undefined\n ? undefined\n : String(row.class_name),\n tenantId:\n row.tenant_id === undefined\n ? undefined\n : (row.tenant_id as string | null),\n tags: parseAiUsageTags(row.tags),\n timestamp: normalizeAiUsageTimestamp(row.created_at),\n }));\n }\n\n /**\n * Summarize persisted AI usage records by a grouping dimension.\n */\n async summarizeAiUsage(\n options: AiUsageSummaryOptions = {},\n ): Promise<Record<string, AiUsageStats>> {\n if (!this._db) {\n throw new Error(\n `AI usage requires a database configuration. ` +\n `Please call initialize() with a db option before querying usage.`,\n );\n }\n\n const groupBy = options.groupBy ?? 'model';\n const bucketExpression =\n groupBy === 'provider'\n ? `provider`\n : groupBy === 'model'\n ? `provider || ':' || model`\n : groupBy === 'class'\n ? `COALESCE(class_name, 'unknown')`\n : groupBy === 'tenant'\n ? `COALESCE(tenant_id, 'global')`\n : groupBy === 'operation'\n ? `operation`\n : `substr(CAST(created_at AS TEXT), 1, 10)`;\n\n const { conditions, params } = buildAiUsageWhereClause(options);\n\n let sql = `\n SELECT ${bucketExpression} AS bucket,\n COUNT(*) AS call_count,\n COALESCE(SUM(prompt_tokens), 0) AS prompt_tokens,\n COALESCE(SUM(completion_tokens), 0) AS completion_tokens,\n COALESCE(SUM(total_tokens), 0) AS total_tokens,\n COALESCE(SUM(duration), 0) AS total_duration,\n COALESCE(SUM(estimated_cost), 0) AS estimated_cost,\n MAX(created_at) AS last_used\n FROM _smrt_ai_usage\n `;\n\n if (conditions.length > 0) {\n sql += ` WHERE ${conditions.join(' AND ')}`;\n }\n\n sql += ' GROUP BY bucket ORDER BY bucket ASC';\n\n const rows = getQueryRows(await this._db.query(sql, ...params));\n const summary: Record<string, AiUsageStats> = {};\n\n for (const row of rows) {\n const bucket = String(row.bucket);\n summary[bucket] = {\n callCount: Number(row.call_count ?? 0),\n promptTokens: Number(row.prompt_tokens ?? 0),\n completionTokens: Number(row.completion_tokens ?? 0),\n totalTokens: Number(row.total_tokens ?? 0),\n totalDuration: Number(row.total_duration ?? 0),\n estimatedCost: Number(row.estimated_cost ?? 0),\n lastUsed: row.last_used ? new Date(String(row.last_used)).getTime() : 0,\n };\n }\n\n return summary;\n }\n\n /**\n * Gets the signal bus instance\n *\n * @returns Signal bus if signals are enabled, undefined otherwise\n */\n get signalBus(): SignalBus | undefined {\n return this._signalBus;\n }\n\n /**\n * Cleanup method to prevent memory leaks\n *\n * Unregisters all adapters from the signal bus that were registered\n * by this instance. Call this when the SmrtClass instance is no longer\n * needed to prevent memory leaks.\n *\n * @example\n * ```typescript\n * const product = new Product({ name: 'Widget' });\n * await product.initialize();\n * // ... use product ...\n * product.destroy(); // Clean up when done\n * ```\n */\n destroy(): void {\n // Only unregister adapters if we own the bus (not shared)\n if (this._signalBus && !this.options.signals?.bus) {\n for (const adapter of this._registeredAdapters) {\n this._signalBus.unregister(adapter);\n }\n this._registeredAdapters = [];\n }\n\n // TODO: If SmrtClass grows a broader async teardown lifecycle, move\n // AI usage handler cleanup there alongside other connection-bound state.\n this._aiUsageCollector = undefined;\n this._aiUsageHandlers = [];\n }\n\n private mergeAiUsageConfig(\n globalConfig: GlobalSignalConfig,\n ): ResolvedAiUsageConfig {\n const globalUsage = globalConfig.usage ?? {};\n const instanceUsage = this.options.usage ?? {};\n\n return {\n enabled: instanceUsage.enabled ?? globalUsage.enabled ?? true,\n persist: instanceUsage.persist ?? globalUsage.persist ?? true,\n estimateCosts:\n instanceUsage.estimateCosts ?? globalUsage.estimateCosts ?? true,\n costRates: {\n ...(globalUsage.costRates ?? {}),\n ...(instanceUsage.costRates ?? {}),\n },\n handlers: [\n ...(globalUsage.handlers ?? []),\n ...(instanceUsage.handlers ?? []),\n ],\n };\n }\n\n private initializeAiUsageHandlers(config: ResolvedAiUsageConfig): void {\n this._aiUsageCollector = undefined;\n this._aiUsageHandlers = [];\n\n if (!config.enabled) {\n return;\n }\n\n this._aiUsageCollector = new AiUsageCollector();\n this._aiUsageHandlers.push(this._aiUsageCollector);\n\n if (config.persist && this._db) {\n this._aiUsageHandlers.push(new AiUsagePersistenceHandler(this._db));\n }\n\n this._aiUsageHandlers.push(...config.handlers);\n }\n\n private async handleAiUsageCallback(\n event: unknown,\n aiConfig: Record<string, unknown>,\n usageConfig: ResolvedAiUsageConfig,\n ): Promise<void> {\n if (!usageConfig.enabled || this._aiUsageHandlers.length === 0) {\n return;\n }\n\n const normalizedEvent = this.normalizeAiUsageEvent(event, aiConfig);\n if (!normalizedEvent) {\n return;\n }\n\n if (usageConfig.estimateCosts) {\n normalizedEvent.estimatedCost = estimateAiUsageCost(\n normalizedEvent.provider,\n normalizedEvent.model,\n normalizedEvent.usage,\n usageConfig.costRates,\n );\n }\n\n const results = await Promise.allSettled(\n this._aiUsageHandlers.map((handler) => handler.handle(normalizedEvent)),\n );\n\n for (const result of results) {\n if (result.status === 'rejected') {\n logger.warn(\n `[smrt] AI usage handler failed for ${normalizedEvent.provider}:${normalizedEvent.model}: ${\n result.reason instanceof Error\n ? result.reason.message\n : String(result.reason)\n }`,\n );\n }\n }\n }\n\n private normalizeAiUsageEvent(\n event: unknown,\n aiConfig: Record<string, unknown>,\n ): SmrtAiUsageEvent | undefined {\n const raw = (event ?? {}) as Record<string, unknown>;\n const provider = firstString(\n raw.provider,\n raw.type,\n aiConfig.provider,\n aiConfig.type,\n );\n const model = firstString(\n raw.model,\n raw.defaultModel,\n aiConfig.model,\n aiConfig.defaultModel,\n );\n const operation =\n firstString(raw.operation, raw.kind, raw.method) ?? 'unknown';\n const usage =\n normalizeIncomingAiUsageTokens(raw.usage) ??\n normalizeIncomingAiUsageTokens(raw.tokenUsage) ??\n normalizeIncomingAiUsageTokens({\n promptTokens: raw.promptTokens,\n completionTokens: raw.completionTokens,\n totalTokens: raw.totalTokens,\n });\n\n if (!provider || !model) {\n return undefined;\n }\n\n const duration =\n firstNumber(raw.duration, raw.durationMs, raw.latency) ?? 0;\n const tenantId =\n 'tenantId' in this &&\n (this as { tenantId?: string | null }).tenantId !== undefined\n ? ((this as { tenantId?: string | null }).tenantId ?? null)\n : undefined;\n\n return {\n provider,\n model,\n operation,\n usage,\n duration,\n timestamp: normalizeAiUsageTimestamp(raw.timestamp),\n tags: normalizeAiUsageTags(raw.tags),\n className: this._className,\n tenantId,\n };\n }\n}\n"],"names":["config","createLogger","logger"],"mappings":"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"}
1
+ {"version":3,"file":"class.js","sources":["../src/class.ts"],"sourcesContent":["import type { AIClientOptions } from '@happyvertical/ai';\nimport { type AIClient, getAI } from '@happyvertical/ai';\nimport type { FilesystemAdapterOptions } from '@happyvertical/files';\nimport { FilesystemAdapter } from '@happyvertical/files';\nimport { createLogger, type LoggerConfig } from '@happyvertical/logger';\nimport type {\n AiTokenUsage,\n AiUsageHandler,\n AiUsageListOptions,\n AiUsageSnapshot,\n AiUsageStats,\n AiUsageSummaryOptions,\n SignalAdapter,\n SmrtAiUsageEvent,\n SmrtAiUsageRecord,\n} from '@happyvertical/smrt-types';\nimport {\n type DatabaseInterface,\n getDatabase,\n type TransactionHandle,\n} from '@happyvertical/sql';\nimport {\n AiUsageCollector,\n AiUsagePersistenceHandler,\n} from './adapters/ai-usage.js';\nimport { estimateAiUsageCost } from './adapters/cost-rates.js';\nimport type {\n AIConfig,\n AiUsageConfig,\n GlobalSignalConfig,\n MetricsConfig,\n PubSubConfig,\n} from './config.js';\nimport { config } from './config.js';\nimport type { DatabaseConfig } from './database.js';\nimport { detectEngine } from './schema/ddl/index.js';\nimport { SignalBus } from './signals/bus.js';\nimport {\n ensureLegacySystemTableCompatibility,\n tableExists,\n} from './system/compatibility.js';\nimport { ALL_SYSTEM_TABLES, SMRT_SCHEMA_VERSION } from './system/schema.js';\n\nconst SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL =\n \"SELECT pg_advisory_xact_lock(hashtext('smrt'), hashtext('system-tables'))\";\n\nconst logger = createLogger({ level: 'info' });\n\ntype DatabaseWithConfig = DatabaseInterface & {\n config?: {\n type?: string;\n url?: string;\n };\n type?: string;\n};\n\ntype TransactionCapableDatabase = DatabaseInterface & {\n transaction?: <T>(\n this: DatabaseInterface,\n callback: (tx: DatabaseInterface) => Promise<T>,\n ) => Promise<T>;\n};\n\ninterface ResolvedAiUsageConfig {\n enabled: boolean;\n persist: boolean;\n estimateCosts: boolean;\n costRates?: Record<string, { input: number; output: number }>;\n handlers: AiUsageHandler[];\n}\n\ntype AiUsageFilterOptions = Pick<\n AiUsageListOptions,\n | 'since'\n | 'until'\n | 'provider'\n | 'model'\n | 'operation'\n | 'className'\n | 'tenantId'\n>;\n\ninterface AiUsageWhereClause {\n conditions: string[];\n params: unknown[];\n nextParamIndex: number;\n}\n\nfunction firstString(...candidates: unknown[]): string | undefined {\n return candidates.find((candidate): candidate is string => {\n return typeof candidate === 'string';\n });\n}\n\nfunction firstNumber(...candidates: unknown[]): number | undefined {\n return candidates.find((candidate): candidate is number => {\n return typeof candidate === 'number';\n });\n}\n\nfunction getDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseWithConfig;\n return db.url || dbWithConfig.config?.url || '';\n}\n\nfunction getDatabaseTypeHint(\n config: DatabaseConfig | undefined,\n): string | undefined {\n if (!config || typeof config === 'string') {\n return undefined;\n }\n\n const configWithType = config as DatabaseWithConfig & {\n client?: unknown;\n };\n\n if (typeof configWithType.type === 'string') {\n return configWithType.type;\n }\n\n if (typeof configWithType.config?.type === 'string') {\n return configWithType.config.type;\n }\n\n if ('query' in configWithType && typeof configWithType.query === 'function') {\n return undefined;\n }\n\n if ('client' in configWithType && configWithType.client) {\n return 'postgres';\n }\n\n return undefined;\n}\n\nfunction normalizeIncomingAiUsageTokens(\n value: unknown,\n): AiTokenUsage | undefined {\n if (!value || typeof value !== 'object') {\n return undefined;\n }\n\n const usage = value as Record<string, unknown>;\n const promptTokens =\n typeof usage.promptTokens === 'number'\n ? usage.promptTokens\n : typeof usage.inputTokens === 'number'\n ? usage.inputTokens\n : undefined;\n const completionTokens =\n typeof usage.completionTokens === 'number'\n ? usage.completionTokens\n : typeof usage.outputTokens === 'number'\n ? usage.outputTokens\n : undefined;\n const totalTokens =\n typeof usage.totalTokens === 'number'\n ? usage.totalTokens\n : promptTokens !== undefined || completionTokens !== undefined\n ? (promptTokens ?? 0) + (completionTokens ?? 0)\n : undefined;\n\n if (\n promptTokens === undefined &&\n completionTokens === undefined &&\n totalTokens === undefined\n ) {\n return undefined;\n }\n\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n}\n\nfunction hydratePersistedAiUsageTokens(row: {\n prompt_tokens?: unknown;\n completion_tokens?: unknown;\n total_tokens?: unknown;\n}): AiTokenUsage | undefined {\n const promptTokens =\n typeof row.prompt_tokens === 'number'\n ? row.prompt_tokens\n : row.prompt_tokens === null || row.prompt_tokens === undefined\n ? undefined\n : Number(row.prompt_tokens);\n const completionTokens =\n typeof row.completion_tokens === 'number'\n ? row.completion_tokens\n : row.completion_tokens === null || row.completion_tokens === undefined\n ? undefined\n : Number(row.completion_tokens);\n const totalTokens =\n typeof row.total_tokens === 'number'\n ? row.total_tokens\n : row.total_tokens === null || row.total_tokens === undefined\n ? undefined\n : Number(row.total_tokens);\n\n if (\n promptTokens === undefined &&\n completionTokens === undefined &&\n totalTokens === undefined\n ) {\n return undefined;\n }\n\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n}\n\nfunction normalizeAiUsageTags(\n value: unknown,\n): Record<string, string> | undefined {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return undefined;\n }\n\n const tags: Record<string, string> = {};\n for (const [key, tagValue] of Object.entries(value)) {\n if (tagValue === undefined || tagValue === null) continue;\n tags[key] = String(tagValue);\n }\n\n return Object.keys(tags).length > 0 ? tags : undefined;\n}\n\nfunction parseAiUsageTags(value: unknown): Record<string, string> | undefined {\n if (typeof value !== 'string') {\n return undefined;\n }\n\n try {\n return normalizeAiUsageTags(JSON.parse(value));\n } catch {\n return undefined;\n }\n}\n\nfunction normalizeAiUsageTimestamp(value: unknown): Date {\n if (value instanceof Date) {\n return value;\n }\n\n if (typeof value === 'string' || typeof value === 'number') {\n const date = new Date(value);\n if (!Number.isNaN(date.getTime())) {\n return date;\n }\n }\n\n return new Date();\n}\n\nfunction getQueryRows(\n result: Awaited<ReturnType<DatabaseInterface['query']>>,\n): Record<string, unknown>[] {\n return Array.isArray(result)\n ? (result as Record<string, unknown>[])\n : ((result as { rows?: Record<string, unknown>[] }).rows ?? []);\n}\n\nfunction buildAiUsageWhereClause(\n options: AiUsageFilterOptions,\n): AiUsageWhereClause {\n const conditions: string[] = [];\n const params: unknown[] = [];\n let nextParamIndex = 1;\n\n if (options.since) {\n conditions.push(`created_at >= $${nextParamIndex++}`);\n params.push(options.since.toISOString());\n }\n\n if (options.until) {\n conditions.push(`created_at <= $${nextParamIndex++}`);\n params.push(options.until.toISOString());\n }\n\n if (options.provider) {\n conditions.push(`provider = $${nextParamIndex++}`);\n params.push(options.provider);\n }\n\n if (options.model) {\n conditions.push(`model = $${nextParamIndex++}`);\n params.push(options.model);\n }\n\n if (options.operation) {\n conditions.push(`operation = $${nextParamIndex++}`);\n params.push(options.operation);\n }\n\n if (options.className) {\n conditions.push(`class_name = $${nextParamIndex++}`);\n params.push(options.className);\n }\n\n if (options.tenantId === null) {\n conditions.push(`tenant_id IS NULL`);\n } else if (options.tenantId) {\n conditions.push(`tenant_id = $${nextParamIndex++}`);\n params.push(options.tenantId);\n }\n\n return {\n conditions,\n params,\n nextParamIndex,\n };\n}\n\n/**\n * Configuration options for the SmrtClass\n */\nexport interface SmrtClassOptions {\n /**\n * Optional custom class name override\n */\n _className?: string;\n\n /**\n * Database configuration - unified approach matching @happyvertical/sql\n *\n * Supports three formats:\n * - String shortcut: 'products.db' (auto-detects database type)\n * - Config object: { type: 'sqlite', url: 'products.db' }\n * - DatabaseInterface instance: await getDatabase(...)\n *\n * @see DatabaseConfig for type definition\n */\n db?: DatabaseConfig;\n\n /**\n * Alias for db option - for backward compatibility with documentation\n *\n * @deprecated Use 'db' instead. This alias exists for backward compatibility.\n */\n persistence?: DatabaseConfig;\n\n /**\n * Filesystem adapter configuration options\n */\n fs?: FilesystemAdapterOptions;\n\n /**\n * AI client configuration options or instance\n */\n ai?: AIClientOptions | AIClient;\n\n /**\n * AI usage tracking configuration (overrides global defaults)\n */\n usage?: AiUsageConfig;\n\n /**\n * Logging configuration (overrides global default)\n */\n logging?: LoggerConfig;\n\n /**\n * Metrics configuration (overrides global default)\n */\n metrics?: MetricsConfig;\n\n /**\n * Pub/Sub configuration (overrides global default)\n */\n pubsub?: PubSubConfig;\n\n /**\n * Sanitization configuration (overrides global default)\n */\n sanitization?: import('./config.js').GlobalSignalConfig['sanitization'];\n\n /**\n * Custom signal configuration (overrides global default)\n */\n signals?: {\n /** Shared signal bus instance */\n bus?: SignalBus;\n /** Additional custom adapters */\n adapters?: SignalAdapter[];\n };\n\n /**\n * Internal flag to reuse an already initialized DatabaseInterface instance.\n *\n * Skips database resolution and system-table setup during lightweight hydration.\n * @internal\n */\n _reuseInitializedDb?: boolean;\n\n /**\n * Internal flag to defer runtime-only services such as signals and AI setup.\n *\n * Lightweight hydration paths set this so plain query reads avoid per-row\n * runtime bootstrap costs.\n * @internal\n */\n _deferRuntimeInitialization?: boolean;\n}\n\n/**\n * Foundation class providing core functionality for the SMRT framework\n *\n * SmrtClass provides unified access to database, filesystem, and AI client\n * interfaces. It serves as the foundation for all other classes in the\n * SMRT framework.\n */\nexport class SmrtClass {\n /**\n * AI client instance for interacting with AI models\n */\n protected _ai!: AIClient;\n\n /**\n * Filesystem adapter for file operations\n */\n protected _fs!: FilesystemAdapter;\n\n /**\n * Database interface for data persistence\n */\n protected _db!: DatabaseInterface;\n private _dbEngineHint?: string;\n\n /**\n * Class name used for identification\n */\n protected _className!: string;\n\n /**\n * Signal bus for method execution tracking\n */\n protected _signalBus?: SignalBus;\n\n /**\n * Adapters registered by this instance (for cleanup)\n */\n private _registeredAdapters: SignalAdapter[] = [];\n\n /**\n * In-memory AI usage collector for quick inspection.\n */\n private _aiUsageCollector?: AiUsageCollector;\n\n /**\n * Registered AI usage handlers for this instance.\n */\n private _aiUsageHandlers: AiUsageHandler[] = [];\n\n /**\n * Tracks whether optional runtime services (signals, AI, fs) are ready.\n */\n private _runtimeServicesInitialized = false;\n\n /**\n * Shared in-flight runtime initialization promise for single-flight setup.\n */\n private _runtimeServicesInitPromise?: Promise<void>;\n\n /**\n * Configuration options provided to the class\n */\n public options: SmrtClassOptions;\n\n /**\n * Track which databases have had system tables initialized\n * - WeakSet for :memory: databases (URL not unique, track by instance)\n * - Set<string> for all others (URL is unique identifier)\n */\n private static _systemTablesInitialized = new WeakSet<DatabaseInterface>();\n private static _systemTablesInitializedByUrl = new Set<string>();\n\n /**\n * Creates a new SmrtClass instance\n *\n * @param options - Configuration options for database, filesystem, and AI clients\n */\n constructor(options: SmrtClassOptions = {}) {\n this.options = options;\n this._className = this.constructor.name;\n }\n\n /**\n * Determines whether this class requires a database to function\n *\n * Override this method in subclasses that require database access\n * to enable early validation during initialization.\n *\n * @returns True if database is required, false otherwise\n * @example\n * ```typescript\n * class MyDataModel extends SmrtClass {\n * protected requiresDatabase(): boolean {\n * return true; // This class needs database access\n * }\n * }\n * ```\n */\n protected requiresDatabase(): boolean {\n return false; // Base class doesn't require database by default\n }\n\n /**\n * Initializes database, filesystem, and AI client connections\n *\n * This method sets up all required services based on the provided options.\n * It should be called before using any of the service interfaces.\n *\n * @returns Promise that resolves to this instance for chaining\n * @throws {Error} If database is required but not provided in options\n */\n protected async initialize(): Promise<this> {\n await this.initializeCoreResources();\n\n if (!this.options._deferRuntimeInitialization) {\n await this.initializeRuntimeServices();\n }\n\n return this;\n }\n\n /**\n * Initialize core resources required for ORM behavior.\n *\n * This setup is shared by both full runtime initialization and lightweight\n * query hydration. Hydrated objects reuse an existing DB connection and skip\n * repeated system-table checks.\n */\n protected async initializeCoreResources(): Promise<void> {\n // Map persistence to db for backward compatibility\n if (this.options.persistence && !this.options.db) {\n this.options.db = this.options.persistence;\n }\n\n // Validate database configuration if required\n if (this.requiresDatabase() && !this.options.db) {\n throw new Error(\n `${this._className} requires a database configuration. ` +\n `Please provide 'db' in options: { db: { url: '...' } } or { db: 'database.db' }`,\n );\n }\n\n if (this.options.db) {\n this._dbEngineHint = getDatabaseTypeHint(this.options.db);\n\n if (\n this.options._reuseInitializedDb &&\n typeof this.options.db === 'object' &&\n 'query' in this.options.db\n ) {\n this._db = this.options.db as DatabaseInterface;\n this.options.db = this._db;\n } else {\n // Handle four db config formats (in implementation order):\n // 1. String URL: 'products.db' (shortcut)\n // 2. DatabaseInterface instance: already initialized db (has 'query' method)\n // 3. Config with client: { type: 'postgres', client: pgPool } (SvelteKit pattern)\n // 4. Config object: { type: 'sqlite', url: 'products.db' }\n if (typeof this.options.db === 'string') {\n // Format 1: String shortcut - let getDatabase auto-detect type from URL\n // Preserve connection sharing for file-backed databases while leaving\n // true in-memory databases isolated per instance.\n const isMemoryDb = this.options.db === ':memory:';\n this._db = await getDatabase({\n url: this.options.db,\n ...(isMemoryDb ? {} : { dbid: `smrt:${this.options.db}` }),\n });\n } else if ('query' in this.options.db) {\n // Format 2: Already a DatabaseInterface instance - return as-is\n this._db = this.options.db as DatabaseInterface;\n } else if ('client' in this.options.db && this.options.db.client) {\n // Format 3: Config with pre-created client (e.g., from SvelteKit's $env-based connection)\n // Pass the client to getDatabase which will use it instead of creating a new connection\n const dbConfig = this.options.db as {\n type?: string;\n client: unknown;\n url?: string;\n };\n // `client` is a runtime-only property the postgres adapter reads\n // but the public `getDatabase` option union does not model, so the\n // literal is routed through the function's own parameter type.\n this._db = await getDatabase({\n type: dbConfig.type || 'postgres',\n client: dbConfig.client,\n url: dbConfig.url,\n } as unknown as Parameters<typeof getDatabase>[0]);\n } else {\n // Format 4: Config object - pass to getDatabase (handles all types uniformly)\n // Preserve connection sharing for file-backed databases while leaving\n // true in-memory databases isolated per instance.\n const dbConfig = this.options.db as { url?: string; type?: string };\n const dbUrl = dbConfig.url || 'memory';\n const isMemoryDb = dbUrl === ':memory:' || dbUrl === 'memory';\n // The loose config-object variant of `DatabaseConfig` carries an\n // index signature that the closed `getDatabase` option union does\n // not accept structurally, so route through its parameter type.\n this._db = await getDatabase({\n ...this.options.db,\n ...(isMemoryDb ? {} : { dbid: `smrt:${dbUrl}` }),\n } as unknown as Parameters<typeof getDatabase>[0]);\n }\n\n /**\n * INTENTIONAL MUTATION: After resolving the database config,\n * we replace options.db with the actual DatabaseInterface instance.\n * This enables child objects to share the same connection via:\n *\n * const child = new ChildObject({ db: parent.options.db });\n *\n * Without this, passing this.options to getCollection() would use the config object\n * which causes a NEW db instance to be created, losing data isolation.\n *\n * See issue #567 for context on why this pattern is necessary.\n */\n this.options.db = this._db;\n\n await this.ensureSystemTables();\n }\n }\n }\n\n /**\n * Initialize optional runtime services that are not required for plain ORM reads.\n */\n protected async initializeRuntimeServices(): Promise<void> {\n if (this._runtimeServicesInitialized) {\n return;\n }\n\n if (!this._runtimeServicesInitPromise) {\n this._runtimeServicesInitPromise = (async () => {\n if (this.options.fs && !this._fs) {\n this._fs = await FilesystemAdapter.create(this.options.fs);\n }\n\n // Initialize AI client with environment variable support\n // Priority: instance options > env vars > global config > defaults\n const globalConfig = config.toJSON();\n const usageConfig = this.mergeAiUsageConfig(globalConfig);\n this.initializeAiUsageHandlers(usageConfig);\n\n if (\n !this._ai &&\n (this.options.ai || globalConfig.ai || process.env.SMRT_AI_PROVIDER)\n ) {\n // Check if options.ai is already a client-like object with embed method\n // This allows passing mock AI clients for testing\n const aiOption = this.options.ai as\n | Record<string, unknown>\n | undefined;\n if (\n aiOption &&\n typeof aiOption === 'object' &&\n typeof aiOption.embed === 'function' &&\n !aiOption.provider\n ) {\n this._ai = aiOption as unknown as AIClient;\n } else {\n // CC-8 follow-up: ideally this would route through\n // `@happyvertical/smrt-config` for sanitization parity (e.g. via\n // `getPackageConfig('ai', ...)`), but smrt-config currently only\n // merges file-based config + runtime overrides — it does NOT\n // read from `process.env` with a typed prefix/schema. Until\n // smrt-config grows an env-loader (or wraps `loadEnvConfig`),\n // we continue to use the underlying utility directly. Tracked\n // alongside the CC-8 audit on issue #1199.\n const { loadEnvConfig } = await import('@happyvertical/utils');\n\n // Start with global defaults\n const baseConfig = globalConfig.ai || {};\n\n // Merge with instance options (takes priority over global)\n const userConfig = { ...baseConfig, ...this.options.ai };\n\n // Load environment variables and merge (user options take priority).\n // `AIConfig` carries an index signature, so provider-specific keys\n // (`type`, `onUsage`, `defaultModel`, …) read back as `unknown`.\n const aiConfig = loadEnvConfig<AIConfig>(userConfig, {\n packageName: 'ai',\n prefix: 'SMRT',\n schema: {\n provider: 'string',\n model: 'string',\n apiKey: 'string',\n timeout: 'number',\n maxRetries: 'number',\n temperature: 'number',\n maxTokens: 'number',\n },\n });\n\n const existingOnUsage =\n aiConfig.onUsage ??\n (userConfig as Record<string, unknown>).onUsage ??\n undefined;\n aiConfig.onUsage = async (event: unknown) => {\n if (typeof existingOnUsage === 'function') {\n await (existingOnUsage as (usageEvent: unknown) => unknown)(\n event,\n );\n }\n await this.handleAiUsageCallback(event, aiConfig, usageConfig);\n };\n\n // Only initialize if we have a provider configured\n if (aiConfig.provider || aiConfig.type || aiConfig.apiKey) {\n // Use getAI() factory to support all AI providers (OpenAI, Anthropic, Gemini, etc.)\n // getAI() returns AIInterface, which we narrow to AIClient for\n // backward compatibility. The index-signature `AIConfig` is routed\n // through getAI's own parameter type rather than the closed union.\n this._ai = (await getAI(\n aiConfig as unknown as Parameters<typeof getAI>[0],\n )) as unknown as AIClient;\n }\n }\n }\n\n await this.initializeSignals();\n this._runtimeServicesInitialized = true;\n })();\n }\n\n try {\n await this._runtimeServicesInitPromise;\n } finally {\n this._runtimeServicesInitPromise = undefined;\n }\n }\n\n /**\n * Ensure deferred runtime services are ready before using them.\n */\n protected async ensureRuntimeServicesInitialized(): Promise<void> {\n if (!this._runtimeServicesInitialized) {\n await this.initializeRuntimeServices();\n }\n }\n\n /**\n * Resolve the AI client, initializing deferred runtime services on demand.\n */\n protected async getAiClient(): Promise<AIClient> {\n await this.ensureRuntimeServicesInitialized();\n\n if (!this._ai) {\n throw new Error(\n `${this._className} does not have an AI client configured. ` +\n `Provide 'ai' in options or configure a global SMRT AI provider.`,\n );\n }\n\n return this._ai;\n }\n\n /**\n * Resolve the AI client if one is configured, otherwise return undefined.\n */\n protected async getOptionalAiClient(): Promise<AIClient | undefined> {\n await this.ensureRuntimeServicesInitialized();\n return this._ai;\n }\n\n private async isSystemSchemaVersionApplied(\n db: DatabaseInterface,\n version: string,\n ): Promise<boolean> {\n const engine = detectEngine(getDatabaseUrl(db), this._dbEngineHint);\n\n if (\n engine === 'postgres' &&\n !(await tableExists(db, '_smrt_migrations', this._dbEngineHint))\n ) {\n return false;\n }\n\n try {\n const versionParam = engine === 'postgres' ? '$1' : '?';\n const rows = await db.query(\n `SELECT 1 FROM _smrt_migrations WHERE version = ${versionParam} LIMIT 1`,\n version,\n );\n return getQueryRows(rows).length > 0;\n } catch (error) {\n if (engine === 'postgres') {\n throw error;\n }\n\n return false;\n }\n }\n\n /**\n * Ensure SMRT system tables exist in the database\n *\n * System tables use _smrt_ prefix and store framework metadata:\n * - _smrt_contexts: Context memory storage for remembered patterns\n * - _smrt_migrations: Schema version tracking\n * - _smrt_registry: Object registry persistence\n * - _smrt_signals: Signal history/audit log\n *\n * This method is idempotent and safe to call multiple times.\n * Tables are only created once per database connection.\n */\n private async ensureSystemTables(): Promise<void> {\n if (!this._db) return;\n\n const dbUrl = getDatabaseUrl(this._db);\n\n // Some databases share URLs but are different instances:\n // - :memory: databases (SQLite/DuckDB in-memory)\n // - JSON databases (may have undefined or shared URLs)\n // - Any database with undefined URL\n // Use WeakSet for instance tracking, Set<string> for URL tracking\n const dbConstructorName = this._db.constructor?.name || '';\n const isMemoryDb = dbUrl === ':memory:';\n const isJsonDb = dbConstructorName.toLowerCase().includes('json');\n const hasUndefinedUrl = !dbUrl;\n const useInstanceTracking = isMemoryDb || isJsonDb || hasUndefinedUrl;\n\n if (useInstanceTracking) {\n // Check WeakSet for databases that may share URLs (track by instance)\n if (SmrtClass._systemTablesInitialized.has(this._db)) {\n return;\n }\n } else {\n // Check Set<string> for URL-based databases (track by URL)\n if (SmrtClass._systemTablesInitializedByUrl.has(dbUrl)) {\n return;\n }\n }\n\n try {\n await this.withSystemTableBootstrapLock((db) =>\n this.bootstrapSystemTables(db),\n );\n await this.ensureNativeVectorStorage();\n\n // Mark as initialized using appropriate tracking mechanism\n if (useInstanceTracking) {\n SmrtClass._systemTablesInitialized.add(this._db);\n } else {\n SmrtClass._systemTablesInitializedByUrl.add(dbUrl);\n }\n } catch (error) {\n // DO NOT SWALLOW ERRORS - fail loudly so we know what's wrong\n const dbInfo = this._db.constructor?.name || 'unknown database';\n throw new Error(\n `Failed to create system tables for ${dbInfo}: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n }\n\n private async withSystemTableBootstrapLock<T>(\n callback: (db: DatabaseInterface) => Promise<T>,\n ): Promise<T> {\n if (!this._db) {\n throw new Error('Database is not initialized');\n }\n\n const beginTransaction = this._db.beginTransaction;\n const transaction = (this._db as TransactionCapableDatabase).transaction;\n const shouldUsePostgresLock =\n detectEngine(getDatabaseUrl(this._db), this._dbEngineHint) === 'postgres';\n\n if (!shouldUsePostgresLock) {\n return callback(this._db);\n }\n\n if (typeof beginTransaction === 'function') {\n const tx = await beginTransaction.call(this._db);\n if (!tx) {\n throw new Error('Database transaction could not be started');\n }\n\n try {\n await tx.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);\n const result = await callback(tx);\n await tx.commit();\n return result;\n } catch (error) {\n await this.rollbackSystemTableBootstrap(tx);\n throw error;\n }\n }\n\n if (typeof transaction === 'function') {\n const runInTransaction = transaction.bind(this._db) as <R>(\n callback: (tx: DatabaseInterface) => Promise<R>,\n ) => Promise<R>;\n\n return runInTransaction(async (tx) => {\n await tx.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);\n return callback(tx);\n });\n }\n\n throw new Error(\n 'Postgres system table bootstrap requires a transaction-capable database adapter',\n );\n }\n\n private async rollbackSystemTableBootstrap(\n tx: TransactionHandle,\n ): Promise<void> {\n try {\n if (typeof tx.isActive !== 'function' || tx.isActive()) {\n await tx.rollback();\n }\n } catch (rollbackError) {\n logger.warn(\n `[smrt] Failed to rollback system table bootstrap transaction: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`,\n );\n }\n }\n\n private async bootstrapSystemTables(db: DatabaseInterface): Promise<void> {\n // Fast path: check if system tables already exist by querying _smrt_migrations.\n // This avoids 29 sequential DDL round-trips on high-latency connections\n // (e.g. remote Postgres over Tailscale where each round-trip is ~650ms).\n // For Postgres, this runs after acquiring the advisory lock so concurrent\n // initializers can observe a completed bootstrap and skip replaying DDL.\n const version = SMRT_SCHEMA_VERSION;\n if (await this.isSystemSchemaVersionApplied(db, version)) {\n return;\n }\n\n // Older installs can have a subset of system columns already created.\n // Upgrade those tables before replaying idempotent DDL so index creation\n // does not fail on missing legacy columns.\n await ensureLegacySystemTableCompatibility(db, this._dbEngineHint);\n\n // Create all system tables\n // Split multi-statement SQL into individual statements to avoid race conditions\n // Each ALL_SYSTEM_TABLES entry contains CREATE TABLE + CREATE INDEX statements\n const allStatements: string[] = [];\n for (const multiStatementSQL of ALL_SYSTEM_TABLES) {\n // Split on semicolon, filter out empty statements\n const statements = multiStatementSQL\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n allStatements.push(...statements);\n }\n\n // Use db.query() for system tables — they use CREATE TABLE/INDEX IF NOT EXISTS\n // which databases handle natively in a single round-trip. The syncSchema()\n // approach does per-column existence checks (multiple round-trips per table)\n // which is unnecessary for framework-owned system tables and extremely slow\n // on high-latency connections (e.g. remote postgres over Tailscale).\n for (const statement of allStatements) {\n await db.query(statement);\n }\n\n // Record current schema version\n // Use ON CONFLICT for DuckDB compatibility (not INSERT OR IGNORE)\n const id = crypto.randomUUID();\n const description = 'Initial SMRT system tables';\n await db.execute`\n INSERT INTO _smrt_migrations (id, version, description)\n VALUES (${id}, ${version}, ${description})\n ON CONFLICT(version) DO NOTHING\n `;\n }\n\n private async ensureNativeVectorStorage(): Promise<void> {\n if (!this._db) {\n return;\n }\n\n try {\n const { ObjectRegistry } = await import('./registry.js');\n const embeddingConfig = ObjectRegistry.getProjectEmbeddingConfig();\n if (embeddingConfig?.storage === 'native') {\n const { EmbeddingStorage } = await import('./embeddings/storage.js');\n const vector = this._db.vector;\n if (vector) {\n const dimensions = embeddingConfig.dimensions || 768;\n await EmbeddingStorage.ensureVectorStorage(\n this._db,\n dimensions,\n vector,\n );\n } else {\n logger.warn(\n '[smrt] Embedding storage set to \"native\" but database has no vector capability. Falling back to JSON storage.',\n );\n }\n }\n } catch (error) {\n // Don't fail system table initialization for vector setup errors\n logger.warn(\n `[smrt] Failed to initialize vector storage: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n /**\n * Access system tables through standard database interface\n * System tables use _smrt_ prefix to avoid conflicts with user tables\n */\n protected get systemDb(): DatabaseInterface {\n return this._db;\n }\n\n /**\n * Initialize signal bus and adapters\n *\n * Merges global configuration with instance-specific overrides.\n * Registers built-in and custom adapters based on configuration.\n */\n private async initializeSignals(): Promise<void> {\n const globalConfig = config.toJSON();\n const effectiveConfig = this.mergeSignalConfig(globalConfig);\n\n // If a shared bus is provided, always use it (don't create new adapters)\n if (this.options.signals?.bus) {\n this._signalBus = this.options.signals.bus;\n return;\n }\n\n // Otherwise, check if we should initialize signals based on config\n if (!this.shouldInitializeSignals(effectiveConfig)) {\n return;\n }\n\n this._signalBus = new SignalBus({\n sanitization: effectiveConfig.sanitization,\n });\n await this.registerAdapters(effectiveConfig);\n }\n\n /**\n * Merge global and instance signal configuration\n *\n * Instance configuration takes priority over global defaults.\n *\n * @param globalConfig - Global configuration from smrt.configure()\n * @returns Merged configuration\n */\n private mergeSignalConfig(\n globalConfig: GlobalSignalConfig,\n ): GlobalSignalConfig {\n return {\n logging: this.options.logging ?? globalConfig.logging,\n metrics: this.options.metrics ?? globalConfig.metrics,\n pubsub: this.options.pubsub ?? globalConfig.pubsub,\n usage: this.mergeAiUsageConfig(globalConfig),\n sanitization: this.options.sanitization ?? globalConfig.sanitization,\n signals: {\n bus: this.options.signals?.bus ?? globalConfig.signals?.bus,\n adapters: [\n ...(globalConfig.signals?.adapters ?? []),\n ...(this.options.signals?.adapters ?? []),\n ],\n },\n };\n }\n\n /**\n * Check if signals should be initialized\n *\n * Signals are initialized if any adapter is configured.\n *\n * @param config - Effective signal configuration\n * @returns True if signals should be initialized\n */\n private shouldInitializeSignals(config: GlobalSignalConfig): boolean {\n return !!(\n config.logging !== false ||\n config.metrics?.enabled ||\n config.pubsub?.enabled ||\n config.signals?.adapters?.length\n );\n }\n\n /**\n * Register signal adapters based on configuration\n *\n * @param config - Effective signal configuration\n */\n private async registerAdapters(config: GlobalSignalConfig): Promise<void> {\n if (!this._signalBus) return;\n\n // Logging adapter (default: enabled with console)\n if (config.logging !== false) {\n const { createLogger, LoggerAdapter } = await import(\n '@happyvertical/logger'\n );\n const logger = createLogger(config.logging ?? true);\n const adapter = new LoggerAdapter(logger);\n this._signalBus.register(adapter);\n this._registeredAdapters.push(adapter);\n }\n\n // Metrics adapter (default: disabled)\n if (config.metrics?.enabled) {\n const { MetricsAdapter } = await import('./adapters/metrics.js');\n const adapter = new MetricsAdapter();\n this._signalBus.register(adapter);\n this._registeredAdapters.push(adapter);\n }\n\n // Pub/Sub adapter (default: disabled)\n if (config.pubsub?.enabled) {\n const { PubSubAdapter } = await import('./adapters/pubsub.js');\n const adapter = new PubSubAdapter();\n this._signalBus.register(adapter);\n this._registeredAdapters.push(adapter);\n }\n\n // Custom adapters\n if (config.signals?.adapters) {\n for (const adapter of config.signals.adapters) {\n this._signalBus.register(adapter);\n this._registeredAdapters.push(adapter);\n }\n }\n }\n\n /**\n * Gets the filesystem adapter instance\n */\n get fs() {\n return this._fs;\n }\n\n /**\n * Gets the database interface instance\n */\n get db() {\n // Throw helpful error if database is accessed before initialization\n if (!this._db) {\n throw new Error(\n `Database accessed before initialization. ` +\n `Please call await instance.initialize() before accessing the database.`,\n );\n }\n return this._db;\n }\n\n /**\n * Gets the AI client instance\n */\n get ai() {\n return this._ai;\n }\n\n /**\n * Get the in-memory AI usage snapshot for this instance.\n */\n getAiUsageSnapshot(): AiUsageSnapshot | undefined {\n return this._aiUsageCollector?.getSnapshot();\n }\n\n /**\n * Reset the in-memory AI usage collector.\n */\n resetAiUsage(): void {\n this._aiUsageCollector?.reset();\n }\n\n /**\n * List persisted AI usage records.\n */\n async listAiUsage(\n options: AiUsageListOptions = {},\n ): Promise<SmrtAiUsageRecord[]> {\n if (!this._db) {\n throw new Error(\n `AI usage requires a database configuration. ` +\n `Please call initialize() with a db option before querying usage.`,\n );\n }\n\n const { conditions, params } = buildAiUsageWhereClause(options);\n let paramIndex = params.length + 1;\n\n let sql = 'SELECT * FROM _smrt_ai_usage';\n if (conditions.length > 0) {\n sql += ` WHERE ${conditions.join(' AND ')}`;\n }\n\n sql += ` ORDER BY ${\n options.orderBy === 'timestamp ASC' ? 'created_at ASC' : 'created_at DESC'\n }`;\n\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 = getQueryRows(await this._db.query(sql, ...params));\n\n return rows.map((row) => ({\n id: String(row.id),\n provider: String(row.provider),\n model: String(row.model),\n operation: String(row.operation),\n usage: hydratePersistedAiUsageTokens(row),\n estimatedCost:\n row.estimated_cost === null || row.estimated_cost === undefined\n ? undefined\n : Number(row.estimated_cost),\n duration: Number(row.duration ?? 0),\n className:\n row.class_name === null || row.class_name === undefined\n ? undefined\n : String(row.class_name),\n tenantId:\n row.tenant_id === undefined\n ? undefined\n : (row.tenant_id as string | null),\n tags: parseAiUsageTags(row.tags),\n timestamp: normalizeAiUsageTimestamp(row.created_at),\n }));\n }\n\n /**\n * Summarize persisted AI usage records by a grouping dimension.\n */\n async summarizeAiUsage(\n options: AiUsageSummaryOptions = {},\n ): Promise<Record<string, AiUsageStats>> {\n if (!this._db) {\n throw new Error(\n `AI usage requires a database configuration. ` +\n `Please call initialize() with a db option before querying usage.`,\n );\n }\n\n const groupBy = options.groupBy ?? 'model';\n const bucketExpression =\n groupBy === 'provider'\n ? `provider`\n : groupBy === 'model'\n ? `provider || ':' || model`\n : groupBy === 'class'\n ? `COALESCE(class_name, 'unknown')`\n : groupBy === 'tenant'\n ? `COALESCE(tenant_id, 'global')`\n : groupBy === 'operation'\n ? `operation`\n : `substr(CAST(created_at AS TEXT), 1, 10)`;\n\n const { conditions, params } = buildAiUsageWhereClause(options);\n\n let sql = `\n SELECT ${bucketExpression} AS bucket,\n COUNT(*) AS call_count,\n COALESCE(SUM(prompt_tokens), 0) AS prompt_tokens,\n COALESCE(SUM(completion_tokens), 0) AS completion_tokens,\n COALESCE(SUM(total_tokens), 0) AS total_tokens,\n COALESCE(SUM(duration), 0) AS total_duration,\n COALESCE(SUM(estimated_cost), 0) AS estimated_cost,\n MAX(created_at) AS last_used\n FROM _smrt_ai_usage\n `;\n\n if (conditions.length > 0) {\n sql += ` WHERE ${conditions.join(' AND ')}`;\n }\n\n sql += ' GROUP BY bucket ORDER BY bucket ASC';\n\n const rows = getQueryRows(await this._db.query(sql, ...params));\n const summary: Record<string, AiUsageStats> = {};\n\n for (const row of rows) {\n const bucket = String(row.bucket);\n summary[bucket] = {\n callCount: Number(row.call_count ?? 0),\n promptTokens: Number(row.prompt_tokens ?? 0),\n completionTokens: Number(row.completion_tokens ?? 0),\n totalTokens: Number(row.total_tokens ?? 0),\n totalDuration: Number(row.total_duration ?? 0),\n estimatedCost: Number(row.estimated_cost ?? 0),\n lastUsed: row.last_used ? new Date(String(row.last_used)).getTime() : 0,\n };\n }\n\n return summary;\n }\n\n /**\n * Gets the signal bus instance\n *\n * @returns Signal bus if signals are enabled, undefined otherwise\n */\n get signalBus(): SignalBus | undefined {\n return this._signalBus;\n }\n\n /**\n * Cleanup method to prevent memory leaks\n *\n * Unregisters all adapters from the signal bus that were registered\n * by this instance. Call this when the SmrtClass instance is no longer\n * needed to prevent memory leaks.\n *\n * @example\n * ```typescript\n * const product = new Product({ name: 'Widget' });\n * await product.initialize();\n * // ... use product ...\n * product.destroy(); // Clean up when done\n * ```\n */\n destroy(): void {\n // Only unregister adapters if we own the bus (not shared)\n if (this._signalBus && !this.options.signals?.bus) {\n for (const adapter of this._registeredAdapters) {\n this._signalBus.unregister(adapter);\n }\n this._registeredAdapters = [];\n }\n\n // TODO: If SmrtClass grows a broader async teardown lifecycle, move\n // AI usage handler cleanup there alongside other connection-bound state.\n this._aiUsageCollector = undefined;\n this._aiUsageHandlers = [];\n }\n\n private mergeAiUsageConfig(\n globalConfig: GlobalSignalConfig,\n ): ResolvedAiUsageConfig {\n const globalUsage = globalConfig.usage ?? {};\n const instanceUsage = this.options.usage ?? {};\n\n return {\n enabled: instanceUsage.enabled ?? globalUsage.enabled ?? true,\n persist: instanceUsage.persist ?? globalUsage.persist ?? true,\n estimateCosts:\n instanceUsage.estimateCosts ?? globalUsage.estimateCosts ?? true,\n costRates: {\n ...(globalUsage.costRates ?? {}),\n ...(instanceUsage.costRates ?? {}),\n },\n handlers: [\n ...(globalUsage.handlers ?? []),\n ...(instanceUsage.handlers ?? []),\n ],\n };\n }\n\n private initializeAiUsageHandlers(config: ResolvedAiUsageConfig): void {\n this._aiUsageCollector = undefined;\n this._aiUsageHandlers = [];\n\n if (!config.enabled) {\n return;\n }\n\n this._aiUsageCollector = new AiUsageCollector();\n this._aiUsageHandlers.push(this._aiUsageCollector);\n\n if (config.persist && this._db) {\n this._aiUsageHandlers.push(new AiUsagePersistenceHandler(this._db));\n }\n\n this._aiUsageHandlers.push(...config.handlers);\n }\n\n private async handleAiUsageCallback(\n event: unknown,\n aiConfig: Record<string, unknown>,\n usageConfig: ResolvedAiUsageConfig,\n ): Promise<void> {\n if (!usageConfig.enabled || this._aiUsageHandlers.length === 0) {\n return;\n }\n\n const normalizedEvent = this.normalizeAiUsageEvent(event, aiConfig);\n if (!normalizedEvent) {\n return;\n }\n\n if (usageConfig.estimateCosts) {\n normalizedEvent.estimatedCost = estimateAiUsageCost(\n normalizedEvent.provider,\n normalizedEvent.model,\n normalizedEvent.usage,\n usageConfig.costRates,\n );\n }\n\n const results = await Promise.allSettled(\n this._aiUsageHandlers.map((handler) => handler.handle(normalizedEvent)),\n );\n\n for (const result of results) {\n if (result.status === 'rejected') {\n logger.warn(\n `[smrt] AI usage handler failed for ${normalizedEvent.provider}:${normalizedEvent.model}: ${\n result.reason instanceof Error\n ? result.reason.message\n : String(result.reason)\n }`,\n );\n }\n }\n }\n\n private normalizeAiUsageEvent(\n event: unknown,\n aiConfig: Record<string, unknown>,\n ): SmrtAiUsageEvent | undefined {\n const raw = (event ?? {}) as Record<string, unknown>;\n const provider = firstString(\n raw.provider,\n raw.type,\n aiConfig.provider,\n aiConfig.type,\n );\n const model = firstString(\n raw.model,\n raw.defaultModel,\n aiConfig.model,\n aiConfig.defaultModel,\n );\n const operation =\n firstString(raw.operation, raw.kind, raw.method) ?? 'unknown';\n const usage =\n normalizeIncomingAiUsageTokens(raw.usage) ??\n normalizeIncomingAiUsageTokens(raw.tokenUsage) ??\n normalizeIncomingAiUsageTokens({\n promptTokens: raw.promptTokens,\n completionTokens: raw.completionTokens,\n totalTokens: raw.totalTokens,\n });\n\n if (!provider || !model) {\n return undefined;\n }\n\n const duration =\n firstNumber(raw.duration, raw.durationMs, raw.latency) ?? 0;\n const tenantId =\n 'tenantId' in this &&\n (this as { tenantId?: string | null }).tenantId !== undefined\n ? ((this as { tenantId?: string | null }).tenantId ?? null)\n : undefined;\n\n return {\n provider,\n model,\n operation,\n usage,\n duration,\n timestamp: normalizeAiUsageTimestamp(raw.timestamp),\n tags: normalizeAiUsageTags(raw.tags),\n className: this._className,\n tenantId,\n };\n }\n}\n"],"names":["config","createLogger","logger"],"mappings":";;;;;;;;;;;AA2CA,MAAM,kCACJ;AAEF,MAAM,SAAS,aAAa,EAAE,OAAO,QAAQ;AA0C7C,SAAS,eAAe,YAA2C;AACjE,SAAO,WAAW,KAAK,CAAC,cAAmC;AACzD,WAAO,OAAO,cAAc;AAAA,EAC9B,CAAC;AACH;AAEA,SAAS,eAAe,YAA2C;AACjE,SAAO,WAAW,KAAK,CAAC,cAAmC;AACzD,WAAO,OAAO,cAAc;AAAA,EAC9B,CAAC;AACH;AAEA,SAAS,eAAe,IAA+B;AACrD,QAAM,eAAe;AACrB,SAAO,GAAG,OAAO,aAAa,QAAQ,OAAO;AAC/C;AAEA,SAAS,oBACPA,SACoB;AACpB,MAAI,CAACA,WAAU,OAAOA,YAAW,UAAU;AACzC,WAAO;AAAA,EACT;AAEA,QAAM,iBAAiBA;AAIvB,MAAI,OAAO,eAAe,SAAS,UAAU;AAC3C,WAAO,eAAe;AAAA,EACxB;AAEA,MAAI,OAAO,eAAe,QAAQ,SAAS,UAAU;AACnD,WAAO,eAAe,OAAO;AAAA,EAC/B;AAEA,MAAI,WAAW,kBAAkB,OAAO,eAAe,UAAU,YAAY;AAC3E,WAAO;AAAA,EACT;AAEA,MAAI,YAAY,kBAAkB,eAAe,QAAQ;AACvD,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEA,SAAS,+BACP,OAC0B;AAC1B,MAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACvC,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ;AACd,QAAM,eACJ,OAAO,MAAM,iBAAiB,WAC1B,MAAM,eACN,OAAO,MAAM,gBAAgB,WAC3B,MAAM,cACN;AACR,QAAM,mBACJ,OAAO,MAAM,qBAAqB,WAC9B,MAAM,mBACN,OAAO,MAAM,iBAAiB,WAC5B,MAAM,eACN;AACR,QAAM,cACJ,OAAO,MAAM,gBAAgB,WACzB,MAAM,cACN,iBAAiB,UAAa,qBAAqB,UAChD,gBAAgB,MAAM,oBAAoB,KAC3C;AAER,MACE,iBAAiB,UACjB,qBAAqB,UACrB,gBAAgB,QAChB;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;AAEA,SAAS,8BAA8B,KAIV;AAC3B,QAAM,eACJ,OAAO,IAAI,kBAAkB,WACzB,IAAI,gBACJ,IAAI,kBAAkB,QAAQ,IAAI,kBAAkB,SAClD,SACA,OAAO,IAAI,aAAa;AAChC,QAAM,mBACJ,OAAO,IAAI,sBAAsB,WAC7B,IAAI,oBACJ,IAAI,sBAAsB,QAAQ,IAAI,sBAAsB,SAC1D,SACA,OAAO,IAAI,iBAAiB;AACpC,QAAM,cACJ,OAAO,IAAI,iBAAiB,WACxB,IAAI,eACJ,IAAI,iBAAiB,QAAQ,IAAI,iBAAiB,SAChD,SACA,OAAO,IAAI,YAAY;AAE/B,MACE,iBAAiB,UACjB,qBAAqB,UACrB,gBAAgB,QAChB;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;AAEA,SAAS,qBACP,OACoC;AACpC,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;AAC/D,WAAO;AAAA,EACT;AAEA,QAAM,OAA+B,CAAA;AACrC,aAAW,CAAC,KAAK,QAAQ,KAAK,OAAO,QAAQ,KAAK,GAAG;AACnD,QAAI,aAAa,UAAa,aAAa,KAAM;AACjD,SAAK,GAAG,IAAI,OAAO,QAAQ;AAAA,EAC7B;AAEA,SAAO,OAAO,KAAK,IAAI,EAAE,SAAS,IAAI,OAAO;AAC/C;AAEA,SAAS,iBAAiB,OAAoD;AAC5E,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO;AAAA,EACT;AAEA,MAAI;AACF,WAAO,qBAAqB,KAAK,MAAM,KAAK,CAAC;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,0BAA0B,OAAsB;AACvD,MAAI,iBAAiB,MAAM;AACzB,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UAAU;AAC1D,UAAM,OAAO,IAAI,KAAK,KAAK;AAC3B,QAAI,CAAC,OAAO,MAAM,KAAK,QAAA,CAAS,GAAG;AACjC,aAAO;AAAA,IACT;AAAA,EACF;AAEA,6BAAW,KAAA;AACb;AAEA,SAAS,aACP,QAC2B;AAC3B,SAAO,MAAM,QAAQ,MAAM,IACtB,SACC,OAAgD,QAAQ,CAAA;AAChE;AAEA,SAAS,wBACP,SACoB;AACpB,QAAM,aAAuB,CAAA;AAC7B,QAAM,SAAoB,CAAA;AAC1B,MAAI,iBAAiB;AAErB,MAAI,QAAQ,OAAO;AACjB,eAAW,KAAK,kBAAkB,gBAAgB,EAAE;AACpD,WAAO,KAAK,QAAQ,MAAM,YAAA,CAAa;AAAA,EACzC;AAEA,MAAI,QAAQ,OAAO;AACjB,eAAW,KAAK,kBAAkB,gBAAgB,EAAE;AACpD,WAAO,KAAK,QAAQ,MAAM,YAAA,CAAa;AAAA,EACzC;AAEA,MAAI,QAAQ,UAAU;AACpB,eAAW,KAAK,eAAe,gBAAgB,EAAE;AACjD,WAAO,KAAK,QAAQ,QAAQ;AAAA,EAC9B;AAEA,MAAI,QAAQ,OAAO;AACjB,eAAW,KAAK,YAAY,gBAAgB,EAAE;AAC9C,WAAO,KAAK,QAAQ,KAAK;AAAA,EAC3B;AAEA,MAAI,QAAQ,WAAW;AACrB,eAAW,KAAK,gBAAgB,gBAAgB,EAAE;AAClD,WAAO,KAAK,QAAQ,SAAS;AAAA,EAC/B;AAEA,MAAI,QAAQ,WAAW;AACrB,eAAW,KAAK,iBAAiB,gBAAgB,EAAE;AACnD,WAAO,KAAK,QAAQ,SAAS;AAAA,EAC/B;AAEA,MAAI,QAAQ,aAAa,MAAM;AAC7B,eAAW,KAAK,mBAAmB;AAAA,EACrC,WAAW,QAAQ,UAAU;AAC3B,eAAW,KAAK,gBAAgB,gBAAgB,EAAE;AAClD,WAAO,KAAK,QAAQ,QAAQ;AAAA,EAC9B;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;AAoGO,MAAM,UAAU;AAAA;AAAA;AAAA;AAAA,EAIX;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKE;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA;AAAA;AAAA;AAAA,EAKF,sBAAuC,CAAA;AAAA;AAAA;AAAA;AAAA,EAKvC;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAqC,CAAA;AAAA;AAAA;AAAA;AAAA,EAKrC,8BAA8B;AAAA;AAAA;AAAA;AAAA,EAK9B;AAAA;AAAA;AAAA;AAAA,EAKD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOP,OAAe,2BAA2B,oBAAI,QAAA;AAAA,EAC9C,OAAe,gCAAgC,oBAAI,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOnD,YAAY,UAA4B,IAAI;AAC1C,SAAK,UAAU;AACf,SAAK,aAAa,KAAK,YAAY;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBU,mBAA4B;AACpC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAgB,aAA4B;AAC1C,UAAM,KAAK,wBAAA;AAEX,QAAI,CAAC,KAAK,QAAQ,6BAA6B;AAC7C,YAAM,KAAK,0BAAA;AAAA,IACb;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAgB,0BAAyC;AAEvD,QAAI,KAAK,QAAQ,eAAe,CAAC,KAAK,QAAQ,IAAI;AAChD,WAAK,QAAQ,KAAK,KAAK,QAAQ;AAAA,IACjC;AAGA,QAAI,KAAK,iBAAA,KAAsB,CAAC,KAAK,QAAQ,IAAI;AAC/C,YAAM,IAAI;AAAA,QACR,GAAG,KAAK,UAAU;AAAA,MAAA;AAAA,IAGtB;AAEA,QAAI,KAAK,QAAQ,IAAI;AACnB,WAAK,gBAAgB,oBAAoB,KAAK,QAAQ,EAAE;AAExD,UACE,KAAK,QAAQ,uBACb,OAAO,KAAK,QAAQ,OAAO,YAC3B,WAAW,KAAK,QAAQ,IACxB;AACA,aAAK,MAAM,KAAK,QAAQ;AACxB,aAAK,QAAQ,KAAK,KAAK;AAAA,MACzB,OAAO;AAML,YAAI,OAAO,KAAK,QAAQ,OAAO,UAAU;AAIvC,gBAAM,aAAa,KAAK,QAAQ,OAAO;AACvC,eAAK,MAAM,MAAM,YAAY;AAAA,YAC3B,KAAK,KAAK,QAAQ;AAAA,YAClB,GAAI,aAAa,CAAA,IAAK,EAAE,MAAM,QAAQ,KAAK,QAAQ,EAAE,GAAA;AAAA,UAAG,CACzD;AAAA,QACH,WAAW,WAAW,KAAK,QAAQ,IAAI;AAErC,eAAK,MAAM,KAAK,QAAQ;AAAA,QAC1B,WAAW,YAAY,KAAK,QAAQ,MAAM,KAAK,QAAQ,GAAG,QAAQ;AAGhE,gBAAM,WAAW,KAAK,QAAQ;AAQ9B,eAAK,MAAM,MAAM,YAAY;AAAA,YAC3B,MAAM,SAAS,QAAQ;AAAA,YACvB,QAAQ,SAAS;AAAA,YACjB,KAAK,SAAS;AAAA,UAAA,CACiC;AAAA,QACnD,OAAO;AAIL,gBAAM,WAAW,KAAK,QAAQ;AAC9B,gBAAM,QAAQ,SAAS,OAAO;AAC9B,gBAAM,aAAa,UAAU,cAAc,UAAU;AAIrD,eAAK,MAAM,MAAM,YAAY;AAAA,YAC3B,GAAG,KAAK,QAAQ;AAAA,YAChB,GAAI,aAAa,CAAA,IAAK,EAAE,MAAM,QAAQ,KAAK,GAAA;AAAA,UAAG,CACC;AAAA,QACnD;AAcA,aAAK,QAAQ,KAAK,KAAK;AAEvB,cAAM,KAAK,mBAAA;AAAA,MACb;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAgB,4BAA2C;AACzD,QAAI,KAAK,6BAA6B;AACpC;AAAA,IACF;AAEA,QAAI,CAAC,KAAK,6BAA6B;AACrC,WAAK,+BAA+B,YAAY;AAC9C,YAAI,KAAK,QAAQ,MAAM,CAAC,KAAK,KAAK;AAChC,eAAK,MAAM,MAAM,kBAAkB,OAAO,KAAK,QAAQ,EAAE;AAAA,QAC3D;AAIA,cAAM,eAAe,OAAO,OAAA;AAC5B,cAAM,cAAc,KAAK,mBAAmB,YAAY;AACxD,aAAK,0BAA0B,WAAW;AAE1C,YACE,CAAC,KAAK,QACL,KAAK,QAAQ,MAAM,aAAa,MAAM,QAAQ,IAAI,mBACnD;AAGA,gBAAM,WAAW,KAAK,QAAQ;AAG9B,cACE,YACA,OAAO,aAAa,YACpB,OAAO,SAAS,UAAU,cAC1B,CAAC,SAAS,UACV;AACA,iBAAK,MAAM;AAAA,UACb,OAAO;AASL,kBAAM,EAAE,cAAA,IAAkB,MAAM,OAAO,sBAAsB;AAG7D,kBAAM,aAAa,aAAa,MAAM,CAAA;AAGtC,kBAAM,aAAa,EAAE,GAAG,YAAY,GAAG,KAAK,QAAQ,GAAA;AAKpD,kBAAM,WAAW,cAAwB,YAAY;AAAA,cACnD,aAAa;AAAA,cACb,QAAQ;AAAA,cACR,QAAQ;AAAA,gBACN,UAAU;AAAA,gBACV,OAAO;AAAA,gBACP,QAAQ;AAAA,gBACR,SAAS;AAAA,gBACT,YAAY;AAAA,gBACZ,aAAa;AAAA,gBACb,WAAW;AAAA,cAAA;AAAA,YACb,CACD;AAED,kBAAM,kBACJ,SAAS,WACR,WAAuC,WACxC;AACF,qBAAS,UAAU,OAAO,UAAmB;AAC3C,kBAAI,OAAO,oBAAoB,YAAY;AACzC,sBAAO;AAAA,kBACL;AAAA,gBAAA;AAAA,cAEJ;AACA,oBAAM,KAAK,sBAAsB,OAAO,UAAU,WAAW;AAAA,YAC/D;AAGA,gBAAI,SAAS,YAAY,SAAS,QAAQ,SAAS,QAAQ;AAKzD,mBAAK,MAAO,MAAM;AAAA,gBAChB;AAAA,cAAA;AAAA,YAEJ;AAAA,UACF;AAAA,QACF;AAEA,cAAM,KAAK,kBAAA;AACX,aAAK,8BAA8B;AAAA,MACrC,GAAA;AAAA,IACF;AAEA,QAAI;AACF,YAAM,KAAK;AAAA,IACb,UAAA;AACE,WAAK,8BAA8B;AAAA,IACrC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAgB,mCAAkD;AAChE,QAAI,CAAC,KAAK,6BAA6B;AACrC,YAAM,KAAK,0BAAA;AAAA,IACb;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAgB,cAAiC;AAC/C,UAAM,KAAK,iCAAA;AAEX,QAAI,CAAC,KAAK,KAAK;AACb,YAAM,IAAI;AAAA,QACR,GAAG,KAAK,UAAU;AAAA,MAAA;AAAA,IAGtB;AAEA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,MAAgB,sBAAqD;AACnE,UAAM,KAAK,iCAAA;AACX,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAc,6BACZ,IACA,SACkB;AAClB,UAAM,SAAS,aAAa,eAAe,EAAE,GAAG,KAAK,aAAa;AAElE,QACE,WAAW,cACX,CAAE,MAAM,YAAY,IAAI,oBAAoB,KAAK,aAAa,GAC9D;AACA,aAAO;AAAA,IACT;AAEA,QAAI;AACF,YAAM,eAAe,WAAW,aAAa,OAAO;AACpD,YAAM,OAAO,MAAM,GAAG;AAAA,QACpB,kDAAkD,YAAY;AAAA,QAC9D;AAAA,MAAA;AAEF,aAAO,aAAa,IAAI,EAAE,SAAS;AAAA,IACrC,SAAS,OAAO;AACd,UAAI,WAAW,YAAY;AACzB,cAAM;AAAA,MACR;AAEA,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAc,qBAAoC;AAChD,QAAI,CAAC,KAAK,IAAK;AAEf,UAAM,QAAQ,eAAe,KAAK,GAAG;AAOrC,UAAM,oBAAoB,KAAK,IAAI,aAAa,QAAQ;AACxD,UAAM,aAAa,UAAU;AAC7B,UAAM,WAAW,kBAAkB,YAAA,EAAc,SAAS,MAAM;AAChE,UAAM,kBAAkB,CAAC;AACzB,UAAM,sBAAsB,cAAc,YAAY;AAEtD,QAAI,qBAAqB;AAEvB,UAAI,UAAU,yBAAyB,IAAI,KAAK,GAAG,GAAG;AACpD;AAAA,MACF;AAAA,IACF,OAAO;AAEL,UAAI,UAAU,8BAA8B,IAAI,KAAK,GAAG;AACtD;AAAA,MACF;AAAA,IACF;AAEA,QAAI;AACF,YAAM,KAAK;AAAA,QAA6B,CAAC,OACvC,KAAK,sBAAsB,EAAE;AAAA,MAAA;AAE/B,YAAM,KAAK,0BAAA;AAGX,UAAI,qBAAqB;AACvB,kBAAU,yBAAyB,IAAI,KAAK,GAAG;AAAA,MACjD,OAAO;AACL,kBAAU,8BAA8B,IAAI,KAAK;AAAA,MACnD;AAAA,IACF,SAAS,OAAO;AAEd,YAAM,SAAS,KAAK,IAAI,aAAa,QAAQ;AAC7C,YAAM,IAAI;AAAA,QACR,sCAAsC,MAAM,KAAK,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QACvG,EAAE,OAAO,MAAA;AAAA,MAAM;AAAA,IAEnB;AAAA,EACF;AAAA,EAEA,MAAc,6BACZ,UACY;AACZ,QAAI,CAAC,KAAK,KAAK;AACb,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AAEA,UAAM,mBAAmB,KAAK,IAAI;AAClC,UAAM,cAAe,KAAK,IAAmC;AAC7D,UAAM,wBACJ,aAAa,eAAe,KAAK,GAAG,GAAG,KAAK,aAAa,MAAM;AAEjE,QAAI,CAAC,uBAAuB;AAC1B,aAAO,SAAS,KAAK,GAAG;AAAA,IAC1B;AAEA,QAAI,OAAO,qBAAqB,YAAY;AAC1C,YAAM,KAAK,MAAM,iBAAiB,KAAK,KAAK,GAAG;AAC/C,UAAI,CAAC,IAAI;AACP,cAAM,IAAI,MAAM,2CAA2C;AAAA,MAC7D;AAEA,UAAI;AACF,cAAM,GAAG,MAAM,+BAA+B;AAC9C,cAAM,SAAS,MAAM,SAAS,EAAE;AAChC,cAAM,GAAG,OAAA;AACT,eAAO;AAAA,MACT,SAAS,OAAO;AACd,cAAM,KAAK,6BAA6B,EAAE;AAC1C,cAAM;AAAA,MACR;AAAA,IACF;AAEA,QAAI,OAAO,gBAAgB,YAAY;AACrC,YAAM,mBAAmB,YAAY,KAAK,KAAK,GAAG;AAIlD,aAAO,iBAAiB,OAAO,OAAO;AACpC,cAAM,GAAG,MAAM,+BAA+B;AAC9C,eAAO,SAAS,EAAE;AAAA,MACpB,CAAC;AAAA,IACH;AAEA,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEA,MAAc,6BACZ,IACe;AACf,QAAI;AACF,UAAI,OAAO,GAAG,aAAa,cAAc,GAAG,YAAY;AACtD,cAAM,GAAG,SAAA;AAAA,MACX;AAAA,IACF,SAAS,eAAe;AACtB,aAAO;AAAA,QACL,iEAAiE,yBAAyB,QAAQ,cAAc,UAAU,OAAO,aAAa,CAAC;AAAA,MAAA;AAAA,IAEnJ;AAAA,EACF;AAAA,EAEA,MAAc,sBAAsB,IAAsC;AAMxE,UAAM,UAAU;AAChB,QAAI,MAAM,KAAK,6BAA6B,IAAI,OAAO,GAAG;AACxD;AAAA,IACF;AAKA,UAAM,qCAAqC,IAAI,KAAK,aAAa;AAKjE,UAAM,gBAA0B,CAAA;AAChC,eAAW,qBAAqB,mBAAmB;AAEjD,YAAM,aAAa,kBAChB,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAA,CAAM,EACnB,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC7B,oBAAc,KAAK,GAAG,UAAU;AAAA,IAClC;AAOA,eAAW,aAAa,eAAe;AACrC,YAAM,GAAG,MAAM,SAAS;AAAA,IAC1B;AAIA,UAAM,KAAK,OAAO,WAAA;AAClB,UAAM,cAAc;AACpB,UAAM,GAAG;AAAA;AAAA,gBAEG,EAAE,KAAK,OAAO,KAAK,WAAW;AAAA;AAAA;AAAA,EAG5C;AAAA,EAEA,MAAc,4BAA2C;AACvD,QAAI,CAAC,KAAK,KAAK;AACb;AAAA,IACF;AAEA,QAAI;AACF,YAAM,EAAE,eAAA,IAAmB,MAAM,OAAO,eAAe;AACvD,YAAM,kBAAkB,eAAe,0BAAA;AACvC,UAAI,iBAAiB,YAAY,UAAU;AACzC,cAAM,EAAE,iBAAA,IAAqB,MAAM,OAAO,yBAAyB;AACnE,cAAM,SAAS,KAAK,IAAI;AACxB,YAAI,QAAQ;AACV,gBAAM,aAAa,gBAAgB,cAAc;AACjD,gBAAM,iBAAiB;AAAA,YACrB,KAAK;AAAA,YACL;AAAA,YACA;AAAA,UAAA;AAAA,QAEJ,OAAO;AACL,iBAAO;AAAA,YACL;AAAA,UAAA;AAAA,QAEJ;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AAEd,aAAO;AAAA,QACL,+CAA+C,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,MAAA;AAAA,IAEzG;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAc,WAA8B;AAC1C,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,oBAAmC;AAC/C,UAAM,eAAe,OAAO,OAAA;AAC5B,UAAM,kBAAkB,KAAK,kBAAkB,YAAY;AAG3D,QAAI,KAAK,QAAQ,SAAS,KAAK;AAC7B,WAAK,aAAa,KAAK,QAAQ,QAAQ;AACvC;AAAA,IACF;AAGA,QAAI,CAAC,KAAK,wBAAwB,eAAe,GAAG;AAClD;AAAA,IACF;AAEA,SAAK,aAAa,IAAI,UAAU;AAAA,MAC9B,cAAc,gBAAgB;AAAA,IAAA,CAC/B;AACD,UAAM,KAAK,iBAAiB,eAAe;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,kBACN,cACoB;AACpB,WAAO;AAAA,MACL,SAAS,KAAK,QAAQ,WAAW,aAAa;AAAA,MAC9C,SAAS,KAAK,QAAQ,WAAW,aAAa;AAAA,MAC9C,QAAQ,KAAK,QAAQ,UAAU,aAAa;AAAA,MAC5C,OAAO,KAAK,mBAAmB,YAAY;AAAA,MAC3C,cAAc,KAAK,QAAQ,gBAAgB,aAAa;AAAA,MACxD,SAAS;AAAA,QACP,KAAK,KAAK,QAAQ,SAAS,OAAO,aAAa,SAAS;AAAA,QACxD,UAAU;AAAA,UACR,GAAI,aAAa,SAAS,YAAY,CAAA;AAAA,UACtC,GAAI,KAAK,QAAQ,SAAS,YAAY,CAAA;AAAA,QAAC;AAAA,MACzC;AAAA,IACF;AAAA,EAEJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,wBAAwBA,SAAqC;AACnE,WAAO,CAAC,EACNA,QAAO,YAAY,SACnBA,QAAO,SAAS,WAChBA,QAAO,QAAQ,WACfA,QAAO,SAAS,UAAU;AAAA,EAE9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,iBAAiBA,SAA2C;AACxE,QAAI,CAAC,KAAK,WAAY;AAGtB,QAAIA,QAAO,YAAY,OAAO;AAC5B,YAAM,EAAE,cAAAC,eAAc,kBAAkB,MAAM,OAC5C,uBACF;AACA,YAAMC,UAASD,cAAaD,QAAO,WAAW,IAAI;AAClD,YAAM,UAAU,IAAI,cAAcE,OAAM;AACxC,WAAK,WAAW,SAAS,OAAO;AAChC,WAAK,oBAAoB,KAAK,OAAO;AAAA,IACvC;AAGA,QAAIF,QAAO,SAAS,SAAS;AAC3B,YAAM,EAAE,eAAA,IAAmB,MAAM,OAAO,uBAAuB;AAC/D,YAAM,UAAU,IAAI,eAAA;AACpB,WAAK,WAAW,SAAS,OAAO;AAChC,WAAK,oBAAoB,KAAK,OAAO;AAAA,IACvC;AAGA,QAAIA,QAAO,QAAQ,SAAS;AAC1B,YAAM,EAAE,cAAA,IAAkB,MAAM,OAAO,sBAAsB;AAC7D,YAAM,UAAU,IAAI,cAAA;AACpB,WAAK,WAAW,SAAS,OAAO;AAChC,WAAK,oBAAoB,KAAK,OAAO;AAAA,IACvC;AAGA,QAAIA,QAAO,SAAS,UAAU;AAC5B,iBAAW,WAAWA,QAAO,QAAQ,UAAU;AAC7C,aAAK,WAAW,SAAS,OAAO;AAChC,aAAK,oBAAoB,KAAK,OAAO;AAAA,MACvC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,KAAK;AACP,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,KAAK;AAEP,QAAI,CAAC,KAAK,KAAK;AACb,YAAM,IAAI;AAAA,QACR;AAAA,MAAA;AAAA,IAGJ;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,KAAK;AACP,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,qBAAkD;AAChD,WAAO,KAAK,mBAAmB,YAAA;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKA,eAAqB;AACnB,SAAK,mBAAmB,MAAA;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YACJ,UAA8B,IACA;AAC9B,QAAI,CAAC,KAAK,KAAK;AACb,YAAM,IAAI;AAAA,QACR;AAAA,MAAA;AAAA,IAGJ;AAEA,UAAM,EAAE,YAAY,WAAW,wBAAwB,OAAO;AAC9D,QAAI,aAAa,OAAO,SAAS;AAEjC,QAAI,MAAM;AACV,QAAI,WAAW,SAAS,GAAG;AACzB,aAAO,UAAU,WAAW,KAAK,OAAO,CAAC;AAAA,IAC3C;AAEA,WAAO,aACL,QAAQ,YAAY,kBAAkB,mBAAmB,iBAC3D;AAEA,QAAI,QAAQ,UAAU,QAAW;AAC/B,aAAO,WAAW,YAAY;AAC9B,aAAO,KAAK,QAAQ,KAAK;AAAA,IAC3B;AAEA,QAAI,QAAQ,WAAW,QAAW;AAChC,aAAO,YAAY,YAAY;AAC/B,aAAO,KAAK,QAAQ,MAAM;AAAA,IAC5B;AAEA,UAAM,OAAO,aAAa,MAAM,KAAK,IAAI,MAAM,KAAK,GAAG,MAAM,CAAC;AAE9D,WAAO,KAAK,IAAI,CAAC,SAAS;AAAA,MACxB,IAAI,OAAO,IAAI,EAAE;AAAA,MACjB,UAAU,OAAO,IAAI,QAAQ;AAAA,MAC7B,OAAO,OAAO,IAAI,KAAK;AAAA,MACvB,WAAW,OAAO,IAAI,SAAS;AAAA,MAC/B,OAAO,8BAA8B,GAAG;AAAA,MACxC,eACE,IAAI,mBAAmB,QAAQ,IAAI,mBAAmB,SAClD,SACA,OAAO,IAAI,cAAc;AAAA,MAC/B,UAAU,OAAO,IAAI,YAAY,CAAC;AAAA,MAClC,WACE,IAAI,eAAe,QAAQ,IAAI,eAAe,SAC1C,SACA,OAAO,IAAI,UAAU;AAAA,MAC3B,UACE,IAAI,cAAc,SACd,SACC,IAAI;AAAA,MACX,MAAM,iBAAiB,IAAI,IAAI;AAAA,MAC/B,WAAW,0BAA0B,IAAI,UAAU;AAAA,IAAA,EACnD;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,iBACJ,UAAiC,IACM;AACvC,QAAI,CAAC,KAAK,KAAK;AACb,YAAM,IAAI;AAAA,QACR;AAAA,MAAA;AAAA,IAGJ;AAEA,UAAM,UAAU,QAAQ,WAAW;AACnC,UAAM,mBACJ,YAAY,aACR,aACA,YAAY,UACV,6BACA,YAAY,UACV,oCACA,YAAY,WACV,kCACA,YAAY,cACV,cACA;AAEd,UAAM,EAAE,YAAY,WAAW,wBAAwB,OAAO;AAE9D,QAAI,MAAM;AAAA,eACC,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAW3B,QAAI,WAAW,SAAS,GAAG;AACzB,aAAO,UAAU,WAAW,KAAK,OAAO,CAAC;AAAA,IAC3C;AAEA,WAAO;AAEP,UAAM,OAAO,aAAa,MAAM,KAAK,IAAI,MAAM,KAAK,GAAG,MAAM,CAAC;AAC9D,UAAM,UAAwC,CAAA;AAE9C,eAAW,OAAO,MAAM;AACtB,YAAM,SAAS,OAAO,IAAI,MAAM;AAChC,cAAQ,MAAM,IAAI;AAAA,QAChB,WAAW,OAAO,IAAI,cAAc,CAAC;AAAA,QACrC,cAAc,OAAO,IAAI,iBAAiB,CAAC;AAAA,QAC3C,kBAAkB,OAAO,IAAI,qBAAqB,CAAC;AAAA,QACnD,aAAa,OAAO,IAAI,gBAAgB,CAAC;AAAA,QACzC,eAAe,OAAO,IAAI,kBAAkB,CAAC;AAAA,QAC7C,eAAe,OAAO,IAAI,kBAAkB,CAAC;AAAA,QAC7C,UAAU,IAAI,YAAY,IAAI,KAAK,OAAO,IAAI,SAAS,CAAC,EAAE,YAAY;AAAA,MAAA;AAAA,IAE1E;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAI,YAAmC;AACrC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,UAAgB;AAEd,QAAI,KAAK,cAAc,CAAC,KAAK,QAAQ,SAAS,KAAK;AACjD,iBAAW,WAAW,KAAK,qBAAqB;AAC9C,aAAK,WAAW,WAAW,OAAO;AAAA,MACpC;AACA,WAAK,sBAAsB,CAAA;AAAA,IAC7B;AAIA,SAAK,oBAAoB;AACzB,SAAK,mBAAmB,CAAA;AAAA,EAC1B;AAAA,EAEQ,mBACN,cACuB;AACvB,UAAM,cAAc,aAAa,SAAS,CAAA;AAC1C,UAAM,gBAAgB,KAAK,QAAQ,SAAS,CAAA;AAE5C,WAAO;AAAA,MACL,SAAS,cAAc,WAAW,YAAY,WAAW;AAAA,MACzD,SAAS,cAAc,WAAW,YAAY,WAAW;AAAA,MACzD,eACE,cAAc,iBAAiB,YAAY,iBAAiB;AAAA,MAC9D,WAAW;AAAA,QACT,GAAI,YAAY,aAAa,CAAA;AAAA,QAC7B,GAAI,cAAc,aAAa,CAAA;AAAA,MAAC;AAAA,MAElC,UAAU;AAAA,QACR,GAAI,YAAY,YAAY,CAAA;AAAA,QAC5B,GAAI,cAAc,YAAY,CAAA;AAAA,MAAC;AAAA,IACjC;AAAA,EAEJ;AAAA,EAEQ,0BAA0BA,SAAqC;AACrE,SAAK,oBAAoB;AACzB,SAAK,mBAAmB,CAAA;AAExB,QAAI,CAACA,QAAO,SAAS;AACnB;AAAA,IACF;AAEA,SAAK,oBAAoB,IAAI,iBAAA;AAC7B,SAAK,iBAAiB,KAAK,KAAK,iBAAiB;AAEjD,QAAIA,QAAO,WAAW,KAAK,KAAK;AAC9B,WAAK,iBAAiB,KAAK,IAAI,0BAA0B,KAAK,GAAG,CAAC;AAAA,IACpE;AAEA,SAAK,iBAAiB,KAAK,GAAGA,QAAO,QAAQ;AAAA,EAC/C;AAAA,EAEA,MAAc,sBACZ,OACA,UACA,aACe;AACf,QAAI,CAAC,YAAY,WAAW,KAAK,iBAAiB,WAAW,GAAG;AAC9D;AAAA,IACF;AAEA,UAAM,kBAAkB,KAAK,sBAAsB,OAAO,QAAQ;AAClE,QAAI,CAAC,iBAAiB;AACpB;AAAA,IACF;AAEA,QAAI,YAAY,eAAe;AAC7B,sBAAgB,gBAAgB;AAAA,QAC9B,gBAAgB;AAAA,QAChB,gBAAgB;AAAA,QAChB,gBAAgB;AAAA,QAChB,YAAY;AAAA,MAAA;AAAA,IAEhB;AAEA,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC5B,KAAK,iBAAiB,IAAI,CAAC,YAAY,QAAQ,OAAO,eAAe,CAAC;AAAA,IAAA;AAGxE,eAAW,UAAU,SAAS;AAC5B,UAAI,OAAO,WAAW,YAAY;AAChC,eAAO;AAAA,UACL,sCAAsC,gBAAgB,QAAQ,IAAI,gBAAgB,KAAK,KACrF,OAAO,kBAAkB,QACrB,OAAO,OAAO,UACd,OAAO,OAAO,MAAM,CAC1B;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,sBACN,OACA,UAC8B;AAC9B,UAAM,MAAO,SAAS,CAAA;AACtB,UAAM,WAAW;AAAA,MACf,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,SAAS;AAAA,MACT,SAAS;AAAA,IAAA;AAEX,UAAM,QAAQ;AAAA,MACZ,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,SAAS;AAAA,MACT,SAAS;AAAA,IAAA;AAEX,UAAM,YACJ,YAAY,IAAI,WAAW,IAAI,MAAM,IAAI,MAAM,KAAK;AACtD,UAAM,QACJ,+BAA+B,IAAI,KAAK,KACxC,+BAA+B,IAAI,UAAU,KAC7C,+BAA+B;AAAA,MAC7B,cAAc,IAAI;AAAA,MAClB,kBAAkB,IAAI;AAAA,MACtB,aAAa,IAAI;AAAA,IAAA,CAClB;AAEH,QAAI,CAAC,YAAY,CAAC,OAAO;AACvB,aAAO;AAAA,IACT;AAEA,UAAM,WACJ,YAAY,IAAI,UAAU,IAAI,YAAY,IAAI,OAAO,KAAK;AAC5D,UAAM,WACJ,cAAc,QACb,KAAsC,aAAa,SAC9C,KAAsC,YAAY,OACpD;AAEN,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,0BAA0B,IAAI,SAAS;AAAA,MAClD,MAAM,qBAAqB,IAAI,IAAI;AAAA,MACnC,WAAW,KAAK;AAAA,MAChB;AAAA,IAAA;AAAA,EAEJ;AACF;"}
@@ -1 +1 @@
1
- {"version":3,"file":"collection-cache.d.ts","sourceRoot":"","sources":["../src/collection-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAI5D;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;;;;;;;;;;;;OAcG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAcD;;GAEG;AACH,eAAO,MAAM,0BAA0B,0BAA0B,CAAC;AA4BlE;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3E;AAQD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,iBAAiB,GAAG,MAAM,CAa/D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CAEzE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,SAAS,CAQvC;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC/B,GAAG,EAAE,MAAM,EACX,kBAAkB,CAAC,EAAE,MAAM,GAC1B,IAAI,CAqCN;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,IAAI,CAMN;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAK3C;AAaD;;GAEG;AACH,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,IAAI,CAON;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,OAAO,CAET;AAoCD;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAkBf;AAED;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI,CAoD3E;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,IAAI,IAAI,CAOrD"}
1
+ {"version":3,"file":"collection-cache.d.ts","sourceRoot":"","sources":["../src/collection-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAI5D;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;;;;;;;;;;;;OAcG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAcD;;GAEG;AACH,eAAO,MAAM,0BAA0B,0BAA0B,CAAC;AA4BlE;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3E;AAQD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,iBAAiB,GAAG,MAAM,CAa/D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CAEzE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,SAAS,CAQvC;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC/B,GAAG,EAAE,MAAM,EACX,kBAAkB,CAAC,EAAE,MAAM,GAC1B,IAAI,CAqCN;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,IAAI,CAMN;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAK3C;AAaD;;GAEG;AACH,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,IAAI,CAON;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,OAAO,CAET;AAqCD;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAkBf;AAED;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI,CAsD3E;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,IAAI,IAAI,CAOrD"}
@@ -136,9 +136,11 @@ function ensureCacheInvalidationListener(db) {
136
136
  if (done || handle.stopped) break;
137
137
  const notification = value;
138
138
  const payload = typeof notification?.payload === "string" ? safeParse(notification.payload) : notification?.payload;
139
- if (!payload || typeof payload.table !== "string") continue;
140
- if (payload.source === PROCESS_ID) continue;
141
- invalidateCollectionCache(dbKey, payload.table);
139
+ if (!payload || typeof payload !== "object") continue;
140
+ const record = payload;
141
+ if (typeof record.table !== "string") continue;
142
+ if (record.source === PROCESS_ID) continue;
143
+ invalidateCollectionCache(dbKey, record.table);
142
144
  }
143
145
  } catch (error) {
144
146
  if (!handle.stopped) {
@@ -1 +1 @@
1
- {"version":3,"file":"collection-cache.js","sources":["../src/collection-cache.ts"],"sourcesContent":["/**\n * Opt-in read-through cache for collection reads (issue #1498).\n *\n * SSR-rendered SMRT apps re-query read-heavy / write-rare collections on\n * every request, and the per-query round-trip dominates wall time when a\n * page needs many collections. This module memoizes `list()`/`get()` row\n * sets keyed by the final SQL + parameters, for an opt-in TTL.\n *\n * Correctness model:\n * - Caching is OFF by default. It is enabled per call\n * (`list({ cache: { ttl } })`) or per model (`@smrt({ cache: { ttl } })`).\n * - SMRT owns every mutation path (`save()`/`delete()` back\n * `collection.create()`, `getOrUpsert()`, junction attach/detach), so all\n * writes invalidate the affected table's entries in-process automatically.\n * - Entries are scoped per database identity (`db.url`) and per table, so\n * multi-DB processes and STI siblings (which share a table) stay coherent.\n * - Caches are per-process. With multiple replicas, a local invalidation\n * leaves peers stale until TTL unless cross-process invalidation is opted\n * into (`crossProcess: true`), which broadcasts over the database\n * adapter's notification capability (e.g. Postgres LISTEN/NOTIFY) when\n * the adapter provides one.\n *\n * Cached values are raw result rows, not hydrated instances — hydration and\n * read interceptors (tenancy, audit) run on every call, cached or not, and\n * each caller receives isolated row copies.\n *\n * Known limitations:\n * - Invalidation fires when a mutation's SQL executes, not when its\n * surrounding transaction commits. A write inside an uncommitted\n * transaction invalidates (and may broadcast) immediately; a concurrent\n * reader could repopulate the cache from the pre-commit snapshot, and a\n * rollback leaves the cache invalidated for a write that never landed.\n * Caching targets read-heavy / write-rare data where this is rare; for\n * models mutated inside multi-statement transactions, coherence is still\n * bounded by TTL. Per-call `cache: false` forces a fresh read where it\n * matters.\n * - Writes that bypass the framework's mutation paths (raw `db.query`\n * issued outside `collection.query()`, external processes without\n * `crossProcess`) are only bounded by TTL.\n *\n * @see https://github.com/happyvertical/smrt/issues/1498\n * @packageDocumentation\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Read-through cache configuration for collection reads.\n *\n * Used both per call (`collection.list({ cache: { ttl: 60_000 } })`) and per\n * model (`@smrt({ cache: { ttl: 60_000 } })`).\n */\nexport interface CollectionCacheConfig {\n /**\n * Time-to-live for cached query results, in milliseconds. Must be > 0.\n */\n ttl: number;\n\n /**\n * Broadcast invalidations to peer processes through the database\n * adapter's notification capability (`db.notifications`, e.g. Postgres\n * LISTEN/NOTIFY). Without this, peer replicas serve stale rows until TTL.\n *\n * Model-level config (`@smrt({ cache })`) is the reliable opt-in: every\n * process that writes the model knows to broadcast. As a per-call option,\n * writes broadcast only from processes that have already performed a\n * `crossProcess` cached read of the same table (typical for homogeneous\n * replicas running the same routes); a process that only writes never\n * learns about the per-call opt-in, so its peers fall back to TTL expiry.\n *\n * Ignored (with a one-time warning) when the adapter does not expose\n * notifications.\n */\n crossProcess?: boolean;\n}\n\ninterface CacheEntry {\n expiresAt: number;\n rows: Record<string, unknown>[];\n}\n\n/**\n * Per-table entry cap. A table accumulating more distinct query shapes than\n * this within one TTL window evicts its oldest entries (insertion order).\n * Guards against unbounded growth from high-cardinality WHERE values.\n */\nconst MAX_ENTRIES_PER_TABLE = 500;\n\n/**\n * Notification channel used for cross-process invalidation broadcasts.\n */\nexport const CACHE_INVALIDATION_CHANNEL = 'smrt_collection_cache';\n\n/**\n * Identifies this process in broadcast payloads so a replica can skip\n * notifications it published itself (it already invalidated locally).\n */\nconst PROCESS_ID = crypto.randomUUID();\n\n/**\n * dbKey → tableName → (queryKey → entry)\n */\nconst store = new Map<string, Map<string, Map<string, CacheEntry>>>();\n\n/**\n * Monotonic invalidation generation per `dbKey\\0tableName`, bumped on every\n * invalidation. A read captures the generation *before* its DB round-trip and\n * passes it to `setCachedRows`; if an invalidating write landed during the\n * round-trip, the generation no longer matches and the (now-stale) result is\n * dropped instead of cached. This closes the read-miss/concurrent-write race\n * where an in-flight SELECT would otherwise repopulate the cache with\n * pre-write rows for the full TTL.\n */\nconst generations = new Map<string, number>();\n\nfunction generationKey(dbKey: string, tableName: string): string {\n return `${dbKey}\u0000${tableName}`;\n}\n\n/**\n * Current invalidation generation for a table (0 if never invalidated).\n * Capture this before a DB read and pass it to `setCachedRows`.\n */\nexport function getCacheGeneration(dbKey: string, tableName: string): number {\n return generations.get(generationKey(dbKey, tableName)) ?? 0;\n}\n\n/**\n * Fallback identities for database instances that expose no URL\n * (each such instance gets its own scope, never shared).\n */\nconst fallbackDbKeys = new WeakMap<object, string>();\n\n/**\n * Resolve a stable cache scope for a database instance.\n *\n * Mirrors table-verifier's identity resolution (`db.url || config.url`).\n * `:memory:` databases share that URL string while being entirely separate\n * databases, so they (and URL-less instances) are scoped per instance —\n * serving one in-memory database's rows for another would be a correctness\n * bug, not just a stale read.\n */\nexport function resolveDbCacheKey(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseInterface & {\n config?: { url?: string };\n };\n const url = db.url || dbWithConfig.config?.url;\n if (url && url !== ':memory:') return url;\n\n let key = fallbackDbKeys.get(db);\n if (!key) {\n key = `smrt-db:${crypto.randomUUID()}`;\n fallbackDbKeys.set(db, key);\n }\n return key;\n}\n\n/**\n * Build the cache key for a query. The final SQL and bound parameters fully\n * normalize the query shape — they already include STI discriminator\n * filters, interceptor-injected tenant filters, ORDER BY, LIMIT and OFFSET.\n */\nexport function buildQueryCacheKey(sql: string, params: unknown[]): string {\n return `${sql}\u0000${JSON.stringify(params)}`;\n}\n\n/**\n * Read cached rows for a query, or undefined on miss/expiry.\n *\n * Returns a structured clone so callers can never mutate the cached copy\n * (hydration writes into row objects).\n */\nexport function getCachedRows(\n dbKey: string,\n tableName: string,\n queryKey: string,\n): Record<string, unknown>[] | undefined {\n const entry = store.get(dbKey)?.get(tableName)?.get(queryKey);\n if (!entry) return undefined;\n if (entry.expiresAt <= Date.now()) {\n store.get(dbKey)?.get(tableName)?.delete(queryKey);\n return undefined;\n }\n return structuredClone(entry.rows);\n}\n\n/**\n * Store rows for a query under the table's cache scope.\n *\n * `expectedGeneration` is the value {@link getCacheGeneration} returned before\n * the DB read. If an invalidation bumped the table's generation while the read\n * was in flight, the result is stale and is dropped rather than cached.\n */\nexport function setCachedRows(\n dbKey: string,\n tableName: string,\n queryKey: string,\n rows: Record<string, unknown>[],\n ttl: number,\n expectedGeneration?: number,\n): void {\n if (!(ttl > 0)) return;\n\n // Drop a result that a concurrent write invalidated mid-flight.\n if (\n expectedGeneration !== undefined &&\n getCacheGeneration(dbKey, tableName) !== expectedGeneration\n ) {\n return;\n }\n\n let tables = store.get(dbKey);\n if (!tables) {\n tables = new Map();\n store.set(dbKey, tables);\n }\n let entries = tables.get(tableName);\n if (!entries) {\n entries = new Map();\n tables.set(tableName, entries);\n }\n\n // Evict oldest entries (Map preserves insertion order) at the cap. Skip when\n // refreshing an existing key — `set` updates it in place without growing the\n // map, so evicting first would drop a live entry and shrink the effective cap.\n if (!entries.has(queryKey)) {\n while (entries.size >= MAX_ENTRIES_PER_TABLE) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n }\n\n entries.set(queryKey, {\n expiresAt: Date.now() + ttl,\n rows: structuredClone(rows),\n });\n}\n\n/**\n * Drop every cached entry for a table. Called by the framework after any\n * successful mutation against that table — this is the write-invalidation\n * guarantee an app-side cache can't make.\n */\nexport function invalidateCollectionCache(\n dbKey: string,\n tableName: string,\n): void {\n store.get(dbKey)?.delete(tableName);\n // Bump the generation so any read whose DB round-trip is still in flight\n // discards its (now-stale) result instead of repopulating the table.\n const gkey = generationKey(dbKey, tableName);\n generations.set(gkey, (generations.get(gkey) ?? 0) + 1);\n}\n\n/**\n * Clear all cached collection reads and stop cross-process listeners.\n * Call in test setup to ensure isolation between test files.\n */\nexport function resetCollectionCache(): void {\n store.clear();\n generations.clear();\n crossProcessInterest.clear();\n stopCacheInvalidationListeners();\n}\n\n// ============================================================================\n// Per-call crossProcess interest (write-side broadcast decision)\n// ============================================================================\n\n/**\n * dbKey → tables this process has cached with a per-call `crossProcess`\n * opt-in. Writes consult this so per-call usage broadcasts too — model-level\n * config can't be the only trigger when the opt-in lives at the call site.\n */\nconst crossProcessInterest = new Map<string, Set<string>>();\n\n/**\n * Record that a per-call `crossProcess` cached read happened for a table.\n */\nexport function registerCrossProcessCacheInterest(\n dbKey: string,\n tableName: string,\n): void {\n let tables = crossProcessInterest.get(dbKey);\n if (!tables) {\n tables = new Set();\n crossProcessInterest.set(dbKey, tables);\n }\n tables.add(tableName);\n}\n\n/**\n * Whether any per-call `crossProcess` cached read has touched this table.\n */\nexport function hasCrossProcessCacheInterest(\n dbKey: string,\n tableName: string,\n): boolean {\n return crossProcessInterest.get(dbKey)?.has(tableName) ?? false;\n}\n\n// ============================================================================\n// Cross-process invalidation (opt-in, via db.notifications)\n// ============================================================================\n\ninterface ListenerHandle {\n iterator: AsyncIterator<unknown> | null;\n stopped: boolean;\n}\n\nconst listeners = new Map<string, ListenerHandle>();\nconst warnedNoNotifications = new Set<string>();\n\nfunction getNotifications(db: DatabaseInterface) {\n return (db as DatabaseInterface & { notifications?: any }).notifications as\n | {\n notify(channel: string, payload: any): Promise<number>;\n listen(\n channel: string,\n options?: Record<string, unknown>,\n ): AsyncIterable<{ channel: string; payload: any }>;\n }\n | undefined;\n}\n\nfunction warnOnceNoNotifications(dbKey: string, context: string): void {\n if (warnedNoNotifications.has(dbKey)) return;\n warnedNoNotifications.add(dbKey);\n logger.warn(\n `Collection cache: crossProcess invalidation requested but the database ` +\n `adapter exposes no notification capability (${context}). Peer ` +\n `replicas will serve stale rows until TTL.`,\n );\n}\n\n/**\n * Broadcast a table invalidation to peer processes.\n *\n * Fire-and-forget from mutation paths: a broadcast failure must never fail\n * the write that triggered it.\n */\nexport async function broadcastCacheInvalidation(\n db: DatabaseInterface,\n tableName: string,\n): Promise<void> {\n const notifications = getNotifications(db);\n const dbKey = resolveDbCacheKey(db);\n if (!notifications) {\n warnOnceNoNotifications(dbKey, 'broadcast');\n return;\n }\n try {\n await notifications.notify(CACHE_INVALIDATION_CHANNEL, {\n table: tableName,\n source: PROCESS_ID,\n });\n } catch (error) {\n logger.warn(\n `Collection cache: failed to broadcast invalidation for ${tableName}`,\n { error: error instanceof Error ? error.message : error },\n );\n }\n}\n\n/**\n * Ensure a background listener consumes invalidation broadcasts for this\n * database and drops matching local cache entries.\n *\n * Started lazily by the first cached read that opted into `crossProcess`.\n * Notifications published by this process are skipped — the local\n * invalidation already happened synchronously on the write path.\n */\nexport function ensureCacheInvalidationListener(db: DatabaseInterface): void {\n const dbKey = resolveDbCacheKey(db);\n if (listeners.has(dbKey)) return;\n\n const notifications = getNotifications(db);\n if (!notifications) {\n warnOnceNoNotifications(dbKey, 'listen');\n return;\n }\n\n const handle: ListenerHandle = { iterator: null, stopped: false };\n listeners.set(dbKey, handle);\n\n void (async () => {\n try {\n const iterable = notifications.listen(CACHE_INVALIDATION_CHANNEL);\n const iterator = iterable[Symbol.asyncIterator]();\n handle.iterator = iterator;\n\n while (!handle.stopped) {\n const { value, done } = await iterator.next();\n if (done || handle.stopped) break;\n\n const notification = value as { payload?: any };\n const payload =\n typeof notification?.payload === 'string'\n ? safeParse(notification.payload)\n : notification?.payload;\n\n if (!payload || typeof payload.table !== 'string') continue;\n if (payload.source === PROCESS_ID) continue;\n\n invalidateCollectionCache(dbKey, payload.table);\n }\n } catch (error) {\n if (!handle.stopped) {\n logger.warn(\n 'Collection cache: invalidation listener terminated unexpectedly; ' +\n 'cross-process invalidation is inactive for this database ' +\n '(local TTL still bounds staleness)',\n { error: error instanceof Error ? error.message : error },\n );\n }\n } finally {\n // Only retract our own handle. A concurrent stop+restart for the same\n // dbKey may have already installed a replacement; deleting unconditionally\n // would orphan it and leak/duplicate the live listener.\n if (listeners.get(dbKey) === handle) {\n listeners.delete(dbKey);\n }\n }\n })();\n}\n\n/**\n * Stop all cross-process invalidation listeners. Used by tests and during\n * shutdown; safe to call when none are active.\n */\nexport function stopCacheInvalidationListeners(): void {\n for (const handle of listeners.values()) {\n handle.stopped = true;\n void handle.iterator?.return?.(undefined);\n }\n listeners.clear();\n warnedNoNotifications.clear();\n}\n\nfunction safeParse(value: string): unknown {\n try {\n return JSON.parse(value);\n } catch {\n return undefined;\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"collection-cache.js","sources":["../src/collection-cache.ts"],"sourcesContent":["/**\n * Opt-in read-through cache for collection reads (issue #1498).\n *\n * SSR-rendered SMRT apps re-query read-heavy / write-rare collections on\n * every request, and the per-query round-trip dominates wall time when a\n * page needs many collections. This module memoizes `list()`/`get()` row\n * sets keyed by the final SQL + parameters, for an opt-in TTL.\n *\n * Correctness model:\n * - Caching is OFF by default. It is enabled per call\n * (`list({ cache: { ttl } })`) or per model (`@smrt({ cache: { ttl } })`).\n * - SMRT owns every mutation path (`save()`/`delete()` back\n * `collection.create()`, `getOrUpsert()`, junction attach/detach), so all\n * writes invalidate the affected table's entries in-process automatically.\n * - Entries are scoped per database identity (`db.url`) and per table, so\n * multi-DB processes and STI siblings (which share a table) stay coherent.\n * - Caches are per-process. With multiple replicas, a local invalidation\n * leaves peers stale until TTL unless cross-process invalidation is opted\n * into (`crossProcess: true`), which broadcasts over the database\n * adapter's notification capability (e.g. Postgres LISTEN/NOTIFY) when\n * the adapter provides one.\n *\n * Cached values are raw result rows, not hydrated instances — hydration and\n * read interceptors (tenancy, audit) run on every call, cached or not, and\n * each caller receives isolated row copies.\n *\n * Known limitations:\n * - Invalidation fires when a mutation's SQL executes, not when its\n * surrounding transaction commits. A write inside an uncommitted\n * transaction invalidates (and may broadcast) immediately; a concurrent\n * reader could repopulate the cache from the pre-commit snapshot, and a\n * rollback leaves the cache invalidated for a write that never landed.\n * Caching targets read-heavy / write-rare data where this is rare; for\n * models mutated inside multi-statement transactions, coherence is still\n * bounded by TTL. Per-call `cache: false` forces a fresh read where it\n * matters.\n * - Writes that bypass the framework's mutation paths (raw `db.query`\n * issued outside `collection.query()`, external processes without\n * `crossProcess`) are only bounded by TTL.\n *\n * @see https://github.com/happyvertical/smrt/issues/1498\n * @packageDocumentation\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Read-through cache configuration for collection reads.\n *\n * Used both per call (`collection.list({ cache: { ttl: 60_000 } })`) and per\n * model (`@smrt({ cache: { ttl: 60_000 } })`).\n */\nexport interface CollectionCacheConfig {\n /**\n * Time-to-live for cached query results, in milliseconds. Must be > 0.\n */\n ttl: number;\n\n /**\n * Broadcast invalidations to peer processes through the database\n * adapter's notification capability (`db.notifications`, e.g. Postgres\n * LISTEN/NOTIFY). Without this, peer replicas serve stale rows until TTL.\n *\n * Model-level config (`@smrt({ cache })`) is the reliable opt-in: every\n * process that writes the model knows to broadcast. As a per-call option,\n * writes broadcast only from processes that have already performed a\n * `crossProcess` cached read of the same table (typical for homogeneous\n * replicas running the same routes); a process that only writes never\n * learns about the per-call opt-in, so its peers fall back to TTL expiry.\n *\n * Ignored (with a one-time warning) when the adapter does not expose\n * notifications.\n */\n crossProcess?: boolean;\n}\n\ninterface CacheEntry {\n expiresAt: number;\n rows: Record<string, unknown>[];\n}\n\n/**\n * Per-table entry cap. A table accumulating more distinct query shapes than\n * this within one TTL window evicts its oldest entries (insertion order).\n * Guards against unbounded growth from high-cardinality WHERE values.\n */\nconst MAX_ENTRIES_PER_TABLE = 500;\n\n/**\n * Notification channel used for cross-process invalidation broadcasts.\n */\nexport const CACHE_INVALIDATION_CHANNEL = 'smrt_collection_cache';\n\n/**\n * Identifies this process in broadcast payloads so a replica can skip\n * notifications it published itself (it already invalidated locally).\n */\nconst PROCESS_ID = crypto.randomUUID();\n\n/**\n * dbKey → tableName → (queryKey → entry)\n */\nconst store = new Map<string, Map<string, Map<string, CacheEntry>>>();\n\n/**\n * Monotonic invalidation generation per `dbKey\\0tableName`, bumped on every\n * invalidation. A read captures the generation *before* its DB round-trip and\n * passes it to `setCachedRows`; if an invalidating write landed during the\n * round-trip, the generation no longer matches and the (now-stale) result is\n * dropped instead of cached. This closes the read-miss/concurrent-write race\n * where an in-flight SELECT would otherwise repopulate the cache with\n * pre-write rows for the full TTL.\n */\nconst generations = new Map<string, number>();\n\nfunction generationKey(dbKey: string, tableName: string): string {\n return `${dbKey}\u0000${tableName}`;\n}\n\n/**\n * Current invalidation generation for a table (0 if never invalidated).\n * Capture this before a DB read and pass it to `setCachedRows`.\n */\nexport function getCacheGeneration(dbKey: string, tableName: string): number {\n return generations.get(generationKey(dbKey, tableName)) ?? 0;\n}\n\n/**\n * Fallback identities for database instances that expose no URL\n * (each such instance gets its own scope, never shared).\n */\nconst fallbackDbKeys = new WeakMap<object, string>();\n\n/**\n * Resolve a stable cache scope for a database instance.\n *\n * Mirrors table-verifier's identity resolution (`db.url || config.url`).\n * `:memory:` databases share that URL string while being entirely separate\n * databases, so they (and URL-less instances) are scoped per instance —\n * serving one in-memory database's rows for another would be a correctness\n * bug, not just a stale read.\n */\nexport function resolveDbCacheKey(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseInterface & {\n config?: { url?: string };\n };\n const url = db.url || dbWithConfig.config?.url;\n if (url && url !== ':memory:') return url;\n\n let key = fallbackDbKeys.get(db);\n if (!key) {\n key = `smrt-db:${crypto.randomUUID()}`;\n fallbackDbKeys.set(db, key);\n }\n return key;\n}\n\n/**\n * Build the cache key for a query. The final SQL and bound parameters fully\n * normalize the query shape — they already include STI discriminator\n * filters, interceptor-injected tenant filters, ORDER BY, LIMIT and OFFSET.\n */\nexport function buildQueryCacheKey(sql: string, params: unknown[]): string {\n return `${sql}\u0000${JSON.stringify(params)}`;\n}\n\n/**\n * Read cached rows for a query, or undefined on miss/expiry.\n *\n * Returns a structured clone so callers can never mutate the cached copy\n * (hydration writes into row objects).\n */\nexport function getCachedRows(\n dbKey: string,\n tableName: string,\n queryKey: string,\n): Record<string, unknown>[] | undefined {\n const entry = store.get(dbKey)?.get(tableName)?.get(queryKey);\n if (!entry) return undefined;\n if (entry.expiresAt <= Date.now()) {\n store.get(dbKey)?.get(tableName)?.delete(queryKey);\n return undefined;\n }\n return structuredClone(entry.rows);\n}\n\n/**\n * Store rows for a query under the table's cache scope.\n *\n * `expectedGeneration` is the value {@link getCacheGeneration} returned before\n * the DB read. If an invalidation bumped the table's generation while the read\n * was in flight, the result is stale and is dropped rather than cached.\n */\nexport function setCachedRows(\n dbKey: string,\n tableName: string,\n queryKey: string,\n rows: Record<string, unknown>[],\n ttl: number,\n expectedGeneration?: number,\n): void {\n if (!(ttl > 0)) return;\n\n // Drop a result that a concurrent write invalidated mid-flight.\n if (\n expectedGeneration !== undefined &&\n getCacheGeneration(dbKey, tableName) !== expectedGeneration\n ) {\n return;\n }\n\n let tables = store.get(dbKey);\n if (!tables) {\n tables = new Map();\n store.set(dbKey, tables);\n }\n let entries = tables.get(tableName);\n if (!entries) {\n entries = new Map();\n tables.set(tableName, entries);\n }\n\n // Evict oldest entries (Map preserves insertion order) at the cap. Skip when\n // refreshing an existing key — `set` updates it in place without growing the\n // map, so evicting first would drop a live entry and shrink the effective cap.\n if (!entries.has(queryKey)) {\n while (entries.size >= MAX_ENTRIES_PER_TABLE) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n }\n\n entries.set(queryKey, {\n expiresAt: Date.now() + ttl,\n rows: structuredClone(rows),\n });\n}\n\n/**\n * Drop every cached entry for a table. Called by the framework after any\n * successful mutation against that table — this is the write-invalidation\n * guarantee an app-side cache can't make.\n */\nexport function invalidateCollectionCache(\n dbKey: string,\n tableName: string,\n): void {\n store.get(dbKey)?.delete(tableName);\n // Bump the generation so any read whose DB round-trip is still in flight\n // discards its (now-stale) result instead of repopulating the table.\n const gkey = generationKey(dbKey, tableName);\n generations.set(gkey, (generations.get(gkey) ?? 0) + 1);\n}\n\n/**\n * Clear all cached collection reads and stop cross-process listeners.\n * Call in test setup to ensure isolation between test files.\n */\nexport function resetCollectionCache(): void {\n store.clear();\n generations.clear();\n crossProcessInterest.clear();\n stopCacheInvalidationListeners();\n}\n\n// ============================================================================\n// Per-call crossProcess interest (write-side broadcast decision)\n// ============================================================================\n\n/**\n * dbKey → tables this process has cached with a per-call `crossProcess`\n * opt-in. Writes consult this so per-call usage broadcasts too — model-level\n * config can't be the only trigger when the opt-in lives at the call site.\n */\nconst crossProcessInterest = new Map<string, Set<string>>();\n\n/**\n * Record that a per-call `crossProcess` cached read happened for a table.\n */\nexport function registerCrossProcessCacheInterest(\n dbKey: string,\n tableName: string,\n): void {\n let tables = crossProcessInterest.get(dbKey);\n if (!tables) {\n tables = new Set();\n crossProcessInterest.set(dbKey, tables);\n }\n tables.add(tableName);\n}\n\n/**\n * Whether any per-call `crossProcess` cached read has touched this table.\n */\nexport function hasCrossProcessCacheInterest(\n dbKey: string,\n tableName: string,\n): boolean {\n return crossProcessInterest.get(dbKey)?.has(tableName) ?? false;\n}\n\n// ============================================================================\n// Cross-process invalidation (opt-in, via db.notifications)\n// ============================================================================\n\ninterface ListenerHandle {\n iterator: AsyncIterator<unknown> | null;\n stopped: boolean;\n}\n\nconst listeners = new Map<string, ListenerHandle>();\nconst warnedNoNotifications = new Set<string>();\n\nfunction getNotifications(db: DatabaseInterface) {\n return (db as DatabaseInterface & { notifications?: unknown })\n .notifications as\n | {\n notify(channel: string, payload: unknown): Promise<number>;\n listen(\n channel: string,\n options?: Record<string, unknown>,\n ): AsyncIterable<{ channel: string; payload: unknown }>;\n }\n | undefined;\n}\n\nfunction warnOnceNoNotifications(dbKey: string, context: string): void {\n if (warnedNoNotifications.has(dbKey)) return;\n warnedNoNotifications.add(dbKey);\n logger.warn(\n `Collection cache: crossProcess invalidation requested but the database ` +\n `adapter exposes no notification capability (${context}). Peer ` +\n `replicas will serve stale rows until TTL.`,\n );\n}\n\n/**\n * Broadcast a table invalidation to peer processes.\n *\n * Fire-and-forget from mutation paths: a broadcast failure must never fail\n * the write that triggered it.\n */\nexport async function broadcastCacheInvalidation(\n db: DatabaseInterface,\n tableName: string,\n): Promise<void> {\n const notifications = getNotifications(db);\n const dbKey = resolveDbCacheKey(db);\n if (!notifications) {\n warnOnceNoNotifications(dbKey, 'broadcast');\n return;\n }\n try {\n await notifications.notify(CACHE_INVALIDATION_CHANNEL, {\n table: tableName,\n source: PROCESS_ID,\n });\n } catch (error) {\n logger.warn(\n `Collection cache: failed to broadcast invalidation for ${tableName}`,\n { error: error instanceof Error ? error.message : error },\n );\n }\n}\n\n/**\n * Ensure a background listener consumes invalidation broadcasts for this\n * database and drops matching local cache entries.\n *\n * Started lazily by the first cached read that opted into `crossProcess`.\n * Notifications published by this process are skipped — the local\n * invalidation already happened synchronously on the write path.\n */\nexport function ensureCacheInvalidationListener(db: DatabaseInterface): void {\n const dbKey = resolveDbCacheKey(db);\n if (listeners.has(dbKey)) return;\n\n const notifications = getNotifications(db);\n if (!notifications) {\n warnOnceNoNotifications(dbKey, 'listen');\n return;\n }\n\n const handle: ListenerHandle = { iterator: null, stopped: false };\n listeners.set(dbKey, handle);\n\n void (async () => {\n try {\n const iterable = notifications.listen(CACHE_INVALIDATION_CHANNEL);\n const iterator = iterable[Symbol.asyncIterator]();\n handle.iterator = iterator;\n\n while (!handle.stopped) {\n const { value, done } = await iterator.next();\n if (done || handle.stopped) break;\n\n const notification = value as { payload?: unknown };\n const payload: unknown =\n typeof notification?.payload === 'string'\n ? safeParse(notification.payload)\n : notification?.payload;\n\n if (!payload || typeof payload !== 'object') continue;\n const record = payload as { table?: unknown; source?: unknown };\n if (typeof record.table !== 'string') continue;\n if (record.source === PROCESS_ID) continue;\n\n invalidateCollectionCache(dbKey, record.table);\n }\n } catch (error) {\n if (!handle.stopped) {\n logger.warn(\n 'Collection cache: invalidation listener terminated unexpectedly; ' +\n 'cross-process invalidation is inactive for this database ' +\n '(local TTL still bounds staleness)',\n { error: error instanceof Error ? error.message : error },\n );\n }\n } finally {\n // Only retract our own handle. A concurrent stop+restart for the same\n // dbKey may have already installed a replacement; deleting unconditionally\n // would orphan it and leak/duplicate the live listener.\n if (listeners.get(dbKey) === handle) {\n listeners.delete(dbKey);\n }\n }\n })();\n}\n\n/**\n * Stop all cross-process invalidation listeners. Used by tests and during\n * shutdown; safe to call when none are active.\n */\nexport function stopCacheInvalidationListeners(): void {\n for (const handle of listeners.values()) {\n handle.stopped = true;\n void handle.iterator?.return?.(undefined);\n }\n listeners.clear();\n warnedNoNotifications.clear();\n}\n\nfunction safeParse(value: string): unknown {\n try {\n return JSON.parse(value);\n } catch {\n return undefined;\n }\n}\n"],"names":[],"mappings":";AA+CA,MAAM,SAAS,aAAa,EAAE,OAAO,QAAQ;AA0C7C,MAAM,wBAAwB;AAKvB,MAAM,6BAA6B;AAM1C,MAAM,aAAa,OAAO,WAAA;AAK1B,MAAM,4BAAY,IAAA;AAWlB,MAAM,kCAAkB,IAAA;AAExB,SAAS,cAAc,OAAe,WAA2B;AAC/D,SAAO,GAAG,KAAK,KAAI,SAAS;AAC9B;AAMO,SAAS,mBAAmB,OAAe,WAA2B;AAC3E,SAAO,YAAY,IAAI,cAAc,OAAO,SAAS,CAAC,KAAK;AAC7D;AAMA,MAAM,qCAAqB,QAAA;AAWpB,SAAS,kBAAkB,IAA+B;AAC/D,QAAM,eAAe;AAGrB,QAAM,MAAM,GAAG,OAAO,aAAa,QAAQ;AAC3C,MAAI,OAAO,QAAQ,WAAY,QAAO;AAEtC,MAAI,MAAM,eAAe,IAAI,EAAE;AAC/B,MAAI,CAAC,KAAK;AACR,UAAM,WAAW,OAAO,WAAA,CAAY;AACpC,mBAAe,IAAI,IAAI,GAAG;AAAA,EAC5B;AACA,SAAO;AACT;AAOO,SAAS,mBAAmB,KAAa,QAA2B;AACzE,SAAO,GAAG,GAAG,KAAI,KAAK,UAAU,MAAM,CAAC;AACzC;AAQO,SAAS,cACd,OACA,WACA,UACuC;AACvC,QAAM,QAAQ,MAAM,IAAI,KAAK,GAAG,IAAI,SAAS,GAAG,IAAI,QAAQ;AAC5D,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,MAAM,aAAa,KAAK,IAAA,GAAO;AACjC,UAAM,IAAI,KAAK,GAAG,IAAI,SAAS,GAAG,OAAO,QAAQ;AACjD,WAAO;AAAA,EACT;AACA,SAAO,gBAAgB,MAAM,IAAI;AACnC;AASO,SAAS,cACd,OACA,WACA,UACA,MACA,KACA,oBACM;AACN,MAAI,EAAE,MAAM,GAAI;AAGhB,MACE,uBAAuB,UACvB,mBAAmB,OAAO,SAAS,MAAM,oBACzC;AACA;AAAA,EACF;AAEA,MAAI,SAAS,MAAM,IAAI,KAAK;AAC5B,MAAI,CAAC,QAAQ;AACX,iCAAa,IAAA;AACb,UAAM,IAAI,OAAO,MAAM;AAAA,EACzB;AACA,MAAI,UAAU,OAAO,IAAI,SAAS;AAClC,MAAI,CAAC,SAAS;AACZ,kCAAc,IAAA;AACd,WAAO,IAAI,WAAW,OAAO;AAAA,EAC/B;AAKA,MAAI,CAAC,QAAQ,IAAI,QAAQ,GAAG;AAC1B,WAAO,QAAQ,QAAQ,uBAAuB;AAC5C,YAAM,SAAS,QAAQ,KAAA,EAAO,OAAO;AACrC,UAAI,WAAW,OAAW;AAC1B,cAAQ,OAAO,MAAM;AAAA,IACvB;AAAA,EACF;AAEA,UAAQ,IAAI,UAAU;AAAA,IACpB,WAAW,KAAK,IAAA,IAAQ;AAAA,IACxB,MAAM,gBAAgB,IAAI;AAAA,EAAA,CAC3B;AACH;AAOO,SAAS,0BACd,OACA,WACM;AACN,QAAM,IAAI,KAAK,GAAG,OAAO,SAAS;AAGlC,QAAM,OAAO,cAAc,OAAO,SAAS;AAC3C,cAAY,IAAI,OAAO,YAAY,IAAI,IAAI,KAAK,KAAK,CAAC;AACxD;AAMO,SAAS,uBAA6B;AAC3C,QAAM,MAAA;AACN,cAAY,MAAA;AACZ,uBAAqB,MAAA;AACrB,iCAAA;AACF;AAWA,MAAM,2CAA2B,IAAA;AAK1B,SAAS,kCACd,OACA,WACM;AACN,MAAI,SAAS,qBAAqB,IAAI,KAAK;AAC3C,MAAI,CAAC,QAAQ;AACX,iCAAa,IAAA;AACb,yBAAqB,IAAI,OAAO,MAAM;AAAA,EACxC;AACA,SAAO,IAAI,SAAS;AACtB;AAKO,SAAS,6BACd,OACA,WACS;AACT,SAAO,qBAAqB,IAAI,KAAK,GAAG,IAAI,SAAS,KAAK;AAC5D;AAWA,MAAM,gCAAgB,IAAA;AACtB,MAAM,4CAA4B,IAAA;AAElC,SAAS,iBAAiB,IAAuB;AAC/C,SAAQ,GACL;AASL;AAEA,SAAS,wBAAwB,OAAe,SAAuB;AACrE,MAAI,sBAAsB,IAAI,KAAK,EAAG;AACtC,wBAAsB,IAAI,KAAK;AAC/B,SAAO;AAAA,IACL,sHACiD,OAAO;AAAA,EAAA;AAG5D;AAQA,eAAsB,2BACpB,IACA,WACe;AACf,QAAM,gBAAgB,iBAAiB,EAAE;AACzC,QAAM,QAAQ,kBAAkB,EAAE;AAClC,MAAI,CAAC,eAAe;AAClB,4BAAwB,OAAO,WAAW;AAC1C;AAAA,EACF;AACA,MAAI;AACF,UAAM,cAAc,OAAO,4BAA4B;AAAA,MACrD,OAAO;AAAA,MACP,QAAQ;AAAA,IAAA,CACT;AAAA,EACH,SAAS,OAAO;AACd,WAAO;AAAA,MACL,0DAA0D,SAAS;AAAA,MACnE,EAAE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,MAAA;AAAA,IAAM;AAAA,EAE5D;AACF;AAUO,SAAS,gCAAgC,IAA6B;AAC3E,QAAM,QAAQ,kBAAkB,EAAE;AAClC,MAAI,UAAU,IAAI,KAAK,EAAG;AAE1B,QAAM,gBAAgB,iBAAiB,EAAE;AACzC,MAAI,CAAC,eAAe;AAClB,4BAAwB,OAAO,QAAQ;AACvC;AAAA,EACF;AAEA,QAAM,SAAyB,EAAE,UAAU,MAAM,SAAS,MAAA;AAC1D,YAAU,IAAI,OAAO,MAAM;AAE3B,QAAM,YAAY;AAChB,QAAI;AACF,YAAM,WAAW,cAAc,OAAO,0BAA0B;AAChE,YAAM,WAAW,SAAS,OAAO,aAAa,EAAA;AAC9C,aAAO,WAAW;AAElB,aAAO,CAAC,OAAO,SAAS;AACtB,cAAM,EAAE,OAAO,KAAA,IAAS,MAAM,SAAS,KAAA;AACvC,YAAI,QAAQ,OAAO,QAAS;AAE5B,cAAM,eAAe;AACrB,cAAM,UACJ,OAAO,cAAc,YAAY,WAC7B,UAAU,aAAa,OAAO,IAC9B,cAAc;AAEpB,YAAI,CAAC,WAAW,OAAO,YAAY,SAAU;AAC7C,cAAM,SAAS;AACf,YAAI,OAAO,OAAO,UAAU,SAAU;AACtC,YAAI,OAAO,WAAW,WAAY;AAElC,kCAA0B,OAAO,OAAO,KAAK;AAAA,MAC/C;AAAA,IACF,SAAS,OAAO;AACd,UAAI,CAAC,OAAO,SAAS;AACnB,eAAO;AAAA,UACL;AAAA,UAGA,EAAE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,MAAA;AAAA,QAAM;AAAA,MAE5D;AAAA,IACF,UAAA;AAIE,UAAI,UAAU,IAAI,KAAK,MAAM,QAAQ;AACnC,kBAAU,OAAO,KAAK;AAAA,MACxB;AAAA,IACF;AAAA,EACF,GAAA;AACF;AAMO,SAAS,iCAAuC;AACrD,aAAW,UAAU,UAAU,UAAU;AACvC,WAAO,UAAU;AACjB,SAAK,OAAO,UAAU,SAAS,MAAS;AAAA,EAC1C;AACA,YAAU,MAAA;AACV,wBAAsB,MAAA;AACxB;AAEA,SAAS,UAAU,OAAwB;AACzC,MAAI;AACF,WAAO,KAAK,MAAM,KAAK;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;"}