@code-pushup/utils 0.110.0 → 0.111.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-pushup/utils",
3
- "version": "0.110.0",
3
+ "version": "0.111.1",
4
4
  "description": "Low-level utilities (helper functions, etc.) used by Code PushUp CLI",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/code-pushup/cli/tree/main/packages/utils#readme",
@@ -27,7 +27,7 @@
27
27
  "node": ">=18.2.0"
28
28
  },
29
29
  "dependencies": {
30
- "@code-pushup/models": "0.110.0",
30
+ "@code-pushup/models": "0.111.1",
31
31
  "ansis": "^3.3.0",
32
32
  "build-md": "^0.4.2",
33
33
  "bundle-require": "^5.1.0",
@@ -18,4 +18,14 @@ export type ExitHandlerOptions = {
18
18
  exitOnSignal?: boolean;
19
19
  fatalExitCode?: number;
20
20
  };
21
- export declare function installExitHandlers(options?: ExitHandlerOptions): void;
21
+ /**
22
+ *
23
+ * @param options - Options for the exit handler
24
+ * @param options.onExit - Callback to be called when the process exits
25
+ * @param options.onError - Callback to be called when an error occurs
26
+ * @param options.exitOnFatal - Whether to exit the process on fatal errors
27
+ * @param options.exitOnSignal - Whether to exit the process on signals
28
+ * @param options.fatalExitCode - The exit code to use for fatal errors
29
+ * @returns A function to unsubscribe from the exit handlers
30
+ */
31
+ export declare function subscribeProcessExit(options?: ExitHandlerOptions): () => void;
@@ -16,10 +16,21 @@ export const SIGNAL_EXIT_CODES = () => {
16
16
  };
17
17
  };
18
18
  export const DEFAULT_FATAL_EXIT_CODE = 1;
