@plyaz/core 1.8.4 → 1.9.0

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.
@@ -3,8 +3,8 @@ import { mergeConfigs, createApiClient, ApiPackageError, setDefaultApiClient } f
3
3
  import { DEVELOPMENT_CONFIG, STAGING_CONFIG, PRODUCTION_CONFIG } from '@plyaz/config';
4
4
  import { HTTP_STATUS, ERROR_CATEGORY, ERROR_CODES, CORE_EVENTS, PACKAGE_STATUS_CODES, API_ERROR_CODES, OPERATIONS, BACKEND_RUNTIMES, FRONTEND_RUNTIMES } from '@plyaz/types';
5
5
  import { EventEmitter } from 'events';
6
- import { generateRequestId, CorePackageError, initializeErrorSystem, BaseError, DatabasePackageError } from '@plyaz/errors';
7
- import { ERROR_CODES as ERROR_CODES$1, DATABASE_ERROR_CODES } from '@plyaz/types/errors';
6
+ import { generateRequestId, CorePackageError, BaseError, initializeErrorSystem, DatabasePackageError } from '@plyaz/errors';
7
+ import { ERROR_CODES as ERROR_CODES$1, ERROR_CATEGORY as ERROR_CATEGORY$1, DATABASE_ERROR_CODES } from '@plyaz/types/errors';
8
8
  import { OBSERVABILITY_METRICS } from '@plyaz/types/observability';
9
9
  import { clearEventEmitter, setEventEmitter, initializeGlobalErrorHandler } from '@plyaz/errors/middleware';
10
10
  import { STORE_KEYS, useRootStore, createStandaloneFeatureFlagStore } from '@plyaz/store';
@@ -23,6 +23,14 @@ var HEX_RADIX = 16;
23
23
  var HEX_SLICE_START = 2;
24
24
  var HEX_SLICE_END = 18;
25
25
  var SPAN_ID_LENGTH = 16;
