@zendesk/react-measure-timing-hooks
Version:
react hooks for measuring time to interactive and time to render of components
98 lines (91 loc) • 3.18 kB
text/typescript
import { ActionLog } from './ActionLog'
import { debounce, type DebouncedFn } from './debounce'
import type { StaticActionLogOptions, UseActionLogCacheOptions } from './types'
export interface ActionLogRef<
CustomMetadata extends Record<string, unknown> = Record<string, unknown>,
> {
activeId: string
ids: Set<string>
actionLog: ActionLog<CustomMetadata>
getCurrent: (id: string) => ActionLog<CustomMetadata>
}
export class ActionLogCache<
CustomMetadata extends Record<string, unknown> = Record<string, unknown>,
> {
private readonly options: StaticActionLogOptions<string, CustomMetadata>
private readonly cache = new Map<string, ActionLogRef<CustomMetadata>>()
private garbageCollectUnusedIdLater: DebouncedFn<
readonly [ActionLogRef<CustomMetadata>]
>
makeWrapperRef(actionLog: ActionLog<CustomMetadata>, initialId: string) {
let ref: ActionLogRef<CustomMetadata> = {
activeId: initialId,
ids: new Set<string>([initialId]),
actionLog,
getCurrent: (id: string) => {
const cachedRef = this.cache.get(id)
const cachedActionLog = cachedRef?.actionLog
const activeRef = cachedRef ?? ref
if (cachedActionLog !== ref.actionLog) {
// when ID changes, we transfer the state from the old actionLog to the new one
// because of the edge case when the child with the new ID
// is re-rendered *before* the parent beacon gets the new ID
if (cachedActionLog && !ref.actionLog.wasImported) {
cachedActionLog.importState(ref.actionLog)
} else {
activeRef.actionLog = ref.actionLog
ref.ids.forEach((refId) => {
activeRef.ids.add(refId)
})
}
}
this.cache.set(id, activeRef)
activeRef.activeId = id
activeRef.ids.add(id)
this.garbageCollectUnusedIdLater(activeRef)
ref = activeRef
return activeRef.actionLog
},
}
return ref
}
get(id: string) {
const existingRef = this.cache.get(id)
if (existingRef) return existingRef.getCurrent(id)
return undefined
}
makeGetOrCreateFn(id: string) {
const existingRef = this.cache.get(id)
if (existingRef) return existingRef.getCurrent
const actionLog = new ActionLog(this.options)
const ref = this.makeWrapperRef(actionLog, id)
actionLog.onDispose('gc', () => {
ref.ids.forEach((refId) => {
if (this.cache.get(refId)?.actionLog === actionLog) {
this.cache.delete(refId)
}
})
})
this.cache.set(id, ref)
return ref.getCurrent
}
constructor({
garbageCollectMs,
...actionLogOptions
}: UseActionLogCacheOptions<CustomMetadata>) {
this.options = actionLogOptions
this.garbageCollectUnusedIdLater = debounce<[ActionLogRef<CustomMetadata>]>(
{
fn: (ref: ActionLogRef<CustomMetadata>) => {
ref.ids.forEach((id) => {
if (id !== ref.activeId && this.cache.get(id) === ref) {
this.cache.delete(id)
ref.ids.delete(id)
}
})
},
debounceMs: garbageCollectMs,
},
)
}
}