19
- export function installExitHandlers(options = {}) {
19
+ /**
20
+ *
21
+ * @param options - Options for the exit handler
22
+ * @param options.onExit - Callback to be called when the process exits
23
+ * @param options.onError - Callback to be called when an error occurs
24
+ * @param options.exitOnFatal - Whether to exit the process on fatal errors
25
+ * @param options.exitOnSignal - Whether to exit the process on signals
26
+ * @param options.fatalExitCode - The exit code to use for fatal errors
27
+ * @returns A function to unsubscribe from the exit handlers
28
+ */
29
+ // eslint-disable-next-line max-lines-per-function
30
+ export function subscribeProcessExit(options = {}) {
20
31
  // eslint-disable-next-line functional/no-let
21
32
  let closedReason;
22
- const { onExit, onError, exitOnFatal, exitOnSignal, fatalExitCode = DEFAULT_FATAL_EXIT_CODE, } = options;
33
+ const { onExit, onError, exitOnFatal = false, exitOnSignal = false, fatalExitCode = DEFAULT_FATAL_EXIT_CODE, } = options;
23
34
  const close = (code, reason) => {
24
35
  if (closedReason) {
25
36
  return;
@@ -27,7 +38,7 @@ export function installExitHandlers(options = {}) {
27
38
  closedReason = reason;
28
39
  onExit?.(code, reason);
29
40
  };
30
- process.on('uncaughtException', err => {
41
+ const uncaughtExceptionHandler = (err) => {
31
42
  onError?.(err, 'uncaughtException');
32
43
  if (exitOnFatal) {
33
44
  close(fatalExitCode, {
@@ -35,8 +46,8 @@ export function installExitHandlers(options = {}) {
35
46
  fatal: 'uncaughtException',
36
47
  });
37
48
  }
38
- });
39
- process.on('unhandledRejection', reason => {
49
+ };
50
+ const unhandledRejectionHandler = (reason) => {
40
51
  onError?.(reason, 'unhandledRejection');
41
52
  if (exitOnFatal) {
42
53
  close(fatalExitCode, {
@@ -44,21 +55,34 @@ export function installExitHandlers(options = {}) {
44
55
  fatal: 'unhandledRejection',
45
56
  });
46
57
  }
47
- });
48
- ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach(signal => {
49
- process.on(signal, () => {
58
+ };
59
+ const signalHandlers = ['SIGINT', 'SIGTERM', 'SIGQUIT'].map(signal => {
60
+ const handler = () => {
50
61
  close(SIGNAL_EXIT_CODES()[signal], { kind: 'signal', signal });
51
62
  if (exitOnSignal) {
52
- // eslint-disable-next-line n/no-process-exit
63
+ // eslint-disable-next-line unicorn/no-process-exit,n/no-process-exit
53
64
  process.exit(SIGNAL_EXIT_CODES()[signal]);
54
65
  }
55
- });
66
+ };
67
+ process.on(signal, handler);
68
+ return { signal, handler };
56
69
  });
57
- process.on('exit', code => {
70
+ const exitHandler = (code) => {
58
71
  if (closedReason) {
59
72
  return;
60
73
  }
61
74
  close(code, { kind: 'exit' });
62
- });
75
+ };
76
+ process.on('uncaughtException', uncaughtExceptionHandler);
77
+ process.on('unhandledRejection', unhandledRejectionHandler);
78
+ process.on('exit', exitHandler);
79
+ return () => {
80
+ process.removeListener('uncaughtException', uncaughtExceptionHandler);
81
+ process.removeListener('unhandledRejection', unhandledRejectionHandler);
82
+ process.removeListener('exit', exitHandler);
83
+ signalHandlers.forEach(({ signal, handler }) => {
84
+ process.removeListener(signal, handler);
85
+ });
86
+ };
63
87
  }
64
88
  //# sourceMappingURL=exit-process.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"exit-process.js","sourceRoot":"","sources":["../../../src/lib/exit-process.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"exit-process.js","sourceRoot":"","sources":["../../../src/lib/exit-process.ts"],"names":[],"mappings":"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"}
@@ -1,18 +1,199 @@
1
1
  import { type PerformanceEntry } from 'node:perf_hooks';
2
2
  import type { AppendableSink } from './wal.js';
3
+ /**
4
+ * Encoder that converts PerformanceEntry to domain events.
5
+ *
6
+ * Pure function that transforms performance entries into domain events.
7
+ * Should be stateless, synchronous, and have no side effects.
8
+ * Returns a readonly array of encoded items.
9
+ */
10
+ export type PerformanceEntryEncoder<F> = (entry: PerformanceEntry) => readonly F[];
11
+ /**
12
+ * Default threshold for triggering queue flushes based on queue length.
13
+ * When the queue length reaches (maxQueueSize - flushThreshold),
14
+ * a flush is triggered to prevent overflow. This provides a buffer zone
15
+ * before hitting the maximum queue capacity.
16
+ */
3
17
  export declare const DEFAULT_FLUSH_THRESHOLD = 20;
18
+ /**
19
+ * Default maximum number of items allowed in the queue before entries are dropped.
20
+ * This acts as a memory safety limit to prevent unbounded memory growth
21
+ * in case of sink slowdown or high-frequency performance entries.
22
+ */
23
+ export declare const DEFAULT_MAX_QUEUE_SIZE = 10000;
24
+ /**
25
+ * Validates the flush threshold configuration to ensure sensible bounds.
26
+ *
27
+ * The flush threshold must be positive and cannot exceed the maximum queue size,
28
+ * as it represents a buffer zone within the queue capacity.
29
+ *
30
+ * @param flushThreshold - The threshold value to validate (must be > 0)
31
+ * @param maxQueueSize - The maximum queue size for comparison (flushThreshold <= maxQueueSize)
32
+ * @throws {Error} If flushThreshold is not positive or exceeds maxQueueSize
33
+ */
34
+ export declare function validateFlushThreshold(flushThreshold: number, maxQueueSize: number): void;
35
+ /**
36
+ * Configuration options for the PerformanceObserverSink.
37
+ *
38
+ * @template T - The type of encoded performance data that will be written to the sink
39
+ */
4
40
  export type PerformanceObserverOptions<T> = {
41
+ /**
42
+ * The sink where encoded performance entries will be written.
43
+ * Must implement the AppendableSink interface for handling the encoded data.
44
+ */
5
45
  sink: AppendableSink<T>;
6
- encode: (entry: PerformanceEntry) => T[];
7
- buffered?: boolean;
46
+ /**
47
+ * Function that encodes raw PerformanceEntry objects into domain-specific types.
48
+ * This transformer converts Node.js performance entries into application-specific data structures.
49
+ * Returns a readonly array of encoded items.
50
+ */
51
+ encodePerfEntry: PerformanceEntryEncoder<T>;
52
+ /**
53
+ * Whether to enable buffered observation mode.
54
+ * When true, captures all performance entries that occurred before observation started.
55
+ * When false, only captures entries after subscription begins.
56
+ *
57
+ * @default true
58
+ */
59
+ captureBufferedEntries?: boolean;
60
+ /**
61
+ * Threshold for triggering queue flushes.
62
+ * Flushes occur in two scenarios:
63
+ * 1. When queue length reaches (maxQueueSize - flushThreshold)
64
+ * 2. When the number of items added since last flush reaches flushThreshold
65
+ * Larger values provide more buffer space before hitting capacity limits.
66
+ *
67
+ * @default DEFAULT_FLUSH_THRESHOLD (20)
68
+ */
8
69
  flushThreshold?: number;
70
+ /**
71
+ * Maximum number of items allowed in the queue before new entries are dropped.
72
+ * Acts as a memory safety limit to prevent unbounded growth during sink slowdown.
73
+ *
74
+ * @default DEFAULT_MAX_QUEUE_SIZE (10000)
75
+ */
76
+ maxQueueSize?: number;
77
+ /**
78
+ * Name of the environment variable to check for debug mode.
79
+ * When the env var is set to 'true', encode failures create performance marks for debugging.
80
+ *
81
+ * @default 'CP_PROFILER_DEBUG'
82
+ */
83
+ debugEnvVar?: string;
9
84
  };
85
+ /**
86
+ * A sink implementation that observes Node.js performance entries and forwards them to a configurable sink.
87
+ *
88
+ * This class provides a buffered, memory-safe bridge between Node.js PerformanceObserver
89
+ * and application-specific data sinks. It handles performance entry encoding, queue management,
90
+ * and graceful degradation under high load conditions.
91
+ *
92
+ * Performance entries flow through the following lifecycle:
93
+ *
94
+ * - Queued in Memory 💾
95
+ * - Items stored in queue (`#queue`) until flushed
96
+ * - Queue limited by `maxQueueSize` to prevent unbounded growth
97
+ * - Items remain in queue if sink is closed during flush
98
+ *
99
+ * - Successfully Written 📤
100
+ * - Items written to sink and counted in `getStats().written`
101
+ * - Queue cleared after successful batch writes
102
+ *
103
+ * - Item Disposition Scenarios 💥
104
+ * - **Encode Failure**: ❌ Items lost when `encode()` throws. Creates perf mark if debug env var (specified by `debugEnvVar`) is set to 'true'.
105
+ * - **Sink Write Failure**: 💾 Items stay in queue when sink write fails during flush
106
+ * - **Sink Closed**: 💾 Items stay in queue when sink is closed during flush
107
+ * - **Proactive Flush Throws**: 💾 Items stay in queue when `flush()` throws during threshold check
108
+ * - **Final Flush Throws**: 💾 Items stay in queue when `flush()` throws at end of callback
109
+ * - **Buffered Flush Throws**: 💾 Items stay in queue when buffered entries flush fails
110
+ * - **Queue Overflow**: ❌ Items dropped when queue reaches `maxQueueSize`
111
+ *
112
+ * @template T - The type of encoded performance data written to the sink
113
+ * @implements {Observer} - Lifecycle management interface
114
+ * @implements {Buffered} - Queue statistics interface
115
+ */
10
116
  export declare class PerformanceObserverSink<T> {
11
117
  #private;
118
+ /**
119
+ * Creates a new PerformanceObserverSink with the specified configuration.
120
+ *
121
+ * @param options - Configuration options for the performance observer sink
122
+ * @throws {Error} If flushThreshold validation fails (must be > 0 and <= maxQueueSize)
123
+ */
12
124
  constructor(options: PerformanceObserverOptions<T>);
13
- encode(entry: PerformanceEntry): T[];
125
+ /**
126
+ * Returns whether debug mode is enabled for encode failures.
127
+ *
128
+ * Debug mode is determined by the environment variable specified by `debugEnvVar`
129
+ * (defaults to 'CP_PROFILER_DEBUG'). When enabled, encode failures create
130
+ * performance marks for debugging.
131
+ *
132
+ * @returns true if debug mode is enabled, false otherwise
133
+ */
134
+ get debug(): boolean;
135
+ /**
136
+ * Returns current queue statistics for monitoring and debugging.
137
+ *
138
+ * Provides insight into the current state of the performance entry queue,
139
+ * useful for monitoring memory usage and processing throughput.
140
+ *
141
+ * @returns Object containing all states and entry counts
142
+ */
143
+ getStats(): {
144
+ isSubscribed: boolean;
145
+ queued: number;
146
+ dropped: number;
147
+ written: number;
148
+ maxQueueSize: number;
149
+ flushThreshold: number;
150
+ addedSinceLastFlush: number;
151
+ buffered: boolean;
152
+ };
153
+ /**
154
+ * Encodes a raw PerformanceEntry using the configured encoder function.
155
+ *
156
+ * This method delegates to the user-provided encoder function, allowing
157
+ * transformation of Node.js performance entries into application-specific types.
158
+ *
159
+ * @param entry - The raw performance entry to encode
160
+ * @returns Readonly array of encoded items
161
+ */
162
+ encode(entry: PerformanceEntry): readonly T[];
163
+ /**
164
+ * Starts observing performance entries and forwarding them to the sink.
165
+ *
166
+ * Creates a Node.js PerformanceObserver that monitors 'mark' and 'measure' entries.
167
+ * The observer uses a bounded queue with proactive flushing to manage memory usage.
168
+ * When buffered mode is enabled, any existing buffered entries are immediately flushed.
169
+ * If the sink is closed, items stay in the queue until reopened.
170
+ *
171
+ */
14
172
  subscribe(): void;
173
+ /**
174
+ * Flushes all queued performance entries to the sink.
175
+ *
176
+ * Writes all currently queued encoded performance entries to the configured sink.
177
+ * If the sink is closed, flush is a no-op and items stay in the queue until reopened.
178
+ * The queue is always cleared after flush attempt, regardless of success or failure.
179
+ */
15
180
  flush(): void;
181
+ /**
182
+ * Stops observing performance entries and cleans up resources.
183
+ *
184
+ * Performs a final flush of any remaining queued entries, then disconnects
185
+ * the PerformanceObserver and releases all references.
186
+ *
187
+ * This method is idempotent - safe to call multiple times.
188
+ */
16
189
  unsubscribe(): void;
190
+ /**
191
+ * Checks whether the performance observer is currently active.
192
+ *
193
+ * Returns true if the sink is subscribed and actively observing performance entries.
194
+ * This indicates that a PerformanceObserver instance exists and is connected.
195
+ *
196
+ * @returns true if currently subscribed and observing, false otherwise
197
+ */
17
198
  isSubscribed(): boolean;
18
199
  }
@@ -1,35 +1,216 @@
1
1
  import { PerformanceObserver, performance, } from 'node:perf_hooks';
2
+ import { isEnvVarEnabled } from './env.js';
3
+ import { PROFILER_DEBUG_ENV_VAR } from './profiler/constants.js';
4
+ /**
5
+ * Array of performance entry types that this observer monitors.
6
+ * Only 'mark' and 'measure' entries are tracked as they represent
7
+ * user-defined performance markers and measurements.
8
+ */
2
9
  const OBSERVED_TYPES = ['mark', 'measure'];
10
+ const OBSERVED_TYPE_SET = new Set(OBSERVED_TYPES);
11
+ /**
12
+ * Converts an error to a performance mark name for debugging.
13
+ * @param error - The error that occurred
14
+ * @param entry - The performance entry that failed to encode
15
+ * @returns A mark name string
16
+ */
17
+ function errorToPerfMark(error, entry) {
18
+ const errorName = error instanceof Error ? error.name : 'UnknownError';
19
+ const entryName = entry.name || 'unnamed';
20
+ return `encode-error:${errorName}:${entryName}`;
21
+ }
22
+ /**
23
+ * Default threshold for triggering queue flushes based on queue length.
24
+ * When the queue length reaches (maxQueueSize - flushThreshold),
25
+ * a flush is triggered to prevent overflow. This provides a buffer zone
26
+ * before hitting the maximum queue capacity.
27
+ */
3
28
  export const DEFAULT_FLUSH_THRESHOLD = 20;
29
+ /**
30
+ * Default maximum number of items allowed in the queue before entries are dropped.
31
+ * This acts as a memory safety limit to prevent unbounded memory growth
32
+ * in case of sink slowdown or high-frequency performance entries.
33
+ */
34
+ export const DEFAULT_MAX_QUEUE_SIZE = 10_000;
35
+ /**
36
+ * Validates the flush threshold configuration to ensure sensible bounds.
37
+ *
38
+ * The flush threshold must be positive and cannot exceed the maximum queue size,
39
+ * as it represents a buffer zone within the queue capacity.
40
+ *
41
+ * @param flushThreshold - The threshold value to validate (must be > 0)
42
+ * @param maxQueueSize - The maximum queue size for comparison (flushThreshold <= maxQueueSize)
43
+ * @throws {Error} If flushThreshold is not positive or exceeds maxQueueSize
44
+ */
45
+ export function validateFlushThreshold(flushThreshold, maxQueueSize) {
46
+ if (flushThreshold <= 0) {
47
+ throw new Error('flushThreshold must be > 0');
48
+ }
49
+ if (flushThreshold > maxQueueSize) {
50
+ throw new Error('flushThreshold must be <= maxQueueSize');
51
+ }
52
+ }
53
+ /**
54
+ * A sink implementation that observes Node.js performance entries and forwards them to a configurable sink.
55
+ *
56
+ * This class provides a buffered, memory-safe bridge between Node.js PerformanceObserver
57
+ * and application-specific data sinks. It handles performance entry encoding, queue management,
58
+ * and graceful degradation under high load conditions.
59
+ *
60
+ * Performance entries flow through the following lifecycle:
61
+ *
62
+ * - Queued in Memory 💾
63
+ * - Items stored in queue (`#queue`) until flushed
64
+ * - Queue limited by `maxQueueSize` to prevent unbounded growth
65
+ * - Items remain in queue if sink is closed during flush
66
+ *
67
+ * - Successfully Written 📤
68
+ * - Items written to sink and counted in `getStats().written`
69
+ * - Queue cleared after successful batch writes
70
+ *
71
+ * - Item Disposition Scenarios 💥
72
+ * - **Encode Failure**: ❌ Items lost when `encode()` throws. Creates perf mark if debug env var (specified by `debugEnvVar`) is set to 'true'.
73
+ * - **Sink Write Failure**: 💾 Items stay in queue when sink write fails during flush
74
+ * - **Sink Closed**: 💾 Items stay in queue when sink is closed during flush
75
+ * - **Proactive Flush Throws**: 💾 Items stay in queue when `flush()` throws during threshold check
76
+ * - **Final Flush Throws**: 💾 Items stay in queue when `flush()` throws at end of callback
77
+ * - **Buffered Flush Throws**: 💾 Items stay in queue when buffered entries flush fails
78
+ * - **Queue Overflow**: ❌ Items dropped when queue reaches `maxQueueSize`
79
+ *
80
+ * @template T - The type of encoded performance data written to the sink
81
+ * @implements {Observer} - Lifecycle management interface
82
+ * @implements {Buffered} - Queue statistics interface
83
+ */
4
84
  export class PerformanceObserverSink {
5
- #encode;
85
+ /** Encoder function for transforming PerformanceEntry objects into domain types */
86
+ #encodePerfEntry;
87
+ /** Whether buffered observation mode is enabled */
6
88
  #buffered;
89
+ /** Threshold for triggering flushes based on queue length proximity to max capacity */
7
90
  #flushThreshold;
91
+ /** Maximum number of items allowed in queue before dropping new entries (hard memory limit) */
92
+ #maxQueueSize;
93
+ /** The target sink where encoded performance data is written */
8
94
  #sink;
95
+ /** Node.js PerformanceObserver instance, undefined when not subscribed */
9
96
  #observer;
10
- #pendingCount = 0;
11
- // "cursor" per type: how many we already wrote from the global buffer
12
- #written;
97
+ /** Bounded queue storing encoded performance items awaiting flush */
98
+ #queue = [];
99
+ /** Count of performance entries dropped due to queue overflow */
100
+ #dropped = 0;
101
+ /** Count of performance entries successfully written to sink */
102
+ #written = 0;
103
+ /** Number of items added to queue since last successful flush */
104
+ #addedSinceLastFlush = 0;
105
+ /** Whether debug mode is enabled for encode failures */
106
+ #debug;
107
+ /**
108
+ * Creates a new PerformanceObserverSink with the specified configuration.
109
+ *
110
+ * @param options - Configuration options for the performance observer sink
111
+ * @throws {Error} If flushThreshold validation fails (must be > 0 and <= maxQueueSize)
112
+ */
13
113
  constructor(options) {
14
- const { encode, sink, buffered, flushThreshold } = options;
15
- this.#encode = encode;
16
- this.#written = new Map(OBSERVED_TYPES.map(t => [t, 0]));
114
+ const { encodePerfEntry, sink, captureBufferedEntries, flushThreshold = DEFAULT_FLUSH_THRESHOLD, maxQueueSize = DEFAULT_MAX_QUEUE_SIZE, debugEnvVar = PROFILER_DEBUG_ENV_VAR, } = options;
115
+ this.#encodePerfEntry = encodePerfEntry;
17
116
  this.#sink = sink;
18
- this.#buffered = buffered ?? false;
19
- this.#flushThreshold = flushThreshold ?? DEFAULT_FLUSH_THRESHOLD;
117
+ this.#buffered = captureBufferedEntries ?? true;
118
+ this.#maxQueueSize = maxQueueSize;
119
+ validateFlushThreshold(flushThreshold, this.#maxQueueSize);
120
+ this.#flushThreshold = flushThreshold;
121
+ this.#debug = isEnvVarEnabled(debugEnvVar);
122
+ }
123
+ /**
124
+ * Returns whether debug mode is enabled for encode failures.
125
+ *
126
+ * Debug mode is determined by the environment variable specified by `debugEnvVar`
127
+ * (defaults to 'CP_PROFILER_DEBUG'). When enabled, encode failures create
128
+ * performance marks for debugging.
129
+ *
130
+ * @returns true if debug mode is enabled, false otherwise
131
+ */
132
+ get debug() {
133
+ return this.#debug;
20
134
  }
135
+ /**
136
+ * Returns current queue statistics for monitoring and debugging.
137
+ *
138
+ * Provides insight into the current state of the performance entry queue,
139
+ * useful for monitoring memory usage and processing throughput.
140
+ *
141
+ * @returns Object containing all states and entry counts
142
+ */
143
+ getStats() {
144
+ return {
145
+ isSubscribed: this.isSubscribed(),
146
+ queued: this.#queue.length,
147
+ dropped: this.#dropped,
148
+ written: this.#written,
149
+ maxQueueSize: this.#maxQueueSize,
150
+ flushThreshold: this.#flushThreshold,
151
+ addedSinceLastFlush: this.#addedSinceLastFlush,
152
+ buffered: this.#buffered,
153
+ };
154
+ }
155
+ /**
156
+ * Encodes a raw PerformanceEntry using the configured encoder function.
157
+ *
158
+ * This method delegates to the user-provided encoder function, allowing
159
+ * transformation of Node.js performance entries into application-specific types.
160
+ *
161
+ * @param entry - The raw performance entry to encode
162
+ * @returns Readonly array of encoded items
163
+ */
21
164
  encode(entry) {
22
- return this.#encode(entry);
165
+ return this.#encodePerfEntry(entry);
23
166
  }
167
+ /**
168
+ * Starts observing performance entries and forwarding them to the sink.
169
+ *
170
+ * Creates a Node.js PerformanceObserver that monitors 'mark' and 'measure' entries.
171
+ * The observer uses a bounded queue with proactive flushing to manage memory usage.
172
+ * When buffered mode is enabled, any existing buffered entries are immediately flushed.
173
+ * If the sink is closed, items stay in the queue until reopened.
174
+ *
175
+ */
24
176
  subscribe() {
25
177
  if (this.#observer) {
26
178
  return;
27
179
  }
28
- // Only used to trigger the flush - it's not processing the entries, just counting them
29
- this.#observer = new PerformanceObserver((list) => {
30
- const batchCount = OBSERVED_TYPES.reduce((n, t) => n + list.getEntriesByType(t).length, 0);
31
- this.#pendingCount += batchCount;
32
- if (this.#pendingCount >= this.#flushThreshold) {
180
+ this.#observer = new PerformanceObserver(list => {
181
+ list.getEntries().forEach(entry => {
182
+ if (OBSERVED_TYPE_SET.has(entry.entryType)) {
183
+ try {
184
+ const items = this.encode(entry);
185
+ items.forEach(item => {
186
+ // ❌ MAX QUEUE OVERFLOW
187
+ if (this.#queue.length >= this.#maxQueueSize) {
188
+ this.#dropped++; // Items are lost forever
189
+ return;
190
+ }
191
+ if (this.#queue.length >=
192
+ this.#maxQueueSize - this.#flushThreshold) {
193
+ this.flush();
194
+ }
195
+ this.#queue.push(item);
196
+ this.#addedSinceLastFlush++;
197
+ });
198
+ }
199
+ catch (error) {
200
+ // ❌ Encode failure: item lost forever as user has to fix encode function.
201
+ this.#dropped++;
202
+ if (this.#debug) {
203
+ try {
204
+ performance.mark(errorToPerfMark(error, entry));
205
+ }
206
+ catch {
207
+ // Ignore mark failures to prevent double errors
208
+ }
209
+ }
210
+ }
211
+ }
212
+ });
213
+ if (this.#addedSinceLastFlush >= this.#flushThreshold) {
33
214
  this.flush();
34
215
  }
35
216
  });
@@ -37,33 +218,65 @@ export class PerformanceObserverSink {
37
218
  entryTypes: OBSERVED_TYPES,
38
219
  buffered: this.#buffered,
39
220
  });
221
+ if (this.#buffered) {
222
+ this.flush();
223
+ }
40
224
  }
225
+ /**
226
+ * Flushes all queued performance entries to the sink.
227
+ *
228
+ * Writes all currently queued encoded performance entries to the configured sink.
229
+ * If the sink is closed, flush is a no-op and items stay in the queue until reopened.
230
+ * The queue is always cleared after flush attempt, regardless of success or failure.
231
+ */
41
232
  flush() {
42
- if (!this.#observer) {
233
+ if (this.#queue.length === 0) {
234
+ return;
235
+ }
236
+ if (this.#sink.isClosed()) {
43
237
  return;
44
238
  }
45
- OBSERVED_TYPES.forEach(t => {
46
- const written = this.#written.get(t) ?? 0;
47
- const fresh = performance.getEntriesByType(t).slice(written);
239
+ // Process each item in queue
240
+ const failedItems = [];
241
+ this.#queue.forEach(item => {
48
242
  try {
49
- fresh
50
- .flatMap(entry => this.encode(entry))
51
- .forEach(item => this.#sink.append(item));
52
- this.#written.set(t, written + fresh.length);
243
+ this.#sink.append(item);
244
+ this.#written++;
53
245
  }
54
- catch (error) {
55
- throw new Error('PerformanceObserverSink failed to write items to sink.', { cause: error });
246
+ catch {
247
+ failedItems.push(item);
56
248
  }
57
249
  });
58
- this.#pendingCount = 0;
250
+ // Clear queue but keep failed items for retry
251
+ this.#queue.length = 0;
252
+ this.#queue.push(...failedItems);
253
+ this.#addedSinceLastFlush = failedItems.length;
59
254
  }
255
+ /**
256
+ * Stops observing performance entries and cleans up resources.
257
+ *
258
+ * Performs a final flush of any remaining queued entries, then disconnects
259
+ * the PerformanceObserver and releases all references.
260
+ *
261
+ * This method is idempotent - safe to call multiple times.
262
+ */
60
263
  unsubscribe() {
61
264
  if (!this.#observer) {
62
265
  return;
63
266
  }
64
- this.#observer?.disconnect();
267
+ this.flush();
268
+ this.#addedSinceLastFlush = 0;
269
+ this.#observer.disconnect();
65
270
  this.#observer = undefined;
66
271
  }
272
+ /**
273
+ * Checks whether the performance observer is currently active.
274
+ *
275
+ * Returns true if the sink is subscribed and actively observing performance entries.
276
+ * This indicates that a PerformanceObserver instance exists and is connected.
277
+ *
278
+ * @returns true if currently subscribed and observing, false otherwise
279
+ */
67
280
  isSubscribed() {
68
281
  return this.#observer !== undefined;
69
282
  }