@open-mercato/core 0.6.6-develop.5509.1.006f4d4f24 → 0.6.6-develop.5523.1.e223ca1915

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bootstrap.js CHANGED
@@ -16,6 +16,43 @@ import { RateLimiterService } from "@open-mercato/shared/lib/ratelimit/service";
16
16
  import { readRateLimitConfig } from "@open-mercato/shared/lib/ratelimit/config";
17
17
  const RL_GLOBAL_KEY = "__openMercatoRateLimiterService__";
18
18
  const RL_SHUTDOWN_KEY = "__openMercatoRateLimiterShutdown__";
19
+ const CACHE_GLOBAL_KEY = "__openMercatoCacheService__";
20
+ const CACHE_SHUTDOWN_KEY = "__openMercatoCacheShutdown__";
21
+ function isCacheSingletonEnabled() {
22
+ const raw = process.env.OM_CACHE_SINGLETON;
23
+ if (raw === void 0) return true;
24
+ const normalized = raw.trim().toLowerCase();
25
+ if (!normalized.length) return true;
26
+ return !(normalized === "0" || normalized === "off" || normalized === "false" || normalized === "no");
27
+ }
28
+ function getCachedCacheService() {
29
+ if (!isCacheSingletonEnabled()) return null;
30
+ let service = globalThis[CACHE_GLOBAL_KEY] ?? null;
31
+ if (!service) {
32
+ try {
33
+ try {
34
+ service = createCacheService();
35
+ } catch (err) {
36
+ console.warn("Cache service initialization failed; falling back to memory strategy:", err?.message || err);
37
+ service = createCacheService({ strategy: "memory" });
38
+ }
39
+ ;
40
+ globalThis[CACHE_GLOBAL_KEY] = service;
41
+ if (!globalThis[CACHE_SHUTDOWN_KEY]) {
42
+ const shutdown = () => {
43
+ service?.close?.().catch(() => {
44
+ });
45
+ };
46
+ process.once("SIGTERM", shutdown);
47
+ process.once("SIGINT", shutdown);
48
+ globalThis[CACHE_SHUTDOWN_KEY] = true;
49
+ }
50
+ } catch (err) {
51
+ console.warn("[cache] Failed to create cache service:", err?.message || err);
52
+ }
53
+ }
54
+ return service;
55
+ }
19
56
  function getCachedRateLimiterService() {
20
57
  let service = globalThis[RL_GLOBAL_KEY] ?? null;
21
58
  if (!service) {
@@ -42,12 +79,14 @@ function getCachedRateLimiterService() {
42
79
  return service;
43
80
  }
44
81
  async function bootstrap(container) {
45
- let cache;
46
- try {
47
- cache = createCacheService();
48
- } catch (err) {
49
- console.warn("Cache service initialization failed; falling back to memory strategy:", err?.message || err);
50
- cache = createCacheService({ strategy: "memory" });
82
+ let cache = getCachedCacheService();
83
+ if (!cache) {
84
+ try {
85
+ cache = createCacheService();
86
+ } catch (err) {
87
+ console.warn("Cache service initialization failed; falling back to memory strategy:", err?.message || err);
88
+ cache = createCacheService({ strategy: "memory" });
89
+ }
51
90
  }
52
91
  container.register({ cache: asValue(cache) });
53
92
  let eventBus;
@@ -152,6 +191,7 @@ async function bootstrap(container) {
152
191
  }
153
192
  export {
154
193
  bootstrap,
194
+ getCachedCacheService,
155
195
  getCachedRateLimiterService
156
196
  };
157
197
  //# sourceMappingURL=bootstrap.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/bootstrap.ts"],
4
- "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport { asValue } from 'awilix'\nimport { createEventBus } from '@open-mercato/events/index'\nimport { setGlobalEventBus } from '@open-mercato/shared/modules/events'\nimport { createCacheService } from '@open-mercato/cache'\nimport { createKmsService } from '@open-mercato/shared/lib/encryption/kms'\nimport { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { registerTenantEncryptionSubscriber } from '@open-mercato/shared/lib/encryption/subscriber'\nimport { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\nimport { getSearchModuleConfigs } from '@open-mercato/shared/modules/search'\nimport {\n registerSearchModule,\n createSearchDeleteSubscriber,\n searchDeleteMetadata,\n} from '@open-mercato/search'\nimport { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\nimport { readRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport type { EntityManager } from '@mikro-orm/postgresql'\n\n// Use globalThis to survive tsx/webpack module duplication (same pattern as container.ts DI registrars)\nconst RL_GLOBAL_KEY = '__openMercatoRateLimiterService__'\nconst RL_SHUTDOWN_KEY = '__openMercatoRateLimiterShutdown__'\n\nexport function getCachedRateLimiterService(): RateLimiterService | null {\n let service = (globalThis as any)[RL_GLOBAL_KEY] as RateLimiterService | null ?? null\n if (!service) {\n try {\n const rateLimitConfig = readRateLimitConfig()\n service = new RateLimiterService(rateLimitConfig)\n // Fire-and-forget async init (only needed for Redis strategy;\n // memory strategy works synchronously, and Redis has an in-memory\n // insurance limiter so the first few requests are still protected)\n service.initialize().catch((err) => {\n console.warn('[ratelimit] Async initialization failed:', (err as Error)?.message || err)\n })\n ;(globalThis as any)[RL_GLOBAL_KEY] = service\n\n // Register shutdown hook once to disconnect Redis on process exit\n if (!(globalThis as any)[RL_SHUTDOWN_KEY]) {\n const shutdown = () => { service?.destroy().catch(() => {}) }\n process.once('SIGTERM', shutdown)\n process.once('SIGINT', shutdown)\n ;(globalThis as any)[RL_SHUTDOWN_KEY] = true\n }\n } catch (err) {\n console.warn('[ratelimit] Failed to create rate limiter service:', (err as Error)?.message || err)\n }\n }\n return service\n}\n\nexport async function bootstrap(container: AwilixContainer) {\n // Create and register the cache service\n let cache: any\n try {\n cache = createCacheService()\n } catch (err: any) {\n console.warn('Cache service initialization failed; falling back to memory strategy:', err?.message || err)\n cache = createCacheService({ strategy: 'memory' })\n }\n container.register({ cache: asValue(cache) })\n\n // Create and register the DI-aware event bus\n let eventBus: any\n try {\n // Support both QUEUE_STRATEGY and legacy EVENTS_STRATEGY env vars\n const strategyEnv = process.env.QUEUE_STRATEGY || process.env.EVENTS_STRATEGY\n const queueStrategy = strategyEnv === 'async' || strategyEnv === 'redis' ? 'async' : 'local'\n eventBus = createEventBus({ resolve: container.resolve.bind(container) as any, queueStrategy })\n } catch (err: any) {\n // Fall back to local strategy to avoid breaking the app on misconfiguration\n console.warn('Event bus initialization failed; falling back to local strategy:', err?.message || err)\n try {\n eventBus = createEventBus({ resolve: container.resolve.bind(container) as any, queueStrategy: 'local' })\n } catch {\n // In extreme cases, provide a no-op bus to avoid crashes\n eventBus = {\n emit: async () => {},\n on: () => {},\n registerModuleSubscribers: () => {},\n clearQueue: async () => ({ removed: 0 }),\n }\n }\n }\n container.register({ eventBus: asValue(eventBus) })\n // Wire the global event bus so createModuleEvents().emit works outside DI context\n setGlobalEventBus(eventBus)\n // Auto-register discovered module subscribers\n try {\n let loadedModules: any[] = []\n try {\n const { getModules } = await import('@open-mercato/shared/lib/i18n/server')\n loadedModules = getModules()\n } catch {}\n const subs = loadedModules.flatMap((m) => m.subscribers || [])\n if (subs.length) (container.resolve as any)('eventBus').registerModuleSubscribers(subs)\n\n // Extract sync subscribers and register in the sync-subscriber-store\n const syncSubs = subs.filter((s: any) => s.sync === true)\n if (syncSubs.length) {\n try {\n const { registerSyncSubscribers } = await import('@open-mercato/shared/lib/crud/sync-subscriber-store')\n registerSyncSubscribers(\n syncSubs.map((s: any) => ({\n metadata: { event: s.event, sync: true as const, priority: s.priority, id: s.id },\n handler: s.handler,\n })),\n )\n } catch {\n // sync-subscriber-store may not be available\n }\n }\n } catch (err) {\n console.error(\"Failed to register module subscribers:\", err);\n }\n\n // KMS + tenant encryption\n const kmsService = createKmsService()\n container.register({ kmsService: asValue(kmsService) })\n try {\n const em = container.resolve('em') as EntityManager\n const cacheService = (() => {\n try { return container.resolve('cache') as any } catch { return null }\n })()\n const tenantEncryptionService = new TenantDataEncryptionService(em, { cache: cacheService, kms: kmsService })\n container.register({ tenantEncryptionService: asValue(tenantEncryptionService) })\n if (isTenantDataEncryptionEnabled() && kmsService.isHealthy()) {\n try {\n registerTenantEncryptionSubscriber(em, tenantEncryptionService)\n } catch (err) {\n console.warn('[encryption] Failed to register MikroORM encryption subscriber:', (err as Error)?.message || err)\n }\n } else if (isTenantDataEncryptionEnabled() && !kmsService.isHealthy()) {\n console.warn('[encryption] Vault/KMS unhealthy - tenant data encryption is disabled until recovery')\n }\n } catch (err) {\n console.warn('[encryption] Failed to initialize tenant encryption service:', (err as Error)?.message || err)\n }\n\n // Register rate limiter service (singleton via globalThis \u2014 reused across request containers)\n // getCachedRateLimiterService() never throws; returns null on failure\n const rateLimiterService = getCachedRateLimiterService()\n if (rateLimiterService) {\n container.register({ rateLimiterService: asValue(rateLimiterService) })\n }\n\n // Register search module\n try {\n // Get configs from global registry (registered during app bootstrap)\n const searchModuleConfigs = getSearchModuleConfigs()\n registerSearchModule(container as any, { moduleConfigs: searchModuleConfigs })\n\n // Register searchModuleConfigs in container so status API can access vector-enabled entities\n container.register({\n searchModuleConfigs: asValue(searchModuleConfigs),\n })\n\n // Register search delete event subscriber\n // Note: search.index_record is now handled by auto-discovered fulltext_upsert.ts subscriber\n try {\n const searchIndexer = container.resolve('searchIndexer') as any\n if (searchIndexer && eventBus) {\n eventBus.registerModuleSubscribers([\n {\n event: searchDeleteMetadata.event,\n persistent: searchDeleteMetadata.persistent,\n handler: createSearchDeleteSubscriber(searchIndexer),\n },\n ])\n }\n } catch {\n // searchIndexer may not be available\n }\n } catch (err) {\n console.warn('[search] Failed to register search module:', (err as Error)?.message || err)\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport { asValue } from 'awilix'\nimport { createEventBus } from '@open-mercato/events/index'\nimport { setGlobalEventBus } from '@open-mercato/shared/modules/events'\nimport { createCacheService } from '@open-mercato/cache'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { createKmsService } from '@open-mercato/shared/lib/encryption/kms'\nimport { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { registerTenantEncryptionSubscriber } from '@open-mercato/shared/lib/encryption/subscriber'\nimport { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\nimport { getSearchModuleConfigs } from '@open-mercato/shared/modules/search'\nimport {\n registerSearchModule,\n createSearchDeleteSubscriber,\n searchDeleteMetadata,\n} from '@open-mercato/search'\nimport { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\nimport { readRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport type { EntityManager } from '@mikro-orm/postgresql'\n\n// Use globalThis to survive tsx/webpack module duplication (same pattern as container.ts DI registrars)\nconst RL_GLOBAL_KEY = '__openMercatoRateLimiterService__'\nconst RL_SHUTDOWN_KEY = '__openMercatoRateLimiterShutdown__'\n\nconst CACHE_GLOBAL_KEY = '__openMercatoCacheService__'\nconst CACHE_SHUTDOWN_KEY = '__openMercatoCacheShutdown__'\n\n// Escape hatch: set OM_CACHE_SINGLETON=off to fall back to the legacy\n// per-request cache instance (e.g. to isolate a regression). Default ON.\nfunction isCacheSingletonEnabled(): boolean {\n const raw = process.env.OM_CACHE_SINGLETON\n if (raw === undefined) return true\n const normalized = raw.trim().toLowerCase()\n if (!normalized.length) return true\n return !(normalized === '0' || normalized === 'off' || normalized === 'false' || normalized === 'no')\n}\n\n/**\n * Process-wide cache service singleton, mirroring getCachedRateLimiterService.\n *\n * bootstrap() previously built a fresh cache per request container, so under\n * the default memory strategy every cross-request cache was a no-op (each\n * instance is process-local) and under sqlite each request leaked a native\n * handle. Reusing one instance is safe by construction: tenant scope resolves\n * per-call via AsyncLocalStorage (packages/cache/src/tenantContext.ts), so the\n * service holds no request-bound state.\n *\n * Returns null when disabled via the escape hatch or when creation fails, so\n * the caller falls back to a per-request instance.\n */\nexport function getCachedCacheService(): CacheStrategy | null {\n if (!isCacheSingletonEnabled()) return null\n let service = (globalThis as any)[CACHE_GLOBAL_KEY] as CacheStrategy | null ?? null\n if (!service) {\n try {\n try {\n service = createCacheService()\n } catch (err) {\n console.warn('Cache service initialization failed; falling back to memory strategy:', (err as Error)?.message || err)\n service = createCacheService({ strategy: 'memory' })\n }\n ;(globalThis as any)[CACHE_GLOBAL_KEY] = service\n\n // Register shutdown hook once to close persistent handles (sqlite/redis) on process exit\n if (!(globalThis as any)[CACHE_SHUTDOWN_KEY]) {\n const shutdown = () => { service?.close?.().catch(() => {}) }\n process.once('SIGTERM', shutdown)\n process.once('SIGINT', shutdown)\n ;(globalThis as any)[CACHE_SHUTDOWN_KEY] = true\n }\n } catch (err) {\n console.warn('[cache] Failed to create cache service:', (err as Error)?.message || err)\n }\n }\n return service\n}\n\nexport function getCachedRateLimiterService(): RateLimiterService | null {\n let service = (globalThis as any)[RL_GLOBAL_KEY] as RateLimiterService | null ?? null\n if (!service) {\n try {\n const rateLimitConfig = readRateLimitConfig()\n service = new RateLimiterService(rateLimitConfig)\n // Fire-and-forget async init (only needed for Redis strategy;\n // memory strategy works synchronously, and Redis has an in-memory\n // insurance limiter so the first few requests are still protected)\n service.initialize().catch((err) => {\n console.warn('[ratelimit] Async initialization failed:', (err as Error)?.message || err)\n })\n ;(globalThis as any)[RL_GLOBAL_KEY] = service\n\n // Register shutdown hook once to disconnect Redis on process exit\n if (!(globalThis as any)[RL_SHUTDOWN_KEY]) {\n const shutdown = () => { service?.destroy().catch(() => {}) }\n process.once('SIGTERM', shutdown)\n process.once('SIGINT', shutdown)\n ;(globalThis as any)[RL_SHUTDOWN_KEY] = true\n }\n } catch (err) {\n console.warn('[ratelimit] Failed to create rate limiter service:', (err as Error)?.message || err)\n }\n }\n return service\n}\n\nexport async function bootstrap(container: AwilixContainer) {\n // Register the cache service. Prefer the process-wide singleton so caches\n // survive across request containers; fall back to a per-request instance\n // when the singleton is disabled or fails to build.\n let cache: any = getCachedCacheService()\n if (!cache) {\n try {\n cache = createCacheService()\n } catch (err: any) {\n console.warn('Cache service initialization failed; falling back to memory strategy:', err?.message || err)\n cache = createCacheService({ strategy: 'memory' })\n }\n }\n container.register({ cache: asValue(cache) })\n\n // Create and register the DI-aware event bus\n let eventBus: any\n try {\n // Support both QUEUE_STRATEGY and legacy EVENTS_STRATEGY env vars\n const strategyEnv = process.env.QUEUE_STRATEGY || process.env.EVENTS_STRATEGY\n const queueStrategy = strategyEnv === 'async' || strategyEnv === 'redis' ? 'async' : 'local'\n eventBus = createEventBus({ resolve: container.resolve.bind(container) as any, queueStrategy })\n } catch (err: any) {\n // Fall back to local strategy to avoid breaking the app on misconfiguration\n console.warn('Event bus initialization failed; falling back to local strategy:', err?.message || err)\n try {\n eventBus = createEventBus({ resolve: container.resolve.bind(container) as any, queueStrategy: 'local' })\n } catch {\n // In extreme cases, provide a no-op bus to avoid crashes\n eventBus = {\n emit: async () => {},\n on: () => {},\n registerModuleSubscribers: () => {},\n clearQueue: async () => ({ removed: 0 }),\n }\n }\n }\n container.register({ eventBus: asValue(eventBus) })\n // Wire the global event bus so createModuleEvents().emit works outside DI context\n setGlobalEventBus(eventBus)\n // Auto-register discovered module subscribers\n try {\n let loadedModules: any[] = []\n try {\n const { getModules } = await import('@open-mercato/shared/lib/i18n/server')\n loadedModules = getModules()\n } catch {}\n const subs = loadedModules.flatMap((m) => m.subscribers || [])\n if (subs.length) (container.resolve as any)('eventBus').registerModuleSubscribers(subs)\n\n // Extract sync subscribers and register in the sync-subscriber-store\n const syncSubs = subs.filter((s: any) => s.sync === true)\n if (syncSubs.length) {\n try {\n const { registerSyncSubscribers } = await import('@open-mercato/shared/lib/crud/sync-subscriber-store')\n registerSyncSubscribers(\n syncSubs.map((s: any) => ({\n metadata: { event: s.event, sync: true as const, priority: s.priority, id: s.id },\n handler: s.handler,\n })),\n )\n } catch {\n // sync-subscriber-store may not be available\n }\n }\n } catch (err) {\n console.error(\"Failed to register module subscribers:\", err);\n }\n\n // KMS + tenant encryption\n const kmsService = createKmsService()\n container.register({ kmsService: asValue(kmsService) })\n try {\n const em = container.resolve('em') as EntityManager\n const cacheService = (() => {\n try { return container.resolve('cache') as any } catch { return null }\n })()\n const tenantEncryptionService = new TenantDataEncryptionService(em, { cache: cacheService, kms: kmsService })\n container.register({ tenantEncryptionService: asValue(tenantEncryptionService) })\n if (isTenantDataEncryptionEnabled() && kmsService.isHealthy()) {\n try {\n registerTenantEncryptionSubscriber(em, tenantEncryptionService)\n } catch (err) {\n console.warn('[encryption] Failed to register MikroORM encryption subscriber:', (err as Error)?.message || err)\n }\n } else if (isTenantDataEncryptionEnabled() && !kmsService.isHealthy()) {\n console.warn('[encryption] Vault/KMS unhealthy - tenant data encryption is disabled until recovery')\n }\n } catch (err) {\n console.warn('[encryption] Failed to initialize tenant encryption service:', (err as Error)?.message || err)\n }\n\n // Register rate limiter service (singleton via globalThis \u2014 reused across request containers)\n // getCachedRateLimiterService() never throws; returns null on failure\n const rateLimiterService = getCachedRateLimiterService()\n if (rateLimiterService) {\n container.register({ rateLimiterService: asValue(rateLimiterService) })\n }\n\n // Register search module\n try {\n // Get configs from global registry (registered during app bootstrap)\n const searchModuleConfigs = getSearchModuleConfigs()\n registerSearchModule(container as any, { moduleConfigs: searchModuleConfigs })\n\n // Register searchModuleConfigs in container so status API can access vector-enabled entities\n container.register({\n searchModuleConfigs: asValue(searchModuleConfigs),\n })\n\n // Register search delete event subscriber\n // Note: search.index_record is now handled by auto-discovered fulltext_upsert.ts subscriber\n try {\n const searchIndexer = container.resolve('searchIndexer') as any\n if (searchIndexer && eventBus) {\n eventBus.registerModuleSubscribers([\n {\n event: searchDeleteMetadata.event,\n persistent: searchDeleteMetadata.persistent,\n handler: createSearchDeleteSubscriber(searchIndexer),\n },\n ])\n }\n } catch {\n // searchIndexer may not be available\n }\n } catch (err) {\n console.warn('[search] Failed to register search module:', (err as Error)?.message || err)\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.6-develop.5509.1.006f4d4f24",
3
+ "version": "0.6.6-develop.5523.1.e223ca1915",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -245,16 +245,16 @@
245
245
  "zod": "^4.4.3"
246
246
  },
247
247
  "peerDependencies": {
248
- "@open-mercato/ai-assistant": "0.6.6-develop.5509.1.006f4d4f24",
249
- "@open-mercato/shared": "0.6.6-develop.5509.1.006f4d4f24",
250
- "@open-mercato/ui": "0.6.6-develop.5509.1.006f4d4f24",
248
+ "@open-mercato/ai-assistant": "0.6.6-develop.5523.1.e223ca1915",
249
+ "@open-mercato/shared": "0.6.6-develop.5523.1.e223ca1915",
250
+ "@open-mercato/ui": "0.6.6-develop.5523.1.e223ca1915",
251
251
  "react": "^19.0.0",
252
252
  "react-dom": "^19.0.0"
253
253
  },
254
254
  "devDependencies": {
255
- "@open-mercato/ai-assistant": "0.6.6-develop.5509.1.006f4d4f24",
256
- "@open-mercato/shared": "0.6.6-develop.5509.1.006f4d4f24",
257
- "@open-mercato/ui": "0.6.6-develop.5509.1.006f4d4f24",
255
+ "@open-mercato/ai-assistant": "0.6.6-develop.5523.1.e223ca1915",
256
+ "@open-mercato/shared": "0.6.6-develop.5523.1.e223ca1915",
257
+ "@open-mercato/ui": "0.6.6-develop.5523.1.e223ca1915",
258
258
  "@testing-library/dom": "^10.4.1",
259
259
  "@testing-library/jest-dom": "^6.9.1",
260
260
  "@testing-library/react": "^16.3.1",
package/src/bootstrap.ts CHANGED
@@ -3,6 +3,7 @@ import { asValue } from 'awilix'
3
3
  import { createEventBus } from '@open-mercato/events/index'
4
4
  import { setGlobalEventBus } from '@open-mercato/shared/modules/events'
5
5
  import { createCacheService } from '@open-mercato/cache'
6
+ import type { CacheStrategy } from '@open-mercato/cache'
6
7
  import { createKmsService } from '@open-mercato/shared/lib/encryption/kms'
7
8
  import { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
8
9
  import { registerTenantEncryptionSubscriber } from '@open-mercato/shared/lib/encryption/subscriber'
@@ -21,6 +22,59 @@ import type { EntityManager } from '@mikro-orm/postgresql'
21
22
  const RL_GLOBAL_KEY = '__openMercatoRateLimiterService__'
22
23
  const RL_SHUTDOWN_KEY = '__openMercatoRateLimiterShutdown__'
23
24
 
25
+ const CACHE_GLOBAL_KEY = '__openMercatoCacheService__'
26
+ const CACHE_SHUTDOWN_KEY = '__openMercatoCacheShutdown__'
27
+
28
+ // Escape hatch: set OM_CACHE_SINGLETON=off to fall back to the legacy
29
+ // per-request cache instance (e.g. to isolate a regression). Default ON.
30
+ function isCacheSingletonEnabled(): boolean {
31
+ const raw = process.env.OM_CACHE_SINGLETON
32
+ if (raw === undefined) return true
33
+ const normalized = raw.trim().toLowerCase()
34
+ if (!normalized.length) return true
35
+ return !(normalized === '0' || normalized === 'off' || normalized === 'false' || normalized === 'no')
36
+ }
37
+
38
+ /**
39
+ * Process-wide cache service singleton, mirroring getCachedRateLimiterService.
40
+ *
41
+ * bootstrap() previously built a fresh cache per request container, so under
42
+ * the default memory strategy every cross-request cache was a no-op (each
43
+ * instance is process-local) and under sqlite each request leaked a native
44
+ * handle. Reusing one instance is safe by construction: tenant scope resolves
45
+ * per-call via AsyncLocalStorage (packages/cache/src/tenantContext.ts), so the
46
+ * service holds no request-bound state.
47
+ *
48
+ * Returns null when disabled via the escape hatch or when creation fails, so
49
+ * the caller falls back to a per-request instance.
50
+ */
51
+ export function getCachedCacheService(): CacheStrategy | null {
52
+ if (!isCacheSingletonEnabled()) return null
53
+ let service = (globalThis as any)[CACHE_GLOBAL_KEY] as CacheStrategy | null ?? null
54
+ if (!service) {
55
+ try {
56
+ try {
57
+ service = createCacheService()
58
+ } catch (err) {
59
+ console.warn('Cache service initialization failed; falling back to memory strategy:', (err as Error)?.message || err)
60
+ service = createCacheService({ strategy: 'memory' })
61
+ }
62
+ ;(globalThis as any)[CACHE_GLOBAL_KEY] = service
63
+
64
+ // Register shutdown hook once to close persistent handles (sqlite/redis) on process exit
65
+ if (!(globalThis as any)[CACHE_SHUTDOWN_KEY]) {
66
+ const shutdown = () => { service?.close?.().catch(() => {}) }
67
+ process.once('SIGTERM', shutdown)
68
+ process.once('SIGINT', shutdown)
69
+ ;(globalThis as any)[CACHE_SHUTDOWN_KEY] = true
70
+ }
71
+ } catch (err) {
72
+ console.warn('[cache] Failed to create cache service:', (err as Error)?.message || err)
73
+ }
74
+ }
75
+ return service
76
+ }
77
+
24
78
  export function getCachedRateLimiterService(): RateLimiterService | null {
25
79
  let service = (globalThis as any)[RL_GLOBAL_KEY] as RateLimiterService | null ?? null
26
80
  if (!service) {
@@ -50,13 +104,17 @@ export function getCachedRateLimiterService(): RateLimiterService | null {
50
104
  }
51
105
 
52
106
  export async function bootstrap(container: AwilixContainer) {
53
- // Create and register the cache service
54
- let cache: any
55
- try {
56
- cache = createCacheService()
57
- } catch (err: any) {
58
- console.warn('Cache service initialization failed; falling back to memory strategy:', err?.message || err)
59
- cache = createCacheService({ strategy: 'memory' })
107
+ // Register the cache service. Prefer the process-wide singleton so caches
108
+ // survive across request containers; fall back to a per-request instance
109
+ // when the singleton is disabled or fails to build.
110
+ let cache: any = getCachedCacheService()
111
+ if (!cache) {
112
+ try {
113
+ cache = createCacheService()
114
+ } catch (err: any) {
115
+ console.warn('Cache service initialization failed; falling back to memory strategy:', err?.message || err)
116
+ cache = createCacheService({ strategy: 'memory' })
117
+ }
60
118
  }
61
119
  container.register({ cache: asValue(cache) })
62
120