1+ import { performance } from 'node:perf_hooks' ;
12import type {
23 DevToolsColor ,
34 DevToolsProperties ,
@@ -12,13 +13,20 @@ import type {
1213const dataTypeTrackEntry = 'track-entry' ;
1314const dataTypeMarker = 'marker' ;
1415
16+ export function mergePropertiesWithOverwrite <
17+ const T extends DevToolsProperties ,
18+ const U extends DevToolsProperties ,
19+ > ( baseProperties : T , overrideProperties : U ) : ( T [ number ] | U [ number ] ) [ ] ;
20+ export function mergePropertiesWithOverwrite <
21+ const T extends DevToolsProperties ,
22+ > ( baseProperties : T ) : T ;
1523export function mergePropertiesWithOverwrite (
16- baseProperties : DevToolsProperties | undefined ,
17- overrideProperties ?: DevToolsProperties | undefined ,
18- ) {
24+ baseProperties ? : DevToolsProperties ,
25+ overrideProperties ?: DevToolsProperties ,
26+ ) : DevToolsProperties {
1927 return [
2028 ...new Map ( [ ...( baseProperties ?? [ ] ) , ...( overrideProperties ?? [ ] ) ] ) ,
21- ] satisfies DevToolsProperties ;
29+ ] ;
2230}
2331
2432export function markerPayload ( options ?: Omit < MarkerPayload , 'dataType' > ) {
@@ -49,19 +57,15 @@ export function markerErrorPayload<T extends DevToolsColor>(
4957 } satisfies MarkerPayload ;
5058}
5159
52- export function trackEntryErrorPayload <
53- T extends string ,
54- C extends DevToolsColor ,
55- > (
60+ export function trackEntryErrorPayload < T extends string > (
5661 options : Omit < TrackEntryPayload , 'color' | 'dataType' > & {
5762 track : T ;
58- color ?: C ;
5963 } ,
6064) {
61- const { track, color = 'error' as const , ...restOptions } = options ;
65+ const { track, ...restOptions } = options ;
6266 return {
6367 dataType : dataTypeTrackEntry ,
64- color,
68+ color : 'error' as const ,
6569 track,
6670 ...restOptions ,
6771 } satisfies TrackEntryPayload ;
@@ -86,7 +90,7 @@ export function errorToEntryMeta(
8690 const { properties, tooltipText } = options ?? { } ;
8791 const props = mergePropertiesWithOverwrite (
8892 errorToDevToolsProperties ( e ) ,
89- properties ,
93+ properties ?? [ ] ,
9094 ) ;
9195 return {
9296 properties : props ,
@@ -127,19 +131,6 @@ export function errorToMarkerPayload(
127131 } satisfies MarkerPayload ;
128132}
129133
130- /**
131- * asOptions wraps a DevTools payload into the `detail` property of User Timing entry options.
132- *
133- * @example
134- * profiler.mark('mark', asOptions({
135- * dataType: 'marker',
136- * color: 'error',
137- * tooltipText: 'This is a marker',
138- * properties: [
139- * ['str', 'This is a detail property']
140- * ],
141- * }));
142- */
143134export function asOptions < T extends MarkerPayload > (
144135 devtools ?: T | null ,
145136) : MarkOptionsWithDevtools < T > ;
@@ -151,5 +142,156 @@ export function asOptions<T extends MarkerPayload | TrackEntryPayload>(
151142) : {
152143 detail ?: WithDevToolsPayload < T > ;
153144} {
154- return devtools == null ? { detail : { } } : { detail : { devtools } } ;
145+ if ( devtools == null ) {
146+ return { detail : { } } ;
147+ }
148+
149+ return { detail : { devtools } } ;
150+ }
151+
152+ export type Names < N extends string > = {
153+ startName : `${N } :start`;
154+ endName : `${N } :end`;
155+ measureName : N ;
156+ } ;
157+
158+ export function getNames < T extends string > ( base : T ) : Names < T > ;
159+ export function getNames < T extends string , P extends string > (
160+ base : T ,
161+ prefix ?: P ,
162+ ) : Names < `${P } :${T } `> ;
163+ export function getNames ( base : string , prefix ?: string ) {
164+ const n = prefix ? `${ prefix } :${ base } ` : base ;
165+ return {
166+ startName : `${ n } :start` ,
167+ endName : `${ n } :end` ,
168+ measureName : n ,
169+ } as const ;
170+ }
171+
172+ type Simplify < T > = { [ K in keyof T ] : T [ K ] } & object ;
173+
174+ type MergeObjects < T extends readonly object [ ] > = T extends readonly [
175+ infer F extends object ,
176+ ...infer R extends readonly object [ ] ,
177+ ]
178+ ? Simplify < Omit < F , keyof MergeObjects < R > > & MergeObjects < R > >
179+ : object ;
180+
181+ export type MergeResult <
182+ P extends readonly Partial < TrackEntryPayload | MarkerPayload > [ ] ,
183+ > = MergeObjects < P > & { properties ?: DevToolsProperties } ;
184+
185+ export function mergeDevtoolsPayload <
186+ const P extends readonly Partial < TrackEntryPayload | MarkerPayload > [ ] ,
187+ > ( ...parts : P ) : MergeResult < P > {
188+ return parts . reduce (
189+ ( acc , cur ) => ( {
190+ ...acc ,
191+ ...cur ,
192+ ...( cur . properties || acc . properties
193+ ? {
194+ properties : mergePropertiesWithOverwrite (
195+ acc . properties ?? [ ] ,
196+ cur . properties ?? [ ] ,
197+ ) ,
198+ }
199+ : { } ) ,
200+ } ) ,
201+ { } as Partial < TrackEntryPayload > ,
202+ ) as MergeResult < P > ;
203+ }
204+
205+ export function mergeDevtoolsPayloadAction <
206+ const P extends readonly [ ActionTrack , ...Partial < ActionTrack > [ ] ] ,
207+ > ( ...parts : P ) : MergeObjects < P > & { properties ?: DevToolsProperties } {
208+ return mergeDevtoolsPayload (
209+ ...( parts as unknown as readonly Partial <
210+ TrackEntryPayload | MarkerPayload
211+ > [ ] ) ,
212+ ) as MergeObjects < P > & { properties ?: DevToolsProperties } ;
213+ }
214+
215+ export type ActionColorPayload = {
216+ color ?: DevToolsColor ;
217+ } ;
218+ export type ActionTrack = TrackEntryPayload & ActionColorPayload ;
219+
220+ export function setupTracks <
221+ const T extends Record < string , Partial < ActionTrack > > ,
222+ const D extends ActionTrack ,
223+ > ( defaults : D , tracks : T ) : Record < keyof T , ActionTrack > {
224+ return Object . entries ( tracks ) . reduce (
225+ ( result , [ key , track ] ) => ( {
226+ ...result ,
227+ [ key ] : mergeDevtoolsPayload ( defaults , track ) as ActionTrack ,
228+ } ) ,
229+ { } as Record < keyof T , ActionTrack > ,
230+ ) ;
231+ }
232+
233+ /**
234+ * This is a helper function used to ensure that the marks used to create a measure do not contain UI interaction properties.
235+ * @param devtools - The devtools payload to convert to mark options.
236+ * @returns The mark options without tooltipText and properties.
237+ */
238+ function toMarkMeasureOpts ( devtools : TrackEntryPayload ) {
239+ const { tooltipText : _ , properties : __ , ...markDevtools } = devtools ;
240+ return { detail : { devtools : markDevtools } } ;
241+ }
242+
243+ export type MeasureOptions = Partial < ActionTrack > & {
244+ success ?: ( result : unknown ) => EntryMeta ;
245+ error ?: ( error : unknown ) => EntryMeta ;
246+ } ;
247+
248+ export type MeasureCtxOptions = ActionTrack & {
249+ prefix ?: string ;
250+ } & {
251+ error ?: ( error : unknown ) => EntryMeta ;
252+ } ;
253+ export function measureCtx ( cfg : MeasureCtxOptions ) {
254+ const { prefix, error : globalErr , ...defaults } = cfg ;
255+
256+ return ( event : string , opt ?: MeasureOptions ) => {
257+ const { success, error, ...measurePayload } = opt ?? { } ;
258+ const merged = mergeDevtoolsPayloadAction ( defaults , measurePayload , {
259+ dataType : dataTypeTrackEntry ,
260+ } ) as TrackEntryPayload ;
261+
262+ const {
263+ startName : s ,
264+ endName : e ,
265+ measureName : m ,
266+ } = getNames ( event , prefix ) ;
267+
268+ return {
269+ start : ( ) => performance . mark ( s , toMarkMeasureOpts ( merged ) ) ,
270+
271+ success : ( r : unknown ) => {
272+ const successPayload = mergeDevtoolsPayload ( merged , success ?.( r ) ?? { } ) ;
273+ performance . mark ( e , toMarkMeasureOpts ( successPayload ) ) ;
274+ performance . measure ( m , {
275+ start : s ,
276+ end : e ,
277+ ...asOptions ( successPayload ) ,
278+ } ) ;
279+ } ,
280+
281+ error : ( err : unknown ) => {
282+ const errorPayload = mergeDevtoolsPayload (
283+ errorToEntryMeta ( err ) ,
284+ globalErr ?.( err ) ?? { } ,
285+ error ?.( err ) ?? { } ,
286+ { ...merged , color : 'error' } ,
287+ ) ;
288+ performance . mark ( e , toMarkMeasureOpts ( errorPayload ) ) ;
289+ performance . measure ( m , {
290+ start : s ,
291+ end : e ,
292+ ...asOptions ( errorPayload ) ,
293+ } ) ;
294+ } ,
295+ } ;
296+ } ;
155297}
0 commit comments