From 75212c2dab20b7c4e21c77e041bdaa2e1f2ce91b Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Tue, 2 Sep 2025 17:06:28 +0200 Subject: [PATCH] Improve module wrapper singleton creation --- packages/core/src/logs/DdLogs.ts | 4 +- packages/core/src/rum/DdRum.ts | 5 +- packages/core/src/trace/DdTrace.ts | 10 ++- .../utils/__tests__/singletonUtils.test.ts | 75 +++++++++++++++++++ packages/core/src/utils/singletonUtils.ts | 12 +++ 5 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 packages/core/src/utils/__tests__/singletonUtils.test.ts create mode 100644 packages/core/src/utils/singletonUtils.ts diff --git a/packages/core/src/logs/DdLogs.ts b/packages/core/src/logs/DdLogs.ts index 9ac35fa73..e00e4fc0d 100644 --- a/packages/core/src/logs/DdLogs.ts +++ b/packages/core/src/logs/DdLogs.ts @@ -10,6 +10,7 @@ import type { DdNativeLogsType } from '../nativeModulesTypes'; import { DdAttributes } from '../rum/DdAttributes'; import type { ErrorSource } from '../rum/types'; import { validateContext } from '../utils/argsUtils'; +import { getGlobalInstance } from '../utils/singletonUtils'; import { generateEventMapper } from './eventMapper'; import type { @@ -21,6 +22,7 @@ import type { RawLogWithError } from './types'; +const LOGS_MODULE = 'com.datadog.reactnative.logs'; const SDK_NOT_INITIALIZED_MESSAGE = 'DD_INTERNAL_LOG_SENT_BEFORE_SDK_INIT'; const generateEmptyPromise = () => new Promise(resolve => resolve()); @@ -240,4 +242,4 @@ class DdLogsWrapper implements DdLogsType { } } -export const DdLogs = new DdLogsWrapper(); +export const DdLogs = getGlobalInstance(LOGS_MODULE, () => new DdLogsWrapper()); diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 8943ed5b0..45cafca5b 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -13,6 +13,7 @@ import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; import { validateContext } from '../utils/argsUtils'; import { getErrorContext } from '../utils/errorUtils'; +import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; import type { TimeProvider } from '../utils/time-provider/TimeProvider'; @@ -43,6 +44,8 @@ import type { PropagatorType } from './types'; +const RUM_MODULE = 'com.datadog.reactnative.rum'; + const generateEmptyPromise = () => new Promise(resolve => resolve()); class DdRumWrapper implements DdRumType { @@ -501,4 +504,4 @@ const isOldStopActionAPI = ( return typeof args[0] === 'object' || typeof args[0] === 'undefined'; }; -export const DdRum = new DdRumWrapper(); +export const DdRum = getGlobalInstance(RUM_MODULE, () => new DdRumWrapper()); diff --git a/packages/core/src/trace/DdTrace.ts b/packages/core/src/trace/DdTrace.ts index b106a97d8..119716914 100644 --- a/packages/core/src/trace/DdTrace.ts +++ b/packages/core/src/trace/DdTrace.ts @@ -13,8 +13,11 @@ import { } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import type { DdTraceType } from '../types'; import { validateContext } from '../utils/argsUtils'; +import { getGlobalInstance } from '../utils/singletonUtils'; import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider'; +const TRACE_MODULE = 'com.datadog.reactnative.trace'; + const timeProvider = new DefaultTimeProvider(); class DdTraceWrapper implements DdTraceType { @@ -59,6 +62,7 @@ class DdTraceWrapper implements DdTraceType { }; } -const DdTrace: DdTraceType = new DdTraceWrapper(); - -export { DdTrace }; +export const DdTrace: DdTraceType = getGlobalInstance( + TRACE_MODULE, + () => new DdTraceWrapper() +); diff --git a/packages/core/src/utils/__tests__/singletonUtils.test.ts b/packages/core/src/utils/__tests__/singletonUtils.test.ts new file mode 100644 index 000000000..f424562c6 --- /dev/null +++ b/packages/core/src/utils/__tests__/singletonUtils.test.ts @@ -0,0 +1,75 @@ +import { getGlobalInstance } from '../singletonUtils'; + +describe('singletonUtils', () => { + const createdSymbols: symbol[] = []; + const g = (globalThis as unknown) as Record; + + afterEach(() => { + for (const symbol of createdSymbols) { + delete g[symbol]; + } + + createdSymbols.length = 0; + jest.restoreAllMocks(); + }); + + it('only creates one instance for the same key', () => { + const key = 'com.datadog.reactnative.test'; + const symbol = Symbol.for(key); + createdSymbols.push(symbol); + + const objectConstructor = jest.fn(() => ({ id: 1 })); + const a = getGlobalInstance(key, objectConstructor); + const b = getGlobalInstance(key, objectConstructor); + + expect(a).toBe(b); + expect(objectConstructor).toHaveBeenCalledTimes(1); + expect(g[symbol]).toBe(a); + }); + + it('returns a pre-existing instance without creating a new one for the same key', () => { + const key = 'com.datadog.reactnative.test'; + const symbol = Symbol.for(key); + createdSymbols.push(symbol); + + const existing = { pre: true }; + g[symbol] = existing; + + const objectConstructor = jest.fn(() => ({ created: true })); + const result = getGlobalInstance(key, objectConstructor); + + expect(result).toBe(existing); + expect(objectConstructor).not.toHaveBeenCalled(); + }); + + it('creates a new instance for a different key', () => { + const keyA = 'com.datadog.reactnative.test.a'; + const keyB = 'com.datadog.reactnative.test.b'; + const symbolA = Symbol.for(keyA); + const symbolB = Symbol.for(keyB); + createdSymbols.push(symbolA, symbolB); + + const a = getGlobalInstance(keyA, () => ({ id: 'A' })); + const b = getGlobalInstance(keyB, () => ({ id: 'B' })); + + expect(a).not.toBe(b); + expect((a as any).id).toBe('A'); + expect((b as any).id).toBe('B'); + }); + + it('does not overwrite existing instance if called with a different constructor', () => { + const key = 'com.datadog.reactnative.test'; + const symbol = Symbol.for(key); + createdSymbols.push(symbol); + + const firstObjectConstructor = jest.fn(() => ({ id: 1 })); + const first = getGlobalInstance(key, firstObjectConstructor); + + const secondObjectConstructor = jest.fn(() => ({ id: 2 })); + const second = getGlobalInstance(key, secondObjectConstructor); + + expect(first).toBe(second); + expect(firstObjectConstructor).toHaveBeenCalledTimes(1); + expect(secondObjectConstructor).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/core/src/utils/singletonUtils.ts b/packages/core/src/utils/singletonUtils.ts new file mode 100644 index 000000000..9f00c2cd0 --- /dev/null +++ b/packages/core/src/utils/singletonUtils.ts @@ -0,0 +1,12 @@ +export const getGlobalInstance = ( + key: string, + objectConstructor: () => T +): T => { + const symbol = Symbol.for(key); + const g = (globalThis as unknown) as Record; + + if (!(symbol in g)) { + g[symbol] = objectConstructor(); + } + return g[symbol] as T; +};