@open-mercato/shared 0.6.3-develop.3876.1.d40fe4ec2d → 0.6.3-develop.3894.1.352abf4240

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.
@@ -16,6 +16,82 @@ export type DiRegistrar = (container: AppContainer) => void
16
16
  // Use globalThis to survive tsx/esbuild module duplication issue where the same
17
17
  // file can be loaded as multiple module instances when mixing dynamic and static imports
18
18
  const GLOBAL_KEY = '__openMercatoDiRegistrars__'
19
+ // Phase 5 — process-scoped bootstrap cache. The cache/event-bus/encryption
20
+ // services bootstrap() creates are inherently process-scoped (they hold
21
+ // state across requests). Caching them on globalThis after the first
22
+ // successful bootstrap call lets every subsequent request skip the
23
+ // `await bootstrap(container)` body and just re-register the cached
24
+ // instances. Same globalThis pattern as registerDiRegistrars so HMR
25
+ // keeps working.
26
+ const BOOTSTRAP_CACHE_KEY = '__openMercatoBootstrapCache__'
27
+ const ENCRYPTION_ENABLED_KEY = '__openMercatoEncryptionEnabledCache__'
28
+
29
+ const BOOTSTRAP_CACHE_KEYS = [
30
+ 'cache',
31
+ 'eventBus',
32
+ 'kmsService',
33
+ 'tenantEncryptionService',
34
+ 'rateLimiterService',
35
+ 'searchModuleConfigs',
36
+ 'searchIndexer',
37
+ ] as const
38
+
39
+ type BootstrapCacheEntry = Partial<Record<(typeof BOOTSTRAP_CACHE_KEYS)[number], unknown>>
40
+
41
+ // Phase 5 is opt-in. Some bootstrap services close over per-request state
42
+ // (e.g. tenantEncryptionService captures the first request's `em.fork`, the
43
+ // event-bus's resolver closes over the first container) so naively replaying
44
+ // them on later requests yields stale references — observed as a 500 from
45
+ // CRUD list endpoints in `next start`. Default OFF preserves develop's
46
+ // per-request bootstrap. Set `OM_BOOTSTRAP_CACHE=1` to opt in once each
47
+ // cached service is verified safe for cross-request reuse.
48
+ function isBootstrapCacheEnabled(): boolean {
49
+ const raw = process.env.OM_BOOTSTRAP_CACHE
50
+ if (raw === undefined) return false
51
+ const normalized = raw.trim().toLowerCase()
52
+ if (!normalized.length) return false
53
+ if (normalized === '0' || normalized === 'off' || normalized === 'false' || normalized === 'no') return false
54
+ return true
55
+ }
56
+
57
+ function getBootstrapCache(): BootstrapCacheEntry | null {
58
+ if (!isBootstrapCacheEnabled()) return null
59
+ const existing = (globalThis as any)[BOOTSTRAP_CACHE_KEY]
60
+ return existing && typeof existing === 'object' ? (existing as BootstrapCacheEntry) : null
61
+ }
62
+
63
+ function setBootstrapCache(entry: BootstrapCacheEntry): void {
64
+ if (!isBootstrapCacheEnabled()) return
65
+ ;(globalThis as any)[BOOTSTRAP_CACHE_KEY] = entry
66
+ }
67
+
68
+ function harvestBootstrapCache(container: AwilixContainer): BootstrapCacheEntry {
69
+ const entry: BootstrapCacheEntry = {}
70
+ for (const key of BOOTSTRAP_CACHE_KEYS) {
71
+ try {
72
+ const value: unknown = container.resolve(key as never)
73
+ if (value !== undefined && value !== null) entry[key] = value
74
+ } catch {
75
+ // not registered — skip
76
+ }
77
+ }
78
+ return entry
79
+ }
80
+
81
+ type EncryptionEnabledProbe = { isEnabled?: () => boolean } | null | undefined
82
+
83
+ function getCachedEncryptionEnabled(service: EncryptionEnabledProbe): boolean | null {
84
+ if (!service || typeof service.isEnabled !== 'function') return false
85
+ const cached = (globalThis as Record<string, unknown>)[ENCRYPTION_ENABLED_KEY]
86
+ if (typeof cached === 'boolean') return cached
87
+ try {
88
+ const result = !!service.isEnabled()
89
+ ;(globalThis as Record<string, unknown>)[ENCRYPTION_ENABLED_KEY] = result
90
+ return result
91
+ } catch {
92
+ return null
93
+ }
94
+ }
19
95
 
