diff --git a/packages/utils/src/lib/user-timing-extensibility-api.type.ts b/packages/utils/src/lib/user-timing-extensibility-api.type.ts index 9c0ed19c7..ed93242ad 100644 --- a/packages/utils/src/lib/user-timing-extensibility-api.type.ts +++ b/packages/utils/src/lib/user-timing-extensibility-api.type.ts @@ -135,6 +135,12 @@ export type WithDevToolsPayload = { devtools?: T; }; +/** + * Combined detail payload type for performance entries with DevTools support. + */ +export type DetailPayloadWithDevtools = WithDevToolsPayload< + TrackEntryPayload | MarkerPayload +>; /** * Extended MarkOptions that supports DevTools payload in detail. * @example diff --git a/packages/utils/src/perf-hooks.d.ts b/packages/utils/src/perf-hooks.d.ts new file mode 100644 index 000000000..be23fff12 --- /dev/null +++ b/packages/utils/src/perf-hooks.d.ts @@ -0,0 +1,102 @@ +import type { + Performance, + PerformanceEntry, + PerformanceMark, + PerformanceMeasure, + PerformanceObserverEntryList, +} from 'node:perf_hooks'; +import type { DetailPayloadWithDevtools } from './lib/user-timing-extensibility-api.type'; + +export type EntryType = 'mark' | 'measure'; + +export type DOMHighResTimeStamp = number; + +/* == Internal Overrides Start == */ +interface PerformanceEntryExtended extends PerformanceEntry { + readonly detail?: DetailPayloadWithDevtools; +} + +interface MarkEntryExtended extends PerformanceMark { + readonly entryType: 'mark'; +} + +interface MeasureEntryExtended extends PerformanceMeasure { + readonly entryType: 'measure'; +} + +interface PerformanceMarkExtended extends PerformanceMark { + readonly detail?: DetailPayloadWithDevtools; +} + +interface PerformanceMeasureExtended extends PerformanceMeasure { + readonly detail?: DetailPayloadWithDevtools; +} + +interface PerformanceObserverEntryListExtended + extends PerformanceObserverEntryList { + getEntries(): PerformanceEntryExtended[]; + getEntriesByName(name: string, type?: EntryType): PerformanceEntryExtended[]; + getEntriesByType(type: EntryType): PerformanceEntryExtended[]; +} + +interface PerformanceMarkOptionsExtended { + detail?: DetailPayloadWithDevtools; + startTime?: DOMHighResTimeStamp; +} + +interface PerformanceMeasureOptionsExtended { + detail?: DetailPayloadWithDevtools; + start?: string | number; + end?: string | number; + duration?: number; +} + +interface PerformanceEntryListExtended { + getEntries(): PerformanceEntryExtended[]; + getEntriesByName(name: string, type?: EntryType): PerformanceEntryExtended[]; + getEntriesByType(type: EntryType): PerformanceEntryExtended[]; +} + +interface PerformanceExtended extends Performance { + mark: ( + name: string, + options?: PerformanceMarkOptionsExtended, + ) => PerformanceMarkExtended; + measure: ( + name: string, + startOrOptions?: string | number | PerformanceMeasureOptionsExtended, + end?: string | number, + ) => PerformanceMeasureExtended; + getEntriesByType: (type: EntryType) => PerformanceEntryExtended[]; + getEntries(): PerformanceEntryExtended[]; + getEntriesByName(name: string, type?: EntryType): PerformanceEntryExtended[]; + takeRecords(): PerformanceEntryExtended[]; +} +/* == Internal Overrides End == */ + +declare module 'node:perf_hooks' { + export interface PerformanceEntry extends PerformanceEntryExtended {} + + export interface PerformanceEntryList extends PerformanceEntryListExtended {} + + export interface MarkEntry extends PerformanceMark, MarkEntryExtended {} + + export interface MeasureEntry + extends PerformanceMeasure, + MeasureEntryExtended {} + + export interface PerformanceMark extends PerformanceMarkExtended {} + + export interface PerformanceMeasure extends PerformanceMeasureExtended {} + + export interface PerformanceMarkOptions + extends PerformanceMarkOptionsExtended {} + + export interface PerformanceMeasureOptions + extends PerformanceMeasureOptionsExtended {} + + export interface PerformanceObserverEntryList + extends PerformanceObserverEntryListExtended {} + + const performance: PerformanceExtended; +} diff --git a/packages/utils/src/perf-hooks.type.test.ts b/packages/utils/src/perf-hooks.type.test.ts new file mode 100644 index 000000000..b171971aa --- /dev/null +++ b/packages/utils/src/perf-hooks.type.test.ts @@ -0,0 +1,205 @@ +import { + type PerformanceEntry, + type PerformanceMarkOptions, + type PerformanceMeasureOptions, + performance, +} from 'node:perf_hooks'; +import { describe, expect, it } from 'vitest'; + +describe('interfaces', () => { + it('PerformanceMarkOptions should be type safe', () => { + // Valid complete example + expect( + () => + ({ + startTime: 0, + detail: { + devtools: { + color: 'error', + track: 'test-track', + trackGroup: 'test-trackGroup', + properties: [['Key', '42']], + tooltipText: 'test-tooltipText', + }, + }, + }) satisfies PerformanceMarkOptions, + ).not.toThrow(); + // Invalid Examples + expect( + () => + ({ + startTime: 0, + detail: { + devtools: { + // @ts-expect-error - dataType should be marker | track + dataType: 'markerr', + // @ts-expect-error - color should be DevToolsColor + color: 'other', + // @ts-expect-error - properties should be an array of [string, string] + properties: { wrong: 'shape' }, + }, + }, + }) satisfies PerformanceMarkOptions, + ).not.toThrow(); + }); + + it('PerformanceMeasureOptions should be type safe', () => { + // Valid complete example + expect( + () => + ({ + start: 'start-mark', + end: 'end-mark', + detail: { + devtools: { + dataType: 'track-entry', + track: 'test-track', + color: 'primary', + }, + }, + }) satisfies PerformanceMeasureOptions, + ).not.toThrow(); + // Invalid Examples + expect( + () => + ({ + start: 'start-mark', + end: 'end-mark', + detail: { + devtools: { + // @ts-expect-error - dataType should be track-entry | marker + dataType: 'markerr', + track: 'test-track', + color: 'primary', + }, + }, + }) satisfies PerformanceMeasureOptions, + ).not.toThrow(); + }); + + it.todo('PerformanceEntry should be type safe', () => { + // Valid complete example + expect( + () => + ({ + name: 'test-entry', + entryType: 'mark', + startTime: 0, + duration: 0, + toJSON: () => ({}), + detail: { + devtools: { + dataType: 'marker', + color: 'primary', + }, + }, + }) satisfies PerformanceEntry, + ).not.toThrow(); + // Invalid Examples + expect( + () => + ({ + name: 'test-entry', + entryType: 'mark', + startTime: 0, + duration: 0, + toJSON: () => ({}), + detail: { + devtools: { + dataType: 'invalid-type', // Should fail + color: 'invalid-color', // Should fail + properties: { wrong: 'shape' }, // Should fail + }, + }, + }) satisfies PerformanceEntry, + ).not.toThrow(); + }); + + it('performance.getEntriesByType returns extended entries', () => { + const entries = performance.getEntriesByType('mark'); + + entries.forEach(e => { + e.detail?.devtools; + }); + }); +}); + +describe('API', () => { + it('performance.mark should be type safe', () => { + // Valid complete example + expect(() => + performance.mark('name', { + detail: { + devtools: { + dataType: 'marker', + color: 'error', + }, + }, + }), + ).not.toThrow(); + // Invalid Examples + expect(() => + performance.mark('name', { + detail: { + devtools: { + // @ts-expect-error - dataType should be marker | track + dataType: 'markerrr', + // @ts-expect-error - color should be DevToolsColor + color: 'invalid-color', + // @ts-expect-error - properties should be an array of [string, string] + properties: 'invalid-properties', + }, + }, + }), + ).not.toThrow(); + }); + + it('performance.measure should be type safe', () => { + // Create marks for measurement + performance.mark('start-mark'); + performance.mark('end-mark'); + + // Valid examples + expect(() => + performance.measure('measure-name', 'start-mark', 'end-mark'), + ).not.toThrow(); + expect(() => + performance.measure('measure-name', { + start: 'start-mark', + end: 'end-mark', + detail: { + devtools: { + dataType: 'track-entry', + track: 'test-track', + color: 'primary', + }, + }, + }), + ).not.toThrow(); + // Invalid Examples + expect(() => + performance.measure('measure-name', { + start: 'start-mark', + end: 'end-mark', + detail: { + devtools: { + // @ts-expect-error - dataType should be track-entry | marker + dataType: 'invalid-type', + // @ts-expect-error - color should be DevToolsColor + color: 'invalid-color', + }, + }, + }), + ).not.toThrow(); + }); + + it('performance.getEntriesByType should be type safe', () => { + // Invalid Examples + expect(() => + performance.getEntriesByType('mark').forEach(e => { + // @ts-expect-error - dataType should be valid + e.detail?.devtools?.dataType === 'markerr'; + }), + ).not.toThrow(); + }); +});