@open-mercato/shared 0.6.7-develop.6828.1.ab1620a63e → 0.6.7-develop.6842.1.8cd8a8883c

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 (40) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +1 -1
  3. package/dist/lib/bootstrap/dynamicLoader.js +54 -14
  4. package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
  5. package/dist/lib/di/container.js +12 -1
  6. package/dist/lib/di/container.js.map +2 -2
  7. package/dist/lib/i18n/config.js +1 -1
  8. package/dist/lib/i18n/config.js.map +2 -2
  9. package/dist/lib/i18n/server.js +4 -4
  10. package/dist/lib/i18n/server.js.map +3 -3
  11. package/dist/lib/logger/extension.js +29 -0
  12. package/dist/lib/logger/extension.js.map +7 -0
  13. package/dist/lib/logger/index.js +47 -3
  14. package/dist/lib/logger/index.js.map +3 -3
  15. package/dist/lib/modules/registry.js +5 -1
  16. package/dist/lib/modules/registry.js.map +2 -2
  17. package/dist/lib/redis/connection.js +7 -2
  18. package/dist/lib/redis/connection.js.map +2 -2
  19. package/dist/lib/telemetry/runtime.js +35 -0
  20. package/dist/lib/telemetry/runtime.js.map +7 -0
  21. package/dist/lib/version.js +1 -1
  22. package/dist/lib/version.js.map +1 -1
  23. package/package.json +2 -2
  24. package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +98 -0
  25. package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +211 -0
  26. package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +192 -0
  27. package/src/lib/bootstrap/dynamicLoader.ts +111 -32
  28. package/src/lib/di/__tests__/container-app-di-absent.test.ts +17 -1
  29. package/src/lib/di/container.ts +19 -1
  30. package/src/lib/i18n/__tests__/config.test.ts +8 -0
  31. package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +29 -0
  32. package/src/lib/i18n/config.ts +2 -2
  33. package/src/lib/i18n/server.ts +6 -2
  34. package/src/lib/logger/__tests__/logger.test.ts +77 -0
  35. package/src/lib/logger/extension.ts +59 -0
  36. package/src/lib/logger/index.ts +59 -1
  37. package/src/lib/modules/registry.ts +10 -0
  38. package/src/lib/redis/__tests__/connection.test.ts +16 -0
  39. package/src/lib/redis/connection.ts +10 -7
  40. package/src/lib/telemetry/runtime.ts +72 -0
@@ -0,0 +1,59 @@
1
+ import type { LogLevel } from './level'
2
+
3
+ export type LoggerExtensionRecord = {
4
+ level: LogLevel
5
+ namespace: string
6
+ message: string
7
+ fields: Record<string, unknown>
8
+ time: number
9
+ }
10
+
11
+ export type LoggerExtension = {
12
+ /**
13
+ * Add process-local context (for example trace/span ids) to both the local
14
+ * line and the optional remote sink.
15
+ */
16
+ enrich?(): Record<string, unknown> | undefined
17
+ /**
18
+ * Observe a record after the local transport writes it. Implementations must
19
+ * never throw into application code.
20
+ */
21
+ emit?(record: LoggerExtensionRecord): void
22
+ }
23
+
24
+ const GLOBAL_KEY = Symbol.for('@open-mercato/shared.loggerExtension')
25
+
26
+ type LoggerExtensionStore = {
27
+ active?: LoggerExtension
28
+ }
29
+
30
+ function store(): LoggerExtensionStore {
31
+ const globalStore = globalThis as unknown as Record<symbol, LoggerExtensionStore | undefined>
32
+ let current = globalStore[GLOBAL_KEY]
33
+ if (!current) {
34
+ current = {}
35
+ globalStore[GLOBAL_KEY] = current
36
+ }
37
+ return current
38
+ }
39
+
40
+ /**
41
+ * Register the single process-wide logger extension. The returned disposer only
42
+ * clears the extension when it is still the active registration.
43
+ */
44
+ export function registerLoggerExtension(extension: LoggerExtension): () => void {
45
+ store().active = extension
46
+ return () => {
47
+ const current = store()
48
+ if (current.active === extension) current.active = undefined
49
+ }
50
+ }
51
+
52
+ export function getLoggerExtension(): LoggerExtension | undefined {
53
+ return store().active
54
+ }
55
+
56
+ /** Test-only: clear the process-wide extension. */
57
+ export function resetLoggerExtension(): void {
58
+ store().active = undefined
59
+ }
@@ -1,9 +1,17 @@
1
1
  import { selectTransport } from './transport'
2
+ import { getLoggerExtension } from './extension'
3
+ import { isLevelEnabled, type LogLevel } from './level'
2
4
 
3
5
  export type { LogLevel } from './level'
4
6
  export { getLogLevel, isLevelEnabled, resetLogLevelCache, OM_LOG_LEVEL_ENV } from './level'
5
7
  export { resetServerLoggerCache, OM_LOG_DESTINATION_ENV } from './transport.server'