20
96
  function getGlobalRegistrars(): DiRegistrar[] | null {
21
97
  return (globalThis as any)[GLOBAL_KEY] ?? null
@@ -31,6 +107,9 @@ export function registerDiRegistrars(registrars: DiRegistrar[]) {
31
107
  console.debug('[Bootstrap] DI registrars re-registered (this may occur during HMR)')
32
108
  }
33
109
  setGlobalRegistrars(registrars)
110
+ // Force re-bootstrap on HMR — module subscribers may have changed.
111
+ ;(globalThis as any)[BOOTSTRAP_CACHE_KEY] = null
112
+ ;(globalThis as any)[ENCRYPTION_ENABLED_KEY] = undefined
34
113
  }
35
114
 
36
115
  export function getDiRegistrars(): DiRegistrar[] {
@@ -41,6 +120,12 @@ export function getDiRegistrars(): DiRegistrar[] {
41
120
  return registrars
42
121
  }
43
122
 
123
+ /** Test-only helper to drop the process-scoped bootstrap cache. */
124
+ export function resetBootstrapCache(): void {
125
+ (globalThis as any)[BOOTSTRAP_CACHE_KEY] = null
126
+ ;(globalThis as any)[ENCRYPTION_ENABLED_KEY] = undefined
127
+ }
128
+
44
129
  function isAwilixResolver(value: unknown): value is Resolver<unknown> {
45
130
  return Boolean(value && typeof value === 'object' && typeof (value as { resolve?: unknown }).resolve === 'function')
46
131
  }
@@ -76,16 +161,30 @@ export async function createRequestContainer(): Promise<AppContainer> {
76
161
  try { reg?.(container) } catch {}
77
162
  }
78
163
  // Core bootstrap (cache, event bus, encryption subscriber/KMS, module subscribers)
79
- try {
80
- const { bootstrap } = await import('@open-mercato/core/bootstrap') as any
81
- if (bootstrap && typeof bootstrap === 'function') {
82
- // Avoid double bootstrap if caller already wired it
83
- const alreadyBootstrapped = !!container.registrations?.eventBus
84
- if (!alreadyBootstrapped) {
85
- await bootstrap(container)
164
+ // Phase 5 — process-scoped once-guard. The first request runs the full
165
+ // bootstrap() body; later requests re-register the cached services
166
+ // directly on this request's container without re-importing or
167
+ // re-initializing anything. HMR clears the cache (see
168
+ // registerDiRegistrars). Skippable if a caller already wired eventBus.
169
+ const alreadyBootstrappedOnThisContainer = !!container.registrations?.eventBus
170
+ if (!alreadyBootstrappedOnThisContainer) {
171
+ const cached = getBootstrapCache()
172
+ if (cached) {
173
+ const replay: Record<string, any> = {}
174
+ for (const [key, value] of Object.entries(cached)) {
175
+ if (value !== undefined && value !== null) replay[key] = asValue(value)
86
176
  }
177
+ if (Object.keys(replay).length > 0) container.register(replay)
178
+ } else {
179
+ try {
180
+ const { bootstrap } = await import('@open-mercato/core/bootstrap') as any
181
+ if (bootstrap && typeof bootstrap === 'function') {
182
+ await bootstrap(container)
183
+ setBootstrapCache(harvestBootstrapCache(container))
184
+ }
185
+ } catch { /* optional */ }
87
186
  }
88
- } catch { /* optional */ }
187
+ }
89
188
  // App-level DI override (last chance)
90
189
  // This import path resolves only in the app context, not in packages
91
190
  try {
@@ -103,12 +202,15 @@ export async function createRequestContainer(): Promise<AppContainer> {
103
202
  unregister: (key) => container.register({ [key]: asValue(undefined) }),
104
203
  })
105
204
  // Ensure tenant encryption subscriber is always registered on the fresh request-scoped EM
205
+ // Phase 5 — cache `tenantEncryptionService.isEnabled()` for the process
206
+ // lifetime. The result depends only on config that does not change at
207
+ // runtime, so reading it once skips a config lookup per request.
106
208
  try {
107
209
  const emForEnc = container.resolve('em') as any
108
210
  const tenantEncryptionService = container.hasRegistration('tenantEncryptionService')
109
211
  ? (container.resolve('tenantEncryptionService') as any)
110
212
  : null
111
- if (emForEnc && tenantEncryptionService?.isEnabled?.()) {
213
+ if (emForEnc && tenantEncryptionService && getCachedEncryptionEnabled(tenantEncryptionService) === true) {
112
214
  const { registerTenantEncryptionSubscriber } = await import('@open-mercato/shared/lib/encryption/subscriber')
113
215
  registerTenantEncryptionSubscriber(emForEnc, tenantEncryptionService)
114
216
  }