@devp0nt/error0 1.0.0-next.47 → 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 +165 -85
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/index.d.cts +47 -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 +47 -16
  23. package/dist/esm/index.js +165 -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 +96 -23
  45. package/src/index.ts +229 -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>,
@@ -404,6 +430,17 @@ export type ClassError0<TPluginsMap extends ErrorPluginsMap = EmptyPluginsMap> =
404
430
  from: (
405
431
  error: unknown,
406
432
  ) => Error0 & ErrorResolved<TPluginsMap> & ErrorOwnMethods<TPluginsMap> & ErrorResolveMethods<TPluginsMap>
433
+ round: (
434
+ error: unknown,
435
+ isPublic?: boolean,
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
+ }
407
444
  resolve: (error: unknown) => ErrorResolvedProps<TPluginsMap>
408
445
  serialize: (error: unknown, isPublic?: boolean) => Record<string, unknown>
409
446
  own: {
@@ -465,7 +502,9 @@ export type ClassError0<TPluginsMap extends ErrorPluginsMap = EmptyPluginsMap> =
465
502
  export class Error0 extends Error {
466
503
  static readonly __pluginsMap?: EmptyPluginsMap
467
504
  readonly __pluginsMap?: EmptyPluginsMap
505
+ static MAX_CAUSES_DEPTH = 99
468
506
  protected static _plugins: ErrorPlugin[] = []
507
+ protected static _resolvedPlugin?: ErrorPluginResolved
469
508
 
470
509
  private static readonly _emptyPlugin: ErrorPluginResolved = {
471
510
  props: {},
@@ -474,35 +513,86 @@ export class Error0 extends Error {
474
513
  stack: undefined,
475
514
  cause: undefined,
476
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)
477
571
  }
478
572
 
479
573
  private static _getResolvedPlugin(this: typeof Error0): ErrorPluginResolved {
574
+ if (Object.prototype.hasOwnProperty.call(this, '_resolvedPlugin') && this._resolvedPlugin) {
575
+ return this._resolvedPlugin
576
+ }
480
577
  const resolved: ErrorPluginResolved = {
481
578
  props: {},
482
579
  methods: {},
483
580
  adapt: [],
581
+ propKeys: [],
582
+ propEntries: [],
583
+ methodEntries: [],
484
584
  }
485
585
  for (const plugin of this._plugins) {
486
- if (plugin.props && 'stack' in plugin.props) {
487
- throw new Error(RESERVED_STACK_PROP_ERROR)
488
- }
489
- if (plugin.props && 'message' in plugin.props) {
490
- throw new Error(RESERVED_MESSAGE_PROP_ERROR)
491
- }
492
- Object.assign(resolved.props, plugin.props ?? this._emptyPlugin.props)
493
- Object.assign(resolved.methods, plugin.methods ?? this._emptyPlugin.methods)
494
- resolved.adapt.push(...(plugin.adapt ?? this._emptyPlugin.adapt))
495
- if (typeof plugin.stack !== 'undefined') {
496
- resolved.stack = plugin.stack
497
- }
498
- if (typeof plugin.cause !== 'undefined') {
499
- resolved.cause = plugin.cause
500
- }
501
- if (typeof plugin.message !== 'undefined') {
502
- resolved.message = plugin.message
503
- }
586
+ this._applyPlugin(resolved, plugin)
504
587
  }
505
- 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
506
596
  }
507
597
 
508
598
  constructor(message: string, input?: ErrorInput<EmptyPluginsMap>)
@@ -520,37 +610,47 @@ export class Error0 extends Error {
520
610
 
521
611
  const ctor = this.constructor as typeof Error0
522
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 })
523
615
 
524
- for (const [key, prop] of Object.entries(plugin.props)) {
616
+ for (const [key, prop] of plugin.propEntries) {
525
617
  if (key === 'stack') {
526
618
  continue
527
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
+ })
528
633
  if (key in input) {
529
634
  const ownValue = (input as Record<string, unknown>)[key]
530
- if (typeof prop.init === 'function') {
531
- ;(this as Record<string, unknown>)[key] = prop.init(ownValue)
532
- } else {
533
- ;(this as Record<string, unknown>)[key] = ownValue
534
- }
535
- } else {
536
- Object.defineProperty(this, key, {
537
- get: () => prop.resolve({ own: undefined, flow: this.flow(key), error: this }),
538
- set: (value) => {
539
- Object.defineProperty(this, key, {
540
- value,
541
- writable: true,
542
- enumerable: true,
543
- configurable: true,
544
- })
545
- },
546
- enumerable: true,
547
- configurable: true,
548
- })
635
+ ownStore[key] = typeof prop.init === 'function' ? prop.init(ownValue) : ownValue
549
636
  }
550
637
  }
551
638
  }
552
639
 
553
- 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
+ }
554
654
  const d = Object.getOwnPropertyDescriptor(object, key)
