@hkdigital/lib-core 0.4.24 → 0.4.26

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.
Files changed (30) hide show
  1. package/dist/logging/internal/adapters/pino.d.ts +7 -3
  2. package/dist/logging/internal/adapters/pino.js +200 -67
  3. package/dist/logging/internal/transports/pretty-transport.d.ts +17 -0
  4. package/dist/logging/internal/transports/pretty-transport.js +104 -0
  5. package/dist/logging/internal/transports/test-transport.d.ts +19 -0
  6. package/dist/logging/internal/transports/test-transport.js +79 -0
  7. package/dist/network/loaders/audio/AudioScene.svelte.d.ts +19 -10
  8. package/dist/network/loaders/audio/AudioScene.svelte.js +50 -75
  9. package/dist/network/loaders/image/ImageScene.svelte.d.ts +13 -13
  10. package/dist/network/loaders/image/ImageScene.svelte.js +56 -83
  11. package/dist/network/states/NetworkLoader.svelte.d.ts +6 -0
  12. package/dist/network/states/NetworkLoader.svelte.js +15 -6
  13. package/dist/services/service-base/ServiceBase.d.ts +12 -8
  14. package/dist/services/service-base/ServiceBase.js +8 -6
  15. package/dist/state/machines/finite-state-machine/FiniteStateMachine.svelte.d.ts +5 -9
  16. package/dist/state/machines/finite-state-machine/FiniteStateMachine.svelte.js +62 -32
  17. package/dist/state/machines/finite-state-machine/README.md +48 -46
  18. package/dist/state/machines/finite-state-machine/constants.d.ts +13 -0
  19. package/dist/state/machines/finite-state-machine/constants.js +15 -0
  20. package/dist/state/machines/finite-state-machine/index.d.ts +1 -0
  21. package/dist/state/machines/finite-state-machine/index.js +1 -0
  22. package/dist/state/machines/finite-state-machine/typedef.d.ts +3 -3
  23. package/dist/state/machines/finite-state-machine/typedef.js +21 -15
  24. package/dist/state/machines/loading-state-machine/LoadingStateMachine.svelte.d.ts +12 -0
  25. package/dist/state/machines/loading-state-machine/LoadingStateMachine.svelte.js +27 -2
  26. package/dist/state/machines/loading-state-machine/README.md +89 -41
  27. package/dist/state/machines/loading-state-machine/constants.d.ts +2 -0
  28. package/dist/state/machines/loading-state-machine/constants.js +2 -0
  29. package/package.json +1 -1
  30. package/dist/logging/internal/adapters/pino.js__ +0 -260