26
+ function generateId() {
27
+ const cryptoApi = globalThis.crypto;
28
+ if (cryptoApi?.randomUUID) {
29
+ return cryptoApi.randomUUID();
30
+ }
31
+ return `${Date.now()}-${Math.random().toString(RANDOM_ID_RADIX).slice(RANDOM_ID_SLICE_START)}`;
32
+ }
33
+ __name(generateId, "generateId");
26
34
  function generateShortId() {
27
35
  const cryptoApi = globalThis.crypto;
28
36
  if (cryptoApi?.randomUUID) {
@@ -303,6 +311,80 @@ var ApiClientService = class _ApiClientService {
303
311
  static {
304
312
  this.initPromise = null;
305
313
  }
314
+ /**
315
+ * Build the core error handler for API clients.
316
+ * This handler emits errors to CoreEventManager for global error handling.
317
+ *
318
+ * Handles:
319
+ * - Single errors (network, timeout)
320
+ * - Array of errors from API responses (validation, business logic)
321
+ * - Serialization to unified SerializedError format
322
+ * - Event emission to CORE_EVENTS.SYSTEM.ERROR and CORE_EVENTS.API.REQUEST_ERROR
323
+ *
324
+ * @returns Error handler function compatible with ApiClientOptions.onError
325
+ */
326
+ static buildCoreErrorHandler() {
327
+ return async (error) => {
328
+ const requestId = generateRequestId();
329
+ const method = error.config?.method ?? "UNKNOWN";
330
+ const url = error.config?.url ?? "unknown";
331
+ try {
332
+ const errorDetails = Array.isArray(error.response?.data) ? error.response.data : [];
333
+ if (errorDetails.length === 0) {
334
+ const serializedError = {
335
+ id: requestId,
336
+ code: ERROR_CODES.CORE_API_CLIENT_REQUEST_FAILED,
337
+ message: error.message ?? "API request failed",
338
+ status: error.status,
339
+ category: ERROR_CATEGORY.Network,
340
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
341
+ isRetryable: error.status ? error.status >= HTTP_STATUS.INTERNAL_SERVER_ERROR : false,
342
+ source: "api-client",
343
+ dismissed: false,
344
+ context: {
345
+ method,
346
+ url,
347
+ requestId
348
+ }
349
+ };
350
+ CoreEventManager.emit(CORE_EVENTS.SYSTEM.ERROR, { errors: [serializedError] });
351
+ } else {
352
+ const serializedErrors = errorDetails.map(
353
+ (detail, index) => ({
354
+ id: `${requestId}-${index}`,
355
+ code: detail.errorCode ?? ERROR_CODES.CORE_API_CLIENT_REQUEST_FAILED,
356
+ message: detail.message ?? error.message ?? "API request failed",
357
+ status: error.status,
358
+ category: ERROR_CATEGORY.Network,
359
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
360
+ isRetryable: error.status ? error.status >= HTTP_STATUS.INTERNAL_SERVER_ERROR : false,
361
+ source: "api-client",
362
+ dismissed: false,
363
+ context: {
364
+ method,
365
+ url,
366
+ requestId,
367
+ field: detail.field,
368
+ valueGiven: detail.valueGiven,
369
+ allowedValues: detail.allowedValues,
370
+ constraints: detail.constraints
371
+ }
372
+ })
373
+ );
374
+ CoreEventManager.emit(CORE_EVENTS.SYSTEM.ERROR, { errors: serializedErrors });
375
+ }
376
+ } catch (e) {
377
+ console.error("[ApiClientService] Failed to emit error event:", e);
378
+ }
379
+ _ApiClientService.emitApiError(error, {
380
+ method,
381
+ url,
382
+ requestId,
383
+ status: error.status,
384
+ duration: 0
385
+ });
386
+ };
387
+ }
306
388
  /**
307
389
  * Initialize the API client with environment config and API options
308
390
  *
@@ -340,71 +422,12 @@ var ApiClientService = class _ApiClientService {
340
422
  * 2. Environment metadata (envConfig - apiKey)
341
423
  * 3. API configuration (apiConfig - baseURL, encryption, timeout, etc.)
342
424
  */
343
- // eslint-disable-next-line max-lines-per-function, complexity
425
+ // eslint-disable-next-line complexity
344
426
  static async createClient(envConfig, apiConfig) {
345
427
  try {
346
428
  const envDefaults = getConfigForEnvironment(envConfig.env);
347
429
  const envMetadataMapped = mapEnvironmentMetadata(envConfig, envDefaults);
348
- const coreErrorHandler = /* @__PURE__ */ __name(async (error) => {
349
- const requestId = generateRequestId();
350
- const method = error.config?.method ?? "UNKNOWN";
351
- const url = error.config?.url ?? "unknown";
352
- try {
353
- const errorDetails = Array.isArray(error.response?.data) ? error.response.data : [];
354
- if (errorDetails.length === 0) {
355
- const serializedError = {
356
- id: requestId,
357
- code: ERROR_CODES.CORE_API_CLIENT_REQUEST_FAILED,
358
- message: error.message ?? "API request failed",
359
- status: error.status,
360
- category: ERROR_CATEGORY.Network,
361
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
362
- isRetryable: error.status ? error.status >= HTTP_STATUS.INTERNAL_SERVER_ERROR : false,
363
- source: "api-client",
364
- dismissed: false,
365
- context: {
366
- method,
367
- url,
368
- requestId
369
- }
370
- };
371
- CoreEventManager.emit(CORE_EVENTS.SYSTEM.ERROR, { errors: [serializedError] });
372
- } else {
373
- const serializedErrors = errorDetails.map(
374
- (detail, index) => ({
375
- id: `${requestId}-${index}`,
376
- code: detail.errorCode ?? ERROR_CODES.CORE_API_CLIENT_REQUEST_FAILED,
377
- message: detail.message ?? error.message ?? "API request failed",
378
- status: error.status,
379
- category: ERROR_CATEGORY.Network,
380
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
381
- isRetryable: error.status ? error.status >= HTTP_STATUS.INTERNAL_SERVER_ERROR : false,
382
- source: "api-client",
383
- dismissed: false,
384
- context: {
385
- method,
386
- url,
387
- requestId,
388
- field: detail.field,
389
- valueGiven: detail.valueGiven,
390
- allowedValues: detail.allowedValues,
391
- constraints: detail.constraints
392
- }
393
- })
394
- );
395
- CoreEventManager.emit(CORE_EVENTS.SYSTEM.ERROR, { errors: serializedErrors });
396
- }
397
- } catch (e) {
398
- console.error("[ApiClientService] Failed to emit error event:", e);
399
- }
400
- _ApiClientService.emitApiError(error, {
401
- method,
402
- url,
403
- requestId,
404
- status: error.status,
405
- duration: 0
406
- });
407
- }, "coreErrorHandler");
430
+ const coreErrorHandler = _ApiClientService.buildCoreErrorHandler();
408
431
  const userOnError = apiConfig?.onError;
409
432
  const combinedOnError = userOnError ? Array.isArray(userOnError) ? [...userOnError, coreErrorHandler] : [userOnError, coreErrorHandler] : coreErrorHandler;
410
433
  const mergedOptions = mergeConfigs(
@@ -579,11 +602,13 @@ var ApiClientService = class _ApiClientService {
579
602
  try {
580
603
  const envDefaults = getConfigForEnvironment(envConfig.env);
581
604
  const envMetadataMapped = mapEnvironmentMetadata(envConfig, envDefaults);
582
- const mergedOptions = mergeConfigs(
583
- envDefaults,
584
- envMetadataMapped,
585
- apiConfig ?? {}
586
- );
605
+ const coreErrorHandler = _ApiClientService.buildCoreErrorHandler();
606
+ const userOnError = apiConfig?.onError;
607
+ const combinedOnError = userOnError ? Array.isArray(userOnError) ? [...userOnError, coreErrorHandler] : [userOnError, coreErrorHandler] : coreErrorHandler;
608
+ const mergedOptions = mergeConfigs(envDefaults, envMetadataMapped, {
609
+ ...apiConfig ?? {},
610
+ onError: combinedOnError
611
+ });
587
612
  validateEnvironmentConfig(envConfig, mergedOptions);
588
613
  const dedicatedClient = await createApiClient(mergedOptions);
589
614
  return dedicatedClient;
@@ -605,6 +630,61 @@ var ApiClientService = class _ApiClientService {
605
630
  );
606
631
  }
607
632
  }
633
+ /**
634
+ * Create a standalone API client with Core error handling.
635
+ *
636
+ * This is a simpler alternative to `createInstance()` that doesn't require
637
+ * environment config. Use this when you just need the error handling without
638
+ * environment-specific defaults (production validation, etc.).
639
+ *
640
+ * **Use cases:**
641
+ * - Domain services that need their own API client
642
+ * - Testing with isolated API clients
643
+ * - Simple client creation without environment setup
644
+ *
645
+ * @param apiConfig - API configuration (baseURL, timeout, etc.)
646
+ * @returns Promise that resolves to a client with Core error handling
647
+ *
648
+ * @example
649
+ * ```typescript
650
+ * // In BaseDomainService or any service
651
+ * const client = await ApiClientService.createStandaloneClient({
652
+ * baseURL: '/api/examples',
653
+ * timeout: 10000,
654
+ * });
655
+ *
656
+ * // Errors are automatically emitted to CoreEventManager
657
+ * const response = await client.get('/items');
658
+ * ```
659
+ */
660
+ static async createStandaloneClient(apiConfig) {
661
+ try {
662
+ const coreErrorHandler = _ApiClientService.buildCoreErrorHandler();
663
+ const userOnError = apiConfig.onError;
664
+ const combinedOnError = userOnError ? Array.isArray(userOnError) ? [...userOnError, coreErrorHandler] : [userOnError, coreErrorHandler] : coreErrorHandler;
665
+ const client = await createApiClient({
666
+ ...apiConfig,
667
+ onError: combinedOnError
668
+ });
669
+ return client;
670
+ } catch (error) {
671
+ throw new ApiPackageError(
672
+ "service.standalone_client_creation.failed",
673
+ PACKAGE_STATUS_CODES.INITIALIZATION_FAILED,
674
+ API_ERROR_CODES.CLIENT_INITIALIZATION_FAILED,
675
+ {
676
+ cause: error instanceof Error ? error : void 0,
677
+ context: {
678
+ operation: OPERATIONS.INITIALIZATION,
679
+ originalError: error instanceof Error ? error.message : String(error),
680
+ i18n: {
681
+ error: error instanceof Error ? error.message : String(error)
682
+ }
683
+ }
684
+ }
685
+ );
686
+ }
687
+ }
608
688
  };
609
689
  var BaseAdapter = class {
610
690
  constructor() {
@@ -3161,6 +3241,7 @@ var Core = class _Core {
3161
3241
  _Core._errorHandler.destroy();
3162
3242
  _Core._errorHandler = null;
3163
3243
  clearEventEmitter();
3244
+ BaseError.clearEventEmitter();
3164
3245
  }
3165
3246
  _Core._errorConfig = {};
3166
3247
  _Core._httpErrorHandler = null;
@@ -3349,6 +3430,28 @@ var Core = class _Core {
3349
3430
  };
3350
3431
  }
3351
3432
  }
3433
+ /**
3434
+ * Serialize a PackageErrorLike to SerializedError format.
3435
+ * Used by BaseError.setEventEmitter() to convert errors for the store.
3436
+ */
3437
+ static serializePackageError(error) {
3438
+ const serviceName = typeof error.context?.service === "string" ? error.context.service : "core";
3439
+ return {
3440
+ id: generateId(),
3441
+ code: error.errorCode ?? ERROR_CODES.UNKNOWN_ERROR,
3442
+ message: error.message,
3443
+ status: error.statusCode,
3444
+ category: error.category ?? ERROR_CATEGORY$1.Server,
3445
+ timestamp: error.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
3446
+ isRetryable: error.retryable ?? false,
3447
+ source: serviceName,
3448
+ dismissed: false,
3449
+ context: {
3450
+ correlationId: error.correlationId,
3451
+ ...error.context
3452
+ }
3453
+ };
3454
+ }
3352
3455
  /** Get error store actions from root store */
3353
3456
  static getErrorStoreActions() {
3354
3457
  if (!_Core._rootStore) {
@@ -3360,12 +3463,17 @@ var Core = class _Core {
3360
3463
  return _Core._rootStore.getState().errors;
3361
3464
  }
3362
3465
  /** Build global error handler config */
3363
- // eslint-disable-next-line complexity
3364
3466
  static buildErrorHandlerConfig(config) {
3467
+ const baseErrorFilter = /* @__PURE__ */ __name((error) => {
3468
+ if (error instanceof BaseError) {
3469
+ return false;
3470
+ }
3471
+ return config?.filter ? config.filter(error) : true;
3472
+ }, "baseErrorFilter");
3365
3473
  return {
3366
3474
  source: config?.source ?? "global",
3367
3475
  maxErrors: config?.maxErrors ?? DEFAULT_MAX_ERRORS,
3368
- filter: config?.filter,
3476
+ filter: baseErrorFilter,
3369
3477
  onError: config?.onError,
3370
3478
  logToConsole: config?.logToConsole ?? false
3371
3479
  };
@@ -3386,6 +3494,15 @@ var Core = class _Core {
3386
3494
  });
3387
3495
  await _Core.initializeRootStore(_Core._errorConfig, verbose);
3388
3496
  setEventEmitter(CoreEventManager.emit.bind(CoreEventManager));
3497
+ BaseError.setEventEmitter((error) => {
3498
+ const serializedError = _Core.serializePackageError(error);
3499
+ const serviceName = serializedError.source ?? "core";
3500
+ CoreEventManager.emit(CORE_EVENTS$1.SYSTEM.ERROR, {
3501
+ errors: [serializedError],
3502
+ context: serviceName,
3503
+ recoverable: error.retryable ?? false
3504
+ });
3505
+ });
3389
3506
  const errorStore = _Core.getErrorStoreActions();
3390
3507
  _Core._errorHandler = initializeGlobalErrorHandler(
3391
3508
  errorStore,
@@ -3481,9 +3598,15 @@ var Core = class _Core {
3481
3598
  }
3482
3599
  /**
3483
3600
  * Subscribe to CoreEventManager error events
3484
- * Forwards system, entity, API, validation, database, and auth errors to the global error handler.
3601
+ * Forwards entity, API, validation, database, and auth errors to the global error handler.
3602
+ *
3603
+ * NOTE: SYSTEM.ERROR is NOT subscribed here - it's handled in initializeErrorHandler()
3604
+ * via the BaseError.setEventEmitter() + addErrors() pattern to avoid duplicate subscriptions.
3605
+ *
3606
+ * For non-BaseError errors, domain-specific events (ENTITY.ERROR, API.REQUEST_ERROR, etc.)
3607
+ * are captured here. BaseError instances are skipped (they already auto-emit via SYSTEM.ERROR).
3485
3608
  *
3486
- * Note: Database errors (DATABASE.ERROR) are only subscribed on backend runtimes since
3609
+ * Database errors (DATABASE.ERROR) are only subscribed on backend runtimes since
3487
3610
  * DbService is backend-only (skipDb: true on frontend).
3488
3611
  */
3489
3612
  static subscribeToErrorEvents(verbose) {
@@ -3491,78 +3614,36 @@ var Core = class _Core {
3491
3614
  return;
3492
3615
  }
3493
3616
  _Core.log("Subscribing to error events...", verbose);
3494
- const cleanupSystemError = CoreEventManager.on(CORE_EVENTS$1.SYSTEM.ERROR, (event) => {
3495
- if (_Core._errorHandler && event.data?.errors?.length) {
3496
- for (const err of event.data.errors) {
3497
- _Core._errorHandler.captureError(err, "system");
3617
+ const subscribeError = /* @__PURE__ */ __name((event, source) => {
3618
+ return CoreEventManager.on(event, (e) => {
3619
+ if (_Core._errorHandler && e.data?.error && !(e.data.error instanceof BaseError)) {
3620
+ _Core._errorHandler.captureError(e.data.error, source);
3498
3621
  }
3499
- }
3500
- });
3501
- const cleanupEntityError = CoreEventManager.on(CORE_EVENTS$1.ENTITY.ERROR, (event) => {
3502
- if (_Core._errorHandler && event.data) {
3503
- _Core._errorHandler.captureError(event.data.error, "entity");
3504
- }
3505
- });
3506
- const cleanupApiError = CoreEventManager.on(CORE_EVENTS$1.API.REQUEST_ERROR, (event) => {
3507
- if (_Core._errorHandler && event.data) {
3508
- _Core._errorHandler.captureError(event.data.error, "api");
3509
- }
3510
- });
3511
- const cleanupValidationError = CoreEventManager.on(CORE_EVENTS$1.VALIDATION.FAILED, (event) => {
3512
- if (_Core._errorHandler && event.data) {
3513
- _Core._errorHandler.captureError(event.data.error, "validation");
3514
- }
3515
- });
3516
- const cleanupAuthUnauthorized = CoreEventManager.on(CORE_EVENTS$1.AUTH.UNAUTHORIZED, (event) => {
3517
- if (_Core._errorHandler && event.data?.error) {
3518
- _Core._errorHandler.captureError(event.data.error, "auth");
3519
- }
3520
- });
3521
- const cleanupAuthSessionExpired = CoreEventManager.on(
3522
- CORE_EVENTS$1.AUTH.SESSION_EXPIRED,
3523
- (event) => {
3524
- if (_Core._errorHandler && event.data?.error) {
3525
- _Core._errorHandler.captureError(event.data.error, "auth");
3526
- }
3527
- }
3528
- );
3622
+ });
3623
+ }, "subscribeError");
3529
3624
  _Core._eventCleanupFns.push(
3530
- cleanupSystemError,
3531
- cleanupEntityError,
3532
- cleanupApiError,
3533
- cleanupValidationError,
3534
- cleanupAuthUnauthorized,
3535
- cleanupAuthSessionExpired
3625
+ subscribeError(CORE_EVENTS$1.ENTITY.ERROR, "entity"),
3626
+ subscribeError(CORE_EVENTS$1.API.REQUEST_ERROR, "api"),
3627
+ subscribeError(CORE_EVENTS$1.VALIDATION.FAILED, "validation"),
3628
+ subscribeError(CORE_EVENTS$1.AUTH.UNAUTHORIZED, "auth"),
3629
+ subscribeError(CORE_EVENTS$1.AUTH.SESSION_EXPIRED, "auth")
3536
3630
  );
3537
3631
  if (_Core.isRuntimeCompatible("backend")) {
3538
- const cleanupDatabaseError = CoreEventManager.on(CORE_EVENTS$1.DATABASE.ERROR, (event) => {
3539
- if (_Core._errorHandler && event.data) {
3540
- _Core._errorHandler.captureError(event.data.error, "database");
3541
- }
3542
- });
3543
- _Core._eventCleanupFns.push(cleanupDatabaseError);
3544
- const cleanupStorageError = CoreEventManager.on(CORE_EVENTS$1.STORAGE.ERROR, (event) => {
3545
- if (_Core._errorHandler && event.data) {
3546
- _Core._errorHandler.captureError(event.data.error, "storage");
3547
- }
3548
- });
3549
- _Core._eventCleanupFns.push(cleanupStorageError);
3550
- const cleanupNotificationError = CoreEventManager.on(
3551
- CORE_EVENTS$1.NOTIFICATION.ERROR,
3552
- (event) => {
3553
- if (_Core._errorHandler && event.data) {
3554
- _Core._errorHandler.captureError(event.data.error, "notification");
3555
- }
3556
- }
3632
+ _Core._eventCleanupFns.push(
3633
+ subscribeError(CORE_EVENTS$1.DATABASE.ERROR, "database"),
3634
+ subscribeError(CORE_EVENTS$1.STORAGE.ERROR, "storage"),
3635
+ subscribeError(CORE_EVENTS$1.NOTIFICATION.ERROR, "notification"),
3636
+ subscribeError(CORE_EVENTS$1.CACHE.ERROR, "cache")
3637
+ );
3638
+ _Core.log(
3639
+ "Subscribed to backend error events (database, storage, notification, cache)",
3640
+ verbose
3557
3641
  );
3558
- _Core._eventCleanupFns.push(cleanupNotificationError);
3559
- _Core.log("Subscribed to backend error events (database, storage, notification)", verbose);
3560
- }
3561
- const eventTypes = ["system", "entity", "api", "validation", "auth"];
3562
- if (_Core.isRuntimeCompatible("backend")) {
3563
- eventTypes.push("database", "storage", "notification");
3564
3642
  }
3565
- _Core.log(`Subscribed to error events: ${eventTypes.join(", ")}`, verbose);
3643
+ const eventTypes = ["entity", "api", "validation", "auth"];
3644
+ if (_Core.isRuntimeCompatible("backend"))
3645
+ eventTypes.push("database", "storage", "notification", "cache");
3646
+ _Core.log(`Subscribed to domain error events: ${eventTypes.join(", ")}`, verbose);
3566
3647
  }
3567
3648
  /** Handle fetch flags error and return empty flags */
3568
3649
  static handleFetchFlagsError(error, config, verbose) {