6
8
  export { resetLogPrettyCache, OM_LOG_PRETTY_ENV } from './transport.pretty'
9
+ export {
10
+ getLoggerExtension,
11
+ registerLoggerExtension,
12
+ resetLoggerExtension,
13
+ } from './extension'
14
+ export type { LoggerExtension, LoggerExtensionRecord } from './extension'
7
15
 
8
16
  export type LogBindings = Record<string, unknown>
9
17
 
@@ -18,11 +26,61 @@ export interface Logger {
18
26
 
19
27
  const loggerRegistry = new Map<string, Logger>()
20
28
 
29
+ function createExtendedLogger(
30
+ namespace: string,
31
+ transport: Logger,
32
+ bindings: LogBindings = {},
33
+ ): Logger {
34
+ const emit = (
35
+ level: LogLevel,
36
+ msg: string,
37
+ fields?: LogBindings,
38
+ ): void => {
39
+ if (!isLevelEnabled(level)) return
40
+
41
+ const extension = getLoggerExtension()
42
+ let context: LogBindings = {}
43
+ try {
44
+ context = extension?.enrich?.() ?? {}
45
+ } catch {
46
+ // Observability must never alter application behavior.
47
+ }
48
+
49
+ const mergedFields = { ...fields, ...context }
50
+ transport[level](msg, mergedFields)
51
+
52
+ try {
53
+ extension?.emit?.({
54
+ level,
55
+ namespace,
56
+ message: msg,
57
+ fields: { ...bindings, ...mergedFields },
58
+ time: Date.now(),
59
+ })
60
+ } catch {
61
+ // Remote logging is best-effort; the local line has already been written.
62
+ }
63
+ }
64
+
65
+ return {
66
+ debug: (msg, fields) => emit('debug', msg, fields),
67
+ info: (msg, fields) => emit('info', msg, fields),
68
+ warn: (msg, fields) => emit('warn', msg, fields),
69
+ error: (msg, fields) => emit('error', msg, fields),
70
+ child: (childBindings) =>
71
+ createExtendedLogger(
72
+ namespace,
73
+ transport.child(childBindings),
74
+ { ...bindings, ...childBindings },
75
+ ),
76
+ }
77
+ }
78
+
21
79
  /** Create (or reuse) a namespaced logger. `namespace` is attached as `name`. */
22
80
  export function createLogger(namespace: string): Logger {
23
81
  const existing = loggerRegistry.get(namespace)
24
82
  if (existing) return existing
25
- const created = selectTransport(namespace)
83
+ const created = createExtendedLogger(namespace, selectTransport(namespace))
26
84
  loggerRegistry.set(namespace, created)
27
85
  return created
28
86
  }
@@ -69,3 +69,13 @@ export function getModules(): Module[] {
69
69
  }
70
70
  return modules
71
71
  }
72
+
73
+ /**
74
+ * Non-throwing counterpart of `getModules()` for call sites that have a
75
+ * meaningful degraded behavior when bootstrap has not run — route unit tests
76
+ * exercise a single handler without `registerModules()`, and a hard throw there
77
+ * turns an unrelated assertion into a bootstrap error.
78
+ */
79
+ export function tryGetModules(): Module[] | null {
80
+ return getGlobalModules()
81
+ }
@@ -5,9 +5,11 @@ describe('parseRedisUrl', () => {
5
5
  expect(parseRedisUrl('rediss://:secret@example.cache.amazonaws.com:6379/2')).toEqual({
6
6
  host: 'example.cache.amazonaws.com',
7
7
  port: 6379,
8
+ username: undefined,
8
9
  password: 'secret',
9
10
  db: 2,
10
11
  tls: {},
12
+ family: undefined,
11
13
  })
12
14
  })
13
15
 
@@ -15,9 +17,23 @@ describe('parseRedisUrl', () => {
15
17
  expect(parseRedisUrl('redis://:secret@localhost:6379/0')).toEqual({
16
18
  host: 'localhost',
17
19
  port: 6379,
20
+ username: undefined,
18
21
  password: 'secret',
19
22
  db: 0,
20
23
  tls: undefined,
24
+ family: undefined,
25
+ })
26
+ })
27
+
28
+ it('preserves ACL credentials and supported address-family options', () => {
29
+ expect(parseRedisUrl('rediss://app%2Duser:p%40ss@example.com:6380/4?family=6')).toEqual({
30
+ host: 'example.com',
31
+ port: 6380,
32
+ username: 'app-user',
33
+ password: 'p@ss',
34
+ db: 4,
35
+ tls: {},
36
+ family: 6,
21
37
  })
22
38
  })
23
39
  })
@@ -21,9 +21,11 @@ const logger = createLogger('shared').child({ component: 'redis' })
21
21
  export type ParsedRedisConnection = {
22
22
  host: string
23
23
  port: number
24
+ username?: string
24
25
  password?: string
25
26
  db?: number
26
27
  tls?: Record<string, unknown>
28
+ family?: number
27
29
  }