@@ -8,13 +8,18 @@ export class PinoAdapter {
8
8
  * @param {Object} [options] - Pino configuration options
9
9
  */
10
10
  constructor(options?: any);
11
- pino: pino.Logger<never, boolean>;
11
+ /**
12
+ * Promise that resolves when transport is ready
13
+ *
14
+ * @returns {Promise<void>} Promise that resolves when ready
15
+ */
16
+ ready(): Promise<void>;
12
17
  /**
13
18
  * Handle log events from Logger
14
19
  *
15
20
  * @param {Object} logEvent - Log event from Logger
16
21
  */
17
- handleLog(logEvent: any): void;
22
+ handleLog(logEvent: any): Promise<void>;
18
23
  /**
19
24
  * Create a child logger with additional context
20
25
  *
@@ -24,4 +29,3 @@ export class PinoAdapter {
24
29
  child(context: any): PinoAdapter;
25
30
  #private;
26
31
  }
27
- import pino from 'pino';
@@ -4,11 +4,12 @@
4
4
  import pino from 'pino';
5
5
  import { dev } from '$app/environment';
6
6
 
7
+ import { HkPromise } from '../../../generic/promises.js';
8
+
7
9
  import {
8
10
  detectErrorMeta,
9
11
  findRelevantFrameIndex,
10
- formatErrorDisplay,
11
- parseFunctionName
12
+ formatErrorDisplay
12
13
  } from './formatting.js';
13
14
 
14
15
  /**
@@ -17,26 +18,38 @@ import {
17
18
  export class PinoAdapter {
18
19
  #projectRoot = null;
19
20
 
21
+ /** @type {import('pino').Logger|Object|null} */
22
+ #pino = null;
23
+
24
+ #options = null;
25
+ #messageQueue = [];
26
+ #isInitializing = false;
27
+ #isTransportReady = false;
28
+ #retryCount = 0;
29
+ #maxRetries = 3;
30
+ #retryDelay = 1000;
31
+ #readyPromise = null;
32
+
20
33
  /**
21
34
  * Create a new PinoAdapter
22
35
  *
23
36
  * @param {Object} [options] - Pino configuration options
24
37
  */
25
38
  constructor(options = {}) {
26
- // Determine project root once for stack trace cleaning
27
39
  this.#projectRoot = import.meta.env.VITE_PROJECT_ROOT || process.cwd();
28
- const baseOptions = {
29
- serializers: {
40
+ this.#options = options;
41
+ this.#readyPromise = new HkPromise();
42
+ }
30
43
 
31
- //
32
- // Use 'errors' property to trigger Pino's error serializer
33
- // The serializer traverses the error.cause chain and outputs an array
34
- // of serialized error objects, which is why we use 'errors' (plural)
35
- // instead of Pino's standard 'err' property (which expects a single
36
- // error)
37
- //
44
+ /**
45
+ * Get base configuration options for pino
46
+ *
47
+ * @returns {Object} Base pino configuration
48
+ */
49
+ #getBaseOptions() {
50
+ return {
51
+ serializers: {
38
52
  errors: (err) => {
39
-
40
53
  /** @type {import('./typedef').ErrorSummary[]} */
41
54
  const chain = [];
42
55
  let loggedAt = null;
@@ -45,7 +58,6 @@ export class PinoAdapter {
45
58
  let isFirst = true;
46
59
 
47
60
  while (current && current instanceof Error) {
48
- // Check if this is the first error and it's a LoggerError - extract logging context
49
61
  if (isFirst && current.name === 'LoggerError') {
50
62
  if (current.stack) {
51
63
  const cleanedStackString = this.#cleanStackTrace(current.stack);
@@ -57,24 +69,23 @@ export class PinoAdapter {
57
69
  line && line !== current.name + ': ' + current.message
58
70
  );
59
71
 
60
- // For LoggerError, we know it's a logger.error call, so find the relevant frame
61
- const loggerErrorIndex = cleanedStackArray.findIndex(frame =>
62
- (frame.includes('Logger.error') && frame.includes('logger/Logger.js')) ||
63
- (frame.includes('error@') && frame.includes('logger/Logger.js'))
72
+ const loggerErrorIndex = cleanedStackArray.findIndex(
73
+ (frame) =>
74
+ (frame.includes('Logger.error') &&
75
+ frame.includes('logger/Logger.js')) ||
76
+ (frame.includes('error@') &&
77
+ frame.includes('logger/Logger.js'))
64
78
  );
65
79
 
66
- if (loggerErrorIndex >= 0 && loggerErrorIndex + 1 < cleanedStackArray.length) {
80
+ if (
81
+ loggerErrorIndex >= 0 &&
82
+ loggerErrorIndex + 1 < cleanedStackArray.length
83
+ ) {
67
84
  const relevantFrame = cleanedStackArray[loggerErrorIndex + 1];
68
-
69
- // Extract function name from the relevant frame
70
- // const functionName = parseFunctionName(relevantFrame);
71
-
72
- // const errorType = functionName ? `logger.error in ${functionName}` : 'logger.error';
73
- loggedAt = relevantFrame.slice(3); // remove "at "
85
+ loggedAt = relevantFrame.slice(3);
74
86
  }
75
87
  }
76
88
 
77
- // Skip the LoggerError and move to the actual error
78
89
  current = current.cause;
79
90
  isFirst = false;
80
91
  continue;
@@ -85,9 +96,7 @@ export class PinoAdapter {
85
96
  message: current.message
86
97
  };
87
98
 
88
- // Add error metadata for structured logging and terminal display
89
99
  if (current.stack) {
90
- // Convert cleaned stack string to array format expected by formatting functions
91
100
  const cleanedStackString = this.#cleanStackTrace(current.stack);
92
101
  const cleanedStackArray = cleanedStackString
93
102
  .split('\n')
@@ -106,18 +115,18 @@ export class PinoAdapter {
106
115
  serialized.meta = errorMeta;
107
116
  serialized.errorType = formatErrorDisplay(errorMeta);
108
117
 
109
- // Include stack frames for terminal display
110
118
  serialized.stackFrames = cleanedStackArray
111
119
  .slice(0, 9)
112
120
  .map((frame, index) => {
113
121
  const marker = index === relevantFrameIndex ? '→' : ' ';
114
-
115
122
  return `${marker} ${frame}`;
116
123
  });
117
124
  }
118
125
 
119
- // Include HttpError-specific properties
120
- const httpError = /** @type {import('../../../network/errors.js').HttpError} */ (current);
126
+ const httpError =
127
+ /** @type {import('../../../network/errors.js').HttpError} */ (
128
+ current
129
+ );
121
130
  if (httpError.status !== undefined) {
122
131
  serialized.status = httpError.status;
123
132
  }
@@ -134,27 +143,50 @@ export class PinoAdapter {
134
143
  }
135
144
  }
136
145
  };
146
+ }
147
+
148
+ /**
149
+ * Initialize pino instance with retry logic for transport setup
150
+ */
151
+ async #initializePino() {
152
+ if (this.#isInitializing || this.#pino) {
153
+ return;
154
+ }
155
+
156
+ this.#isInitializing = true;
137
157
 
138
- // Add error handling for missing pino-pretty in dev
139
- if ( dev) {
140
- const devOptions = {
141
- level: 'debug',
142
- transport: {
143
- target: 'pino-pretty',
144
- options: {
158
+ const baseOptions = this.#getBaseOptions();
159
+
160
+ while (this.#retryCount <= this.#maxRetries) {
161
+ try {
162
+ if (dev) {
163
+ // Use intermediate transport to avoid worker thread issues
164
+ const { default: createPrettyTransport } = await import('../transports/pretty-transport.js');
165
+ const prettyTransport = await createPrettyTransport({
145
166
  colorize: true,
146
167
  ignore: 'hostname,pid'
147
- }
168
+ });
169
+
170
+ const devOptions = {
171
+ level: 'debug'
172
+ };
173
+
174
+ this.#pino = pino({ ...baseOptions, ...devOptions, ...this.#options }, prettyTransport);
175
+ } else {
176
+ this.#pino = pino({ ...baseOptions, ...this.#options });
148
177
  }
149
- };
150
178
 
151
- try {
152
- this.pino = pino({ ...baseOptions, ...devOptions, ...options });
179
+ this.#isTransportReady = true;
180
+ this.#isInitializing = false;
181
+ this.#flushMessageQueue();
182
+ this.#readyPromise.tryResolve();
183
+ return;
184
+
153
185
  } catch (error) {
154
- if (
155
- error.message.includes('Cannot find module') &&
156
- error.message.includes('pino-pretty')
157
- ) {
186
+ this.#retryCount++;
187
+
188
+ if (error.message.includes('Cannot find module') &&
189
+ error.message.includes('pino-pretty')) {
158
190
  const errorMessage = `
159
191
  ╭─────────────────────────────────────────────────────────────╮
160
192
  │ Missing Dependency │
@@ -165,13 +197,82 @@ export class PinoAdapter {
165
197
  console.error(errorMessage);
166
198
  throw new Error('pino-pretty is required for development mode');
167
199
  }
168
- throw error;
200
+
201
+ if (this.#retryCount > this.#maxRetries) {
202
+ console.error('Failed to initialize pino transport after retries:', error.message);
203
+ this.#fallbackToConsole();
204
+ this.#readyPromise.tryResolve();
205
+ return;
206
+ }
207
+
208
+ await new Promise(resolve => setTimeout(resolve, this.#retryDelay * this.#retryCount));
209
+ }
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Flush queued messages to pino transport
215
+ */
216
+ #flushMessageQueue() {
217
+ if (!this.#pino || !this.#isTransportReady) {
218
+ return;
219
+ }
220
+
221
+ while (this.#messageQueue.length > 0) {
222
+ const queuedLog = this.#messageQueue.shift();
223
+ try {
224
+ this.#pino[queuedLog.level](queuedLog.data, queuedLog.message);
225
+ } catch (error) {
226
+ console.error('Failed to flush queued log message:', error.message);
227
+ this.#isTransportReady = false;
228
+ this.#messageQueue.unshift(queuedLog);
229
+ break;
169
230
  }
170
- } else {
171
- this.pino = pino({ ...baseOptions, ...options });
172
231
  }
173
232
  }
174
233
 
234
+ /**
235
+ * Fallback to console logging when transport fails
236
+ */
237
+ #fallbackToConsole() {
238
+ this.#isTransportReady = true;
239
+ this.#isInitializing = false;
240
+
241
+ while (this.#messageQueue.length > 0) {
242
+ const queuedLog = this.#messageQueue.shift();
243
+ console[queuedLog.level === 'error' ? 'error' : 'log'](
244
+ `[${queuedLog.level.toUpperCase()}] ${queuedLog.message}`,
245
+ queuedLog.data
246
+ );
247
+ }
248
+
249
+ this.#pino = {
250
+ debug: (data, msg) => console.log(`[DEBUG] ${msg}`, data),
251
+ info: (data, msg) => console.log(`[INFO] ${msg}`, data),
252
+ warn: (data, msg) => console.warn(`[WARN] ${msg}`, data),
253
+ error: (data, msg) => console.error(`[ERROR] ${msg}`, data),
254
+ // eslint-disable-next-line no-unused-vars
255
+ child: (context) => this
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Promise that resolves when transport is ready
261
+ *
262
+ * @returns {Promise<void>} Promise that resolves when ready
263
+ */
264
+ ready() {
265
+ if (this.#isTransportReady) {
266
+ return Promise.resolve();
267
+ }
268
+
269
+ if (!this.#isInitializing) {
270
+ this.#initializePino();
271
+ }
272
+
273
+ return this.#readyPromise;
274
+ }
275
+
175
276
  /**
176
277
  * Clean stack trace by removing project root path and simplifying node_modules
177
278
  *
@@ -201,8 +302,7 @@ export class PinoAdapter {
201
302
 
202
303
  // Simplify pnpm paths: node_modules/.pnpm/package@version_deps/node_modules/package
203
304
  // becomes: node_modules/package
204
- const pnpmRegex =
205
- /node_modules\/\.pnpm\/([^@/]+)@[^/]+\/node_modules\/\1/g;
305
+ const pnpmRegex = /node_modules\/\.pnpm\/([^@/]+)@[^/]+\/node_modules\/\1/g;
206
306
  cleaned = cleaned.replace(pnpmRegex, 'node_modules/$1');
207
307
 
208
308
  // Also handle cases where the package name might be different in the final path
@@ -211,9 +311,10 @@ export class PinoAdapter {
211
311
 
212
312
  // Filter out Node.js internal modules and internal logger methods
213
313
  const lines = cleaned.split('\n');
214
- const filteredLines = lines.filter(line =>
215
- !line.includes('node:internal') &&
216
- !(line.includes('#toError') && line.includes('logger/Logger.js'))
314
+ const filteredLines = lines.filter(
315
+ (line) =>
316
+ !line.includes('node:internal') &&
317
+ !(line.includes('#toError') && line.includes('logger/Logger.js'))
217
318
  );
218
319
  cleaned = filteredLines.join('\n');
219
320
 
@@ -225,7 +326,7 @@ export class PinoAdapter {
225
326
  *
226
327
  * @param {Object} logEvent - Log event from Logger
227
328
  */
228
- handleLog(logEvent) {
329
+ async handleLog(logEvent) {
229
330
  const { level, message, details, source, timestamp } = logEvent;
230
331
 
231
332
  const logData = {
@@ -233,34 +334,58 @@ export class PinoAdapter {
233
334
  timestamp
234
335
  };
235
336
 
236
- // Check if details contains an error and promote it to error property for
237
- // pino serializer
238
337
  if (details) {
239
338
  if (details instanceof Error) {
240
- // details is directly an error
241
339
  logData.errors = details;
242
340
  } else if (details.error instanceof Error) {
243
- // details has an error property
244
341
  logData.errors = details.error;
245
- // Include other details except the error
246
342
  // eslint-disable-next-line no-unused-vars
247
343
  const { error, ...otherDetails } = details;
248
344
  if (Object.keys(otherDetails).length > 0) {
249
345
  logData.details = otherDetails;
250
346
  }
251
347
  } else {
252
- // No error found in details, include all details
253
348
  logData.details = details;
254
349
  }
255
350
  }
256
351
 
257
- // Check if we have loggedAt info from the serializer
258
- if (logData.errors && typeof logData.errors === 'object' && logData.errors.loggedAt) {
352
+ if (
353
+ logData.errors &&
354
+ typeof logData.errors === 'object' &&
355
+ logData.errors.loggedAt
356
+ ) {
259
357
  logData.loggedAt = logData.errors.loggedAt;
260
358
  logData.errors = logData.errors.chain;
261
359
  }
262
360
 
263
- this.pino[level](logData, message);
361
+ // Queue message if transport not ready
362
+ if (!this.#isTransportReady) {
363
+ this.#messageQueue.push({ level, data: logData, message });
364
+
365
+ // Limit queue size to prevent memory issues
366
+ if (this.#messageQueue.length > 1000) {
367
+ this.#messageQueue.shift();
368
+ }
369
+
370
+ // Initialize transport if not already doing so
371
+ if (!this.#isInitializing) {
372
+ this.#initializePino();
373
+ }
374
+ return;
375
+ }
376
+
377
+ // Try to log directly, queue on failure
378
+ try {
379
+ this.#pino[level](logData, message);
380
+ } catch (error) {
381
+ console.error('Transport failed, queuing message:', error.message);
382
+ this.#isTransportReady = false;
383
+ this.#messageQueue.push({ level, data: logData, message });
384
+
385
+ // Retry transport initialization
386
+ this.#retryCount = 0;
387
+ setTimeout(() => this.#initializePino(), this.#retryDelay);
388
+ }
264
389
  }
265
390
 
266
391
  /**
@@ -270,9 +395,17 @@ export class PinoAdapter {
270
395
  * @returns {PinoAdapter} New adapter instance with context
271
396
  */
272
397
  child(context) {
273
- const childPino = this.pino.child(context);
398
+ if (!this.#pino) {
399
+ // Return new adapter that inherits parent options with context
400
+ return new PinoAdapter({ ...this.#options, ...context });
401
+ }
402
+
403
+ const childPino = this.#pino.child(context);
274
404
  const adapter = new PinoAdapter();
275
- adapter.pino = childPino;
405
+ adapter.#pino = childPino;
406
+ adapter.#isTransportReady = this.#isTransportReady;
407
+ adapter.#readyPromise = new HkPromise();
408
+ adapter.#readyPromise.tryResolve();
276
409
  return adapter;
277
410
  }
278
411
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Custom transport wrapper for pino-pretty that avoids worker threads
3
+ * and provides lazy loading to prevent Vite hot reload interference
4
+ */
5
+ /**
6
+ * Create a pino-pretty transport that loads synchronously but initializes
7
+ * pino-pretty lazily to avoid worker thread conflicts with Vite
8
+ *
9
+ * @param {Object} [opts] - pino-pretty options
10
+ * @param {boolean} [opts.colorize=true] - Enable colored output
11
+ * @param {string} [opts.ignore] - Fields to ignore in output
12
+ * @returns {Promise<Object>} Transport-compatible object
13
+ */
14
+ export default function createPrettyTransport(opts?: {
15
+ colorize?: boolean;
16
+ ignore?: string;
17
+ }): Promise<any>;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Custom transport wrapper for pino-pretty that avoids worker threads
3
+ * and provides lazy loading to prevent Vite hot reload interference
4
+ */
5
+
6
+ /**
7
+ * Create a pino-pretty transport that loads synchronously but initializes
8
+ * pino-pretty lazily to avoid worker thread conflicts with Vite
9
+ *
10
+ * @param {Object} [opts] - pino-pretty options
11
+ * @param {boolean} [opts.colorize=true] - Enable colored output
12
+ * @param {string} [opts.ignore] - Fields to ignore in output
13
+ * @returns {Promise<Object>} Transport-compatible object
14
+ */
15
+ export default async function createPrettyTransport(opts = {}) {
16
+ let prettyStream = null;
17
+ let messageQueue = [];
18
+ let isInitializing = false;
19
+
20
+ /**
21
+ * Initialize pino-pretty stream
22
+ */
23
+ async function initializePrettyStream() {
24
+ if (isInitializing || prettyStream) {
25
+ return;
26
+ }
27
+
28
+ isInitializing = true;
29
+
30
+ try {
31
+ const { default: pinoPretty } = await import('pino-pretty');
32
+ prettyStream = pinoPretty({
33
+ colorize: true,
34
+ ignore: 'hostname,pid',
35
+ ...opts
36
+ });
37
+
38
+ // Flush queued messages
39
+ while (messageQueue.length > 0) {
40
+ const chunk = messageQueue.shift();
41
+ prettyStream.write(chunk);
42
+ }
43
+
44
+ } catch (error) {
45
+ console.error('Failed to initialize pino-pretty:', error.message);
46
+
47
+ // Fallback to console output
48
+ prettyStream = {
49
+ write(chunk) {
50
+ try {
51
+ const log = JSON.parse(chunk);
52
+ const level = log.level >= 50 ? 'ERROR' :
53
+ log.level >= 40 ? 'WARN' :
54
+ log.level >= 30 ? 'INFO' : 'DEBUG';
55
+ console.log(`[${level}] ${log.msg}`, log);
56
+ } catch {
57
+ console.log(chunk);
58
+ }
59
+ }
60
+ };
61
+
62
+ // Flush queue to console fallback
63
+ while (messageQueue.length > 0) {
64
+ const chunk = messageQueue.shift();
65
+ prettyStream.write(chunk);
66
+ }
67
+ }
68
+
69
+ isInitializing = false;
70
+ }
71
+
72
+ return {
73
+ /**
74
+ * Write log message to transport
75
+ *
76
+ * @param {string} chunk - JSON log message
77
+ */
78
+ write(chunk) {
79
+ if (!prettyStream) {
80
+ messageQueue.push(chunk);
81
+
82
+ // Limit queue size
83
+ if (messageQueue.length > 1000) {
84
+ messageQueue.shift();
85
+ }
86
+
87
+ // Start initialization if not already started
88
+ if (!isInitializing) {
89
+ initializePrettyStream();
90
+ }
91
+ return;
92
+ }
93
+
94
+ try {
95
+ prettyStream.write(chunk);
96
+ } catch (error) {
97
+ console.error('Pretty stream write failed:', error.message);
98
+ messageQueue.push(chunk);
99
+ prettyStream = null;
100
+ initializePrettyStream();
101
+ }
102
+ }
103
+ };
104
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Test transport for validating lazy initialization behavior
3
+ */
4
+ /**
5
+ * Create a mock transport for testing lazy initialization scenarios
6
+ *
7
+ * @param {Object} [opts] - Test configuration options
8
+ * @param {number} [opts.delay=100] - Initialization delay in milliseconds
9
+ * @param {boolean} [opts.shouldFail=false] - Whether initialization should fail
10
+ * @param {string} [opts.failureMessage] - Custom failure message
11
+ * @param {boolean} [opts.shouldFailOnWrite=false] - Fail on write operations
12
+ * @returns {Promise<Object>} Transport-compatible object
13
+ */
14
+ export default function createTestTransport(opts?: {
15
+ delay?: number;
16
+ shouldFail?: boolean;
17
+ failureMessage?: string;
18
+ shouldFailOnWrite?: boolean;
19
+ }): Promise<any>;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Test transport for validating lazy initialization behavior
3
+ */
4
+
5
+ /**
6
+ * Create a mock transport for testing lazy initialization scenarios
7
+ *
8
+ * @param {Object} [opts] - Test configuration options
9
+ * @param {number} [opts.delay=100] - Initialization delay in milliseconds
10
+ * @param {boolean} [opts.shouldFail=false] - Whether initialization should fail
11
+ * @param {string} [opts.failureMessage] - Custom failure message
12
+ * @param {boolean} [opts.shouldFailOnWrite=false] - Fail on write operations
13
+ * @returns {Promise<Object>} Transport-compatible object
14
+ */
15
+ export default async function createTestTransport(opts = {}) {
16
+ const {
17
+ delay = 100,
18
+ shouldFail = false,
19
+ failureMessage = 'Test transport setup failed',
20
+ shouldFailOnWrite = false
21
+ } = opts;
22
+
23
+ return new Promise((resolve, reject) => {
24
+ setTimeout(() => {
25
+ if (shouldFail) {
26
+ reject(new Error(failureMessage));
27
+ return;
28
+ }
29
+
30
+ const messages = [];
31
+
32
+ resolve({
33
+ /**
34
+ * Write log message to test transport
35
+ *
36
+ * @param {string} chunk - JSON log message
37
+ */
38
+ write(chunk) {
39
+ if (shouldFailOnWrite) {
40
+ throw new Error('Test transport write failure');
41
+ }
42
+
43
+ messages.push(chunk);
44
+ },
45
+
46
+ /**
47
+ * Get all captured messages
48
+ *
49
+ * @returns {string[]} Array of log message chunks
50
+ */
51
+ getMessages() {
52
+ return [...messages];
53
+ },
54
+
55
+ /**
56
+ * Clear captured messages
57
+ */
58
+ clear() {
59
+ messages.length = 0;
60
+ },
61
+
62
+ /**
63
+ * Get parsed log objects from captured messages
64
+ *
65
+ * @returns {Object[]} Array of parsed log objects
66
+ */
67
+ getParsedMessages() {
68
+ return messages.map(chunk => {
69
+ try {
70
+ return JSON.parse(chunk);
71
+ } catch {
72
+ return { raw: chunk };
73
+ }
74
+ });
75
+ }
76
+ });
77
+ }, delay);
78
+ });
79
+ }