@devp0nt/error0 1.0.0-next.48 → 1.0.0-next.49

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 (57) hide show
  1. package/dist/cjs/index.cjs +158 -85
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/index.d.cts +44 -16
  4. package/dist/cjs/plugins/cause-serialize.cjs +4 -1
  5. package/dist/cjs/plugins/cause-serialize.cjs.map +1 -1
  6. package/dist/cjs/plugins/cause-serialize.d.cts +3 -1
  7. package/dist/cjs/plugins/expected.cjs +7 -2
  8. package/dist/cjs/plugins/expected.cjs.map +1 -1
  9. package/dist/cjs/plugins/expected.d.cts +3 -1
  10. package/dist/cjs/plugins/message-merge.cjs +5 -2
  11. package/dist/cjs/plugins/message-merge.cjs.map +1 -1
  12. package/dist/cjs/plugins/message-merge.d.cts +4 -1
  13. package/dist/cjs/plugins/meta.cjs +7 -2
  14. package/dist/cjs/plugins/meta.cjs.map +1 -1
  15. package/dist/cjs/plugins/meta.d.cts +3 -1
  16. package/dist/cjs/plugins/stack-merge.cjs +6 -3
  17. package/dist/cjs/plugins/stack-merge.cjs.map +1 -1
  18. package/dist/cjs/plugins/stack-merge.d.cts +4 -1
  19. package/dist/cjs/plugins/tags.cjs +7 -2
  20. package/dist/cjs/plugins/tags.cjs.map +1 -1
  21. package/dist/cjs/plugins/tags.d.cts +3 -1
  22. package/dist/esm/index.d.ts +44 -16
  23. package/dist/esm/index.js +158 -85
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/plugins/cause-serialize.d.ts +3 -1
  26. package/dist/esm/plugins/cause-serialize.js +4 -1
  27. package/dist/esm/plugins/cause-serialize.js.map +1 -1
  28. package/dist/esm/plugins/expected.d.ts +3 -1
  29. package/dist/esm/plugins/expected.js +7 -2
  30. package/dist/esm/plugins/expected.js.map +1 -1
  31. package/dist/esm/plugins/message-merge.d.ts +4 -1
  32. package/dist/esm/plugins/message-merge.js +5 -2
  33. package/dist/esm/plugins/message-merge.js.map +1 -1
  34. package/dist/esm/plugins/meta.d.ts +3 -1
  35. package/dist/esm/plugins/meta.js +7 -2
  36. package/dist/esm/plugins/meta.js.map +1 -1
  37. package/dist/esm/plugins/stack-merge.d.ts +4 -1
  38. package/dist/esm/plugins/stack-merge.js +6 -3
  39. package/dist/esm/plugins/stack-merge.js.map +1 -1
  40. package/dist/esm/plugins/tags.d.ts +3 -1
  41. package/dist/esm/plugins/tags.js +7 -2
  42. package/dist/esm/plugins/tags.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/index.test.ts +79 -21
  45. package/src/index.ts +216 -101
  46. package/src/plugins/cause-serialize.test.ts +6 -4
  47. package/src/plugins/cause-serialize.ts +13 -9
  48. package/src/plugins/expected.test.ts +4 -4
  49. package/src/plugins/expected.ts +16 -10
  50. package/src/plugins/message-merge.test.ts +3 -3
  51. package/src/plugins/message-merge.ts +17 -13
  52. package/src/plugins/meta.test.ts +2 -2
  53. package/src/plugins/meta.ts +28 -22
  54. package/src/plugins/stack-merge.test.ts +4 -4
  55. package/src/plugins/stack-merge.ts +18 -14
  56. package/src/plugins/tags.test.ts +2 -2
  57. package/src/plugins/tags.ts +24 -18
package/src/index.ts CHANGED
@@ -12,19 +12,31 @@ type InferPluginPropInput<TProp extends ErrorPluginPropOptions<any, any, any, an
12
12
  ? NormalizeUnknownToUndefined<InferFirstArg<TInit>>
13
13
  : undefined
14
14
  type ErrorPluginPropInit<TInputValue, TOutputValue> = ((input: TInputValue) => TOutputValue) | (() => TOutputValue)
15
- type ErrorPluginPropSerialize<TOutputValue, TError extends Error0> =
16
- | ((options: { value: TOutputValue; error: TError; isPublic: boolean }) => unknown)
15
+ type ErrorPluginPropSerializeOptions<
16
+ TOutputValue,
17
+ TError extends Error0,
18
+ TResolveValue extends TOutputValue | undefined,
19
+ > = {
20
+ own: TOutputValue | undefined
21
+ flow: Array<TOutputValue | undefined>
22
+ resolved: TResolveValue
23
+ error: TError
24
+ isPublic: boolean
25
+ }
26
+ type ErrorPluginPropSerialize<TOutputValue, TError extends Error0, TResolveValue extends TOutputValue | undefined> =
27
+ | ((options: ErrorPluginPropSerializeOptions<TOutputValue, TError, TResolveValue>) => unknown)
17
28
  | false