28
30
 
29
31
  /**
@@ -59,12 +61,8 @@ export function getRedisUrlOrThrow(prefix?: string): string {
59
61
  }
60
62
 
61
63
  /**
62
- * Parse a redis:// URL into a {host, port, password, db} object
63
- * suitable for BullMQ / ioredis structured connection options.
64
- *
65
- * @deprecated Prefer passing the full URL via `{ url: getRedisUrl(...) }` to
66
- * BullMQ/ioredis — this preserves rediss://, username, database, and query
67
- * params that structured parsing may lose. Kept for backward compatibility.
64
+ * Parse a redis:// URL into structured connection options suitable for
65
+ * BullMQ and ioredis.
68
66
  */
69
67
  export function parseRedisUrl(url: string): ParsedRedisConnection {
70
68
  try {
@@ -72,12 +70,17 @@ export function parseRedisUrl(url: string): ParsedRedisConnection {
72
70
  const dbStr = parsed.pathname ? parsed.pathname.slice(1) : ''
73
71
  const dbParsed = dbStr !== '' ? parseInt(dbStr, 10) : NaN
74
72
  const db = Number.isNaN(dbParsed) ? undefined : dbParsed
73
+ const familyValue = parsed.searchParams.get('family')
74
+ const familyParsed = familyValue !== null ? parseInt(familyValue, 10) : NaN
75
+ const family = [0, 4, 6].includes(familyParsed) ? familyParsed : undefined
75
76
  return {
76
77
  host: parsed.hostname || 'localhost',
77
78
  port: parseInt(parsed.port, 10) || 6379,
78
- password: parsed.password || undefined,
79
+ username: parsed.username ? decodeURIComponent(parsed.username) : undefined,
80
+ password: parsed.password ? decodeURIComponent(parsed.password) : undefined,
79
81
  db,
80
82
  tls: parsed.protocol === 'rediss:' ? {} : undefined,
83
+ family,
81
84
  }
82
85
  } catch {
83
86
  const safeUrl = url.replace(/\/\/[^:]*:[^@]*@/, '//<redacted>@')
@@ -0,0 +1,72 @@
1
+ export type TelemetryTraceCarrier = Record<string, string>
2
+
3
+ export type TelemetryRuntime = {
4
+ /**
5
+ * True only when the active SDK may safely use the process-global W3C
6
+ * propagator for cross-boundary extraction.
7
+ */
8
+ canUseGlobalTracePropagation(): boolean
9
+ captureTraceContext(): TelemetryTraceCarrier
10
+ continueTrace<T>(
11
+ carrier: TelemetryTraceCarrier | undefined,
12
+ name: string,
13
+ fn: () => T,
14
+ options?: { kind?: 'internal' | 'server' | 'client' | 'producer' | 'consumer' },
15
+ ): T
16
+ recordHttpDuration(method: string, route: string, status: number, startedAt: number): void
17
+ reportError(
18
+ error: unknown,
19
+ context?: {
20
+ module?: string
21
+ attributes?: Record<string, string | number | boolean | undefined>
22
+ },
23
+ ): void
24
+ shutdown(): Promise<void>
25
+ }
26
+
27
+ const GLOBAL_KEY = Symbol.for('@open-mercato/shared.telemetryRuntime')
28
+ const ENABLED_BACKENDS = new Set(['console', 'signoz', 'newrelic', 'otlp'])
29
+
30
+ type TelemetryRuntimeStore = {
31
+ active?: TelemetryRuntime
32
+ }
33
+
34
+ function store(): TelemetryRuntimeStore {
35
+ const globalStore = globalThis as unknown as Record<symbol, TelemetryRuntimeStore | undefined>
36
+ let current = globalStore[GLOBAL_KEY]
37
+ if (!current) {
38
+ current = {}
39
+ globalStore[GLOBAL_KEY] = current
40
+ }
41
+ return current
42
+ }
43
+
44
+ /**
45
+ * This check is intentionally owned by shared code so hosts can decide whether
46
+ * to dynamically import the telemetry package without evaluating that package.
47
+ */
48
+ export function isTelemetryBackendEnabled(raw?: string): boolean {
49
+ const value = raw ?? (
50
+ typeof process === 'undefined'
51
+ ? undefined
52
+ : process.env.TELEMETRY_BACKEND
53
+ )
54
+ return ENABLED_BACKENDS.has((value ?? '').trim().toLowerCase())
55
+ }
56
+
57
+ export function registerTelemetryRuntime(runtime: TelemetryRuntime): () => void {
58
+ store().active = runtime
59
+ return () => {
60
+ const current = store()
61
+ if (current.active === runtime) current.active = undefined
62
+ }
63
+ }
64
+
65
+ export function getTelemetryRuntime(): TelemetryRuntime | undefined {
66
+ return store().active
67
+ }
68
+
69
+ /** Test-only: clear the process-wide telemetry bridge. */
70
+ export function resetTelemetryRuntime(): void {
71
+ store().active = undefined
72
+ }