555
655
  if (!d) return false
556
656
  if (typeof d.get === 'function' || typeof d.set === 'function') {
@@ -563,15 +663,22 @@ export class Error0 extends Error {
563
663
  return true
564
664
  }
565
665
  private static _ownByKey(error: object, key: string): unknown {
566
- 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)) {
567
671
  return (error as Record<string, unknown>)[key]
568
672
  }
569
673
  return undefined
570
674
  }
571
675
  private static _flowByKey(error: object, key: string): unknown[] {
572
- return this.causes(error, true).map((cause) => {
573
- return this._ownByKey(cause, key)
574
- })
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
575
682
  }
576
683
 
577
684
  static own<TThis extends typeof Error0>(this: TThis, error: unknown): ErrorOwnProps<PluginsMapOf<TThis>>
@@ -585,7 +692,7 @@ export class Error0 extends Error {
585
692
  if (key === undefined) {
586
693
  const ownValues: Record<string, unknown> = {}
587
694
  const plugin = this._getResolvedPlugin()
588
- for (const ownKey of Object.keys(plugin.props)) {
695
+ for (const ownKey of plugin.propKeys) {
589
696
  ownValues[ownKey] = this._ownByKey(error0, ownKey)
590
697
  }
591
698
  return ownValues
@@ -610,7 +717,6 @@ export class Error0 extends Error {
610
717
  error: unknown,
611
718
  key: TKey,
612
719
  ): Array<ErrorOwnProps<PluginsMapOf<TThis>>[TKey]>
613
- static flow(error: unknown, key: string): unknown[]
614
720
  static flow(error: unknown, key: string): unknown[] {
615
721
  const error0 = this.from(error)
616
722
  return this._flowByKey(error0, key)
@@ -619,45 +725,48 @@ export class Error0 extends Error {
619
725
  this: TThis,
620
726
  key: TKey,
621
727
  ): Array<ErrorOwnProps<PluginsMapOfInstance<TThis>>[TKey]>
622
- flow(key: string): unknown[]
623
728
  flow(key: string): unknown[] {
624
729
  const ctor = this.constructor as typeof Error0
625
730
  return ctor._flowByKey(this, key)
626
731
  }
627
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
+
628
758
  static resolve<TThis extends typeof Error0>(this: TThis, error: unknown): ErrorResolvedProps<PluginsMapOf<TThis>>
629
759
  static resolve(error: unknown): Record<string, unknown>
630
760
  static resolve(error: unknown): Record<string, unknown> {
631
761
  const error0 = this.from(error)
632
762
  const resolved: Record<string, unknown> = {}
633
763
  const plugin = this._getResolvedPlugin()
634
- for (const [key, prop] of Object.entries(plugin.props)) {
635
- try {
636
- // resolved[key] = (error0 as unknown as Record<string, unknown>)[key]
637
- const options = Object.defineProperties(
638
- {
639
- error: error0,
640
- },
641
- {
642
- own: {
643
- get: () => error0.own(key as never),
644
- },
645
- flow: {
646
- get: () => error0.flow(key),
647
- },
648
- },
649
- )
650
- resolved[key] = prop.resolve(options as never)
651
- // resolved[key] = prop.resolve({ own: error0.own(key as never), flow: error0.flow(key), error: error0 })
652
- } catch {
653
- // eslint-disable-next-line no-console
654
- console.error(`Error0: failed to resolve property ${key}`, error0)
655
- }
764
+ for (const key of plugin.propKeys) {
765
+ resolved[key] = this._resolveByKey(error0, key, plugin)
656
766
  }
657
767
  return resolved
658
768
  }
659
769
  resolve<TThis extends Error0>(this: TThis): ErrorResolvedProps<PluginsMapOfInstance<TThis>>
660
- resolve(): Record<string, unknown>
661
770
  resolve(): Record<string, unknown> {
662
771
  const ctor = this.constructor as typeof Error0
663
772
  return ctor.resolve(this)
@@ -668,9 +777,9 @@ export class Error0 extends Error {
668
777
  static causes(error: unknown, instancesOnly?: boolean): unknown[] {
669
778
  const causes: unknown[] = []
670
779
  let current: unknown = error
671
- const maxDepth = 99
672
780
  const seen = new Set<unknown>()
673
- for (let depth = 0; depth < maxDepth; depth += 1) {
781
+ let depth = 0
782
+ while (depth < this.MAX_CAUSES_DEPTH) {
674
783
  if (seen.has(current)) {
675
784
  break
676
785
  }
@@ -682,6 +791,7 @@ export class Error0 extends Error {
682
791
  break
683
792
  }
684
793
  current = (current as { cause?: unknown }).cause
794
+ depth += 1
685
795
  }
686
796
  return causes
687
797
  }
@@ -713,6 +823,10 @@ export class Error0 extends Error {
713
823
  return this._fromNonError0(error)
714
824
  }
715
825
 
826
+ static round(error: unknown, isPublic = false): Error0 {
827
+ return this.from(this.serialize(error, isPublic))
828
+ }
829
+
716
830
  private static _applyAdapt(error: Error0): Error0 {
717
831
  const plugin = this._getResolvedPlugin()
718
832
  for (const adapt of plugin.adapt) {
@@ -732,8 +846,7 @@ export class Error0 extends Error {
732
846
  const errorRecord = error as Record<string, unknown>
733
847
  const recreated = new this(message)
734
848
  const plugin = this._getResolvedPlugin()
735
- const propsEntries = Object.entries(plugin.props)
736
- for (const [key, prop] of propsEntries) {
849
+ for (const [key, prop] of plugin.propEntries) {
737
850
  if (prop.deserialize === false) {
738
851
  continue
739
852
  }
@@ -741,7 +854,7 @@ export class Error0 extends Error {
741
854
  continue
742
855
  }
743
856
  try {
744
- const value = prop.deserialize({ value: errorRecord[key], serialized: errorRecord })
857
+ const value = prop.deserialize({ value: errorRecord[key], record: errorRecord })
745
858
  ;(recreated as unknown as Record<string, unknown>)[key] = value
746
859
  } catch {
747
860
  // eslint-disable-next-line no-console
@@ -798,9 +911,9 @@ export class Error0 extends Error {
798
911
  const Base = this as unknown as typeof Error0
799
912
  const Error0Extended = class Error0 extends Base {}
800
913
  ;(Error0Extended as typeof Error0)._plugins = [...Base._plugins, plugin]
801
-
802
- const resolved = (Error0Extended as typeof Error0)._getResolvedPlugin()
803
- 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) {
804
917
  Object.defineProperty((Error0Extended as typeof Error0).prototype, key, {
805
918
  value: function (...args: unknown[]) {
806
919
  return method(this as Error0, ...args)
@@ -997,9 +1110,9 @@ export class Error0 extends Error {
997
1110
 
998
1111
  static serialize(error: unknown, isPublic = true): Record<string, unknown> {
999
1112
  const error0 = this.from(error)
1000
- const resolvedProps = this.resolve(error0)
1001
- const resolvedRecord = resolvedProps as Record<string, unknown>
1002
1113
  const plugin = this._getResolvedPlugin()
1114
+ const resolveByKey = (targetError: Error0, key: string, targetPlugin: ErrorPluginResolved): unknown =>
1115
+ this._resolveByKey(targetError, key, targetPlugin)
1003
1116
  const messagePlugin = plugin.message
1004
1117
  let serializedMessage: unknown = error0.message
1005
1118
  try {
@@ -1013,25 +1126,38 @@ export class Error0 extends Error {
1013
1126
  }
1014
1127
  const json: Record<string, unknown> = {
1015
1128
  name: error0.name,
1016
- message: serializedMessage,
1017
1129
  // we do not serialize causes, it is enough that we have floated props and adapt helper
1018
1130
  // cause: error0.cause,
1019
1131
  }
1132
+ if (serializedMessage !== undefined) {
1133
+ json.message = serializedMessage
1134
+ }
1020
1135
 
1021
- const propsEntries = Object.entries(plugin.props)
1022
- for (const [key, prop] of propsEntries) {
1136
+ for (const [key, prop] of plugin.propEntries) {
1023
1137
  if (prop.serialize === false) {
1024
1138
  continue
1025
1139
  }
1026
1140
  try {
1027
- const value = resolvedRecord[key]
1028
- 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>)
1029
1155
  if (jsonValue !== undefined) {
1030
1156
  json[key] = jsonValue
1031
1157
  }
1032
1158
  } catch {
1033
1159
  // eslint-disable-next-line no-console
1034
- console.error(`Error0: failed to serialize property ${key}`, resolvedRecord)
1160
+ console.error(`Error0: failed to serialize property ${key}`, error0)
1035
1161
  }
1036
1162
  }
1037
1163
  const stackPlugin = plugin.stack
@@ -1065,14 +1191,16 @@ export class Error0 extends Error {
1065
1191
  console.error('Error0: failed to serialize cause', error0)
1066
1192
  }
1067
1193
  }
1068
- return Object.fromEntries(Object.entries(json).filter(([, value]) => value !== undefined)) as Record<
1069
- string,
1070
- unknown
1071
- >
1194
+ return json
1072
1195
  }
1073
1196
 
1074
1197
  serialize(isPublic = true): Record<string, unknown> {
1075
1198
  const ctor = this.constructor as typeof Error0
1076
1199
  return ctor.serialize(this, isPublic)
1077
1200
  }
1201
+
1202
+ round<TThis extends Error0>(this: TThis, isPublic = true): TThis {
1203
+ const ctor = this.constructor as typeof Error0
1204
+ return ctor.round(this, isPublic) as TThis
1205
+ }
1078
1206
  }
@@ -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')