18
29
  type ErrorPluginPropDeserialize<TOutputValue> =
19
- | ((options: { value: unknown; serialized: Record<string, unknown> }) => TOutputValue | undefined)
30
+ | ((options: { value: unknown; record: Record<string, unknown> }) => TOutputValue | undefined)
20
31
  | false
32
+ type ErrorPluginPropOptionsResolveOptions<TOutputValue, TError extends Error0> = {
33
+ own: TOutputValue | undefined
34
+ flow: Array<TOutputValue | undefined>
35
+ error: TError
36
+ }
21
37
  type ErrorPluginPropOptionsBase<TOutputValue, TError extends Error0, TResolveValue extends TOutputValue | undefined> = {
22
- resolve: (options: {
23
- own: TOutputValue | undefined
24
- flow: Array<TOutputValue | undefined>
25
- error: TError
26
- }) => TResolveValue
27
- serialize: ErrorPluginPropSerialize<TResolveValue, TError>
38
+ resolve: (options: ErrorPluginPropOptionsResolveOptions<TOutputValue, TError>) => TResolveValue
39
+ serialize: ErrorPluginPropSerialize<TOutputValue, TError, TResolveValue>
28
40
  deserialize: ErrorPluginPropDeserialize<TOutputValue>
29
41
  }
30
42
  type ErrorPluginPropOptionsWithInit<
@@ -180,6 +192,9 @@ type ErrorPluginResolved = {
180
192
  stack?: ErrorPluginStack
181
193
  cause?: ErrorPluginCause
182
194
  message?: ErrorPluginMessage
195
+ propKeys: string[]
196
+ propEntries: Array<[string, ErrorPluginPropOptions<unknown>]>
197
+ methodEntries: Array<[string, ErrorPluginMethodFn<unknown>]>
183
198
  }
184
199
  const RESERVED_STACK_PROP_ERROR = 'Error0: "stack" is a reserved prop key. Use .stack(...) plugin API instead'
185
200
  const RESERVED_MESSAGE_PROP_ERROR = 'Error0: "message" is a reserved prop key. Use .message(...) plugin API instead'
@@ -249,9 +264,16 @@ type PluginsMapFromParts<
249
264
  TProps extends ErrorPluginProps,
250
265
  TMethods extends ErrorPluginMethods,
251
266
  > = ErrorPluginsMapOfPlugin<ErrorPlugin<TProps, TMethods>>
252
- type ErrorInstanceOfMap<TMap extends ErrorPluginsMap> = Error0 & ErrorResolved<TMap>
267
+ type ErrorInstanceOfMap<TMap extends ErrorPluginsMap> = Error0 &
268
+ ErrorResolved<TMap> &
269
+ ErrorOwnMethods<TMap> &
270
+ ErrorResolveMethods<TMap> & { readonly __pluginsMap?: TMap }
253
271
  type BuilderError0<TProps extends ErrorPluginProps, TMethods extends ErrorPluginMethods> = Error0 &
254
- ErrorResolved<PluginsMapFromParts<TProps, TMethods>>
272
+ ErrorResolved<PluginsMapFromParts<TProps, TMethods>> &
273
+ ErrorOwnMethods<PluginsMapFromParts<TProps, TMethods>> &
274
+ ErrorResolveMethods<PluginsMapFromParts<TProps, TMethods>> & {
275
+ readonly __pluginsMap?: PluginsMapFromParts<TProps, TMethods>
276
+ }
255
277
 
256
278
  type PluginOfBuilder<TBuilder> =
257
279
  TBuilder extends PluginError0<infer TProps, infer TMethods> ? ErrorPlugin<TProps, TMethods> : never
@@ -386,7 +408,11 @@ export class PluginError0<
386
408
  }
387
409
  }
388
410
 
