Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⚗️[Logs] POC dependency injection #2520

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
7 changes: 6 additions & 1 deletion packages/core/src/browser/pageExitObservable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { isExperimentalFeatureEnabled, ExperimentalFeature } from '../tools/expe
import { Observable } from '../tools/observable'
import { objectValues, includes } from '../tools/utils/polyfills'
import { noop } from '../tools/utils/functionUtils'
import { getConfiguration } from '../domain/configuration'
import type { Configuration } from '../domain/configuration'
import type { Component } from '../tools/injector'
import { addEventListeners, addEventListener, DOM_EVENT } from './addEventListener'

export const PageExitReason = {
Expand All @@ -18,7 +20,7 @@ export interface PageExitEvent {
reason: PageExitReason
}

export function createPageExitObservable(configuration: Configuration): Observable<PageExitEvent> {
export const createPageExitObservable: Component<Observable<PageExitEvent>, [Configuration]> = (configuration) => {
const observable = new Observable<PageExitEvent>(() => {
const pagehideEnabled = isExperimentalFeatureEnabled(ExperimentalFeature.PAGEHIDE)
const { stop: stopListeners } = addEventListeners(
Expand Down Expand Up @@ -64,6 +66,9 @@ export function createPageExitObservable(configuration: Configuration): Observab
return observable
}

// eslint-disable-next-line local-rules/disallow-side-effects
createPageExitObservable.$deps = [getConfiguration]

export function isPageExitReason(reason: string | undefined): reason is PageExitReason {
return includes(objectValues(PageExitReason), reason)
}
8 changes: 8 additions & 0 deletions packages/core/src/domain/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export interface InitConfiguration {
telemetryConfigurationSampleRate?: number
}

export function getInitConfiguration(): InitConfiguration {
throw new Error('No init configuration')
}

// This type is only used to build the core configuration. Logs and RUM SDKs are using a proper type
// for this option.
type GenericBeforeSendCallback = (event: any, context?: any) => unknown
Expand Down Expand Up @@ -92,6 +96,10 @@ export interface Configuration extends TransportConfiguration {
messageBytesLimit: number
}

export function getConfiguration(): Configuration {
throw new Error('No configuration')
}

export function validateAndBuildConfiguration(initConfiguration: InitConfiguration): Configuration | undefined {
if (!initConfiguration || !initConfiguration.clientToken) {
display.error('Client Token is not configured, we will not send any data.')
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/domain/configuration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export {
DefaultPrivacyLevel,
validateAndBuildConfiguration,
serializeConfiguration,
getConfiguration,
getInitConfiguration,
} from './configuration'
export { createEndpointBuilder, EndpointBuilder, TrackType } from './endpointBuilder'
export * from './intakeSites'
3 changes: 3 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export {
Configuration,
InitConfiguration,
getConfiguration,
getInitConfiguration,
validateAndBuildConfiguration,
DefaultPrivacyLevel,
EndpointBuilder,
Expand Down Expand Up @@ -40,6 +42,7 @@ export {
} from './domain/telemetry'
export { monitored, monitor, callMonitored, setDebugMode } from './tools/monitor'
export { Observable, Subscription } from './tools/observable'
export { Injector, createInjector, getInjector, Component, AnyComponent } from './tools/injector'
export {
startSessionManager,
SessionManager,
Expand Down
37 changes: 37 additions & 0 deletions packages/core/src/tools/injector.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { noop } from './utils/functionUtils'
import type { Component, Injector } from './injector'
import { createInjector } from './injector'

type Foo = { key: string; stop: () => void }
const initFoo: Component<Foo, [Bar]> = (bar) => ({
key: `foo+${bar.key}`,
stop: noop,
})

initFoo.$deps = [initBar]

type Bar = ReturnType<typeof initBar>
function initBar() {
return {
key: 'bar',
}
}

describe('injector', () => {
let injector: Injector

beforeEach(() => {
injector = createInjector()
})

it('should init component with its dependencies', () => {
expect(injector.run(initFoo).key).toBe('foo+bar')
})

it('should override context with a stub component', () => {
injector.override(initBar, () => ({
key: 'qux',
}))
expect(injector.run(initFoo).key).toBe('foo+qux')
})
})
75 changes: 75 additions & 0 deletions packages/core/src/tools/injector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
export interface Component<Instance, Deps extends any[]> {
(...args: Deps): Instance
$deps: Readonly<{ [K in keyof Deps]: AnyComponent<Deps[K]> }>
}

interface SimpleComponent<Instance> {
(): Instance
}

export type AnyComponent<Instance> = Component<Instance, any> | SimpleComponent<Instance>

export interface Injector {
run: <T>(component: AnyComponent<T>) => T
get: <T>(component: AnyComponent<T>) => T
override: <T>(originalComponent: AnyComponent<T>, newComponent: AnyComponent<T>) => void
stop: () => void
}

export function createInjector(): Injector {
const instances = new Map<AnyComponent<any>, any>()
const overrides = new Map<AnyComponent<any>, AnyComponent<any>>()
overrides.set(getInjector, () => injector)

const injector: Injector = {
run(component) {
if (instances.has(component)) {
throw new Error(`Component ${component.name} already started`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This assertion and the following ones (in get and override) are not strictly needed, but helps to debug weird injection cases.

}
if (overrides.has(component)) {
component = overrides.get(component)!
}

const args = ('$deps' in component ? component.$deps : []).map((dependency): any =>
instances.has(dependency) ? instances.get(dependency) : injector.run(dependency)
)

const instance: any = component(...args)

instances.set(component, instance)

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return instance
},

get<T>(component: AnyComponent<T>) {
if (!instances.has(component)) {
throw new Error(`Component ${component.name} not started`)
}
return instances.get(component) as T
},

override(originalComponent, newComponent) {
if (instances.has(originalComponent)) {
throw new Error(`Component ${originalComponent.name} already started`)
}
overrides.set(originalComponent, newComponent)
},

stop() {
instances.forEach((instance) => {
if (instance !== injector && instance?.stop) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
instance.stop()
}
})
instances.clear()
},
}

return injector
}

export function getInjector(): Injector {
throw new Error('No injector available')
}
Loading