411
+ const OWN_SYMBOL: unique symbol = Symbol('Error0.own')
412
+ type ErrorOwnStore = Record<string, unknown>
413
+
389
414
  export type ClassError0<TPluginsMap extends ErrorPluginsMap = EmptyPluginsMap> = {
415
+ MAX_CAUSES_DEPTH: number
390
416
  new (
391
417
  message: string,
392
418
  input?: ErrorInput<TPluginsMap>,
@@ -408,6 +434,13 @@ export type ClassError0<TPluginsMap extends ErrorPluginsMap = EmptyPluginsMap> =
408
434
  error: unknown,
409
435
  isPublic?: boolean,
410
436
  ) => Error0 & ErrorResolved<TPluginsMap> & ErrorOwnMethods<TPluginsMap> & ErrorResolveMethods<TPluginsMap>
437
+ causes: {
438
+ (error: unknown, instancesOnly?: false): unknown[]
439
+ (
440
+ error: unknown,
441
+ instancesOnly: true,
442
+ ): Array<Error0 & ErrorResolved<TPluginsMap> & ErrorOwnMethods<TPluginsMap> & ErrorResolveMethods<TPluginsMap>>
443
+ }
411
444
  resolve: (error: unknown) => ErrorResolvedProps<TPluginsMap>
412
445
  serialize: (error: unknown, isPublic?: boolean) => Record<string, unknown>
413
446
  own: {
@@ -469,7 +502,9 @@ export type ClassError0<TPluginsMap extends ErrorPluginsMap = EmptyPluginsMap> =
469
502
  export class Error0 extends Error {
470
503
  static readonly __pluginsMap?: EmptyPluginsMap
471
504
  readonly __pluginsMap?: EmptyPluginsMap
505
+ static MAX_CAUSES_DEPTH = 99
472
506
  protected static _plugins: ErrorPlugin[] = []
507
+ protected static _resolvedPlugin?: ErrorPluginResolved
473
508
 
474
509
  private static readonly _emptyPlugin: ErrorPluginResolved = {
475
510
  props: {},
@@ -478,35 +513,86 @@ export class Error0 extends Error {
478
513
  stack: undefined,
479
514
  cause: undefined,
480
515
  message: undefined,
516
+ propKeys: [],
517
+ propEntries: [],
518
+ methodEntries: [],
519
+ }
520
+
521
+ private static _indexResolvedPlugin(
522
+ resolved: Omit<ErrorPluginResolved, 'propKeys' | 'propEntries' | 'methodEntries'>,
523
+ ): ErrorPluginResolved {
524
+ return {
525
+ ...resolved,
526
+ propKeys: Object.keys(resolved.props),
527
+ propEntries: Object.entries(resolved.props),
528
+ methodEntries: Object.entries(resolved.methods),
529
+ }
530
+ }
531
+
532
+ private static _applyPlugin(
533
+ resolved: Omit<ErrorPluginResolved, 'propKeys' | 'propEntries' | 'methodEntries'>,
534
+ plugin: ErrorPlugin,
535
+ ): void {
536
+ if (plugin.props && 'stack' in plugin.props) {
537
+ throw new Error(RESERVED_STACK_PROP_ERROR)
538
+ }
539
+ if (plugin.props && 'message' in plugin.props) {
540
+ throw new Error(RESERVED_MESSAGE_PROP_ERROR)
541
+ }
542
+ Object.assign(resolved.props, plugin.props ?? this._emptyPlugin.props)
543
+ Object.assign(resolved.methods, plugin.methods ?? this._emptyPlugin.methods)
544
+ resolved.adapt.push(...(plugin.adapt ?? this._emptyPlugin.adapt))
545
+ if (typeof plugin.stack !== 'undefined') {
546
+ resolved.stack = plugin.stack
547
+ }
548
+ if (typeof plugin.cause !== 'undefined') {
549
+ resolved.cause = plugin.cause
550
+ }
551
+ if (typeof plugin.message !== 'undefined') {
552
+ resolved.message = plugin.message
553
+ }
554
+ }
555
+
556
+ private static _mergeResolvedPlugin(
557
+ this: typeof Error0,
558
+ base: ErrorPluginResolved,
559
+ plugin: ErrorPlugin,
560
+ ): ErrorPluginResolved {
561
+ const merged: Omit<ErrorPluginResolved, 'propKeys' | 'propEntries' | 'methodEntries'> = {
562
+ props: { ...base.props },
563
+ methods: { ...base.methods },
564
+ adapt: [...base.adapt],
565
+ stack: base.stack,
566
+ cause: base.cause,
567
+ message: base.message,
568
+ }
569
+ this._applyPlugin(merged, plugin)
570
+ return this._indexResolvedPlugin(merged)
481
571
  }
482
572
 
483
573
  private static _getResolvedPlugin(this: typeof Error0): ErrorPluginResolved {
574
+ if (Object.prototype.hasOwnProperty.call(this, '_resolvedPlugin') && this._resolvedPlugin) {
575
+ return this._resolvedPlugin
576
+ }
484
577
  const resolved: ErrorPluginResolved = {
485
578
  props: {},
486
579
  methods: {},
487
580
  adapt: [],
581
+ propKeys: [],
582
+ propEntries: [],
583
+ methodEntries: [],
488
584
  }
489
585
  for (const plugin of this._plugins) {
490
- if (plugin.props && 'stack' in plugin.props) {
491
- throw new Error(RESERVED_STACK_PROP_ERROR)
492
- }
493
- if (plugin.props && 'message' in plugin.props) {
494
- throw new Error(RESERVED_MESSAGE_PROP_ERROR)
495
- }
496
- Object.assign(resolved.props, plugin.props ?? this._emptyPlugin.props)
497
- Object.assign(resolved.methods, plugin.methods ?? this._emptyPlugin.methods)
498
- resolved.adapt.push(...(plugin.adapt ?? this._emptyPlugin.adapt))
499
- if (typeof plugin.stack !== 'undefined') {
500
- resolved.stack = plugin.stack
501
- }
502
- if (typeof plugin.cause !== 'undefined') {
503
- resolved.cause = plugin.cause
504
- }
505
- if (typeof plugin.message !== 'undefined') {
506
- resolved.message = plugin.message
507
- }
586
+ this._applyPlugin(resolved, plugin)
508
587
  }
509
- return resolved
588
+ const indexed = this._indexResolvedPlugin(resolved)
589
+ Object.defineProperty(this, '_resolvedPlugin', {
590
+ value: indexed,
591
+ writable: true,
592
+ enumerable: false,
593
+ configurable: true,
594
+ })
595
+ return indexed
510
596
  }
511
597
 
512
598
  constructor(message: string, input?: ErrorInput<EmptyPluginsMap>)
@@ -524,37 +610,47 @@ export class Error0 extends Error {
524
610
 
525
611
  const ctor = this.constructor as typeof Error0
526
612
  const plugin = ctor._getResolvedPlugin()
613
+ const ownStore = Object.create(null) as ErrorOwnStore
614
+ Object.defineProperty(this, OWN_SYMBOL, { value: ownStore, writable: true, enumerable: false, configurable: true })
527
615
 
528
- for (const [key, prop] of Object.entries(plugin.props)) {
616
+ for (const [key, prop] of plugin.propEntries) {
529
617
  if (key === 'stack') {
530
618
  continue
531
619
  }
620
+ Object.defineProperty(this, key, {
621
+ get: () =>
622
+ prop.resolve({
623
+ own: ownStore[key],
624
+ flow: this.flow(key as never),
625
+ error: this,
626
+ }),
627
+ set: (value) => {
628
+ ownStore[key] = value
629
+ },
630
+ enumerable: true,
631
+ configurable: true,
632
+ })
532
633
  if (key in input) {
533
634
  const ownValue = (input as Record<string, unknown>)[key]
534
- if (typeof prop.init === 'function') {
535
- ;(this as Record<string, unknown>)[key] = prop.init(ownValue)
536
- } else {
537
- ;(this as Record<string, unknown>)[key] = ownValue
538
- }
539
- } else {
540
- Object.defineProperty(this, key, {
541
- get: () => prop.resolve({ own: undefined, flow: this.flow(key), error: this }),
542
- set: (value) => {
543
- Object.defineProperty(this, key, {
544
- value,
545
- writable: true,
546
- enumerable: true,
547
- configurable: true,
548
- })
549
- },
550
- enumerable: true,
551
- configurable: true,
552
- })
635
+ ownStore[key] = typeof prop.init === 'function' ? prop.init(ownValue) : ownValue
553
636
  }
554
637
  }
555
638
  }
556
639
 
557
- private static readonly isSelfProperty = (object: object, key: string): boolean => {
640
+ private static _getOwnStore(object: object): ErrorOwnStore | undefined {
641
+ const record = object as Record<string | symbol, unknown>
642
+ const existing = record[OWN_SYMBOL]
643
+ if (existing && typeof existing === 'object') {
644
+ return existing as ErrorOwnStore
645
+ }
646
+ return undefined
647
+ }
648
+
649
+ private static readonly isOwnProperty = (object: object, key: string): boolean => {
650
+ const ownStore = this._getOwnStore(object)
651
+ if (ownStore && Object.prototype.hasOwnProperty.call(ownStore, key)) {
652
+ return true
653
+ }
558
654
  const d = Object.getOwnPropertyDescriptor(object, key)
559
655
  if (!d) return false
560
656
  if (typeof d.get === 'function' || typeof d.set === 'function') {
@@ -567,15 +663,22 @@ export class Error0 extends Error {
567
663
  return true
568
664
  }
569
665
  private static _ownByKey(error: object, key: string): unknown {
570
- if (this.isSelfProperty(error, key)) {
666
+ const ownStore = this._getOwnStore(error)
667
+ if (ownStore && Object.prototype.hasOwnProperty.call(ownStore, key)) {
668
+ return ownStore[key]
669
+ }
670
+ if (this.isOwnProperty(error, key)) {
571
671
  return (error as Record<string, unknown>)[key]
572
672
  }
573
673
  return undefined
574
674
  }
575
675
  private static _flowByKey(error: object, key: string): unknown[] {
576
- return this.causes(error, true).map((cause) => {
577
- return this._ownByKey(cause, key)
578
- })
676
+ const causes = this.causes(error, true)
677
+ const values = new Array<unknown>(causes.length)
678
+ for (let i = 0; i < causes.length; i += 1) {
679
+ values[i] = this._ownByKey(causes[i], key)
680
+ }
681
+ return values
579
682
  }
580
683
 
581
684
  static own<TThis extends typeof Error0>(this: TThis, error: unknown): ErrorOwnProps<PluginsMapOf<TThis>>
@@ -589,7 +692,7 @@ export class Error0 extends Error {
589
692
  if (key === undefined) {
590
693
  const ownValues: Record<string, unknown> = {}
591
694
  const plugin = this._getResolvedPlugin()
592
- for (const ownKey of Object.keys(plugin.props)) {
695
+ for (const ownKey of plugin.propKeys) {
593
696
  ownValues[ownKey] = this._ownByKey(error0, ownKey)
594
697
  }
595
698
  return ownValues
@@ -614,7 +717,6 @@ export class Error0 extends Error {
614
717
  error: unknown,
615
718
  key: TKey,
616
719
  ): Array<ErrorOwnProps<PluginsMapOf<TThis>>[TKey]>
617
- static flow(error: unknown, key: string): unknown[]
618
720
  static flow(error: unknown, key: string): unknown[] {
619
721
  const error0 = this.from(error)
620
722
  return this._flowByKey(error0, key)
@@ -623,45 +725,48 @@ export class Error0 extends Error {
623
725
  this: TThis,
624
726
  key: TKey,
625
727
  ): Array<ErrorOwnProps<PluginsMapOfInstance<TThis>>[TKey]>
626
- flow(key: string): unknown[]
627
728
  flow(key: string): unknown[] {
628
729
  const ctor = this.constructor as typeof Error0
629
730
  return ctor._flowByKey(this, key)
630
731
  }
631
732
 
733
+ static _resolveByKey(error: Error0, key: string, plugin: ErrorPluginResolved): unknown {
734
+ try {
735
+ const options = {
736
+ get own() {
737
+ return error.own(key as never)
738
+ },
739
+ get flow() {
740
+ return error.flow(key as never)
741
+ },
742
+ error,
743
+ }
744
+ const prop = plugin.props[key]
745
+ const resolver = prop.resolve
746
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
747
+ if (!resolver) {
748
+ return (error as any)[key]
749
+ }
750
+ return resolver(options as ErrorPluginPropOptionsResolveOptions<any, any>)
751
+ } catch {
752
+ // eslint-disable-next-line no-console
753
+ console.error(`Error0: failed to resolve property ${key}`, error)
754
+ return undefined
755
+ }
756
+ }
757
+
632
758
  static resolve<TThis extends typeof Error0>(this: TThis, error: unknown): ErrorResolvedProps<PluginsMapOf<TThis>>
633
759
  static resolve(error: unknown): Record<string, unknown>
634
760
  static resolve(error: unknown): Record<string, unknown> {
635
761
  const error0 = this.from(error)
636
762
  const resolved: Record<string, unknown> = {}
637
763
  const plugin = this._getResolvedPlugin()
638
- for (const [key, prop] of Object.entries(plugin.props)) {
639
- try {
640
- // resolved[key] = (error0 as unknown as Record<string, unknown>)[key]
641
- const options = Object.defineProperties(
642
- {
643
- error: error0,
644
- },
645
- {
646
- own: {
647
- get: () => error0.own(key as never),
648
- },
649
- flow: {
650
- get: () => error0.flow(key),
651
- },
652
- },
653
- )
654
- resolved[key] = prop.resolve(options as never)
655
- // resolved[key] = prop.resolve({ own: error0.own(key as never), flow: error0.flow(key), error: error0 })
656
- } catch {
657
- // eslint-disable-next-line no-console
658
- console.error(`Error0: failed to resolve property ${key}`, error0)
659
- }
764
+ for (const key of plugin.propKeys) {
765
+ resolved[key] = this._resolveByKey(error0, key, plugin)
660
766
  }
661
767
  return resolved
662
768
  }
663
769
  resolve<TThis extends Error0>(this: TThis): ErrorResolvedProps<PluginsMapOfInstance<TThis>>
664
- resolve(): Record<string, unknown>
665
770
  resolve(): Record<string, unknown> {
666
771
  const ctor = this.constructor as typeof Error0
667
772
  return ctor.resolve(this)
@@ -672,9 +777,9 @@ export class Error0 extends Error {
672
777
  static causes(error: unknown, instancesOnly?: boolean): unknown[] {
673
778
  const causes: unknown[] = []
674
779
  let current: unknown = error
675
- const maxDepth = 99
676
780
  const seen = new Set<unknown>()
677
- for (let depth = 0; depth < maxDepth; depth += 1) {
781
+ let depth = 0
782
+ while (depth < this.MAX_CAUSES_DEPTH) {
678
783
  if (seen.has(current)) {
679
784
  break
680
785
  }
@@ -686,6 +791,7 @@ export class Error0 extends Error {
686
791
  break
687
792
  }
688
793
  current = (current as { cause?: unknown }).cause
794
+ depth += 1
689
795
  }
690
796
  return causes
691
797
  }
@@ -740,8 +846,7 @@ export class Error0 extends Error {
740
846
  const errorRecord = error as Record<string, unknown>
741
847
  const recreated = new this(message)
742
848
  const plugin = this._getResolvedPlugin()
743
- const propsEntries = Object.entries(plugin.props)
744
- for (const [key, prop] of propsEntries) {
849
+ for (const [key, prop] of plugin.propEntries) {
745
850
  if (prop.deserialize === false) {
746
851
  continue
747
852
  }
@@ -749,7 +854,7 @@ export class Error0 extends Error {
749
854
  continue
750
855
  }
751
856
  try {
752
- const value = prop.deserialize({ value: errorRecord[key], serialized: errorRecord })
857
+ const value = prop.deserialize({ value: errorRecord[key], record: errorRecord })
753
858
  ;(recreated as unknown as Record<string, unknown>)[key] = value
754
859
  } catch {
755
860
  // eslint-disable-next-line no-console
@@ -806,9 +911,9 @@ export class Error0 extends Error {
806
911
  const Base = this as unknown as typeof Error0
807
912
  const Error0Extended = class Error0 extends Base {}
808
913
  ;(Error0Extended as typeof Error0)._plugins = [...Base._plugins, plugin]
809
-
810
- const resolved = (Error0Extended as typeof Error0)._getResolvedPlugin()
811
- for (const [key, method] of Object.entries(resolved.methods)) {
914
+ const resolved = this._mergeResolvedPlugin(Base._getResolvedPlugin(), plugin)
915
+ ;(Error0Extended as typeof Error0)._resolvedPlugin = resolved
916
+ for (const [key, method] of resolved.methodEntries) {
812
917
  Object.defineProperty((Error0Extended as typeof Error0).prototype, key, {
813
918
  value: function (...args: unknown[]) {
814
919
  return method(this as Error0, ...args)
@@ -1005,9 +1110,9 @@ export class Error0 extends Error {
1005
1110
 
1006
1111
  static serialize(error: unknown, isPublic = true): Record<string, unknown> {
1007
1112
  const error0 = this.from(error)
1008
- const resolvedProps = this.resolve(error0)
1009
- const resolvedRecord = resolvedProps as Record<string, unknown>
1010
1113
  const plugin = this._getResolvedPlugin()
1114
+ const resolveByKey = (targetError: Error0, key: string, targetPlugin: ErrorPluginResolved): unknown =>
1115
+ this._resolveByKey(targetError, key, targetPlugin)
1011
1116
  const messagePlugin = plugin.message
1012
1117
  let serializedMessage: unknown = error0.message
1013
1118
  try {
@@ -1021,25 +1126,38 @@ export class Error0 extends Error {
1021
1126
  }
1022
1127
  const json: Record<string, unknown> = {
1023
1128
  name: error0.name,
1024
- message: serializedMessage,
1025
1129
  // we do not serialize causes, it is enough that we have floated props and adapt helper
1026
1130
  // cause: error0.cause,
1027
1131
  }
1132
+ if (serializedMessage !== undefined) {
1133
+ json.message = serializedMessage
1134
+ }
1028
1135
 
1029
- const propsEntries = Object.entries(plugin.props)
1030
- for (const [key, prop] of propsEntries) {
1136
+ for (const [key, prop] of plugin.propEntries) {
1031
1137
  if (prop.serialize === false) {
1032
1138
  continue
1033
1139
  }
1034
1140
  try {
1035
- const value = resolvedRecord[key]
1036
- const jsonValue = prop.serialize({ value, error: error0, isPublic })
1141
+ const options = {
1142
+ get own() {
1143
+ return error0.own(key as never)
1144
+ },
1145
+ get flow() {
1146
+ return error0.flow(key as never)
1147
+ },
1148
+ get resolved() {
1149
+ return resolveByKey(error0, key, plugin)
1150
+ },
1151
+ error: error0,
1152
+ isPublic,
1153
+ }
1154
+ const jsonValue = prop.serialize(options as ErrorPluginPropSerializeOptions<any, any, any>)
1037
1155
  if (jsonValue !== undefined) {
1038
1156
  json[key] = jsonValue
1039
1157
  }
1040
1158
  } catch {
1041
1159
  // eslint-disable-next-line no-console
1042
- console.error(`Error0: failed to serialize property ${key}`, resolvedRecord)
1160
+ console.error(`Error0: failed to serialize property ${key}`, error0)
1043
1161
  }
1044
1162
  }
1045
1163
  const stackPlugin = plugin.stack
@@ -1073,10 +1191,7 @@ export class Error0 extends Error {
1073
1191
  console.error('Error0: failed to serialize cause', error0)
1074
1192
  }
1075
1193
  }
1076
- return Object.fromEntries(Object.entries(json).filter(([, value]) => value !== undefined)) as Record<
1077
- string,
1078
- unknown
1079
- >
1194
+ return json
1080
1195
  }
1081
1196
 
1082
1197
  serialize(isPublic = true): Record<string, unknown> {
@@ -6,7 +6,7 @@ describe('causeSerializePlugin', () => {
6
6
  const statusPlugin = Error0.plugin().use('prop', 'status', {
7
7
  init: (input: number) => input,
8
8
  resolve: ({ flow }) => flow.find((value) => typeof value === 'number'),
9
- serialize: ({ value }) => value,
9
+ serialize: ({ resolved }) => resolved,
10
10
  deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
11
11
  })
12
12
 
@@ -14,14 +14,16 @@ describe('causeSerializePlugin', () => {
14
14
  type Code = (typeof codes)[number]
15
15
  const codePlugin = Error0.plugin().use('prop', 'code', {
16
16
  init: (input: Code) => input,
17
- resolve: ({ flow }) => flow.find((value) => typeof value === 'string' && codes.includes(value as Code)),
18
- serialize: ({ value, isPublic }) => (isPublic ? undefined : value),
17
+ resolve: ({ flow }) => flow.find((value) => typeof value === 'string' && codes.includes(value)),
18
+ serialize: ({ resolved, isPublic }) => (isPublic ? undefined : resolved),
19
19
  deserialize: ({ value }) =>
20
20
  typeof value === 'string' && codes.includes(value as Code) ? (value as Code) : undefined,
21
21
  })
22
22
 
23
23
  it('serializes and deserializes nested Error0 causes', () => {
24
- const AppError = Error0.use(statusPlugin).use(codePlugin).use(causeSerializePlugin)
24
+ const AppError = Error0.use(statusPlugin)
25
+ .use(codePlugin)
26
+ .use(causeSerializePlugin({ hideWhenPublic: false }))
25
27
  const deepCauseError = new AppError('deep cause')
26
28
  const causeError = new AppError('cause', { status: 409, code: 'NOT_FOUND', cause: deepCauseError })
27
29
  const error = new AppError('root', { status: 500, cause: causeError })
@@ -1,11 +1,15 @@
1
1
  import { Error0 } from '../index.js'
2
2
 
3
- export const causeSerializePlugin = Error0.plugin().use('cause', {
4
- serialize: ({ value, error, isPublic }) => {
5
- const ctor = error.constructor as typeof Error0
6
- if (ctor.is(value)) {
7
- return ctor.serialize(value, isPublic)
8
- }
9
- return undefined
10
- },
11
- })
3
+ export const causeSerializePlugin = ({ hideWhenPublic = true }: { hideWhenPublic?: boolean } = {}) =>
4
+ Error0.plugin().use('cause', {
5
+ serialize: ({ value, error, isPublic }) => {
6
+ if (hideWhenPublic && isPublic) {
7
+ return undefined
8
+ }
9
+ const ctor = error.constructor as typeof Error0
10
+ if (ctor.is(value)) {
11
+ return ctor.serialize(value, isPublic)
12
+ }
13
+ return undefined
14
+ },
15
+ })
@@ -6,12 +6,12 @@ describe('expectedPlugin', () => {
6
6
  const statusPlugin = Error0.plugin().use('prop', 'status', {
7
7
  init: (input: number) => input,
8
8
  resolve: ({ flow }) => flow.find((value) => typeof value === 'number'),
9
- serialize: ({ value }) => value,
9
+ serialize: ({ resolved }) => resolved,
10
10
  deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
11
11
  })
12
12
 
13
13
  it('can be used to control error tracker behavior', () => {
14
- const AppError = Error0.use(statusPlugin).use(expectedPlugin)
14
+ const AppError = Error0.use(statusPlugin).use(expectedPlugin({ hideWhenPublic: false }))
15
15
  const errorExpected = new AppError('test', { status: 400, expected: true })
16
16
  const errorUnexpected = new AppError('test', { status: 400, expected: false })
17
17
  const usualError = new Error('test')
@@ -30,7 +30,7 @@ describe('expectedPlugin', () => {
30
30
  })
31
31
 
32
32
  it('resolves to false when any cause has false', () => {
33
- const AppError = Error0.use(expectedPlugin)
33
+ const AppError = Error0.use(expectedPlugin({ hideWhenPublic: false }))
34
34
  const root = new AppError('root', { expected: true })
35
35
  const middle = new AppError('middle', { expected: false, cause: root })
36
36
  const leaf = new AppError('leaf', { expected: false, cause: middle })
@@ -39,7 +39,7 @@ describe('expectedPlugin', () => {
39
39
  })
40
40
 
41
41
  it('treats undefined expected as unexpected', () => {
42
- const AppError = Error0.use(expectedPlugin)
42
+ const AppError = Error0.use(expectedPlugin({ hideWhenPublic: false }))
43
43
  const error = new AppError('without expected')
44
44
  expect(error.expected).toBe(false)
45
45
  expect(error.isExpected()).toBe(false)
@@ -13,13 +13,19 @@ const isExpected = (flow: unknown[]) => {
13
13
  return expected
14
14
  }
15
15
 
16
- export const expectedPlugin = Error0.plugin()
17
- .use('prop', 'expected', {
18
- init: (input: boolean) => input,
19
- resolve: ({ flow }) => isExpected(flow),
20
- serialize: ({ value }) => value,
21
- deserialize: ({ value }) => (typeof value === 'boolean' ? value : undefined),
22
- })
23
- .use('method', 'isExpected', (error) => {
24
- return isExpected(error.flow('expected'))
25
- })
16
+ export const expectedPlugin = ({ hideWhenPublic = true }: { hideWhenPublic?: boolean } = {}) =>
17
+ Error0.plugin()
18
+ .use('prop', 'expected', {
19
+ init: (input: boolean) => input,
20
+ resolve: ({ flow }) => isExpected(flow),
21
+ serialize: ({ resolved, isPublic }) => {
22
+ if (hideWhenPublic && isPublic) {
23
+ return undefined
24
+ }
25
+ return resolved
26
+ },
27
+ deserialize: ({ value }) => (typeof value === 'boolean' ? value : undefined),
28
+ })
29
+ .use('method', 'isExpected', (error) => {
30
+ return isExpected(error.flow('expected'))
31
+ })
@@ -6,7 +6,7 @@ describe('messageMergePlugin', () => {
6
6
  const statusPlugin = Error0.plugin().use('prop', 'status', {
7
7
  init: (input: number) => input,
8
8
  resolve: ({ flow }) => flow.find((value) => typeof value === 'number'),
9
- serialize: ({ value }) => value,
9
+ serialize: ({ resolved }) => resolved,
10
10
  deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
11
11
  })
12
12
 
@@ -15,13 +15,13 @@ describe('messageMergePlugin', () => {
15
15
  const codePlugin = Error0.plugin().use('prop', 'code', {
16
16
  init: (input: Code) => input,
17
17
  resolve: ({ flow }) => flow.find((value) => typeof value === 'string' && codes.includes(value)),
18
- serialize: ({ value, isPublic }) => (isPublic ? undefined : value),
18
+ serialize: ({ resolved, isPublic }) => (isPublic ? undefined : resolved),
19
19
  deserialize: ({ value }) =>
20
20
  typeof value === 'string' && codes.includes(value as Code) ? (value as Code) : undefined,
21
21
  })
22
22
 
23
23
  it('can merge message across causes in one serialized value', () => {
24
- const AppError = Error0.use(statusPlugin).use(codePlugin).use(messageMergePlugin)
24
+ const AppError = Error0.use(statusPlugin).use(codePlugin).use(messageMergePlugin())
25
25
  const error1 = new AppError('test1', { status: 400, code: 'NOT_FOUND' })
26
26
  const error2 = new AppError('test2', { status: 401, cause: error1 })
27
27
  expect(error1.message).toBe('test1')