From 265cc7ef7f083ce7d1785d600d0325f5b5af9319 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 11 Oct 2024 14:56:41 +0200 Subject: [PATCH 01/24] feat: Add custom error classes --- yarn.lock | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4072cb441..b8c0f0edd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8171,16 +8171,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.5.1": - version: 2.6.0 - resolution: "yaml@npm:2.6.0" - bin: - yaml: bin.mjs - checksum: 10/f4369f667c7626c216ea81b5840fe9b530cdae4cff2d84d166ec1239e54bf332dbfac4a71bf60d121f8e85e175364a4e280a520292269b6cf9d074368309adf9 - languageName: node - linkType: hard - -"yaml@npm:~2.5.0": +"yaml@npm:^2.5.1, yaml@npm:~2.5.0": version: 2.5.1 resolution: "yaml@npm:2.5.1" bin: From 15d5ac72c4db02924b5407bb34f479b79965cd2e Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 11 Oct 2024 15:24:59 +0200 Subject: [PATCH 02/24] feat: Use custom error in kernel package --- packages/errors/src/utils/asError.test.ts | 49 +++++++++++++++++++ packages/errors/src/utils/asError.ts | 11 +++++ .../streams/src/envelope-kit.types.test.ts | 2 - 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 packages/errors/src/utils/asError.test.ts create mode 100644 packages/errors/src/utils/asError.ts diff --git a/packages/errors/src/utils/asError.test.ts b/packages/errors/src/utils/asError.test.ts new file mode 100644 index 000000000..524c05196 --- /dev/null +++ b/packages/errors/src/utils/asError.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; + +import { asError } from './asError.js'; + +describe('asError', () => { + it('should return the input if it is already an Error', () => { + const originalError = new Error('Existing error'); + const result = asError(originalError); + + expect(result).toBe(originalError); + expect(result.message).toBe('Existing error'); + }); + + it.each([ + { input: 'Some error string', expectedCause: 'Some error string' }, + { input: 404, expectedCause: 404 }, + { input: { key: 'value' }, expectedCause: { key: 'value' } }, + { input: ['error', 'details'], expectedCause: ['error', 'details'] }, + { input: null, expectedCause: null }, + { input: undefined, expectedCause: undefined }, + { input: true, expectedCause: true }, + ])( + 'should create a new Error if the input is not an Error object', + ({ input, expectedCause }) => { + const result = asError(input); + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe('Unknown'); + expect(result.cause).toStrictEqual(expectedCause); + }, + ); + + it('should create a new Error if the input is a symbol', () => { + const problem = Symbol('error'); + const result = asError(problem); + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe('Unknown'); + expect(typeof result.cause).toBe('symbol'); + expect(result.cause).toBe(problem); + }); + + it('should create a new Error if the input is a function', () => { + const problem = (): string => 'problem'; + const result = asError(problem); + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe('Unknown'); + expect(typeof result.cause).toBe('function'); + expect(result.cause).toBe(problem); + }); +}); diff --git a/packages/errors/src/utils/asError.ts b/packages/errors/src/utils/asError.ts new file mode 100644 index 000000000..68a5faed1 --- /dev/null +++ b/packages/errors/src/utils/asError.ts @@ -0,0 +1,11 @@ +/** + * Coerce an unknown problem into an Error object. + * + * @param problem - Whatever was caught. + * @returns The problem if it is an Error, or a new Error with the problem as the cause. + */ +export function asError(problem: unknown): Error { + return problem instanceof Error + ? problem + : new Error('Unknown', { cause: problem }); +} diff --git a/packages/streams/src/envelope-kit.types.test.ts b/packages/streams/src/envelope-kit.types.test.ts index 76ed93812..9823ec9f2 100644 --- a/packages/streams/src/envelope-kit.types.test.ts +++ b/packages/streams/src/envelope-kit.types.test.ts @@ -105,7 +105,6 @@ describe('StreamEnveloper', () => { envelope.content.c; switch (envelope.label) { case Label.Foo: - // eslint-disable-next-line vitest/no-conditional-expect expect(envelope.label).toMatch(Label.Foo); break; // @ts-expect-error label is Label.Foo @@ -132,7 +131,6 @@ describe('StreamEnveloper', () => { envelope.label.length; break; case Label.Bar: - // eslint-disable-next-line vitest/no-conditional-expect expect(envelope.label).toMatch(Label.Bar); break; default: // unreachable From 2de12f43350d3a8421af6612f6742487807b2a1d Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 11 Oct 2024 15:47:30 +0200 Subject: [PATCH 03/24] revert rule --- packages/streams/src/envelope-kit.types.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/streams/src/envelope-kit.types.test.ts b/packages/streams/src/envelope-kit.types.test.ts index 9823ec9f2..76ed93812 100644 --- a/packages/streams/src/envelope-kit.types.test.ts +++ b/packages/streams/src/envelope-kit.types.test.ts @@ -105,6 +105,7 @@ describe('StreamEnveloper', () => { envelope.content.c; switch (envelope.label) { case Label.Foo: + // eslint-disable-next-line vitest/no-conditional-expect expect(envelope.label).toMatch(Label.Foo); break; // @ts-expect-error label is Label.Foo @@ -131,6 +132,7 @@ describe('StreamEnveloper', () => { envelope.label.length; break; case Label.Bar: + // eslint-disable-next-line vitest/no-conditional-expect expect(envelope.label).toMatch(Label.Bar); break; default: // unreachable From 50e2dd28b30a760a975437fa747c9d4644f2bbf3 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 14 Oct 2024 14:37:59 +0200 Subject: [PATCH 04/24] Reason with naming --- packages/errors/src/utils/asError.test.ts | 12 ++++++------ packages/errors/src/utils/asError.ts | 2 +- packages/kernel/src/Supervisor.test.ts | 2 +- packages/kernel/src/Supervisor.ts | 2 +- packages/kernel/src/Vat.test.ts | 2 +- packages/kernel/src/Vat.ts | 12 ++++++------ yarn.lock | 11 ++++++++++- 7 files changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/errors/src/utils/asError.test.ts b/packages/errors/src/utils/asError.test.ts index 524c05196..32a32654e 100644 --- a/packages/errors/src/utils/asError.test.ts +++ b/packages/errors/src/utils/asError.test.ts @@ -1,11 +1,11 @@ import { describe, it, expect } from 'vitest'; -import { asError } from './asError.js'; +import { toError } from './toError.js'; -describe('asError', () => { +describe('toError', () => { it('should return the input if it is already an Error', () => { const originalError = new Error('Existing error'); - const result = asError(originalError); + const result = toError(originalError); expect(result).toBe(originalError); expect(result.message).toBe('Existing error'); @@ -22,7 +22,7 @@ describe('asError', () => { ])( 'should create a new Error if the input is not an Error object', ({ input, expectedCause }) => { - const result = asError(input); + const result = toError(input); expect(result).toBeInstanceOf(Error); expect(result.message).toBe('Unknown'); expect(result.cause).toStrictEqual(expectedCause); @@ -31,7 +31,7 @@ describe('asError', () => { it('should create a new Error if the input is a symbol', () => { const problem = Symbol('error'); - const result = asError(problem); + const result = toError(problem); expect(result).toBeInstanceOf(Error); expect(result.message).toBe('Unknown'); expect(typeof result.cause).toBe('symbol'); @@ -40,7 +40,7 @@ describe('asError', () => { it('should create a new Error if the input is a function', () => { const problem = (): string => 'problem'; - const result = asError(problem); + const result = toError(problem); expect(result).toBeInstanceOf(Error); expect(result.message).toBe('Unknown'); expect(typeof result.cause).toBe('function'); diff --git a/packages/errors/src/utils/asError.ts b/packages/errors/src/utils/asError.ts index 68a5faed1..ac925df54 100644 --- a/packages/errors/src/utils/asError.ts +++ b/packages/errors/src/utils/asError.ts @@ -4,7 +4,7 @@ * @param problem - Whatever was caught. * @returns The problem if it is an Error, or a new Error with the problem as the cause. */ -export function asError(problem: unknown): Error { +export function toError(problem: unknown): Error { return problem instanceof Error ? problem : new Error('Unknown', { cause: problem }); diff --git a/packages/kernel/src/Supervisor.test.ts b/packages/kernel/src/Supervisor.test.ts index dbc885100..4c5a43833 100644 --- a/packages/kernel/src/Supervisor.test.ts +++ b/packages/kernel/src/Supervisor.test.ts @@ -86,7 +86,7 @@ describe('Supervisor', () => { expect(replySpy).toHaveBeenCalledWith('v0:0', { method: VatCommandMethod.CapTpInit, - params: '~~~ CapTP Initialized ~~~', + params: '~~~ CapTp Initialized ~~~', }); }); diff --git a/packages/kernel/src/Supervisor.ts b/packages/kernel/src/Supervisor.ts index f50039a88..cea4d620e 100644 --- a/packages/kernel/src/Supervisor.ts +++ b/packages/kernel/src/Supervisor.ts @@ -101,7 +101,7 @@ export class Supervisor { ); await this.replyToMessage(id, { method: VatCommandMethod.CapTpInit, - params: '~~~ CapTP Initialized ~~~', + params: '~~~ CapTp Initialized ~~~', }); break; } diff --git a/packages/kernel/src/Vat.test.ts b/packages/kernel/src/Vat.test.ts index c559afad8..d6c94329f 100644 --- a/packages/kernel/src/Vat.test.ts +++ b/packages/kernel/src/Vat.test.ts @@ -203,7 +203,7 @@ describe('Vat', () => { ); expect(consoleLogSpy).toHaveBeenCalledWith( - 'CapTP from vat', + 'CapTp from vat', stringify(capTpQuestion), ); diff --git a/packages/kernel/src/Vat.ts b/packages/kernel/src/Vat.ts index 2514036e3..4f0a34b06 100644 --- a/packages/kernel/src/Vat.ts +++ b/packages/kernel/src/Vat.ts @@ -110,9 +110,9 @@ export class Vat { } /** - * Make a CapTP connection. + * Make a CapTp connection. * - * @returns A promise that resolves when the CapTP connection is made. + * @returns A promise that resolves when the CapTp connection is made. */ async makeCapTp(): Promise { if (this.capTp !== undefined) { @@ -129,7 +129,7 @@ export class Vat { this.streamEnvelopeReplyHandler.contentHandlers.capTp = async ( content: CapTpMessage, ) => { - this.logger.log('CapTP from vat', stringify(content)); + this.logger.log('CapTp from vat', stringify(content)); ctp.dispatch(content); }; @@ -140,10 +140,10 @@ export class Vat { } /** - * Call a CapTP method. + * Call a CapTp method. * - * @param payload - The CapTP payload. - * @returns A promise that resolves the result of the CapTP call. + * @param payload - The CapTp payload. + * @returns A promise that resolves the result of the CapTp call. */ async callCapTp(payload: CapTpPayload): Promise { if (!this.capTp) { diff --git a/yarn.lock b/yarn.lock index b8c0f0edd..4072cb441 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8171,7 +8171,16 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.5.1, yaml@npm:~2.5.0": +"yaml@npm:^2.5.1": + version: 2.6.0 + resolution: "yaml@npm:2.6.0" + bin: + yaml: bin.mjs + checksum: 10/f4369f667c7626c216ea81b5840fe9b530cdae4cff2d84d166ec1239e54bf332dbfac4a71bf60d121f8e85e175364a4e280a520292269b6cf9d074368309adf9 + languageName: node + linkType: hard + +"yaml@npm:~2.5.0": version: 2.5.1 resolution: "yaml@npm:2.5.1" bin: From 265ec1ce1640892e4a288582f0c35484d14fa444 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 14 Oct 2024 17:46:15 +0200 Subject: [PATCH 05/24] fix filenames --- packages/errors/src/utils/asError.test.ts | 49 ----------------------- packages/errors/src/utils/asError.ts | 11 ----- 2 files changed, 60 deletions(-) delete mode 100644 packages/errors/src/utils/asError.test.ts delete mode 100644 packages/errors/src/utils/asError.ts diff --git a/packages/errors/src/utils/asError.test.ts b/packages/errors/src/utils/asError.test.ts deleted file mode 100644 index 32a32654e..000000000 --- a/packages/errors/src/utils/asError.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { toError } from './toError.js'; - -describe('toError', () => { - it('should return the input if it is already an Error', () => { - const originalError = new Error('Existing error'); - const result = toError(originalError); - - expect(result).toBe(originalError); - expect(result.message).toBe('Existing error'); - }); - - it.each([ - { input: 'Some error string', expectedCause: 'Some error string' }, - { input: 404, expectedCause: 404 }, - { input: { key: 'value' }, expectedCause: { key: 'value' } }, - { input: ['error', 'details'], expectedCause: ['error', 'details'] }, - { input: null, expectedCause: null }, - { input: undefined, expectedCause: undefined }, - { input: true, expectedCause: true }, - ])( - 'should create a new Error if the input is not an Error object', - ({ input, expectedCause }) => { - const result = toError(input); - expect(result).toBeInstanceOf(Error); - expect(result.message).toBe('Unknown'); - expect(result.cause).toStrictEqual(expectedCause); - }, - ); - - it('should create a new Error if the input is a symbol', () => { - const problem = Symbol('error'); - const result = toError(problem); - expect(result).toBeInstanceOf(Error); - expect(result.message).toBe('Unknown'); - expect(typeof result.cause).toBe('symbol'); - expect(result.cause).toBe(problem); - }); - - it('should create a new Error if the input is a function', () => { - const problem = (): string => 'problem'; - const result = toError(problem); - expect(result).toBeInstanceOf(Error); - expect(result.message).toBe('Unknown'); - expect(typeof result.cause).toBe('function'); - expect(result.cause).toBe(problem); - }); -}); diff --git a/packages/errors/src/utils/asError.ts b/packages/errors/src/utils/asError.ts deleted file mode 100644 index ac925df54..000000000 --- a/packages/errors/src/utils/asError.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Coerce an unknown problem into an Error object. - * - * @param problem - Whatever was caught. - * @returns The problem if it is an Error, or a new Error with the problem as the cause. - */ -export function toError(problem: unknown): Error { - return problem instanceof Error - ? problem - : new Error('Unknown', { cause: problem }); -} From 3f46881fa9dce3324622264524bcdaffde43e09a Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 14 Oct 2024 17:30:07 +0200 Subject: [PATCH 06/24] feat: marshal ocap error --- packages/errors/src/index.ts | 2 + .../errors/src/utils/isCodedError.test.ts | 39 ++++++++++++++ packages/errors/src/utils/isCodedError.ts | 13 +++++ packages/errors/src/utils/isOcapError.test.ts | 54 +++++++++++++++++++ packages/errors/src/utils/isOcapError.ts | 11 ++++ packages/streams/package.json | 1 + packages/streams/src/utils.test.ts | 37 +++++++++++++ packages/streams/src/utils.ts | 18 ++++++- packages/streams/tsconfig.build.json | 5 +- packages/streams/tsconfig.json | 2 +- yarn.lock | 1 + 11 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 packages/errors/src/utils/isCodedError.test.ts create mode 100644 packages/errors/src/utils/isCodedError.ts create mode 100644 packages/errors/src/utils/isOcapError.test.ts create mode 100644 packages/errors/src/utils/isOcapError.ts diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts index 2b653611d..1be575286 100644 --- a/packages/errors/src/index.ts +++ b/packages/errors/src/index.ts @@ -8,3 +8,5 @@ export { StreamReadError, } from './errors.js'; export { toError } from './utils/toError.js'; +export { isCodedError } from './utils/isCodedError.js'; +export { isOcapError } from './utils/isOcapError.js'; diff --git a/packages/errors/src/utils/isCodedError.test.ts b/packages/errors/src/utils/isCodedError.test.ts new file mode 100644 index 000000000..5558df11b --- /dev/null +++ b/packages/errors/src/utils/isCodedError.test.ts @@ -0,0 +1,39 @@ +import { BaseError } from 'src/BaseError.js'; +import { VatAlreadyExistsError } from 'src/errors.js'; +import { describe, it, expect } from 'vitest'; + +import { isCodedError } from './isCodedError.js'; +import { ErrorCode } from '../constants.js'; + +class MockCodedError extends Error { + code: string; + + constructor(message: string, code: string) { + super(message); + this.code = code; + } +} + +describe('isCodedError', () => { + it.each([ + [ + new MockCodedError('An error occurred', 'ERROR_CODE'), + true, + 'coded error', + ], + [ + new BaseError(ErrorCode.VatNotFound, 'Base Error'), + true, + 'Base class error', + ], + [new VatAlreadyExistsError('v1'), true, 'VatAlreadyExistsError error'], + [new Error('An error without a code'), false, 'error without a code'], + [ + { message: 'Not an error', code: 'SOME_CODE' } as unknown as Error, + false, + 'non-error object', + ], + ])('should return %s for %s', (inputError, expectedResult) => { + expect(isCodedError(inputError)).toBe(expectedResult); + }); +}); diff --git a/packages/errors/src/utils/isCodedError.ts b/packages/errors/src/utils/isCodedError.ts new file mode 100644 index 000000000..402a619a8 --- /dev/null +++ b/packages/errors/src/utils/isCodedError.ts @@ -0,0 +1,13 @@ +type CodedError = { + code: string; +} & Error; + +/** + * Checks if an error has a code. + * + * @param error - The error to check. + * @returns Whether the error has a code. + */ +export function isCodedError(error: Error): error is CodedError { + return error instanceof Error && 'code' in error; +} diff --git a/packages/errors/src/utils/isOcapError.test.ts b/packages/errors/src/utils/isOcapError.test.ts new file mode 100644 index 000000000..ea96cab7e --- /dev/null +++ b/packages/errors/src/utils/isOcapError.test.ts @@ -0,0 +1,54 @@ +import type { Json } from '@metamask/utils'; +import { BaseError } from 'src/BaseError.js'; +import { VatAlreadyExistsError } from 'src/errors.js'; +import { describe, it, expect } from 'vitest'; + +import { isOcapError } from './isOcapError.js'; +import { ErrorCode } from '../constants.js'; + +class MockCodedError extends Error { + code: string; + + data: Json | undefined; + + constructor(message: string, code: string, data?: Json) { + super(message); + this.code = code; + this.data = data; + } +} + +describe('isOcapError', () => { + it.each([ + [ + new BaseError(ErrorCode.VatNotFound, 'Base Error'), + true, + 'Base class error', + ], + [new VatAlreadyExistsError('v1'), true, 'VatAlreadyExistsError error'], + [ + new MockCodedError('An error occurred', 'ERROR_CODE'), + false, + 'coded error', + ], + [ + new MockCodedError('An error with data occurred', 'ERROR_CODE_DATA', { + test: 1, + }), + false, + 'coded error', + ], + [new Error('A regular error'), false, 'regular error'], + [ + { + message: 'Not an error', + code: 'SOME_CODE', + data: { test: 1 }, + } as unknown as Error, + false, + 'non-error object', + ], + ])('should return %s for %s', (inputError, expectedResult) => { + expect(isOcapError(inputError)).toBe(expectedResult); + }); +}); diff --git a/packages/errors/src/utils/isOcapError.ts b/packages/errors/src/utils/isOcapError.ts new file mode 100644 index 000000000..1f2c11969 --- /dev/null +++ b/packages/errors/src/utils/isOcapError.ts @@ -0,0 +1,11 @@ +import { BaseError } from '../BaseError.js'; + +/** + * Type guard to check if an error is a custom Ocap error (BaseError). + * + * @param error - The error to check. + * @returns `true` if the error is an instance of BaseError. + */ +export function isOcapError(error: Error): error is BaseError { + return error instanceof BaseError; +} diff --git a/packages/streams/package.json b/packages/streams/package.json index 3bec7129e..cc5000eca 100644 --- a/packages/streams/package.json +++ b/packages/streams/package.json @@ -50,6 +50,7 @@ "@endo/stream": "^1.2.6", "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^9.3.0", + "@ocap/errors": "workspace:^", "@ocap/utils": "workspace:^" }, "devDependencies": { diff --git a/packages/streams/src/utils.test.ts b/packages/streams/src/utils.test.ts index 0ac97a4f8..7b2d9992b 100644 --- a/packages/streams/src/utils.test.ts +++ b/packages/streams/src/utils.test.ts @@ -1,4 +1,5 @@ import type { Json } from '@metamask/utils'; +import { ErrorCode, VatNotFoundError } from '@ocap/errors'; import { makeErrorMatcherFactory } from '@ocap/test-utils'; import { stringify } from '@ocap/utils'; import { describe, expect, it } from 'vitest'; @@ -133,6 +134,42 @@ describe('marshalError', () => { }), ); }); + + it('should marshal a coded error', () => { + class CodedError extends Error { + code: string; + + constructor(message: string, code: string) { + super(message); + this.code = code; + } + } + + const error = new CodedError('Coded error', 'CODE'); + const marshaledError = marshalError(error); + expect(marshaledError).toStrictEqual( + expect.objectContaining({ + [ErrorSentinel]: true, + message: 'Coded error', + stack: expect.any(String), + code: 'CODE', + }), + ); + }); + + it('should marshal an ocap error', () => { + const error = new VatNotFoundError('v1'); + const marshaledError = marshalError(error); + expect(marshaledError).toStrictEqual( + expect.objectContaining({ + [ErrorSentinel]: true, + message: 'Vat does not exist.', + stack: expect.any(String), + code: ErrorCode.VatNotFound, + data: stringify({ vatId: 'v1' }), + }), + ); + }); }); describe('unmarshalError', () => { diff --git a/packages/streams/src/utils.ts b/packages/streams/src/utils.ts index 91b999653..f32fcdb9b 100644 --- a/packages/streams/src/utils.ts +++ b/packages/streams/src/utils.ts @@ -9,7 +9,13 @@ import { string, union, } from '@metamask/superstruct'; -import { type Json, UnsafeJsonStruct, object } from '@metamask/utils'; +import { + type Json, + JsonStruct, + UnsafeJsonStruct, + object, +} from '@metamask/utils'; +import { isCodedError, isOcapError } from '@ocap/errors'; import { stringify } from '@ocap/utils'; export type { Reader, Writer }; @@ -97,6 +103,8 @@ export const ErrorSentinel = '@@MARSHALED_ERROR'; type MarshaledError = { [ErrorSentinel]: true; message: string; + code?: string; + data?: Json; stack?: string; cause?: MarshaledError | string; }; @@ -104,6 +112,8 @@ type MarshaledError = { const MarshaledErrorStruct: Struct = object({ [ErrorSentinel]: literal(true), message: string(), + code: optional(string()), + data: optional(JsonStruct), stack: optional(string()), cause: optional(union([string(), lazy(() => MarshaledErrorStruct)])), }) as Struct; @@ -138,6 +148,12 @@ export function marshalError(error: Error): MarshaledError { if (error.stack) { output.stack = error.stack; } + if (isCodedError(error) && error.code) { + output.code = error.code; + } + if (isOcapError(error) && error.data) { + output.data = stringify(error.data); + } return output; } diff --git a/packages/streams/tsconfig.build.json b/packages/streams/tsconfig.build.json index 0f6059b8b..7f786ed3f 100644 --- a/packages/streams/tsconfig.build.json +++ b/packages/streams/tsconfig.build.json @@ -7,6 +7,9 @@ "rootDir": "./src", "types": ["ses"] }, - "references": [{ "path": "../utils/tsconfig.build.json" }], + "references": [ + { "path": "../utils/tsconfig.build.json" }, + { "path": "../errors/tsconfig.build.json" } + ], "include": ["./src"] } diff --git a/packages/streams/tsconfig.json b/packages/streams/tsconfig.json index 1c4926288..e431a82af 100644 --- a/packages/streams/tsconfig.json +++ b/packages/streams/tsconfig.json @@ -5,7 +5,7 @@ "lib": ["DOM", "ES2022"], "types": ["ses", "vitest", "vitest/jsdom"] }, - "references": [{ "path": "../test-utils" }], + "references": [{ "path": "../test-utils" }, { "path": "../errors" }], "include": [ "./src", "./src/chrome.d.ts", diff --git a/yarn.lock b/yarn.lock index 4072cb441..0b2d08f23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1683,6 +1683,7 @@ __metadata: "@metamask/eslint-config-typescript": "npm:^14.0.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.3.0" + "@ocap/errors": "workspace:^" "@ocap/test-utils": "workspace:^" "@ocap/utils": "workspace:^" "@ts-bridge/cli": "npm:^0.5.1" From 831170be780a3bb95748ee91aa799c02b4c4f602 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 14 Oct 2024 17:39:40 +0200 Subject: [PATCH 07/24] Improve isCodedError type guard --- .../errors/src/utils/isCodedError.test.ts | 20 ++++++++++++++----- packages/errors/src/utils/isCodedError.ts | 8 ++++++-- packages/errors/src/utils/isOcapError.test.ts | 4 ++-- packages/streams/src/utils.ts | 2 +- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/errors/src/utils/isCodedError.test.ts b/packages/errors/src/utils/isCodedError.test.ts index 5558df11b..9e448625f 100644 --- a/packages/errors/src/utils/isCodedError.test.ts +++ b/packages/errors/src/utils/isCodedError.test.ts @@ -1,14 +1,14 @@ -import { BaseError } from 'src/BaseError.js'; -import { VatAlreadyExistsError } from 'src/errors.js'; import { describe, it, expect } from 'vitest'; import { isCodedError } from './isCodedError.js'; +import { BaseError } from '../BaseError.js'; import { ErrorCode } from '../constants.js'; +import { VatAlreadyExistsError } from '../errors.js'; class MockCodedError extends Error { - code: string; + code: string | number; - constructor(message: string, code: string) { + constructor(message: string, code: string | number) { super(message); this.code = code; } @@ -19,7 +19,12 @@ describe('isCodedError', () => { [ new MockCodedError('An error occurred', 'ERROR_CODE'), true, - 'coded error', + 'coded error with string code', + ], + [ + new MockCodedError('An error occurred', 12345), + true, + 'coded error with number code', ], [ new BaseError(ErrorCode.VatNotFound, 'Base Error'), @@ -33,6 +38,11 @@ describe('isCodedError', () => { false, 'non-error object', ], + [ + { message: 'Invalid code type', code: {} } as unknown as Error, + false, + 'non-string/non-number code', + ], ])('should return %s for %s', (inputError, expectedResult) => { expect(isCodedError(inputError)).toBe(expectedResult); }); diff --git a/packages/errors/src/utils/isCodedError.ts b/packages/errors/src/utils/isCodedError.ts index 402a619a8..1b0ff65d4 100644 --- a/packages/errors/src/utils/isCodedError.ts +++ b/packages/errors/src/utils/isCodedError.ts @@ -1,5 +1,5 @@ type CodedError = { - code: string; + code: string | number; } & Error; /** @@ -9,5 +9,9 @@ type CodedError = { * @returns Whether the error has a code. */ export function isCodedError(error: Error): error is CodedError { - return error instanceof Error && 'code' in error; + return ( + error instanceof Error && + 'code' in error && + (typeof error.code === 'string' || typeof error.code === 'number') + ); } diff --git a/packages/errors/src/utils/isOcapError.test.ts b/packages/errors/src/utils/isOcapError.test.ts index ea96cab7e..edd00ae98 100644 --- a/packages/errors/src/utils/isOcapError.test.ts +++ b/packages/errors/src/utils/isOcapError.test.ts @@ -1,10 +1,10 @@ import type { Json } from '@metamask/utils'; -import { BaseError } from 'src/BaseError.js'; -import { VatAlreadyExistsError } from 'src/errors.js'; import { describe, it, expect } from 'vitest'; import { isOcapError } from './isOcapError.js'; +import { BaseError } from '../BaseError.js'; import { ErrorCode } from '../constants.js'; +import { VatAlreadyExistsError } from '../errors.js'; class MockCodedError extends Error { code: string; diff --git a/packages/streams/src/utils.ts b/packages/streams/src/utils.ts index f32fcdb9b..0e09a8c49 100644 --- a/packages/streams/src/utils.ts +++ b/packages/streams/src/utils.ts @@ -149,7 +149,7 @@ export function marshalError(error: Error): MarshaledError { output.stack = error.stack; } if (isCodedError(error) && error.code) { - output.code = error.code; + output.code = String(error.code); } if (isOcapError(error) && error.data) { output.data = stringify(error.data); From edca60829cca4313ff919f22d2d7668c2639f2b2 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 14 Oct 2024 17:42:00 +0200 Subject: [PATCH 08/24] fix error tests --- packages/errors/src/index.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/errors/src/index.test.ts b/packages/errors/src/index.test.ts index bb1757b67..b18c6c7aa 100644 --- a/packages/errors/src/index.test.ts +++ b/packages/errors/src/index.test.ts @@ -12,6 +12,8 @@ describe('index', () => { 'VatCapTpConnectionNotFoundError', 'VatDeletedError', 'VatNotFoundError', + 'isCodedError', + 'isOcapError', 'toError', ]); }); From f584118e779dac7efdae3b9de40589eb33d1d2e3 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 14 Oct 2024 20:23:41 +0200 Subject: [PATCH 09/24] remove isCodeError --- packages/errors/src/BaseError.test.ts | 2 +- packages/errors/src/BaseError.ts | 6 +-- packages/errors/src/errors.test.ts | 2 +- packages/errors/src/errors.ts | 2 +- packages/errors/src/index.test.ts | 1 + packages/errors/src/index.ts | 4 +- .../errors/src/{constants.ts => types.ts} | 7 +++ .../errors/src/utils/isCodedError.test.ts | 49 ------------------- packages/errors/src/utils/isCodedError.ts | 17 ------- packages/errors/src/utils/isOcapError.test.ts | 2 +- packages/streams/src/utils.test.ts | 22 --------- packages/streams/src/utils.ts | 16 +++--- 12 files changed, 26 insertions(+), 104 deletions(-) rename packages/errors/src/{constants.ts => types.ts} (69%) delete mode 100644 packages/errors/src/utils/isCodedError.test.ts delete mode 100644 packages/errors/src/utils/isCodedError.ts diff --git a/packages/errors/src/BaseError.test.ts b/packages/errors/src/BaseError.test.ts index 63538b7d1..add15b5be 100644 --- a/packages/errors/src/BaseError.test.ts +++ b/packages/errors/src/BaseError.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { BaseError } from './BaseError.js'; -import { ErrorCode } from './constants.js'; +import { ErrorCode } from './types.js'; describe('BaseError', () => { const mockCode = ErrorCode.VatNotFound; diff --git a/packages/errors/src/BaseError.ts b/packages/errors/src/BaseError.ts index c62815162..f5aa9dce2 100644 --- a/packages/errors/src/BaseError.ts +++ b/packages/errors/src/BaseError.ts @@ -1,14 +1,12 @@ import type { Json } from '@metamask/utils'; -import type { ErrorCode } from './constants.js'; +import type { ErrorCode, OcapError } from './types.js'; -export class BaseError extends Error { +export class BaseError extends Error implements OcapError { public readonly code: ErrorCode; public data: Json | undefined; - public cause: unknown; - constructor(code: ErrorCode, message: string, data?: Json, cause?: unknown) { super(message, { cause }); diff --git a/packages/errors/src/errors.test.ts b/packages/errors/src/errors.test.ts index 558bec27f..240e78aeb 100644 --- a/packages/errors/src/errors.test.ts +++ b/packages/errors/src/errors.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { ErrorCode } from './constants.js'; import { VatAlreadyExistsError, VatNotFoundError, @@ -9,6 +8,7 @@ import { VatCapTpConnectionNotFoundError, VatDeletedError, } from './errors.js'; +import { ErrorCode } from './types.js'; describe('Errors classes', () => { const mockVatId = 'mockVatId'; diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts index 5fbddea50..e7d2e6b43 100644 --- a/packages/errors/src/errors.ts +++ b/packages/errors/src/errors.ts @@ -1,5 +1,5 @@ import { BaseError } from './BaseError.js'; -import { ErrorCode } from './constants.js'; +import { ErrorCode } from './types.js'; export class VatAlreadyExistsError extends BaseError { constructor(vatId: string) { diff --git a/packages/errors/src/index.test.ts b/packages/errors/src/index.test.ts index b18c6c7aa..13178d1fd 100644 --- a/packages/errors/src/index.test.ts +++ b/packages/errors/src/index.test.ts @@ -5,6 +5,7 @@ import * as indexModule from './index.js'; describe('index', () => { it('has the expected exports', () => { expect(Object.keys(indexModule).sort()).toStrictEqual([ + 'OcapError', 'ErrorCode', 'StreamReadError', 'VatAlreadyExistsError', diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts index 1be575286..d00c91c11 100644 --- a/packages/errors/src/index.ts +++ b/packages/errors/src/index.ts @@ -1,4 +1,5 @@ -export { ErrorCode } from './constants.js'; +export type { OcapError } from './types.js'; +export { ErrorCode } from './types.js'; export { VatCapTpConnectionExistsError, VatCapTpConnectionNotFoundError, @@ -8,5 +9,4 @@ export { StreamReadError, } from './errors.js'; export { toError } from './utils/toError.js'; -export { isCodedError } from './utils/isCodedError.js'; export { isOcapError } from './utils/isOcapError.js'; diff --git a/packages/errors/src/constants.ts b/packages/errors/src/types.ts similarity index 69% rename from packages/errors/src/constants.ts rename to packages/errors/src/types.ts index ec0d435a2..c23296e07 100644 --- a/packages/errors/src/constants.ts +++ b/packages/errors/src/types.ts @@ -1,3 +1,5 @@ +import type { Json } from '@metamask/utils'; + export enum ErrorCode { StreamReadError = 'STREAM_READ_ERROR', VatAlreadyExists = 'VAT_ALREADY_EXISTS', @@ -6,3 +8,8 @@ export enum ErrorCode { VatDeleted = 'VAT_DELETED', VatNotFound = 'VAT_NOT_FOUND', } + +export type OcapError = { + code: ErrorCode; + data: Json | undefined; +} & Error; diff --git a/packages/errors/src/utils/isCodedError.test.ts b/packages/errors/src/utils/isCodedError.test.ts deleted file mode 100644 index 9e448625f..000000000 --- a/packages/errors/src/utils/isCodedError.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { isCodedError } from './isCodedError.js'; -import { BaseError } from '../BaseError.js'; -import { ErrorCode } from '../constants.js'; -import { VatAlreadyExistsError } from '../errors.js'; - -class MockCodedError extends Error { - code: string | number; - - constructor(message: string, code: string | number) { - super(message); - this.code = code; - } -} - -describe('isCodedError', () => { - it.each([ - [ - new MockCodedError('An error occurred', 'ERROR_CODE'), - true, - 'coded error with string code', - ], - [ - new MockCodedError('An error occurred', 12345), - true, - 'coded error with number code', - ], - [ - new BaseError(ErrorCode.VatNotFound, 'Base Error'), - true, - 'Base class error', - ], - [new VatAlreadyExistsError('v1'), true, 'VatAlreadyExistsError error'], - [new Error('An error without a code'), false, 'error without a code'], - [ - { message: 'Not an error', code: 'SOME_CODE' } as unknown as Error, - false, - 'non-error object', - ], - [ - { message: 'Invalid code type', code: {} } as unknown as Error, - false, - 'non-string/non-number code', - ], - ])('should return %s for %s', (inputError, expectedResult) => { - expect(isCodedError(inputError)).toBe(expectedResult); - }); -}); diff --git a/packages/errors/src/utils/isCodedError.ts b/packages/errors/src/utils/isCodedError.ts deleted file mode 100644 index 1b0ff65d4..000000000 --- a/packages/errors/src/utils/isCodedError.ts +++ /dev/null @@ -1,17 +0,0 @@ -type CodedError = { - code: string | number; -} & Error; - -/** - * Checks if an error has a code. - * - * @param error - The error to check. - * @returns Whether the error has a code. - */ -export function isCodedError(error: Error): error is CodedError { - return ( - error instanceof Error && - 'code' in error && - (typeof error.code === 'string' || typeof error.code === 'number') - ); -} diff --git a/packages/errors/src/utils/isOcapError.test.ts b/packages/errors/src/utils/isOcapError.test.ts index edd00ae98..a9e11842f 100644 --- a/packages/errors/src/utils/isOcapError.test.ts +++ b/packages/errors/src/utils/isOcapError.test.ts @@ -3,8 +3,8 @@ import { describe, it, expect } from 'vitest'; import { isOcapError } from './isOcapError.js'; import { BaseError } from '../BaseError.js'; -import { ErrorCode } from '../constants.js'; import { VatAlreadyExistsError } from '../errors.js'; +import { ErrorCode } from '../types.js'; class MockCodedError extends Error { code: string; diff --git a/packages/streams/src/utils.test.ts b/packages/streams/src/utils.test.ts index 7b2d9992b..2666c2c24 100644 --- a/packages/streams/src/utils.test.ts +++ b/packages/streams/src/utils.test.ts @@ -135,28 +135,6 @@ describe('marshalError', () => { ); }); - it('should marshal a coded error', () => { - class CodedError extends Error { - code: string; - - constructor(message: string, code: string) { - super(message); - this.code = code; - } - } - - const error = new CodedError('Coded error', 'CODE'); - const marshaledError = marshalError(error); - expect(marshaledError).toStrictEqual( - expect.objectContaining({ - [ErrorSentinel]: true, - message: 'Coded error', - stack: expect.any(String), - code: 'CODE', - }), - ); - }); - it('should marshal an ocap error', () => { const error = new VatNotFoundError('v1'); const marshaledError = marshalError(error); diff --git a/packages/streams/src/utils.ts b/packages/streams/src/utils.ts index 0e09a8c49..b6a6ce805 100644 --- a/packages/streams/src/utils.ts +++ b/packages/streams/src/utils.ts @@ -15,7 +15,7 @@ import { UnsafeJsonStruct, object, } from '@metamask/utils'; -import { isCodedError, isOcapError } from '@ocap/errors'; +import { isOcapError } from '@ocap/errors'; import { stringify } from '@ocap/utils'; export type { Reader, Writer }; @@ -139,21 +139,25 @@ export function marshalError(error: Error): MarshaledError { [ErrorSentinel]: true, message: error.message, }; + if (error.cause) { output.cause = error.cause instanceof Error ? marshalError(error.cause) : stringify(error.cause); } + if (error.stack) { output.stack = error.stack; } - if (isCodedError(error) && error.code) { - output.code = String(error.code); - } - if (isOcapError(error) && error.data) { - output.data = stringify(error.data); + + if (isOcapError(error)) { + output.code = error.code; + if (error.data) { + output.data = stringify(error.data); + } } + return output; } From 9f658b94dfc2040642ecfcaefb0ffdc5040ce172 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 14 Oct 2024 20:31:38 +0200 Subject: [PATCH 10/24] handle not error cause --- packages/utils/package.json | 3 +- packages/utils/src/stringify.test.ts | 104 ++++++++++++++++----------- packages/utils/src/stringify.ts | 73 ++++++++++++++----- 3 files changed, 119 insertions(+), 61 deletions(-) diff --git a/packages/utils/package.json b/packages/utils/package.json index ee0eab4e2..fb3a34a70 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -43,7 +43,8 @@ }, "dependencies": { "@endo/captp": "^4.4.0", - "@metamask/utils": "^9.3.0" + "@metamask/utils": "^9.3.0", + "@ocap/errors": "workspace:^" }, "devDependencies": { "@arethetypeswrong/cli": "^0.16.4", diff --git a/packages/utils/src/stringify.test.ts b/packages/utils/src/stringify.test.ts index 3d3fe7c08..dee9a139b 100644 --- a/packages/utils/src/stringify.test.ts +++ b/packages/utils/src/stringify.test.ts @@ -1,39 +1,35 @@ +import { VatNotFoundError } from '@ocap/errors'; import { describe, it, expect } from 'vitest'; import { stringify } from './stringify.js'; describe('stringify', () => { - it('stringifies a simple object with default indent', () => { - const input = { key: 'value' }; - const result = stringify(input); - expect(result).toBe(`{\n "key": "value"\n}`); - }); - - it('stringifies a simple object with custom indent', () => { - const input = { key: 'value' }; - const result = stringify(input, 4); - expect(result).toBe(`{\n "key": "value"\n}`); - }); - - it('stringifies an array', () => { - const input = [1, 2, 3]; - const result = stringify(input); - expect(result).toBe(`[\n 1,\n 2,\n 3\n]`); - }); - - it('returns a string for a simple primitive', () => { - expect(stringify(42)).toBe('42'); - expect(stringify('hello')).toBe('"hello"'); - expect(stringify(true)).toBe('true'); - }); - - it('handles null', () => { - expect(stringify(null)).toBe('null'); - }); - - it('handles undefined', () => { - expect(stringify(undefined)).toBe('undefined'); - }); + it.each([ + [ + { key: 'value' }, + 2, + `{\n "key": "value"\n}`, + 'stringifies a simple object with default indent', + ], + [ + { key: 'value' }, + 4, + `{\n "key": "value"\n}`, + 'stringifies a simple object with custom indent', + ], + [[1, 2, 3], 2, `[\n 1,\n 2,\n 3\n]`, 'stringifies an array'], + [42, 2, '42', 'returns a string for a number'], + ['hello', 2, '"hello"', 'returns a string for a string primitive'], + [true, 2, 'true', 'returns a string for a boolean primitive'], + [null, 2, 'null', 'handles null'], + [undefined, 2, 'undefined', 'handles undefined'], + ])( + 'should stringify %s with indent %i', + (input, indent, expected, _description) => { + const result = stringify(input, indent); + expect(result).toBe(expected); + }, + ); it('handles circular references gracefully', () => { const obj: Record = {}; @@ -50,31 +46,53 @@ describe('stringify', () => { ).toBe('function example() {\n return "hello";\n }'); }); - it('handles error objects with default indent', () => { - const error = new Error('An error occurred'); - const result = stringify(error); + it.each([ + [ + new Error('An error occurred'), + 2, + `An error occurred`, + 'handles error objects with default indent', + ], + [ + new Error('Another error occurred'), + 4, + `Another error occurred`, + 'handles error objects with custom indent', + ], + ])('should stringify simple error objects: %s', (error, indent, message) => { + const stackNewlines = error.stack?.replace(/\n/gu, '\\n'); + const result = stringify(error, indent); + expect(result).toContain(`"name": "Error"`); + expect(result).toContain(`"message": "${message}"`); + expect(result).toContain(`"stack": "${stackNewlines}"`); + }); + + it('handles error objects with an error cause that is an error', () => { + const rootCause = new Error('Root cause error'); + const error = new Error('Caused error', { cause: rootCause }); + const result = stringify(error, 2); const stackNewlines = error.stack?.replace(/\n/gu, '\\n'); + const rootStackNewlines = rootCause.stack?.replace(/\n/gu, '\\n'); expect(result).toBe( - `{\n "name": "Error",\n "message": "An error occurred",\n "stack": "${stackNewlines}"\n}`, + `{\n "name": "Error",\n "message": "Caused error",\n "stack": "${stackNewlines}",\n "cause": {\n "name": "Error",\n "message": "Root cause error",\n "stack": "${rootStackNewlines}"\n }\n}`, ); }); - it('handles error objects with custom indent', () => { - const error = new Error('Another error occurred'); - const result = stringify(error, 4); + it('handles error objects with an error cause that is not an error', () => { + const error = new Error('Caused error', { cause: 'root cause' }); + const result = stringify(error, 2); const stackNewlines = error.stack?.replace(/\n/gu, '\\n'); expect(result).toBe( - `{\n "name": "Error",\n "message": "Another error occurred",\n "stack": "${stackNewlines}"\n}`, + `{\n "name": "Error",\n "message": "Caused error",\n "stack": "${stackNewlines}",\n "cause": "\\"root cause\\""\n}`, ); }); - it('handles error objects with a cause', () => { - const rootCause = new Error('Root cause error'); - const error = new Error('Caused error', { cause: rootCause }); + it('handles ocap errors', () => { + const error = new VatNotFoundError('v1'); const result = stringify(error, 2); const stackNewlines = error.stack?.replace(/\n/gu, '\\n'); expect(result).toBe( - `{\n "name": "Error",\n "message": "Caused error",\n "stack": "${stackNewlines}",\n "cause": {\n "name": "Error",\n "message": "Root cause error"\n }\n}`, + `{\n "name": "VatNotFoundError",\n "message": "Vat does not exist.",\n "stack": "${stackNewlines}",\n "code": "VAT_NOT_FOUND",\n "data": "{\\n \\"vatId\\": \\"v1\\"\\n}"\n}`, ); }); }); diff --git a/packages/utils/src/stringify.ts b/packages/utils/src/stringify.ts index 870383dae..ba9d8723d 100644 --- a/packages/utils/src/stringify.ts +++ b/packages/utils/src/stringify.ts @@ -1,3 +1,6 @@ +import type { OcapError } from '@ocap/errors'; +import { isOcapError } from '@ocap/errors'; + /** * Stringify an evaluation result. * @@ -8,28 +11,64 @@ export const stringify = (value: unknown, indent: number = 2): string => { try { if (value instanceof Error) { - const errorObject: Record = { - name: value.name, - message: value.message, - stack: value.stack, - }; - - if (value.cause instanceof Error) { - errorObject.cause = { - name: value.cause.name, - message: value.cause.message, - }; - } - + const errorObject = stringifyError(value); return JSON.stringify(errorObject, null, indent); } const result = JSON.stringify(value, null, indent); - if (result === undefined) { - return String(value); - } - return result; + return result ?? String(value); } catch { return String(value); } }; + +/** + * Helper function to process an error. + * + * @param error - The error to process. + * @returns The processed object. + */ +function stringifyError(error: Error): Record { + return isOcapError(error) + ? createOcapErrorObject(error) + : createErrorObject(error); +} + +/** + * Helper function to create a simplified error object. + * + * @param error - The error to create an object from. + * @returns The error object. + */ +function createErrorObject(error: Error): Record { + const errorObject: Record = { + name: error.name, + message: error.message, + stack: error.stack, + }; + + if (error.cause) { + errorObject.cause = + error.cause instanceof Error + ? stringifyError(error.cause) + : stringify(error.cause); + } + + return errorObject; +} + +/** + * Helper function to create an Ocap error object. + * + * @param error - The Ocap error to create an object from. + * @returns The Ocap error object. + */ +function createOcapErrorObject(error: OcapError): Record { + const errorObject = { + ...createErrorObject(error), + code: error.code, + data: stringify(error.data), + }; + + return errorObject; +} From 7241b8e7138fa4c8806fdbe4f9f145b57fe06217 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 15 Oct 2024 17:25:32 +0200 Subject: [PATCH 11/24] move marshal error to error package --- packages/errors/package.json | 2 + packages/errors/src/errors.ts | 58 ------ packages/errors/src/errors/StreamReadError.ts | 16 ++ .../src/errors/VatAlreadyExistsError.ts | 10 + .../errors/VatCapTpConnectionExistsError.ts | 14 ++ .../errors/VatCapTpConnectionNotFoundError.ts | 12 ++ packages/errors/src/errors/VatDeletedError.ts | 8 + .../errors/src/errors/VatNotFoundError.ts | 8 + .../errors/src/{ => errors}/errors.test.ts | 16 +- packages/errors/src/index.test.ts | 5 +- packages/errors/src/index.ts | 19 +- packages/errors/src/marshal.test.ts | 175 ++++++++++++++++++ packages/errors/src/marshal.ts | 73 ++++++++ packages/errors/src/types.ts | 29 +++ packages/errors/src/utils/isOcapError.test.ts | 2 +- packages/errors/tsconfig.json | 1 + packages/streams/src/BaseStream.test.ts | 3 +- packages/streams/src/utils.test.ts | 107 +---------- packages/streams/src/utils.ts | 115 +----------- packages/utils/tsconfig.build.json | 2 +- packages/utils/tsconfig.json | 2 +- 21 files changed, 378 insertions(+), 299 deletions(-) delete mode 100644 packages/errors/src/errors.ts create mode 100644 packages/errors/src/errors/StreamReadError.ts create mode 100644 packages/errors/src/errors/VatAlreadyExistsError.ts create mode 100644 packages/errors/src/errors/VatCapTpConnectionExistsError.ts create mode 100644 packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts create mode 100644 packages/errors/src/errors/VatDeletedError.ts create mode 100644 packages/errors/src/errors/VatNotFoundError.ts rename packages/errors/src/{ => errors}/errors.test.ts (89%) create mode 100644 packages/errors/src/marshal.test.ts create mode 100644 packages/errors/src/marshal.ts diff --git a/packages/errors/package.json b/packages/errors/package.json index 46bc3d735..bb358bcf2 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -42,6 +42,7 @@ "test:watch": "vitest --config vitest.config.ts" }, "dependencies": { + "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^9.3.0" }, "devDependencies": { @@ -50,6 +51,7 @@ "@metamask/eslint-config": "^14.0.0", "@metamask/eslint-config-nodejs": "^14.0.0", "@metamask/eslint-config-typescript": "^14.0.0", + "@ocap/test-utils": "workspace:^", "@ts-bridge/cli": "^0.5.1", "@ts-bridge/shims": "^0.1.1", "@typescript-eslint/eslint-plugin": "^8.8.1", diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts deleted file mode 100644 index e7d2e6b43..000000000 --- a/packages/errors/src/errors.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { BaseError } from './BaseError.js'; -import { ErrorCode } from './types.js'; - -export class VatAlreadyExistsError extends BaseError { - constructor(vatId: string) { - super(ErrorCode.VatAlreadyExists, 'Vat already exists.', { - vatId, - }); - } -} - -export class VatNotFoundError extends BaseError { - constructor(vatId: string) { - super(ErrorCode.VatNotFound, 'Vat does not exist.', { vatId }); - } -} - -export class StreamReadError extends BaseError { - constructor( - data: { vatId: string } | { supervisorId: string }, - originalError: Error, - ) { - super( - ErrorCode.StreamReadError, - 'Unexpected stream read error.', - data, - originalError, - ); - } -} - -export class VatCapTpConnectionExistsError extends BaseError { - constructor(vatId: string) { - super( - ErrorCode.VatCapTpConnectionExists, - 'Vat already has a CapTP connection.', - { - vatId, - }, - ); - } -} - -export class VatCapTpConnectionNotFoundError extends BaseError { - constructor(vatId: string) { - super( - ErrorCode.VatCapTpConnectionNotFound, - 'Vat does not have a CapTP connection.', - { vatId }, - ); - } -} - -export class VatDeletedError extends BaseError { - constructor(vatId: string) { - super(ErrorCode.VatDeleted, 'Vat was deleted.', { vatId }); - } -} diff --git a/packages/errors/src/errors/StreamReadError.ts b/packages/errors/src/errors/StreamReadError.ts new file mode 100644 index 000000000..588858178 --- /dev/null +++ b/packages/errors/src/errors/StreamReadError.ts @@ -0,0 +1,16 @@ +import { BaseError } from '../BaseError.js'; +import { ErrorCode } from '../types.js'; + +export class StreamReadError extends BaseError { + constructor( + data: { vatId: string } | { supervisorId: string }, + originalError: Error, + ) { + super( + ErrorCode.StreamReadError, + 'Unexpected stream read error.', + data, + originalError, + ); + } +} diff --git a/packages/errors/src/errors/VatAlreadyExistsError.ts b/packages/errors/src/errors/VatAlreadyExistsError.ts new file mode 100644 index 000000000..2a68a4a71 --- /dev/null +++ b/packages/errors/src/errors/VatAlreadyExistsError.ts @@ -0,0 +1,10 @@ +import { BaseError } from '../BaseError.js'; +import { ErrorCode } from '../types.js'; + +export class VatAlreadyExistsError extends BaseError { + constructor(vatId: string) { + super(ErrorCode.VatAlreadyExists, 'Vat already exists.', { + vatId, + }); + } +} diff --git a/packages/errors/src/errors/VatCapTpConnectionExistsError.ts b/packages/errors/src/errors/VatCapTpConnectionExistsError.ts new file mode 100644 index 000000000..69be6bba0 --- /dev/null +++ b/packages/errors/src/errors/VatCapTpConnectionExistsError.ts @@ -0,0 +1,14 @@ +import { BaseError } from '../BaseError.js'; +import { ErrorCode } from '../types.js'; + +export class VatCapTpConnectionExistsError extends BaseError { + constructor(vatId: string) { + super( + ErrorCode.VatCapTpConnectionExists, + 'Vat already has a CapTp connection.', + { + vatId, + }, + ); + } +} diff --git a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts new file mode 100644 index 000000000..fe9869c3d --- /dev/null +++ b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts @@ -0,0 +1,12 @@ +import { BaseError } from '../BaseError.js'; +import { ErrorCode } from '../types.js'; + +export class VatCapTpConnectionNotFoundError extends BaseError { + constructor(vatId: string) { + super( + ErrorCode.VatCapTpConnectionNotFound, + 'Vat does not have a CapTp connection.', + { vatId }, + ); + } +} diff --git a/packages/errors/src/errors/VatDeletedError.ts b/packages/errors/src/errors/VatDeletedError.ts new file mode 100644 index 000000000..1b78b45be --- /dev/null +++ b/packages/errors/src/errors/VatDeletedError.ts @@ -0,0 +1,8 @@ +import { BaseError } from '../BaseError.js'; +import { ErrorCode } from '../types.js'; + +export class VatDeletedError extends BaseError { + constructor(vatId: string) { + super(ErrorCode.VatDeleted, 'Vat was deleted.', { vatId }); + } +} diff --git a/packages/errors/src/errors/VatNotFoundError.ts b/packages/errors/src/errors/VatNotFoundError.ts new file mode 100644 index 000000000..a5e3d659c --- /dev/null +++ b/packages/errors/src/errors/VatNotFoundError.ts @@ -0,0 +1,8 @@ +import { BaseError } from '../BaseError.js'; +import { ErrorCode } from '../types.js'; + +export class VatNotFoundError extends BaseError { + constructor(vatId: string) { + super(ErrorCode.VatNotFound, 'Vat does not exist.', { vatId }); + } +} diff --git a/packages/errors/src/errors.test.ts b/packages/errors/src/errors/errors.test.ts similarity index 89% rename from packages/errors/src/errors.test.ts rename to packages/errors/src/errors/errors.test.ts index 240e78aeb..783424eaa 100644 --- a/packages/errors/src/errors.test.ts +++ b/packages/errors/src/errors/errors.test.ts @@ -1,14 +1,12 @@ import { describe, it, expect } from 'vitest'; -import { - VatAlreadyExistsError, - VatNotFoundError, - StreamReadError, - VatCapTpConnectionExistsError, - VatCapTpConnectionNotFoundError, - VatDeletedError, -} from './errors.js'; -import { ErrorCode } from './types.js'; +import { StreamReadError } from './StreamReadError.js'; +import { VatAlreadyExistsError } from './VatAlreadyExistsError.js'; +import { VatCapTpConnectionExistsError } from './VatCapTpConnectionExistsError.js'; +import { VatCapTpConnectionNotFoundError } from './VatCapTpConnectionNotFoundError.js'; +import { VatDeletedError } from './VatDeletedError.js'; +import { VatNotFoundError } from './VatNotFoundError.js'; +import { ErrorCode } from '../types.js'; describe('Errors classes', () => { const mockVatId = 'mockVatId'; diff --git a/packages/errors/src/index.test.ts b/packages/errors/src/index.test.ts index 13178d1fd..cbaac9fe7 100644 --- a/packages/errors/src/index.test.ts +++ b/packages/errors/src/index.test.ts @@ -5,17 +5,20 @@ import * as indexModule from './index.js'; describe('index', () => { it('has the expected exports', () => { expect(Object.keys(indexModule).sort()).toStrictEqual([ - 'OcapError', 'ErrorCode', 'StreamReadError', + 'ErrorSentinel', 'VatAlreadyExistsError', 'VatCapTpConnectionExistsError', 'VatCapTpConnectionNotFoundError', 'VatDeletedError', 'VatNotFoundError', 'isCodedError', + 'isMarshaledError', 'isOcapError', + 'marshalError', 'toError', + 'unmarshalError', ]); }); }); diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts index d00c91c11..d2e422318 100644 --- a/packages/errors/src/index.ts +++ b/packages/errors/src/index.ts @@ -1,12 +1,11 @@ -export type { OcapError } from './types.js'; -export { ErrorCode } from './types.js'; -export { - VatCapTpConnectionExistsError, - VatCapTpConnectionNotFoundError, - VatAlreadyExistsError, - VatDeletedError, - VatNotFoundError, - StreamReadError, -} from './errors.js'; +export type { OcapError, MarshaledError } from './types.js'; +export { VatCapTpConnectionExistsError } from './errors/VatCapTpConnectionExistsError.js'; +export { VatCapTpConnectionNotFoundError } from './errors/VatCapTpConnectionNotFoundError.js'; +export { VatAlreadyExistsError } from './errors/VatAlreadyExistsError.js'; +export { VatDeletedError } from './errors/VatDeletedError.js'; +export { VatNotFoundError } from './errors/VatNotFoundError.js'; +export { StreamReadError } from './errors/StreamReadError.js'; +export { ErrorCode, ErrorSentinel } from './types.js'; export { toError } from './utils/toError.js'; export { isOcapError } from './utils/isOcapError.js'; +export { isMarshaledError, marshalError, unmarshalError } from './marshal.js'; diff --git a/packages/errors/src/marshal.test.ts b/packages/errors/src/marshal.test.ts new file mode 100644 index 000000000..9ed551bcf --- /dev/null +++ b/packages/errors/src/marshal.test.ts @@ -0,0 +1,175 @@ +import { makeErrorMatcherFactory } from '@ocap/test-utils'; +import { describe, it, expect } from 'vitest'; + +import { VatNotFoundError } from './errors/VatNotFoundError.js'; +import { isMarshaledError, marshalError, unmarshalError } from './marshal.js'; +import { ErrorCode, ErrorSentinel } from './types.js'; + +const makeErrorMatcher = makeErrorMatcherFactory(expect); + +describe('marshalError', () => { + it('should marshal an error', () => { + const error = new Error('foo'); + const marshaledError = marshalError(error); + expect(marshaledError).toStrictEqual( + expect.objectContaining({ + [ErrorSentinel]: true, + message: 'foo', + stack: expect.any(String), + }), + ); + }); + + it('should marshal an error with a cause', () => { + const cause = new Error('baz'); + const error = new Error('foo', { cause }); + const marshaledError = marshalError(error); + expect(marshaledError).toStrictEqual( + expect.objectContaining({ + [ErrorSentinel]: true, + message: 'foo', + stack: expect.any(String), + cause: { + [ErrorSentinel]: true, + message: 'baz', + stack: expect.any(String), + }, + }), + ); + }); + + it('should marshal an error with a non-error cause', () => { + const cause = { bar: 'baz' }; + const error = new Error('foo', { cause }); + const marshaledError = marshalError(error); + expect(marshaledError).toStrictEqual( + expect.objectContaining({ + [ErrorSentinel]: true, + message: 'foo', + stack: expect.any(String), + cause: JSON.stringify(cause), + }), + ); + }); + + it('should marshal an ocap error', () => { + const error = new VatNotFoundError('v1'); + const marshaledError = marshalError(error); + expect(marshaledError).toStrictEqual( + expect.objectContaining({ + [ErrorSentinel]: true, + message: 'Vat does not exist.', + stack: expect.any(String), + code: ErrorCode.VatNotFound, + data: JSON.stringify({ vatId: 'v1' }), + }), + ); + }); +}); + +describe('unmarshalError', () => { + it('should unmarshal a marshaled error', () => { + const marshaledError = { + [ErrorSentinel]: true, + message: 'foo', + stack: 'bar', + } as const; + expect(unmarshalError(marshaledError)).toStrictEqual( + makeErrorMatcher('foo'), + ); + }); + + it('should unmarshal a marshaled error with a cause', () => { + const marshaledError = { + [ErrorSentinel]: true, + message: 'foo', + stack: 'bar', + cause: { + [ErrorSentinel]: true, + message: 'baz', + stack: 'qux', + }, + } as const; + expect(unmarshalError(marshaledError)).toStrictEqual( + makeErrorMatcher(new Error('foo', { cause: new Error('baz') })), + ); + }); + + it('should unmarshal a marshaled error with a string cause', () => { + const marshaledError = { + [ErrorSentinel]: true, + message: 'foo', + stack: 'bar', + cause: 'baz', + } as const; + expect(unmarshalError(marshaledError)).toStrictEqual( + makeErrorMatcher(new Error('foo', { cause: 'baz' })), + ); + }); +}); + +describe('isMarshaledError', () => { + it.each([ + [ + 'valid marshaled error with required fields only', + { + [ErrorSentinel]: true, + message: 'An error occurred', + }, + true, + ], + [ + 'valid marshaled error with optional fields', + { + [ErrorSentinel]: true, + message: 'An error occurred', + code: 'ERROR_CODE', + data: { key: 'value' }, + stack: 'Error stack trace', + cause: 'Another error', + }, + true, + ], + [ + 'valid marshaled error with nested cause', + { + [ErrorSentinel]: true, + message: 'An error occurred', + cause: { + [ErrorSentinel]: true, + message: 'Nested error occurred', + }, + }, + true, + ], + [ + 'object missing the sentinel value', + { + message: 'An error occurred', + }, + false, + ], + [ + 'object with incorrect sentinel value', + { + [ErrorSentinel]: false, + message: 'An error occurred', + }, + false, + ], + ['null value', null, false], + ['undefined value', undefined, false], + ['string value', 'string', false], + ['number value', 123, false], + ['array value', [], false], + [ + 'object missing the required message field', + { + [ErrorSentinel]: true, + }, + false, + ], + ])('should return %s', (_, value, expected) => { + expect(isMarshaledError(value)).toBe(expected); + }); +}); diff --git a/packages/errors/src/marshal.ts b/packages/errors/src/marshal.ts new file mode 100644 index 000000000..9916828a5 --- /dev/null +++ b/packages/errors/src/marshal.ts @@ -0,0 +1,73 @@ +import { is } from '@metamask/superstruct'; + +import type { MarshaledError, OcapError } from './types.js'; +import { ErrorSentinel, MarshaledErrorStruct } from './types.js'; +import { isOcapError } from './utils/isOcapError.js'; + +/** + * Checks if a value is a {@link MarshaledError}. + * + * @param value - The value to check. + * @returns Whether the value is a {@link MarshaledError}. + */ +export function isMarshaledError(value: unknown): value is MarshaledError { + return is(value, MarshaledErrorStruct); +} + +/** + * Marshals an error into a {@link MarshaledError}. + * + * @param error - The error to marshal. + * @returns The marshaled error. + */ +export function marshalError(error: Error): MarshaledError { + const output: MarshaledError = { + [ErrorSentinel]: true, + message: error.message, + }; + + if (error.cause) { + output.cause = + error.cause instanceof Error + ? marshalError(error.cause) + : JSON.stringify(error.cause); + } + + if (error.stack) { + output.stack = error.stack; + } + + if (isOcapError(error)) { + output.code = error.code; + if (error.data) { + output.data = JSON.stringify(error.data); + } + } + + return output; +} + +/** + * Unmarshals a {@link MarshaledError} into an {@link Error}. + * + * @param marshaledError - The marshaled error to unmarshal. + * @returns The unmarshaled error. + */ +export function unmarshalError( + marshaledError: MarshaledError, +): Error | OcapError { + const output = new Error(marshaledError.message); + + if (marshaledError.cause) { + output.cause = + typeof marshaledError.cause === 'string' + ? marshaledError.cause + : unmarshalError(marshaledError.cause); + } + + if (marshaledError.stack) { + output.stack = marshaledError.stack; + } + + return output; +} diff --git a/packages/errors/src/types.ts b/packages/errors/src/types.ts index c23296e07..fd4f7adcc 100644 --- a/packages/errors/src/types.ts +++ b/packages/errors/src/types.ts @@ -1,3 +1,6 @@ +import type { Struct } from '@metamask/superstruct'; +import { lazy, literal, optional, string, union } from '@metamask/superstruct'; +import { JsonStruct, object } from '@metamask/utils'; import type { Json } from '@metamask/utils'; export enum ErrorCode { @@ -13,3 +16,29 @@ export type OcapError = { code: ErrorCode; data: Json | undefined; } & Error; + +/** + * A sentinel value to detect marshaled errors. + */ +export const ErrorSentinel = '@@MARSHALED_ERROR'; + +/** + * A marshaled error. + */ +export type MarshaledError = { + [ErrorSentinel]: true; + message: string; + code?: ErrorCode; + data?: Json; + stack?: string; + cause?: MarshaledError | string; +}; + +export const MarshaledErrorStruct: Struct = object({ + [ErrorSentinel]: literal(true), + message: string(), + code: optional(string()), + data: optional(JsonStruct), + stack: optional(string()), + cause: optional(union([string(), lazy(() => MarshaledErrorStruct)])), +}) as Struct; diff --git a/packages/errors/src/utils/isOcapError.test.ts b/packages/errors/src/utils/isOcapError.test.ts index a9e11842f..728211606 100644 --- a/packages/errors/src/utils/isOcapError.test.ts +++ b/packages/errors/src/utils/isOcapError.test.ts @@ -3,7 +3,7 @@ import { describe, it, expect } from 'vitest'; import { isOcapError } from './isOcapError.js'; import { BaseError } from '../BaseError.js'; -import { VatAlreadyExistsError } from '../errors.js'; +import { VatAlreadyExistsError } from '../errors/VatAlreadyExistsError.js'; import { ErrorCode } from '../types.js'; class MockCodedError extends Error { diff --git a/packages/errors/tsconfig.json b/packages/errors/tsconfig.json index 743899d8b..e35ee6316 100644 --- a/packages/errors/tsconfig.json +++ b/packages/errors/tsconfig.json @@ -5,5 +5,6 @@ "lib": ["DOM", "ES2022"], "types": ["ses", "vitest", "vitest/jsdom"] }, + "references": [{ "path": "../test-utils" }], "include": ["./src", "./vite.config.ts", "./vitest.config.ts"] } diff --git a/packages/streams/src/BaseStream.test.ts b/packages/streams/src/BaseStream.test.ts index 38df8d69a..ebf608ad6 100644 --- a/packages/streams/src/BaseStream.test.ts +++ b/packages/streams/src/BaseStream.test.ts @@ -1,8 +1,9 @@ +import { marshalError } from '@ocap/errors'; import { makeErrorMatcherFactory, makePromiseKitMock } from '@ocap/test-utils'; import { describe, expect, it, vi } from 'vitest'; import { BaseReader, BaseWriter } from './BaseStream.js'; -import { makeDoneResult, makePendingResult, marshalError } from './utils.js'; +import { makeDoneResult, makePendingResult } from './utils.js'; import { TestReader, TestWriter } from '../test/stream-mocks.js'; vi.mock('@endo/promise-kit', () => makePromiseKitMock()); diff --git a/packages/streams/src/utils.test.ts b/packages/streams/src/utils.test.ts index 2666c2c24..c98ee28fc 100644 --- a/packages/streams/src/utils.test.ts +++ b/packages/streams/src/utils.test.ts @@ -1,20 +1,16 @@ import type { Json } from '@metamask/utils'; -import { ErrorCode, VatNotFoundError } from '@ocap/errors'; +import { ErrorSentinel, marshalError } from '@ocap/errors'; import { makeErrorMatcherFactory } from '@ocap/test-utils'; -import { stringify } from '@ocap/utils'; import { describe, expect, it } from 'vitest'; import type { Dispatchable } from './utils.js'; import { assertIsWritable, - ErrorSentinel, isDispatchable, makeDoneResult, makePendingResult, marshal, - marshalError, unmarshal, - unmarshalError, } from './utils.js'; const makeErrorMatcher = makeErrorMatcherFactory(expect); @@ -90,107 +86,6 @@ describe('makePendingResult', () => { }); }); -describe('marshalError', () => { - it('should marshal an error', () => { - const error = new Error('foo'); - const marshaledError = marshalError(error); - expect(marshaledError).toStrictEqual( - expect.objectContaining({ - [ErrorSentinel]: true, - message: 'foo', - stack: expect.any(String), - }), - ); - }); - - it('should marshal an error with a cause', () => { - const cause = new Error('baz'); - const error = new Error('foo', { cause }); - const marshaledError = marshalError(error); - expect(marshaledError).toStrictEqual( - expect.objectContaining({ - [ErrorSentinel]: true, - message: 'foo', - stack: expect.any(String), - cause: { - [ErrorSentinel]: true, - message: 'baz', - stack: expect.any(String), - }, - }), - ); - }); - - it('should marshal an error with a non-error cause', () => { - const cause = { bar: 'baz' }; - const error = new Error('foo', { cause }); - const marshaledError = marshalError(error); - expect(marshaledError).toStrictEqual( - expect.objectContaining({ - [ErrorSentinel]: true, - message: 'foo', - stack: expect.any(String), - cause: stringify(cause), - }), - ); - }); - - it('should marshal an ocap error', () => { - const error = new VatNotFoundError('v1'); - const marshaledError = marshalError(error); - expect(marshaledError).toStrictEqual( - expect.objectContaining({ - [ErrorSentinel]: true, - message: 'Vat does not exist.', - stack: expect.any(String), - code: ErrorCode.VatNotFound, - data: stringify({ vatId: 'v1' }), - }), - ); - }); -}); - -describe('unmarshalError', () => { - it('should unmarshal a marshaled error', () => { - const marshaledError = { - [ErrorSentinel]: true, - message: 'foo', - stack: 'bar', - } as const; - expect(unmarshalError(marshaledError)).toStrictEqual( - makeErrorMatcher('foo'), - ); - }); - - it('should unmarshal a marshaled error with a cause', () => { - const marshaledError = { - [ErrorSentinel]: true, - message: 'foo', - stack: 'bar', - cause: { - [ErrorSentinel]: true, - message: 'baz', - stack: 'qux', - }, - } as const; - expect(unmarshalError(marshaledError)).toStrictEqual( - makeErrorMatcher(new Error('foo', { cause: new Error('baz') })), - ); - }); - - it('should unmarshal a marshaled error with a string cause', () => { - const marshaledError = { - [ErrorSentinel]: true, - message: 'foo', - stack: 'bar', - cause: 'baz', - } as const; - expect(unmarshalError(marshaledError)).toStrictEqual( - makeErrorMatcher(new Error('foo', { cause: 'baz' })), - ); - }); -}); - describe('marshal', () => { it.each([ ['pending result with string', makePendingResult('foo')], diff --git a/packages/streams/src/utils.ts b/packages/streams/src/utils.ts index b6a6ce805..ba6808466 100644 --- a/packages/streams/src/utils.ts +++ b/packages/streams/src/utils.ts @@ -1,22 +1,9 @@ import type { Reader, Writer } from '@endo/stream'; import type { Struct } from '@metamask/superstruct'; -import { - boolean, - is, - lazy, - literal, - optional, - string, - union, -} from '@metamask/superstruct'; -import { - type Json, - JsonStruct, - UnsafeJsonStruct, - object, -} from '@metamask/utils'; -import { isOcapError } from '@ocap/errors'; -import { stringify } from '@ocap/utils'; +import { boolean, is, optional } from '@metamask/superstruct'; +import { type Json, UnsafeJsonStruct, object } from '@metamask/utils'; +import type { MarshaledError } from '@ocap/errors'; +import { isMarshaledError, marshalError, unmarshalError } from '@ocap/errors'; export type { Reader, Writer }; @@ -92,100 +79,6 @@ export const makePendingResult = ( value, }); -/** - * A sentinel value to detect marshaled errors. - */ -export const ErrorSentinel = '@@MARSHALED_ERROR'; - -/** - * A marshaled error. - */ -type MarshaledError = { - [ErrorSentinel]: true; - message: string; - code?: string; - data?: Json; - stack?: string; - cause?: MarshaledError | string; -}; - -const MarshaledErrorStruct: Struct = object({ - [ErrorSentinel]: literal(true), - message: string(), - code: optional(string()), - data: optional(JsonStruct), - stack: optional(string()), - cause: optional(union([string(), lazy(() => MarshaledErrorStruct)])), -}) as Struct; - -/** - * Checks if a value is a {@link MarshaledError}. - * - * @param value - The value to check. - * @returns Whether the value is a {@link MarshaledError}. - */ -function isMarshaledError(value: unknown): value is MarshaledError { - return is(value, MarshaledErrorStruct); -} - -/** - * Marshals an error into a {@link MarshaledError}. - * - * @param error - The error to marshal. - * @returns The marshaled error. - */ -export function marshalError(error: Error): MarshaledError { - const output: MarshaledError = { - [ErrorSentinel]: true, - message: error.message, - }; - - if (error.cause) { - output.cause = - error.cause instanceof Error - ? marshalError(error.cause) - : stringify(error.cause); - } - - if (error.stack) { - output.stack = error.stack; - } - - if (isOcapError(error)) { - output.code = error.code; - if (error.data) { - output.data = stringify(error.data); - } - } - - return output; -} - -/** - * Unmarshals a {@link MarshaledError} into an {@link Error}. - * - * @param marshaledError - The marshaled error to unmarshal. - * @returns The unmarshaled error. - */ -export function unmarshalError(marshaledError: MarshaledError): Error { - let output: Error; - if (marshaledError.cause) { - output = new Error(marshaledError.message, { - cause: - typeof marshaledError.cause === 'string' - ? marshaledError.cause - : unmarshalError(marshaledError.cause), - }); - } else { - output = new Error(marshaledError.message); - } - - if (marshaledError.stack) { - output.stack = marshaledError.stack; - } - return output; -} - /** * A value that can be dispatched to the internal transport mechanism of a stream. * diff --git a/packages/utils/tsconfig.build.json b/packages/utils/tsconfig.build.json index 9651b671e..219aff97e 100644 --- a/packages/utils/tsconfig.build.json +++ b/packages/utils/tsconfig.build.json @@ -7,7 +7,7 @@ "rootDir": "./src", "types": ["ses"] }, - "references": [], + "references": [{ "path": "../errors/tsconfig.build.json" }], "files": [], "include": ["./src"] } diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index e35ee6316..3978278cb 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -5,6 +5,6 @@ "lib": ["DOM", "ES2022"], "types": ["ses", "vitest", "vitest/jsdom"] }, - "references": [{ "path": "../test-utils" }], + "references": [{ "path": "../test-utils" }, { "path": "../errors" }], "include": ["./src", "./vite.config.ts", "./vitest.config.ts"] } From d59e5a7fec5f0a567f10cf279483d286d5d8ce03 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 15 Oct 2024 17:26:48 +0200 Subject: [PATCH 12/24] fix yarn.lock --- yarn.lock | 149 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 76 insertions(+), 73 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0b2d08f23..8312a704f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1140,12 +1140,12 @@ __metadata: linkType: hard "@metamask/object-multiplex@npm:^2.0.0": - version: 2.0.0 - resolution: "@metamask/object-multiplex@npm:2.0.0" + version: 2.1.0 + resolution: "@metamask/object-multiplex@npm:2.1.0" dependencies: once: "npm:^1.4.0" readable-stream: "npm:^3.6.2" - checksum: 10/54baea752a3ac7c2742c376512e00d4902d383e9da8787574d3b21eb0081523309e24e3915a98f3ae0341d65712b6832d2eb7eeb862f4ef0da1ead52dcde5387 + checksum: 10/e119f695e89eb20c3174f8ac6d74587498d85cff92c37e83e167cb758b3d3147d5b5e1a997d6198d430ebcf2cede6265bf5d4513fe96dbb2d82bbc6167752caa languageName: node linkType: hard @@ -1235,22 +1235,22 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^6.8.0": - version: 6.8.0 - resolution: "@metamask/snaps-sdk@npm:6.8.0" +"@metamask/snaps-sdk@npm:^6.9.0": + version: 6.9.0 + resolution: "@metamask/snaps-sdk@npm:6.9.0" dependencies: "@metamask/key-tree": "npm:^9.1.2" "@metamask/providers": "npm:^17.1.2" "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" - checksum: 10/9c2fa7517f8062f5162a8d5ad115e8e51e811aa6cc852834ec05914fe871267c6d347cdbfe67a44960898cdbe14f1e0395e9b71157fb23cf1e5620998c6f15dd + checksum: 10/ea2c34c4451f671acc6c3c0ad0d46e770e8b7d0741c1d78a30bc36b883f09a10e9a428b8b564ecd0171da95fdf78bb8ac0de261423a1b35de5d22852300a24ee languageName: node linkType: hard "@metamask/snaps-utils@npm:^8.3.0": - version: 8.4.0 - resolution: "@metamask/snaps-utils@npm:8.4.0" + version: 8.4.1 + resolution: "@metamask/snaps-utils@npm:8.4.1" dependencies: "@babel/core": "npm:^7.23.2" "@babel/types": "npm:^7.23.0" @@ -1260,7 +1260,7 @@ __metadata: "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/slip44": "npm:^4.0.0" "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-sdk": "npm:^6.8.0" + "@metamask/snaps-sdk": "npm:^6.9.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" "@noble/hashes": "npm:^1.3.1" @@ -1275,7 +1275,7 @@ __metadata: semver: "npm:^7.5.4" ses: "npm:^1.1.0" validate-npm-package-name: "npm:^5.0.0" - checksum: 10/9948021d33f5c25e8196728dca01d75d285362393eace61f6d31a327de0ab1f0b5b4e8abf33484b94319d06bf2ccfdc101377adad1de65740c077d327d655a6b + checksum: 10/c68a2fe69dc835c2b996d621fd4698435475d419a85aa557aa000aae0ab7ebb68d2a52f0b28bbab94fff895ece9a94077e3910a21b16d904cff3b9419ca575b6 languageName: node linkType: hard @@ -1469,7 +1469,9 @@ __metadata: "@metamask/eslint-config": "npm:^14.0.0" "@metamask/eslint-config-nodejs": "npm:^14.0.0" "@metamask/eslint-config-typescript": "npm:^14.0.0" + "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.3.0" + "@ocap/test-utils": "workspace:^" "@ts-bridge/cli": "npm:^0.5.1" "@ts-bridge/shims": "npm:^0.1.1" "@typescript-eslint/eslint-plugin": "npm:^8.8.1" @@ -1754,6 +1756,7 @@ __metadata: "@metamask/eslint-config-nodejs": "npm:^14.0.0" "@metamask/eslint-config-typescript": "npm:^14.0.0" "@metamask/utils": "npm:^9.3.0" + "@ocap/errors": "workspace:^" "@ocap/test-utils": "workspace:^" "@ts-bridge/cli": "npm:^0.5.1" "@ts-bridge/shims": "npm:^0.1.1" @@ -2300,15 +2303,15 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.8.1, @typescript-eslint/eslint-plugin@npm:^8.8.1": - version: 8.8.1 - resolution: "@typescript-eslint/eslint-plugin@npm:8.8.1" +"@typescript-eslint/eslint-plugin@npm:8.9.0, @typescript-eslint/eslint-plugin@npm:^8.8.1": + version: 8.9.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.9.0" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.8.1" - "@typescript-eslint/type-utils": "npm:8.8.1" - "@typescript-eslint/utils": "npm:8.8.1" - "@typescript-eslint/visitor-keys": "npm:8.8.1" + "@typescript-eslint/scope-manager": "npm:8.9.0" + "@typescript-eslint/type-utils": "npm:8.9.0" + "@typescript-eslint/utils": "npm:8.9.0" + "@typescript-eslint/visitor-keys": "npm:8.9.0" graphemer: "npm:^1.4.0" ignore: "npm:^5.3.1" natural-compare: "npm:^1.4.0" @@ -2319,66 +2322,66 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/6d45d7c3b2993f9d4130794596b029e72646f69581741ff2032b33f5c5d6b46c241b854556d04f769c2ef491e117c7d73013a07d74de3a0e0b557e648bc82a9c + checksum: 10/c1858656d7ab3d37674759c838422d8a1b7540e54a25c67c7508c38ee76594a98e8f1f269749f08700f93a7a425e0dca6eb6d031b36539c537e10a32edb4975c languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.8.1, @typescript-eslint/parser@npm:^8.8.1": - version: 8.8.1 - resolution: "@typescript-eslint/parser@npm:8.8.1" +"@typescript-eslint/parser@npm:8.9.0, @typescript-eslint/parser@npm:^8.8.1": + version: 8.9.0 + resolution: "@typescript-eslint/parser@npm:8.9.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.8.1" - "@typescript-eslint/types": "npm:8.8.1" - "@typescript-eslint/typescript-estree": "npm:8.8.1" - "@typescript-eslint/visitor-keys": "npm:8.8.1" + "@typescript-eslint/scope-manager": "npm:8.9.0" + "@typescript-eslint/types": "npm:8.9.0" + "@typescript-eslint/typescript-estree": "npm:8.9.0" + "@typescript-eslint/visitor-keys": "npm:8.9.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.57.0 || ^9.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10/f19e9be6e8d3e4b574d5f2b1d7e23e3594ea8d5f0b2bd2e59d2fd237bd0a379597f4b7ba466b7e290c5f3c7bce044107a73b20159c17dc54a4cc6b2ca9470b4b + checksum: 10/6f73af7782856b292b37e43dde83c5babbbdae28da1a4fed764474a9ccbbfcce25903cedde82d6847ad53e0c1a7c2ed53f087b281e6f1408a064498169c0e2d1 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.8.1": - version: 8.8.1 - resolution: "@typescript-eslint/scope-manager@npm:8.8.1" +"@typescript-eslint/scope-manager@npm:8.9.0": + version: 8.9.0 + resolution: "@typescript-eslint/scope-manager@npm:8.9.0" dependencies: - "@typescript-eslint/types": "npm:8.8.1" - "@typescript-eslint/visitor-keys": "npm:8.8.1" - checksum: 10/ab86b533d0cadaa3f325404ae8cda2c1c8e0b820d7b2265ad376a233bb073aa89783a8d20c2effa77552426f38405edaa71e4aa6a2676613ae8dec0e1f1ba061 + "@typescript-eslint/types": "npm:8.9.0" + "@typescript-eslint/visitor-keys": "npm:8.9.0" + checksum: 10/44dfb640113e8be2f5d25034f5657a9609ee06082b817dc24116c5e1d7a708ca31e8eedcc47f7d309def2ce63be662d1d0a37a1c7bdc7345968a31d04c0a2377 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.8.1": - version: 8.8.1 - resolution: "@typescript-eslint/type-utils@npm:8.8.1" +"@typescript-eslint/type-utils@npm:8.9.0": + version: 8.9.0 + resolution: "@typescript-eslint/type-utils@npm:8.9.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:8.8.1" - "@typescript-eslint/utils": "npm:8.8.1" + "@typescript-eslint/typescript-estree": "npm:8.9.0" + "@typescript-eslint/utils": "npm:8.9.0" debug: "npm:^4.3.4" ts-api-utils: "npm:^1.3.0" peerDependenciesMeta: typescript: optional: true - checksum: 10/3aed62459e68a49f468004d966c914457db2288979234a9452043bff6d5ac7f2d46490fe13f4bb06fd91af085a50e6ac63b69eb66f9a27ee477f958af4738587 + checksum: 10/aaeb465ed57d140bc0d9a8b81a474eff5d1c63d99479828b4eb83a1a626dcb2b1377052a971be5b4d094d6adcf1cf8e33c41ee13369bd71aed0f9cd9f3528c8a languageName: node linkType: hard -"@typescript-eslint/types@npm:8.8.1": - version: 8.8.1 - resolution: "@typescript-eslint/types@npm:8.8.1" - checksum: 10/5ac571810f24a266e1d46a8ce2a6665498fddf757a70eeeec959c993991f72d06a2bee7b848a6b27db958f7771034d8169a77117fd6ca7ed2c3166da9d27396b +"@typescript-eslint/types@npm:8.9.0": + version: 8.9.0 + resolution: "@typescript-eslint/types@npm:8.9.0" + checksum: 10/4d087153605ec23c980f9bc807b122edefff828e0c3b52ef531f4b8e1d30078c39f95e84019370a395bf97eed0d7886cc50b8cd545c287f8a2a21b301272377a languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.8.1": - version: 8.8.1 - resolution: "@typescript-eslint/typescript-estree@npm:8.8.1" +"@typescript-eslint/typescript-estree@npm:8.9.0": + version: 8.9.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.9.0" dependencies: - "@typescript-eslint/types": "npm:8.8.1" - "@typescript-eslint/visitor-keys": "npm:8.8.1" + "@typescript-eslint/types": "npm:8.9.0" + "@typescript-eslint/visitor-keys": "npm:8.9.0" debug: "npm:^4.3.4" fast-glob: "npm:^3.3.2" is-glob: "npm:^4.0.3" @@ -2388,31 +2391,31 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/b569cd362c5f68cf0e1ca53a85bf78c989f10fe4b680423d47c6089bef7cb60b3ed10927232f57dd666e457e43259cec9415da54f2c7b2425062d7acd2e7c98e + checksum: 10/855b433f24fad5d6791c16510d035ded31ccfd17235b45f4dcb7fa89ed57268e4bf4bf79311c5323037e6243da506b2edcb113aa51339291efb344b6d8035b1a languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.8.1, @typescript-eslint/utils@npm:^8.1.0, @typescript-eslint/utils@npm:^8.8.1": - version: 8.8.1 - resolution: "@typescript-eslint/utils@npm:8.8.1" +"@typescript-eslint/utils@npm:8.9.0, @typescript-eslint/utils@npm:^8.1.0, @typescript-eslint/utils@npm:^8.8.1": + version: 8.9.0 + resolution: "@typescript-eslint/utils@npm:8.9.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:8.8.1" - "@typescript-eslint/types": "npm:8.8.1" - "@typescript-eslint/typescript-estree": "npm:8.8.1" + "@typescript-eslint/scope-manager": "npm:8.9.0" + "@typescript-eslint/types": "npm:8.9.0" + "@typescript-eslint/typescript-estree": "npm:8.9.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - checksum: 10/8ecd827af49d3c69ebe65283e5a4e6b44b48f24392319ed9336b8eec47e84fcbcc3e1b5f855ed6b782996cfc0cd289a0a14e40dd69234fd60eeee0a29047bde5 + checksum: 10/84efd10d6aa212103615cf52211a79f1ca02dc4fbf2dbb3a8d2aa49cd19f582b04c219ee98ed1ab77a503f967d82ce56521b1663359ff3e7faaa1f8798c19697 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.8.1": - version: 8.8.1 - resolution: "@typescript-eslint/visitor-keys@npm:8.8.1" +"@typescript-eslint/visitor-keys@npm:8.9.0": + version: 8.9.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.9.0" dependencies: - "@typescript-eslint/types": "npm:8.8.1" + "@typescript-eslint/types": "npm:8.9.0" eslint-visitor-keys: "npm:^3.4.3" - checksum: 10/b5bfb4c9a98d3320639abcfd5aae52dd9c8af477743c5e324ceee1a9ea5f101e0ff7da3de08d3ef66e57854a86e155359bafff13f184493db9e0dffaf9e363c7 + checksum: 10/809097884b8c706f549d99bafa3e0958bd893b3deb190297110f2f1f9360e12064335c8f2df68f39be7d744d2032b5eb57b710c9671eb38f793877ab9364c731 languageName: node linkType: hard @@ -3608,9 +3611,9 @@ __metadata: linkType: hard "electron-to-chromium@npm:^1.5.28": - version: 1.5.36 - resolution: "electron-to-chromium@npm:1.5.36" - checksum: 10/659f637b7384714d5a732de0e5baca007fa1ae741faa4a0f9eb576d65a6a6d30c553caae27df5df7307c65484c0fbcd2ac453df27848d04f7dd27b81dea072a2 + version: 1.5.38 + resolution: "electron-to-chromium@npm:1.5.38" + checksum: 10/862f57480d42e4218a7be7ce71847fb5cf00423bc0b8b9168a978edadfd77fdcef2eb09e9c37821a4dd73b034a72d7cd1dbf2f096297abcd7e8664cc571602d4 languageName: node linkType: hard @@ -3920,8 +3923,8 @@ __metadata: linkType: hard "eslint-plugin-jsdoc@npm:^50.3.1": - version: 50.4.0 - resolution: "eslint-plugin-jsdoc@npm:50.4.0" + version: 50.4.1 + resolution: "eslint-plugin-jsdoc@npm:50.4.1" dependencies: "@es-joy/jsdoccomment": "npm:~0.49.0" are-docs-informative: "npm:^0.0.2" @@ -3936,7 +3939,7 @@ __metadata: synckit: "npm:^0.9.1" peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - checksum: 10/929a7da3bce3ea7a027d841fb04f590e1f95fa7d644af0721b176d588b64324d19059cc65d4ba07be2ac891f1c5af23ec8aefa69e1c999aaba39945b50249d90 + checksum: 10/0368c68b30bfa15525b3934e6223e593e24dd1903ae9f1cc898207fca039052dbce3955da365ab2b8cdeb7ca25fef2b1cf26f5669244bd28290ac79c70af5188 languageName: node linkType: hard @@ -7455,16 +7458,16 @@ __metadata: linkType: hard "typescript-eslint@npm:^8.8.1": - version: 8.8.1 - resolution: "typescript-eslint@npm:8.8.1" + version: 8.9.0 + resolution: "typescript-eslint@npm:8.9.0" dependencies: - "@typescript-eslint/eslint-plugin": "npm:8.8.1" - "@typescript-eslint/parser": "npm:8.8.1" - "@typescript-eslint/utils": "npm:8.8.1" + "@typescript-eslint/eslint-plugin": "npm:8.9.0" + "@typescript-eslint/parser": "npm:8.9.0" + "@typescript-eslint/utils": "npm:8.9.0" peerDependenciesMeta: typescript: optional: true - checksum: 10/f44f60069a88b98b7be269546990a9e75c8af8bde06ef9e095d6c8d8d416746a821047a580201b24a41eec6ea9457fdb9fcae6a9c32096aca2569133c981407f + checksum: 10/34ec65b9f3b9b2a8a17e02bf85c3eab1b9c8a1ccd51883038a2e69f8064177de68916b92976d3b1fe96e52c95898e0de204f43269b76e2230fbefad41cf9b061 languageName: node linkType: hard From 58053bdad01c0a4b856bce2a5bafe62ea79a7818 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 15 Oct 2024 21:19:01 +0200 Subject: [PATCH 13/24] restructure marshal at errors package and add first error struct and unmarshal --- .../errors/src/errors/VatNotFoundError.ts | 45 ++++- packages/errors/src/index.test.ts | 1 + packages/errors/src/index.ts | 5 +- packages/errors/src/marshal.test.ts | 175 ------------------ packages/errors/src/marshal.ts | 73 -------- .../src/marshal/isMarshaledError.test.ts | 70 +++++++ .../errors/src/marshal/isMarshaledError.ts | 14 ++ .../src/marshal/isMarshaledOcapError.test.ts | 108 +++++++++++ .../src/marshal/isMarshaledOcapError.ts | 20 ++ .../errors/src/marshal/marshalError.test.ts | 65 +++++++ packages/errors/src/marshal/marshalError.ts | 36 ++++ .../errors/src/marshal/unmarshalError.test.ts | 48 +++++ packages/errors/src/marshal/unmarshalError.ts | 26 +++ packages/errors/src/types.ts | 22 ++- 14 files changed, 451 insertions(+), 257 deletions(-) delete mode 100644 packages/errors/src/marshal.test.ts delete mode 100644 packages/errors/src/marshal.ts create mode 100644 packages/errors/src/marshal/isMarshaledError.test.ts create mode 100644 packages/errors/src/marshal/isMarshaledError.ts create mode 100644 packages/errors/src/marshal/isMarshaledOcapError.test.ts create mode 100644 packages/errors/src/marshal/isMarshaledOcapError.ts create mode 100644 packages/errors/src/marshal/marshalError.test.ts create mode 100644 packages/errors/src/marshal/marshalError.ts create mode 100644 packages/errors/src/marshal/unmarshalError.test.ts create mode 100644 packages/errors/src/marshal/unmarshalError.ts diff --git a/packages/errors/src/errors/VatNotFoundError.ts b/packages/errors/src/errors/VatNotFoundError.ts index a5e3d659c..1611faebe 100644 --- a/packages/errors/src/errors/VatNotFoundError.ts +++ b/packages/errors/src/errors/VatNotFoundError.ts @@ -1,8 +1,51 @@ +import { + is, + lazy, + literal, + object, + optional, + string, + union, +} from '@metamask/superstruct'; + import { BaseError } from '../BaseError.js'; -import { ErrorCode } from '../types.js'; +import type { MarshaledOcapError } from '../types.js'; +import { ErrorCode, ErrorSentinel, MarshaledErrorStruct } from '../types.js'; export class VatNotFoundError extends BaseError { constructor(vatId: string) { super(ErrorCode.VatNotFound, 'Vat does not exist.', { vatId }); } + + /** + * A superstruct struct for validating marshaled {@link VatNotFoundError} instances. + */ + public static struct = object({ + [ErrorSentinel]: literal(true), + message: string(), + code: literal(ErrorCode.VatNotFound), + data: object({ + vatId: string(), + }), + stack: optional(string()), + cause: optional( + union([string(), lazy(() => MarshaledErrorStruct), literal(undefined)]), + ), + }); + + /** + * Unmarshals a {@link MarshaledError} into a {@link VatNotFoundError}. + * + * @param marshaledError - The marshaled error to unmarshal. + * @returns The unmarshaled error. + */ + public static unmarshal( + marshaledError: MarshaledOcapError, + ): VatNotFoundError { + if (!is(marshaledError, this.struct)) { + throw new Error('Invalid VatNotFoundError structure'); + } + const data = JSON.parse(marshaledError.data); + return new VatNotFoundError(data.vatId); + } } diff --git a/packages/errors/src/index.test.ts b/packages/errors/src/index.test.ts index cbaac9fe7..c52947f1c 100644 --- a/packages/errors/src/index.test.ts +++ b/packages/errors/src/index.test.ts @@ -15,6 +15,7 @@ describe('index', () => { 'VatNotFoundError', 'isCodedError', 'isMarshaledError', + 'isMarshaledOcapError', 'isOcapError', 'marshalError', 'toError', diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts index d2e422318..8ca0c5775 100644 --- a/packages/errors/src/index.ts +++ b/packages/errors/src/index.ts @@ -8,4 +8,7 @@ export { StreamReadError } from './errors/StreamReadError.js'; export { ErrorCode, ErrorSentinel } from './types.js'; export { toError } from './utils/toError.js'; export { isOcapError } from './utils/isOcapError.js'; -export { isMarshaledError, marshalError, unmarshalError } from './marshal.js'; +export { marshalError } from './marshal/marshalError.js'; +export { unmarshalError } from './marshal/unmarshalError.js'; +export { isMarshaledError } from './marshal/isMarshaledError.js'; +export { isMarshaledOcapError } from './marshal/isMarshaledOcapError.js'; diff --git a/packages/errors/src/marshal.test.ts b/packages/errors/src/marshal.test.ts deleted file mode 100644 index 9ed551bcf..000000000 --- a/packages/errors/src/marshal.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { makeErrorMatcherFactory } from '@ocap/test-utils'; -import { describe, it, expect } from 'vitest'; - -import { VatNotFoundError } from './errors/VatNotFoundError.js'; -import { isMarshaledError, marshalError, unmarshalError } from './marshal.js'; -import { ErrorCode, ErrorSentinel } from './types.js'; - -const makeErrorMatcher = makeErrorMatcherFactory(expect); - -describe('marshalError', () => { - it('should marshal an error', () => { - const error = new Error('foo'); - const marshaledError = marshalError(error); - expect(marshaledError).toStrictEqual( - expect.objectContaining({ - [ErrorSentinel]: true, - message: 'foo', - stack: expect.any(String), - }), - ); - }); - - it('should marshal an error with a cause', () => { - const cause = new Error('baz'); - const error = new Error('foo', { cause }); - const marshaledError = marshalError(error); - expect(marshaledError).toStrictEqual( - expect.objectContaining({ - [ErrorSentinel]: true, - message: 'foo', - stack: expect.any(String), - cause: { - [ErrorSentinel]: true, - message: 'baz', - stack: expect.any(String), - }, - }), - ); - }); - - it('should marshal an error with a non-error cause', () => { - const cause = { bar: 'baz' }; - const error = new Error('foo', { cause }); - const marshaledError = marshalError(error); - expect(marshaledError).toStrictEqual( - expect.objectContaining({ - [ErrorSentinel]: true, - message: 'foo', - stack: expect.any(String), - cause: JSON.stringify(cause), - }), - ); - }); - - it('should marshal an ocap error', () => { - const error = new VatNotFoundError('v1'); - const marshaledError = marshalError(error); - expect(marshaledError).toStrictEqual( - expect.objectContaining({ - [ErrorSentinel]: true, - message: 'Vat does not exist.', - stack: expect.any(String), - code: ErrorCode.VatNotFound, - data: JSON.stringify({ vatId: 'v1' }), - }), - ); - }); -}); - -describe('unmarshalError', () => { - it('should unmarshal a marshaled error', () => { - const marshaledError = { - [ErrorSentinel]: true, - message: 'foo', - stack: 'bar', - } as const; - expect(unmarshalError(marshaledError)).toStrictEqual( - makeErrorMatcher('foo'), - ); - }); - - it('should unmarshal a marshaled error with a cause', () => { - const marshaledError = { - [ErrorSentinel]: true, - message: 'foo', - stack: 'bar', - cause: { - [ErrorSentinel]: true, - message: 'baz', - stack: 'qux', - }, - } as const; - expect(unmarshalError(marshaledError)).toStrictEqual( - makeErrorMatcher(new Error('foo', { cause: new Error('baz') })), - ); - }); - - it('should unmarshal a marshaled error with a string cause', () => { - const marshaledError = { - [ErrorSentinel]: true, - message: 'foo', - stack: 'bar', - cause: 'baz', - } as const; - expect(unmarshalError(marshaledError)).toStrictEqual( - makeErrorMatcher(new Error('foo', { cause: 'baz' })), - ); - }); -}); - -describe('isMarshaledError', () => { - it.each([ - [ - 'valid marshaled error with required fields only', - { - [ErrorSentinel]: true, - message: 'An error occurred', - }, - true, - ], - [ - 'valid marshaled error with optional fields', - { - [ErrorSentinel]: true, - message: 'An error occurred', - code: 'ERROR_CODE', - data: { key: 'value' }, - stack: 'Error stack trace', - cause: 'Another error', - }, - true, - ], - [ - 'valid marshaled error with nested cause', - { - [ErrorSentinel]: true, - message: 'An error occurred', - cause: { - [ErrorSentinel]: true, - message: 'Nested error occurred', - }, - }, - true, - ], - [ - 'object missing the sentinel value', - { - message: 'An error occurred', - }, - false, - ], - [ - 'object with incorrect sentinel value', - { - [ErrorSentinel]: false, - message: 'An error occurred', - }, - false, - ], - ['null value', null, false], - ['undefined value', undefined, false], - ['string value', 'string', false], - ['number value', 123, false], - ['array value', [], false], - [ - 'object missing the required message field', - { - [ErrorSentinel]: true, - }, - false, - ], - ])('should return %s', (_, value, expected) => { - expect(isMarshaledError(value)).toBe(expected); - }); -}); diff --git a/packages/errors/src/marshal.ts b/packages/errors/src/marshal.ts deleted file mode 100644 index 9916828a5..000000000 --- a/packages/errors/src/marshal.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { is } from '@metamask/superstruct'; - -import type { MarshaledError, OcapError } from './types.js'; -import { ErrorSentinel, MarshaledErrorStruct } from './types.js'; -import { isOcapError } from './utils/isOcapError.js'; - -/** - * Checks if a value is a {@link MarshaledError}. - * - * @param value - The value to check. - * @returns Whether the value is a {@link MarshaledError}. - */ -export function isMarshaledError(value: unknown): value is MarshaledError { - return is(value, MarshaledErrorStruct); -} - -/** - * Marshals an error into a {@link MarshaledError}. - * - * @param error - The error to marshal. - * @returns The marshaled error. - */ -export function marshalError(error: Error): MarshaledError { - const output: MarshaledError = { - [ErrorSentinel]: true, - message: error.message, - }; - - if (error.cause) { - output.cause = - error.cause instanceof Error - ? marshalError(error.cause) - : JSON.stringify(error.cause); - } - - if (error.stack) { - output.stack = error.stack; - } - - if (isOcapError(error)) { - output.code = error.code; - if (error.data) { - output.data = JSON.stringify(error.data); - } - } - - return output; -} - -/** - * Unmarshals a {@link MarshaledError} into an {@link Error}. - * - * @param marshaledError - The marshaled error to unmarshal. - * @returns The unmarshaled error. - */ -export function unmarshalError( - marshaledError: MarshaledError, -): Error | OcapError { - const output = new Error(marshaledError.message); - - if (marshaledError.cause) { - output.cause = - typeof marshaledError.cause === 'string' - ? marshaledError.cause - : unmarshalError(marshaledError.cause); - } - - if (marshaledError.stack) { - output.stack = marshaledError.stack; - } - - return output; -} diff --git a/packages/errors/src/marshal/isMarshaledError.test.ts b/packages/errors/src/marshal/isMarshaledError.test.ts new file mode 100644 index 000000000..31843478b --- /dev/null +++ b/packages/errors/src/marshal/isMarshaledError.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from 'vitest'; + +import { isMarshaledError } from './isMarshaledError.js'; +import { ErrorSentinel } from '../types.js'; + +describe('isMarshaledError', () => { + it.each([ + [ + 'valid marshaled error with required fields only', + { + [ErrorSentinel]: true, + message: 'An error occurred', + }, + true, + ], + [ + 'valid marshaled error with optional fields', + { + [ErrorSentinel]: true, + message: 'An error occurred', + code: 'ERROR_CODE', + data: { key: 'value' }, + stack: 'Error stack trace', + cause: 'Another error', + }, + true, + ], + [ + 'valid marshaled error with nested cause', + { + [ErrorSentinel]: true, + message: 'An error occurred', + cause: { + [ErrorSentinel]: true, + message: 'Nested error occurred', + }, + }, + true, + ], + [ + 'object missing the sentinel value', + { + message: 'An error occurred', + }, + false, + ], + [ + 'object missing the message value', + { + [ErrorSentinel]: true, + }, + false, + ], + [ + 'object with incorrect sentinel value', + { + [ErrorSentinel]: false, + message: 'An error occurred', + }, + false, + ], + ['null value', null, false], + ['undefined value', undefined, false], + ['string value', 'string', false], + ['number value', 123, false], + ['array value', [], false], + ])('should return %s', (_, value, expected) => { + expect(isMarshaledError(value)).toBe(expected); + }); +}); diff --git a/packages/errors/src/marshal/isMarshaledError.ts b/packages/errors/src/marshal/isMarshaledError.ts new file mode 100644 index 000000000..9d433fe98 --- /dev/null +++ b/packages/errors/src/marshal/isMarshaledError.ts @@ -0,0 +1,14 @@ +import { is } from '@metamask/superstruct'; + +import type { MarshaledError } from '../types.js'; +import { MarshaledErrorStruct } from '../types.js'; + +/** + * Checks if a value is a {@link MarshaledError}. + * + * @param value - The value to check. + * @returns Whether the value is a {@link MarshaledError}. + */ +export function isMarshaledError(value: unknown): value is MarshaledError { + return is(value, MarshaledErrorStruct); +} diff --git a/packages/errors/src/marshal/isMarshaledOcapError.test.ts b/packages/errors/src/marshal/isMarshaledOcapError.test.ts new file mode 100644 index 000000000..20aceaf57 --- /dev/null +++ b/packages/errors/src/marshal/isMarshaledOcapError.test.ts @@ -0,0 +1,108 @@ +import { describe, it, expect } from 'vitest'; + +import { isMarshaledOcapError } from './isMarshaledOcapError.js'; +import { ErrorCode, ErrorSentinel } from '../types.js'; + +describe('isMarshaledOcapError', () => { + it.each([ + [ + 'valid marshaled error with required fields only', + { + [ErrorSentinel]: true, + message: 'An error occurred', + code: ErrorCode.VatNotFound, + data: { key: 'value' }, + }, + true, + ], + [ + 'valid marshaled error with optional fields', + { + [ErrorSentinel]: true, + message: 'An error occurred', + code: ErrorCode.VatNotFound, + data: { key: 'value' }, + stack: 'Error stack trace', + cause: 'Another error', + }, + true, + ], + [ + 'valid marshaled error with nested cause', + { + [ErrorSentinel]: true, + message: 'An error occurred', + code: ErrorCode.VatNotFound, + data: { key: 'value' }, + cause: { + [ErrorSentinel]: true, + message: 'Nested error occurred', + }, + }, + true, + ], + [ + 'object with invalid code value', + { + [ErrorSentinel]: true, + message: 'An error occurred', + code: 'ERROR_CODE', + data: { key: 'value' }, + }, + false, + ], + [ + 'object missing the sentinel value', + { + message: 'An error occurred', + code: ErrorCode.VatNotFound, + data: { key: 'value' }, + }, + false, + ], + [ + 'object missing the message value', + { + [ErrorSentinel]: true, + code: ErrorCode.VatNotFound, + data: { key: 'value' }, + }, + false, + ], + [ + 'object missing the code value', + { + [ErrorSentinel]: true, + message: 'An error occurred', + data: { key: 'value' }, + }, + false, + ], + [ + 'object missing the data value', + { + [ErrorSentinel]: true, + message: 'An error occurred', + code: ErrorCode.VatNotFound, + }, + false, + ], + [ + 'object with incorrect sentinel value', + { + [ErrorSentinel]: false, + message: 'An error occurred', + code: ErrorCode.VatNotFound, + data: { key: 'value' }, + }, + false, + ], + ['null value', null, false], + ['undefined value', undefined, false], + ['string value', 'string', false], + ['number value', 123, false], + ['array value', [], false], + ])('should return %s', (_, value, expected) => { + expect(isMarshaledOcapError(value)).toBe(expected); + }); +}); diff --git a/packages/errors/src/marshal/isMarshaledOcapError.ts b/packages/errors/src/marshal/isMarshaledOcapError.ts new file mode 100644 index 000000000..432911124 --- /dev/null +++ b/packages/errors/src/marshal/isMarshaledOcapError.ts @@ -0,0 +1,20 @@ +import { is } from '@metamask/superstruct'; + +import type { MarshaledOcapError } from '../types.js'; +import { MarshaledErrorStruct } from '../types.js'; + +/** + * Checks if a value is a {@link MarshaledOcapError}. + * + * @param value - The value to check. + * @returns Whether the value is a {@link MarshaledOcapError}. + */ +export function isMarshaledOcapError( + value: unknown, +): value is MarshaledOcapError { + return ( + is(value, MarshaledErrorStruct) && + Boolean(value.data) && + Boolean(value.code) + ); +} diff --git a/packages/errors/src/marshal/marshalError.test.ts b/packages/errors/src/marshal/marshalError.test.ts new file mode 100644 index 000000000..bd0a98e62 --- /dev/null +++ b/packages/errors/src/marshal/marshalError.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from 'vitest'; + +import { marshalError } from './marshalError.js'; +import { VatNotFoundError } from '../errors/VatNotFoundError.js'; +import { ErrorCode, ErrorSentinel } from '../types.js'; + +describe('marshalError', () => { + it('should marshal an error', () => { + const error = new Error('foo'); + const marshaledError = marshalError(error); + expect(marshaledError).toStrictEqual( + expect.objectContaining({ + [ErrorSentinel]: true, + message: 'foo', + stack: expect.any(String), + }), + ); + }); + + it('should marshal an error with a cause', () => { + const cause = new Error('baz'); + const error = new Error('foo', { cause }); + const marshaledError = marshalError(error); + expect(marshaledError).toStrictEqual( + expect.objectContaining({ + [ErrorSentinel]: true, + message: 'foo', + stack: expect.any(String), + cause: { + [ErrorSentinel]: true, + message: 'baz', + stack: expect.any(String), + }, + }), + ); + }); + + it('should marshal an error with a non-error cause', () => { + const cause = { bar: 'baz' }; + const error = new Error('foo', { cause }); + const marshaledError = marshalError(error); + expect(marshaledError).toStrictEqual( + expect.objectContaining({ + [ErrorSentinel]: true, + message: 'foo', + stack: expect.any(String), + cause: JSON.stringify(cause), + }), + ); + }); + + it('should marshal an ocap error', () => { + const error = new VatNotFoundError('v1'); + const marshaledError = marshalError(error); + expect(marshaledError).toStrictEqual( + expect.objectContaining({ + [ErrorSentinel]: true, + message: 'Vat does not exist.', + stack: expect.any(String), + code: ErrorCode.VatNotFound, + data: JSON.stringify({ vatId: 'v1' }), + }), + ); + }); +}); diff --git a/packages/errors/src/marshal/marshalError.ts b/packages/errors/src/marshal/marshalError.ts new file mode 100644 index 000000000..7f9fddbb1 --- /dev/null +++ b/packages/errors/src/marshal/marshalError.ts @@ -0,0 +1,36 @@ +import type { MarshaledError } from '../types.js'; +import { ErrorSentinel } from '../types.js'; +import { isOcapError } from '../utils/isOcapError.js'; + +/** + * Marshals an error into a {@link MarshaledError}. + * + * @param error - The error to marshal. + * @returns The marshaled error. + */ +export function marshalError(error: Error): MarshaledError { + const output: MarshaledError = { + [ErrorSentinel]: true, + message: error.message, + }; + + if (error.cause) { + output.cause = + error.cause instanceof Error + ? marshalError(error.cause) + : JSON.stringify(error.cause); + } + + if (error.stack) { + output.stack = error.stack; + } + + if (isOcapError(error)) { + output.code = error.code; + if (error.data) { + output.data = JSON.stringify(error.data); + } + } + + return output; +} diff --git a/packages/errors/src/marshal/unmarshalError.test.ts b/packages/errors/src/marshal/unmarshalError.test.ts new file mode 100644 index 000000000..4a94f8be8 --- /dev/null +++ b/packages/errors/src/marshal/unmarshalError.test.ts @@ -0,0 +1,48 @@ +import { makeErrorMatcherFactory } from '@ocap/test-utils'; +import { describe, it, expect } from 'vitest'; + +import { unmarshalError } from './unmarshalError.js'; +import { ErrorSentinel } from '../types.js'; + +const makeErrorMatcher = makeErrorMatcherFactory(expect); + +describe('unmarshalError', () => { + it('should unmarshal a marshaled error', () => { + const marshaledError = { + [ErrorSentinel]: true, + message: 'foo', + stack: 'bar', + } as const; + expect(unmarshalError(marshaledError)).toStrictEqual( + makeErrorMatcher('foo'), + ); + }); + + it('should unmarshal a marshaled error with a cause', () => { + const marshaledError = { + [ErrorSentinel]: true, + message: 'foo', + stack: 'bar', + cause: { + [ErrorSentinel]: true, + message: 'baz', + stack: 'qux', + }, + } as const; + expect(unmarshalError(marshaledError)).toStrictEqual( + makeErrorMatcher(new Error('foo', { cause: new Error('baz') })), + ); + }); + + it('should unmarshal a marshaled error with a string cause', () => { + const marshaledError = { + [ErrorSentinel]: true, + message: 'foo', + stack: 'bar', + cause: 'baz', + } as const; + expect(unmarshalError(marshaledError)).toStrictEqual( + makeErrorMatcher(new Error('foo', { cause: 'baz' })), + ); + }); +}); diff --git a/packages/errors/src/marshal/unmarshalError.ts b/packages/errors/src/marshal/unmarshalError.ts new file mode 100644 index 000000000..76aa2d0ff --- /dev/null +++ b/packages/errors/src/marshal/unmarshalError.ts @@ -0,0 +1,26 @@ +import type { MarshaledError, OcapError } from '../types.js'; + +/** + * Unmarshals a {@link MarshaledError} into an {@link Error}. + * + * @param marshaledError - The marshaled error to unmarshal. + * @returns The unmarshaled error. + */ +export function unmarshalError( + marshaledError: MarshaledError, +): Error | OcapError { + const output = new Error(marshaledError.message); + + if (marshaledError.cause) { + output.cause = + typeof marshaledError.cause === 'string' + ? marshaledError.cause + : unmarshalError(marshaledError.cause); + } + + if (marshaledError.stack) { + output.stack = marshaledError.stack; + } + + return output; +} diff --git a/packages/errors/src/types.ts b/packages/errors/src/types.ts index fd4f7adcc..abe9729e6 100644 --- a/packages/errors/src/types.ts +++ b/packages/errors/src/types.ts @@ -1,7 +1,7 @@ import type { Struct } from '@metamask/superstruct'; import { lazy, literal, optional, string, union } from '@metamask/superstruct'; import { JsonStruct, object } from '@metamask/utils'; -import type { Json } from '@metamask/utils'; +import type { Json, NonEmptyArray } from '@metamask/utils'; export enum ErrorCode { StreamReadError = 'STREAM_READ_ERROR', @@ -22,22 +22,30 @@ export type OcapError = { */ export const ErrorSentinel = '@@MARSHALED_ERROR'; -/** - * A marshaled error. - */ export type MarshaledError = { [ErrorSentinel]: true; message: string; code?: ErrorCode; - data?: Json; + data?: string; stack?: string; cause?: MarshaledError | string; }; -export const MarshaledErrorStruct: Struct = object({ +export type MarshaledOcapError = Omit & { + code: ErrorCode; + data: string; +}; + +const ErrorCodeStruct = union( + Object.values(ErrorCode).map((code) => literal(code)) as NonEmptyArray< + Struct + >, +); + +export const MarshaledErrorStruct = object({ [ErrorSentinel]: literal(true), message: string(), - code: optional(string()), + code: optional(ErrorCodeStruct), data: optional(JsonStruct), stack: optional(string()), cause: optional(union([string(), lazy(() => MarshaledErrorStruct)])), From 96da9af7344364cd2b5dcc6e09684aee2db91367 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Wed, 16 Oct 2024 16:28:51 +0200 Subject: [PATCH 14/24] custom error unmarshal --- packages/errors/src/BaseError.test.ts | 17 +++- packages/errors/src/BaseError.ts | 11 ++- .../errors/src/errors/StreamReadError.test.ts | 98 +++++++++++++++++++ packages/errors/src/errors/StreamReadError.ts | 44 ++++++++- .../src/errors/VatAlreadyExistsError.test.ts | 50 ++++++++++ .../src/errors/VatAlreadyExistsError.ts | 44 ++++++++- .../VatCapTpConnectionExistsError.test.ts | 53 ++++++++++ .../errors/VatCapTpConnectionExistsError.ts | 46 ++++++++- .../VatCapTpConnectionNotFoundError.test.ts | 53 ++++++++++ .../errors/VatCapTpConnectionNotFoundError.ts | 46 ++++++++- .../errors/src/errors/VatDeletedError.test.ts | 50 ++++++++++ packages/errors/src/errors/VatDeletedError.ts | 42 +++++++- .../src/errors/VatNotFoundError.test.ts | 50 ++++++++++ .../errors/src/errors/VatNotFoundError.ts | 3 +- packages/errors/src/errors/errors.test.ts | 96 ------------------ packages/errors/src/errors/index.ts | 16 +++ packages/errors/src/index.test.ts | 3 +- .../src/marshal/isMarshaledError.test.ts | 4 +- .../errors/src/marshal/marshalError.test.ts | 2 +- packages/errors/src/marshal/marshalError.ts | 4 +- .../errors/src/marshal/unmarshalError.test.ts | 70 ++++++++++++- packages/errors/src/marshal/unmarshalError.ts | 19 +++- packages/errors/src/types.ts | 4 +- packages/kernel/src/Supervisor.test.ts | 2 +- packages/kernel/src/Supervisor.ts | 2 +- packages/kernel/src/Vat.test.ts | 2 +- packages/kernel/src/Vat.ts | 12 +-- packages/kernel/src/stream-envelope.ts | 2 +- 28 files changed, 707 insertions(+), 138 deletions(-) create mode 100644 packages/errors/src/errors/StreamReadError.test.ts create mode 100644 packages/errors/src/errors/VatAlreadyExistsError.test.ts create mode 100644 packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts create mode 100644 packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts create mode 100644 packages/errors/src/errors/VatDeletedError.test.ts create mode 100644 packages/errors/src/errors/VatNotFoundError.test.ts delete mode 100644 packages/errors/src/errors/errors.test.ts create mode 100644 packages/errors/src/errors/index.ts diff --git a/packages/errors/src/BaseError.test.ts b/packages/errors/src/BaseError.test.ts index add15b5be..33a99d5db 100644 --- a/packages/errors/src/BaseError.test.ts +++ b/packages/errors/src/BaseError.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest'; import { BaseError } from './BaseError.js'; +import type { MarshaledOcapError } from './types.js'; import { ErrorCode } from './types.js'; describe('BaseError', () => { @@ -9,7 +10,7 @@ describe('BaseError', () => { const mockData = { key: 'value' }; const mockCause = new Error('Root cause error'); - it('should create a BaseError with required properties', () => { + it('creates a BaseError with required properties', () => { const error = new BaseError(mockCode, mockMessage); expect(error).toBeInstanceOf(BaseError); expect(error).toBeInstanceOf(Error); @@ -20,7 +21,7 @@ describe('BaseError', () => { expect(error.cause).toBeUndefined(); }); - it('should create a BaseError with all properties', () => { + it('creates a BaseError with all properties', () => { const error = new BaseError(mockCode, mockMessage, mockData, mockCause); expect(error.name).toBe('BaseError'); expect(error.message).toBe(mockMessage); @@ -29,21 +30,27 @@ describe('BaseError', () => { expect(error.cause).toBe(mockCause); }); - it('should inherit from the Error class and have the correct name', () => { + it('inherits from the Error class and have the correct name', () => { const error = new BaseError(mockCode, mockMessage); expect(error).toBeInstanceOf(Error); expect(error.name).toBe('BaseError'); }); - it('should correctly handle a missing data parameter', () => { + it('handles a missing data parameter', () => { const error = new BaseError(mockCode, mockMessage, undefined, mockCause); expect(error.data).toBeUndefined(); expect(error.cause).toBe(mockCause); }); - it('should correctly handle a missing cause parameter', () => { + it('handles a missing cause parameter', () => { const error = new BaseError(mockCode, mockMessage, mockData); expect(error.data).toStrictEqual(mockData); expect(error.cause).toBeUndefined(); }); + + it('throws an error when unmarshal is called', () => { + expect(() => BaseError.unmarshal({} as MarshaledOcapError)).toThrow( + 'Unmarshal method not implemented', + ); + }); }); diff --git a/packages/errors/src/BaseError.ts b/packages/errors/src/BaseError.ts index f5aa9dce2..b05af1b07 100644 --- a/packages/errors/src/BaseError.ts +++ b/packages/errors/src/BaseError.ts @@ -1,6 +1,6 @@ import type { Json } from '@metamask/utils'; -import type { ErrorCode, OcapError } from './types.js'; +import type { ErrorCode, MarshaledOcapError, OcapError } from './types.js'; export class BaseError extends Error implements OcapError { public readonly code: ErrorCode; @@ -15,4 +15,13 @@ export class BaseError extends Error implements OcapError { this.data = data; this.cause = cause; } + + /** + * A placeholder for unmarshal functionality. Should be implemented in subclasses. + * + * @param _marshaledError - The marshaled error to unmarshal. + */ + public static unmarshal(_marshaledError: MarshaledOcapError): BaseError { + throw new Error('Unmarshal method not implemented'); + } } diff --git a/packages/errors/src/errors/StreamReadError.test.ts b/packages/errors/src/errors/StreamReadError.test.ts new file mode 100644 index 000000000..420711542 --- /dev/null +++ b/packages/errors/src/errors/StreamReadError.test.ts @@ -0,0 +1,98 @@ +import { describe, it, expect } from 'vitest'; + +import { StreamReadError } from './StreamReadError.js'; +import type { MarshaledOcapError } from '../types.js'; +import { ErrorCode, ErrorSentinel } from '../types.js'; + +describe('StreamReadError', () => { + const mockVatId = 'mockVatId'; + const mockSupervisorId = 'mockSupervisorId'; + const mockOriginalError = new Error('Original error'); + + it('creates a StreamReadError for Supervisor with the correct properties', () => { + const error = new StreamReadError( + { supervisorId: mockSupervisorId }, + mockOriginalError, + ); + expect(error).toBeInstanceOf(StreamReadError); + expect(error.code).toBe(ErrorCode.StreamReadError); + expect(error.message).toBe('Unexpected stream read error.'); + expect(error.data).toStrictEqual({ supervisorId: mockSupervisorId }); + expect(error.cause).toBe(mockOriginalError); + }); + + it('creates a StreamReadError for Vat with the correct properties', () => { + const error = new StreamReadError({ vatId: mockVatId }, mockOriginalError); + expect(error).toBeInstanceOf(StreamReadError); + expect(error.code).toBe(ErrorCode.StreamReadError); + expect(error.message).toBe('Unexpected stream read error.'); + expect(error.data).toStrictEqual({ vatId: mockVatId }); + expect(error.cause).toBe(mockOriginalError); + }); + + it('unmarshals a valid marshaled StreamReadError for Vat', () => { + const data = { vatId: mockVatId }; + const marshaledError: MarshaledOcapError = { + [ErrorSentinel]: true, + message: 'Unexpected stream read error.', + stack: 'customStack', + code: ErrorCode.StreamReadError, + data, + cause: { + [ErrorSentinel]: true, + message: 'Original error', + stack: 'bar', + }, + }; + + const unmarshaledError = StreamReadError.unmarshal(marshaledError); + expect(unmarshaledError).toBeInstanceOf(StreamReadError); + expect(unmarshaledError.code).toBe(ErrorCode.StreamReadError); + expect(unmarshaledError.message).toBe('Unexpected stream read error.'); + expect(unmarshaledError.data).toStrictEqual({ + vatId: mockVatId, + }); + expect(unmarshaledError.cause).toBeInstanceOf(Error); + expect((unmarshaledError.cause as Error).message).toBe('Original error'); + }); + + it('unmarshals a valid marshaled StreamReadError for Supervisor', () => { + const data = { supervisorId: mockSupervisorId }; + const marshaledError: MarshaledOcapError = { + [ErrorSentinel]: true, + message: 'Unexpected stream read error.', + stack: 'customStack', + code: ErrorCode.StreamReadError, + data, + cause: { + [ErrorSentinel]: true, + message: 'Original error', + stack: 'bar', + }, + }; + + const unmarshaledError = StreamReadError.unmarshal(marshaledError); + expect(unmarshaledError).toBeInstanceOf(StreamReadError); + expect(unmarshaledError.code).toBe(ErrorCode.StreamReadError); + expect(unmarshaledError.message).toBe('Unexpected stream read error.'); + expect(unmarshaledError.data).toStrictEqual({ + supervisorId: mockSupervisorId, + }); + expect(unmarshaledError.cause).toBeInstanceOf(Error); + expect((unmarshaledError.cause as Error).message).toBe('Original error'); + }); + + it('throws when an invalid messages is unmarshal marshaled', () => { + const marshaledError: MarshaledOcapError = { + [ErrorSentinel]: true, + message: 'Unexpected stream read error.', + code: ErrorCode.StreamReadError, + data: 'invalid data', + stack: 'stack trace', + }; + + expect(() => StreamReadError.unmarshal(marshaledError)).toThrow( + 'Invalid StreamReadError structure', + ); + }); +}); diff --git a/packages/errors/src/errors/StreamReadError.ts b/packages/errors/src/errors/StreamReadError.ts index 588858178..0f5f8ae78 100644 --- a/packages/errors/src/errors/StreamReadError.ts +++ b/packages/errors/src/errors/StreamReadError.ts @@ -1,11 +1,13 @@ +import { is, literal, object, optional, string } from '@metamask/superstruct'; + import { BaseError } from '../BaseError.js'; -import { ErrorCode } from '../types.js'; +import type { MarshaledOcapError } from '../types.js'; +import { ErrorCode, ErrorSentinel, MarshaledErrorStruct } from '../types.js'; + +type StreamReadErrorData = { vatId: string } | { supervisorId: string }; export class StreamReadError extends BaseError { - constructor( - data: { vatId: string } | { supervisorId: string }, - originalError: Error, - ) { + constructor(data: StreamReadErrorData, originalError: Error) { super( ErrorCode.StreamReadError, 'Unexpected stream read error.', @@ -13,4 +15,36 @@ export class StreamReadError extends BaseError { originalError, ); } + + /** + * A superstruct struct for validating marshaled {@link StreamReadError} instances. + */ + public static struct = object({ + [ErrorSentinel]: literal(true), + message: string(), + code: literal(ErrorCode.StreamReadError), + data: object({ + vatId: optional(string()), + supervisorId: optional(string()), + }), + stack: optional(string()), + cause: MarshaledErrorStruct, + }); + + /** + * Unmarshals a {@link MarshaledError} into a {@link StreamReadError}. + * + * @param marshaledError - The marshaled error to unmarshal. + * @returns The unmarshaled error. + */ + public static unmarshal(marshaledError: MarshaledOcapError): StreamReadError { + if (!is(marshaledError, this.struct)) { + throw new Error('Invalid StreamReadError structure'); + } + return new StreamReadError( + marshaledError.data as StreamReadErrorData, + // The cause will be properly unmarshaled during the parent call. + new Error(marshaledError.cause?.message), + ); + } } diff --git a/packages/errors/src/errors/VatAlreadyExistsError.test.ts b/packages/errors/src/errors/VatAlreadyExistsError.test.ts new file mode 100644 index 000000000..8baeceeb4 --- /dev/null +++ b/packages/errors/src/errors/VatAlreadyExistsError.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest'; + +import { VatAlreadyExistsError } from './VatAlreadyExistsError.js'; +import type { MarshaledOcapError } from '../types.js'; +import { ErrorCode, ErrorSentinel } from '../types.js'; + +describe('VatAlreadyExistsError', () => { + const mockVatId = 'mockVatId'; + + it('creates a VatAlreadyExistsError with the correct properties', () => { + const error = new VatAlreadyExistsError(mockVatId); + expect(error).toBeInstanceOf(VatAlreadyExistsError); + expect(error.code).toBe(ErrorCode.VatAlreadyExists); + expect(error.message).toBe('Vat already exists.'); + expect(error.data).toStrictEqual({ vatId: mockVatId }); + expect(error.cause).toBeUndefined(); + }); + + it('unmarshals a valid marshaled error', () => { + const marshaledError: MarshaledOcapError = { + [ErrorSentinel]: true, + message: 'Vat already exists.', + code: ErrorCode.VatAlreadyExists, + data: { vatId: mockVatId }, + stack: 'stack trace', + }; + + const unmarshaledError = VatAlreadyExistsError.unmarshal(marshaledError); + expect(unmarshaledError).toBeInstanceOf(VatAlreadyExistsError); + expect(unmarshaledError.code).toBe(ErrorCode.VatAlreadyExists); + expect(unmarshaledError.message).toBe('Vat already exists.'); + expect(unmarshaledError.data).toStrictEqual({ + vatId: mockVatId, + }); + }); + + it('throws when an invalid messages is unmarshal marshaled', () => { + const marshaledError: MarshaledOcapError = { + [ErrorSentinel]: true, + message: 'Vat already exists.', + code: ErrorCode.VatAlreadyExists, + data: '{ vatId: mockVatId }', + stack: 'stack trace', + }; + + expect(() => VatAlreadyExistsError.unmarshal(marshaledError)).toThrow( + 'Invalid VatAlreadyExistsError structure', + ); + }); +}); diff --git a/packages/errors/src/errors/VatAlreadyExistsError.ts b/packages/errors/src/errors/VatAlreadyExistsError.ts index 2a68a4a71..10b7f6884 100644 --- a/packages/errors/src/errors/VatAlreadyExistsError.ts +++ b/packages/errors/src/errors/VatAlreadyExistsError.ts @@ -1,5 +1,16 @@ +import { + is, + lazy, + literal, + object, + optional, + string, + union, +} from '@metamask/superstruct'; + import { BaseError } from '../BaseError.js'; -import { ErrorCode } from '../types.js'; +import type { MarshaledOcapError } from '../types.js'; +import { ErrorCode, ErrorSentinel, MarshaledErrorStruct } from '../types.js'; export class VatAlreadyExistsError extends BaseError { constructor(vatId: string) { @@ -7,4 +18,35 @@ export class VatAlreadyExistsError extends BaseError { vatId, }); } + + /** + * A superstruct struct for validating marshaled {@link VatAlreadyExistsError} instances. + */ + public static struct = object({ + [ErrorSentinel]: literal(true), + message: string(), + code: literal(ErrorCode.VatAlreadyExists), + data: object({ + vatId: string(), + }), + stack: optional(string()), + cause: optional( + union([string(), lazy(() => MarshaledErrorStruct), literal(undefined)]), + ), + }); + + /** + * Unmarshals a {@link MarshaledError} into a {@link VatAlreadyExistsError}. + * + * @param marshaledError - The marshaled error to unmarshal. + * @returns The unmarshaled error. + */ + public static unmarshal( + marshaledError: MarshaledOcapError, + ): VatAlreadyExistsError { + if (!is(marshaledError, this.struct)) { + throw new Error('Invalid VatAlreadyExistsError structure'); + } + return new VatAlreadyExistsError(marshaledError.data.vatId); + } } diff --git a/packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts b/packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts new file mode 100644 index 000000000..1355a24ae --- /dev/null +++ b/packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from 'vitest'; + +import { VatCapTpConnectionExistsError } from './VatCapTpConnectionExistsError.js'; +import type { MarshaledOcapError } from '../types.js'; +import { ErrorCode, ErrorSentinel } from '../types.js'; + +describe('VatCapTpConnectionExistsError', () => { + const mockVatId = 'mockVatId'; + + it('creates a VatCapTpConnectionExistsError with the correct properties', () => { + const error = new VatCapTpConnectionExistsError(mockVatId); + expect(error).toBeInstanceOf(VatCapTpConnectionExistsError); + expect(error.code).toBe(ErrorCode.VatCapTpConnectionExists); + expect(error.message).toBe('Vat already has a CapTP connection.'); + expect(error.data).toStrictEqual({ vatId: mockVatId }); + expect(error.cause).toBeUndefined(); + }); + + it('unmarshals a valid marshaled error', () => { + const marshaledError: MarshaledOcapError = { + [ErrorSentinel]: true, + message: 'Vat already has a CapTP connection.', + code: ErrorCode.VatCapTpConnectionExists, + data: { vatId: mockVatId }, + stack: 'stack trace', + }; + + const unmarshaledError = + VatCapTpConnectionExistsError.unmarshal(marshaledError); + expect(unmarshaledError).toBeInstanceOf(VatCapTpConnectionExistsError); + expect(unmarshaledError.code).toBe(ErrorCode.VatCapTpConnectionExists); + expect(unmarshaledError.message).toBe( + 'Vat already has a CapTP connection.', + ); + expect(unmarshaledError.data).toStrictEqual({ + vatId: mockVatId, + }); + }); + + it('throws when an invalid messages is unmarshal marshaled', () => { + const marshaledError: MarshaledOcapError = { + [ErrorSentinel]: true, + message: 'Vat already has a CapTP connection.', + code: ErrorCode.VatCapTpConnectionExists, + data: '{ vatId: mockVatId }', + stack: 'stack trace', + }; + + expect(() => + VatCapTpConnectionExistsError.unmarshal(marshaledError), + ).toThrow('Invalid VatCapTpConnectionExistsError structure'); + }); +}); diff --git a/packages/errors/src/errors/VatCapTpConnectionExistsError.ts b/packages/errors/src/errors/VatCapTpConnectionExistsError.ts index 69be6bba0..75aee6d10 100644 --- a/packages/errors/src/errors/VatCapTpConnectionExistsError.ts +++ b/packages/errors/src/errors/VatCapTpConnectionExistsError.ts @@ -1,14 +1,56 @@ +import { + is, + lazy, + literal, + object, + optional, + string, + union, +} from '@metamask/superstruct'; + import { BaseError } from '../BaseError.js'; -import { ErrorCode } from '../types.js'; +import type { MarshaledOcapError } from '../types.js'; +import { ErrorCode, ErrorSentinel, MarshaledErrorStruct } from '../types.js'; export class VatCapTpConnectionExistsError extends BaseError { constructor(vatId: string) { super( ErrorCode.VatCapTpConnectionExists, - 'Vat already has a CapTp connection.', + 'Vat already has a CapTP connection.', { vatId, }, ); } + + /** + * A superstruct struct for validating marshaled {@link VatCapTpConnectionExistsError} instances. + */ + public static struct = object({ + [ErrorSentinel]: literal(true), + message: string(), + code: literal(ErrorCode.VatCapTpConnectionExists), + data: object({ + vatId: string(), + }), + stack: optional(string()), + cause: optional( + union([string(), lazy(() => MarshaledErrorStruct), literal(undefined)]), + ), + }); + + /** + * Unmarshals a {@link MarshaledError} into a {@link VatCapTpConnectionExistsError}. + * + * @param marshaledError - The marshaled error to unmarshal. + * @returns The unmarshaled error. + */ + public static unmarshal( + marshaledError: MarshaledOcapError, + ): VatCapTpConnectionExistsError { + if (!is(marshaledError, this.struct)) { + throw new Error('Invalid VatCapTpConnectionExistsError structure'); + } + return new VatCapTpConnectionExistsError(marshaledError.data.vatId); + } } diff --git a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts new file mode 100644 index 000000000..220e8cd5b --- /dev/null +++ b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from 'vitest'; + +import { VatCapTpConnectionNotFoundError } from './VatCapTpConnectionNotFoundError.js'; +import type { MarshaledOcapError } from '../types.js'; +import { ErrorCode, ErrorSentinel } from '../types.js'; + +describe('VatCapTpConnectionNotFoundError', () => { + const mockVatId = 'mockVatId'; + + it('creates a VatCapTpConnectionNotFoundError with the correct properties', () => { + const error = new VatCapTpConnectionNotFoundError(mockVatId); + expect(error).toBeInstanceOf(VatCapTpConnectionNotFoundError); + expect(error.code).toBe(ErrorCode.VatCapTpConnectionNotFound); + expect(error.message).toBe('Vat does not have a CapTP connection.'); + expect(error.data).toStrictEqual({ vatId: mockVatId }); + expect(error.cause).toBeUndefined(); + }); + + it('unmarshals a valid marshaled error', () => { + const marshaledError: MarshaledOcapError = { + [ErrorSentinel]: true, + message: 'Vat does not have a CapTP connection.', + code: ErrorCode.VatCapTpConnectionNotFound, + data: { vatId: mockVatId }, + stack: 'stack trace', + }; + + const unmarshaledError = + VatCapTpConnectionNotFoundError.unmarshal(marshaledError); + expect(unmarshaledError).toBeInstanceOf(VatCapTpConnectionNotFoundError); + expect(unmarshaledError.code).toBe(ErrorCode.VatCapTpConnectionNotFound); + expect(unmarshaledError.message).toBe( + 'Vat does not have a CapTP connection.', + ); + expect(unmarshaledError.data).toStrictEqual({ + vatId: mockVatId, + }); + }); + + it('throws when an invalid messages is unmarshal marshaled', () => { + const marshaledError: MarshaledOcapError = { + [ErrorSentinel]: true, + message: 'Vat does not have a CapTP connection.', + code: ErrorCode.VatCapTpConnectionNotFound, + data: '{ vatId: mockVatId }', + stack: 'stack trace', + }; + + expect(() => + VatCapTpConnectionNotFoundError.unmarshal(marshaledError), + ).toThrow('Invalid VatCapTpConnectionNotFoundError structure'); + }); +}); diff --git a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts index fe9869c3d..472abc8fe 100644 --- a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts +++ b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts @@ -1,12 +1,54 @@ +import { + is, + lazy, + literal, + object, + optional, + string, + union, +} from '@metamask/superstruct'; + import { BaseError } from '../BaseError.js'; -import { ErrorCode } from '../types.js'; +import type { MarshaledOcapError } from '../types.js'; +import { ErrorCode, ErrorSentinel, MarshaledErrorStruct } from '../types.js'; export class VatCapTpConnectionNotFoundError extends BaseError { constructor(vatId: string) { super( ErrorCode.VatCapTpConnectionNotFound, - 'Vat does not have a CapTp connection.', + 'Vat does not have a CapTP connection.', { vatId }, ); } + + /** + * A superstruct struct for validating marshaled {@link VatCapTpConnectionNotFoundError} instances. + */ + public static struct = object({ + [ErrorSentinel]: literal(true), + message: string(), + code: literal(ErrorCode.VatCapTpConnectionNotFound), + data: object({ + vatId: string(), + }), + stack: optional(string()), + cause: optional( + union([string(), lazy(() => MarshaledErrorStruct), literal(undefined)]), + ), + }); + + /** + * Unmarshals a {@link MarshaledError} into a {@link VatCapTpConnectionNotFoundError}. + * + * @param marshaledError - The marshaled error to unmarshal. + * @returns The unmarshaled error. + */ + public static unmarshal( + marshaledError: MarshaledOcapError, + ): VatCapTpConnectionNotFoundError { + if (!is(marshaledError, this.struct)) { + throw new Error('Invalid VatCapTpConnectionNotFoundError structure'); + } + return new VatCapTpConnectionNotFoundError(marshaledError.data.vatId); + } } diff --git a/packages/errors/src/errors/VatDeletedError.test.ts b/packages/errors/src/errors/VatDeletedError.test.ts new file mode 100644 index 000000000..e77f469e2 --- /dev/null +++ b/packages/errors/src/errors/VatDeletedError.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest'; + +import { VatDeletedError } from './VatDeletedError.js'; +import type { MarshaledOcapError } from '../types.js'; +import { ErrorCode, ErrorSentinel } from '../types.js'; + +describe('VatDeletedError', () => { + const mockVatId = 'mockVatId'; + + it('creates a VatDeletedError with the correct properties', () => { + const error = new VatDeletedError(mockVatId); + expect(error).toBeInstanceOf(VatDeletedError); + expect(error.code).toBe(ErrorCode.VatDeleted); + expect(error.message).toBe('Vat was deleted.'); + expect(error.data).toStrictEqual({ vatId: mockVatId }); + expect(error.cause).toBeUndefined(); + }); + + it('unmarshals a valid marshaled error', () => { + const marshaledError: MarshaledOcapError = { + [ErrorSentinel]: true, + message: 'Vat was deleted.', + code: ErrorCode.VatDeleted, + data: { vatId: mockVatId }, + stack: 'stack trace', + }; + + const unmarshaledError = VatDeletedError.unmarshal(marshaledError); + expect(unmarshaledError).toBeInstanceOf(VatDeletedError); + expect(unmarshaledError.code).toBe(ErrorCode.VatDeleted); + expect(unmarshaledError.message).toBe('Vat was deleted.'); + expect(unmarshaledError.data).toStrictEqual({ + vatId: mockVatId, + }); + }); + + it('throws when an invalid messages is unmarshal marshaled', () => { + const marshaledError: MarshaledOcapError = { + [ErrorSentinel]: true, + message: 'Vat was deleted.', + code: ErrorCode.VatDeleted, + data: '{ vatId: mockVatId }', + stack: 'stack trace', + }; + + expect(() => VatDeletedError.unmarshal(marshaledError)).toThrow( + 'Invalid VatDeletedError structure', + ); + }); +}); diff --git a/packages/errors/src/errors/VatDeletedError.ts b/packages/errors/src/errors/VatDeletedError.ts index 1b78b45be..6783cd7dc 100644 --- a/packages/errors/src/errors/VatDeletedError.ts +++ b/packages/errors/src/errors/VatDeletedError.ts @@ -1,8 +1,48 @@ +import { + is, + lazy, + literal, + object, + optional, + string, + union, +} from '@metamask/superstruct'; + import { BaseError } from '../BaseError.js'; -import { ErrorCode } from '../types.js'; +import type { MarshaledOcapError } from '../types.js'; +import { ErrorCode, ErrorSentinel, MarshaledErrorStruct } from '../types.js'; export class VatDeletedError extends BaseError { constructor(vatId: string) { super(ErrorCode.VatDeleted, 'Vat was deleted.', { vatId }); } + + /** + * A superstruct struct for validating marshaled {@link VatDeletedError} instances. + */ + public static struct = object({ + [ErrorSentinel]: literal(true), + message: string(), + code: literal(ErrorCode.VatDeleted), + data: object({ + vatId: string(), + }), + stack: optional(string()), + cause: optional( + union([string(), lazy(() => MarshaledErrorStruct), literal(undefined)]), + ), + }); + + /** + * Unmarshals a {@link MarshaledError} into a {@link VatDeletedError}. + * + * @param marshaledError - The marshaled error to unmarshal. + * @returns The unmarshaled error. + */ + public static unmarshal(marshaledError: MarshaledOcapError): VatDeletedError { + if (!is(marshaledError, this.struct)) { + throw new Error('Invalid VatDeletedError structure'); + } + return new VatDeletedError(marshaledError.data.vatId); + } } diff --git a/packages/errors/src/errors/VatNotFoundError.test.ts b/packages/errors/src/errors/VatNotFoundError.test.ts new file mode 100644 index 000000000..ca483729e --- /dev/null +++ b/packages/errors/src/errors/VatNotFoundError.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest'; + +import { VatNotFoundError } from './VatNotFoundError.js'; +import type { MarshaledOcapError } from '../types.js'; +import { ErrorCode, ErrorSentinel } from '../types.js'; + +describe('VatNotFoundError', () => { + const mockVatId = 'mockVatId'; + + it('creates a VatNotFoundError with the correct properties', () => { + const error = new VatNotFoundError(mockVatId); + expect(error).toBeInstanceOf(VatNotFoundError); + expect(error.code).toBe(ErrorCode.VatNotFound); + expect(error.message).toBe('Vat does not exist.'); + expect(error.data).toStrictEqual({ vatId: mockVatId }); + expect(error.cause).toBeUndefined(); + }); + + it('unmarshals a valid marshaled error', () => { + const marshaledError: MarshaledOcapError = { + [ErrorSentinel]: true, + message: 'Vat does not exist.', + code: ErrorCode.VatNotFound, + data: { vatId: mockVatId }, + stack: 'stack trace', + }; + + const unmarshaledError = VatNotFoundError.unmarshal(marshaledError); + expect(unmarshaledError).toBeInstanceOf(VatNotFoundError); + expect(unmarshaledError.code).toBe(ErrorCode.VatNotFound); + expect(unmarshaledError.message).toBe('Vat does not exist.'); + expect(unmarshaledError.data).toStrictEqual({ + vatId: mockVatId, + }); + }); + + it('throws when an invalid messages is unmarshal marshaled', () => { + const marshaledError: MarshaledOcapError = { + [ErrorSentinel]: true, + message: 'Vat does not exist.', + code: ErrorCode.VatNotFound, + data: '{ vatId: mockVatId }', + stack: 'stack trace', + }; + + expect(() => VatNotFoundError.unmarshal(marshaledError)).toThrow( + 'Invalid VatNotFoundError structure', + ); + }); +}); diff --git a/packages/errors/src/errors/VatNotFoundError.ts b/packages/errors/src/errors/VatNotFoundError.ts index 1611faebe..8f3696598 100644 --- a/packages/errors/src/errors/VatNotFoundError.ts +++ b/packages/errors/src/errors/VatNotFoundError.ts @@ -45,7 +45,6 @@ export class VatNotFoundError extends BaseError { if (!is(marshaledError, this.struct)) { throw new Error('Invalid VatNotFoundError structure'); } - const data = JSON.parse(marshaledError.data); - return new VatNotFoundError(data.vatId); + return new VatNotFoundError(marshaledError.data.vatId); } } diff --git a/packages/errors/src/errors/errors.test.ts b/packages/errors/src/errors/errors.test.ts deleted file mode 100644 index 783424eaa..000000000 --- a/packages/errors/src/errors/errors.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { StreamReadError } from './StreamReadError.js'; -import { VatAlreadyExistsError } from './VatAlreadyExistsError.js'; -import { VatCapTpConnectionExistsError } from './VatCapTpConnectionExistsError.js'; -import { VatCapTpConnectionNotFoundError } from './VatCapTpConnectionNotFoundError.js'; -import { VatDeletedError } from './VatDeletedError.js'; -import { VatNotFoundError } from './VatNotFoundError.js'; -import { ErrorCode } from '../types.js'; - -describe('Errors classes', () => { - const mockVatId = 'mockVatId'; - const mockSupervisorId = 'mockSupervisorId'; - const mockOriginalError = new Error('Original error'); - - describe('VatAlreadyExistsError', () => { - it('should create a VatAlreadyExistsError with the correct properties', () => { - const error = new VatAlreadyExistsError(mockVatId); - expect(error).toBeInstanceOf(VatAlreadyExistsError); - expect(error.code).toBe(ErrorCode.VatAlreadyExists); - expect(error.message).toBe('Vat already exists.'); - expect(error.data).toStrictEqual({ vatId: mockVatId }); - expect(error.cause).toBeUndefined(); - }); - }); - - describe('VatNotFoundError', () => { - it('should create a VatNotFoundError with the correct properties', () => { - const error = new VatNotFoundError(mockVatId); - expect(error).toBeInstanceOf(VatNotFoundError); - expect(error.code).toBe(ErrorCode.VatNotFound); - expect(error.message).toBe('Vat does not exist.'); - expect(error.data).toStrictEqual({ vatId: mockVatId }); - expect(error.cause).toBeUndefined(); - }); - }); - - describe('StreamReadError', () => { - it('should create a StreamReadError for Supervisor with the correct properties', () => { - const error = new StreamReadError( - { supervisorId: mockSupervisorId }, - mockOriginalError, - ); - expect(error).toBeInstanceOf(StreamReadError); - expect(error.code).toBe(ErrorCode.StreamReadError); - expect(error.message).toBe('Unexpected stream read error.'); - expect(error.data).toStrictEqual({ supervisorId: mockSupervisorId }); - expect(error.cause).toBe(mockOriginalError); - }); - - it('should create a StreamReadError for Vat with the correct properties', () => { - const error = new StreamReadError( - { vatId: mockVatId }, - mockOriginalError, - ); - expect(error).toBeInstanceOf(StreamReadError); - expect(error.code).toBe(ErrorCode.StreamReadError); - expect(error.message).toBe('Unexpected stream read error.'); - expect(error.data).toStrictEqual({ vatId: mockVatId }); - expect(error.cause).toBe(mockOriginalError); - }); - }); - - describe('VatCapTpConnectionExistsError', () => { - it('should create a VatCapTpConnectionExistsError with the correct properties', () => { - const error = new VatCapTpConnectionExistsError(mockVatId); - expect(error).toBeInstanceOf(VatCapTpConnectionExistsError); - expect(error.code).toBe(ErrorCode.VatCapTpConnectionExists); - expect(error.message).toBe('Vat already has a CapTP connection.'); - expect(error.data).toStrictEqual({ vatId: mockVatId }); - expect(error.cause).toBeUndefined(); - }); - }); - - describe('VatCapTpConnectionNotFoundError', () => { - it('should create a VatCapTpConnectionNotFoundError with the correct properties', () => { - const error = new VatCapTpConnectionNotFoundError(mockVatId); - expect(error).toBeInstanceOf(VatCapTpConnectionNotFoundError); - expect(error.code).toBe(ErrorCode.VatCapTpConnectionNotFound); - expect(error.message).toBe('Vat does not have a CapTP connection.'); - expect(error.data).toStrictEqual({ vatId: mockVatId }); - expect(error.cause).toBeUndefined(); - }); - }); - - describe('VatDeletedError', () => { - it('should create a VatDeletedError with the correct properties', () => { - const error = new VatDeletedError(mockVatId); - expect(error).toBeInstanceOf(VatDeletedError); - expect(error.code).toBe(ErrorCode.VatDeleted); - expect(error.message).toBe('Vat was deleted.'); - expect(error.data).toStrictEqual({ vatId: mockVatId }); - expect(error.cause).toBeUndefined(); - }); - }); -}); diff --git a/packages/errors/src/errors/index.ts b/packages/errors/src/errors/index.ts new file mode 100644 index 000000000..e2817b306 --- /dev/null +++ b/packages/errors/src/errors/index.ts @@ -0,0 +1,16 @@ +import { StreamReadError } from './StreamReadError.js'; +import { VatAlreadyExistsError } from './VatAlreadyExistsError.js'; +import { VatCapTpConnectionExistsError } from './VatCapTpConnectionExistsError.js'; +import { VatCapTpConnectionNotFoundError } from './VatCapTpConnectionNotFoundError.js'; +import { VatDeletedError } from './VatDeletedError.js'; +import { VatNotFoundError } from './VatNotFoundError.js'; +import { ErrorCode } from '../types.js'; + +export const errorClasses: { [K in ErrorCode]: unknown } = { + [ErrorCode.StreamReadError]: StreamReadError, + [ErrorCode.VatAlreadyExists]: VatAlreadyExistsError, + [ErrorCode.VatCapTpConnectionExists]: VatCapTpConnectionExistsError, + [ErrorCode.VatCapTpConnectionNotFound]: VatCapTpConnectionNotFoundError, + [ErrorCode.VatDeleted]: VatDeletedError, + [ErrorCode.VatNotFound]: VatNotFoundError, +} as const; diff --git a/packages/errors/src/index.test.ts b/packages/errors/src/index.test.ts index c52947f1c..e7d1e62e0 100644 --- a/packages/errors/src/index.test.ts +++ b/packages/errors/src/index.test.ts @@ -6,14 +6,13 @@ describe('index', () => { it('has the expected exports', () => { expect(Object.keys(indexModule).sort()).toStrictEqual([ 'ErrorCode', - 'StreamReadError', 'ErrorSentinel', + 'StreamReadError', 'VatAlreadyExistsError', 'VatCapTpConnectionExistsError', 'VatCapTpConnectionNotFoundError', 'VatDeletedError', 'VatNotFoundError', - 'isCodedError', 'isMarshaledError', 'isMarshaledOcapError', 'isOcapError', diff --git a/packages/errors/src/marshal/isMarshaledError.test.ts b/packages/errors/src/marshal/isMarshaledError.test.ts index 31843478b..fcd7a22a9 100644 --- a/packages/errors/src/marshal/isMarshaledError.test.ts +++ b/packages/errors/src/marshal/isMarshaledError.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { isMarshaledError } from './isMarshaledError.js'; -import { ErrorSentinel } from '../types.js'; +import { ErrorCode, ErrorSentinel } from '../types.js'; describe('isMarshaledError', () => { it.each([ @@ -18,7 +18,7 @@ describe('isMarshaledError', () => { { [ErrorSentinel]: true, message: 'An error occurred', - code: 'ERROR_CODE', + code: ErrorCode.VatAlreadyExists, data: { key: 'value' }, stack: 'Error stack trace', cause: 'Another error', diff --git a/packages/errors/src/marshal/marshalError.test.ts b/packages/errors/src/marshal/marshalError.test.ts index bd0a98e62..8d0d328fe 100644 --- a/packages/errors/src/marshal/marshalError.test.ts +++ b/packages/errors/src/marshal/marshalError.test.ts @@ -58,7 +58,7 @@ describe('marshalError', () => { message: 'Vat does not exist.', stack: expect.any(String), code: ErrorCode.VatNotFound, - data: JSON.stringify({ vatId: 'v1' }), + data: { vatId: 'v1' }, }), ); }); diff --git a/packages/errors/src/marshal/marshalError.ts b/packages/errors/src/marshal/marshalError.ts index 7f9fddbb1..c9b9b3ef6 100644 --- a/packages/errors/src/marshal/marshalError.ts +++ b/packages/errors/src/marshal/marshalError.ts @@ -1,3 +1,5 @@ +import { getSafeJson } from '@metamask/utils'; + import type { MarshaledError } from '../types.js'; import { ErrorSentinel } from '../types.js'; import { isOcapError } from '../utils/isOcapError.js'; @@ -28,7 +30,7 @@ export function marshalError(error: Error): MarshaledError { if (isOcapError(error)) { output.code = error.code; if (error.data) { - output.data = JSON.stringify(error.data); + output.data = getSafeJson(error.data); } } diff --git a/packages/errors/src/marshal/unmarshalError.test.ts b/packages/errors/src/marshal/unmarshalError.test.ts index 4a94f8be8..42914c9df 100644 --- a/packages/errors/src/marshal/unmarshalError.test.ts +++ b/packages/errors/src/marshal/unmarshalError.test.ts @@ -2,7 +2,11 @@ import { makeErrorMatcherFactory } from '@ocap/test-utils'; import { describe, it, expect } from 'vitest'; import { unmarshalError } from './unmarshalError.js'; -import { ErrorSentinel } from '../types.js'; +import { StreamReadError } from '../errors/StreamReadError.js'; +import { VatAlreadyExistsError } from '../errors/VatAlreadyExistsError.js'; +import type { OcapError } from '../types.js'; +import { ErrorCode, ErrorSentinel } from '../types.js'; +import { isOcapError } from '../utils/isOcapError.js'; const makeErrorMatcher = makeErrorMatcherFactory(expect); @@ -45,4 +49,68 @@ describe('unmarshalError', () => { makeErrorMatcher(new Error('foo', { cause: 'baz' })), ); }); + + it('should unmarshal a custom error class', () => { + const data = { vatId: 'v123' }; + const marshaledError = { + [ErrorSentinel]: true, + message: 'Vat already exists.', + stack: 'customStack', + code: ErrorCode.VatAlreadyExists, + data, + } as const; + + const expectedError = new VatAlreadyExistsError(data.vatId); + expectedError.stack = 'customStack'; + + const unmarshaledError = unmarshalError(marshaledError) as OcapError; + + expect(unmarshaledError).toStrictEqual(makeErrorMatcher(expectedError)); + expect(isOcapError(unmarshaledError)).toBe(true); + expect(unmarshaledError.code).toBe(ErrorCode.VatAlreadyExists); + expect(unmarshaledError.data).toStrictEqual(data); + }); + + it('should unmarshal a custom error class with a cause', () => { + const data = { vatId: 'v123' }; + const marshaledError = { + [ErrorSentinel]: true, + message: 'Unexpected stream read error.', + stack: 'customStack', + code: ErrorCode.StreamReadError, + data, + cause: { + [ErrorSentinel]: true, + message: 'foo', + stack: 'bar', + }, + } as const; + + const expectedCauseError = new Error('foo'); + expectedCauseError.stack = 'bar'; + + const expectedError = new StreamReadError(data, expectedCauseError); + expectedError.stack = 'customStack'; + + const unmarshaledError = unmarshalError(marshaledError) as OcapError; + + expect(unmarshaledError).toStrictEqual(makeErrorMatcher(expectedError)); + expect(isOcapError(unmarshaledError)).toBe(true); + expect(unmarshaledError.code).toBe(ErrorCode.StreamReadError); + expect(unmarshaledError.data).toStrictEqual(data); + }); + + it('should throw if the custom error class is malformed', () => { + const invalidMarshaledError = { + [ErrorSentinel]: true, + message: 'Vat already exists.', + stack: 'customStack', + code: ErrorCode.VatAlreadyExists, + data: 'invalid data', + } as const; + + expect(() => unmarshalError(invalidMarshaledError)).toThrow( + 'Invalid VatAlreadyExistsError structure', + ); + }); }); diff --git a/packages/errors/src/marshal/unmarshalError.ts b/packages/errors/src/marshal/unmarshalError.ts index 76aa2d0ff..3df426856 100644 --- a/packages/errors/src/marshal/unmarshalError.ts +++ b/packages/errors/src/marshal/unmarshalError.ts @@ -1,3 +1,6 @@ +import { isMarshaledOcapError } from './isMarshaledOcapError.js'; +import type { BaseError } from '../BaseError.js'; +import { errorClasses } from '../errors/index.js'; import type { MarshaledError, OcapError } from '../types.js'; /** @@ -9,18 +12,26 @@ import type { MarshaledError, OcapError } from '../types.js'; export function unmarshalError( marshaledError: MarshaledError, ): Error | OcapError { - const output = new Error(marshaledError.message); + let error: Error | OcapError; + + if (isMarshaledOcapError(marshaledError)) { + error = (errorClasses[marshaledError.code] as typeof BaseError).unmarshal( + marshaledError, + ); + } else { + error = new Error(marshaledError.message); + } if (marshaledError.cause) { - output.cause = + error.cause = typeof marshaledError.cause === 'string' ? marshaledError.cause : unmarshalError(marshaledError.cause); } if (marshaledError.stack) { - output.stack = marshaledError.stack; + error.stack = marshaledError.stack; } - return output; + return error; } diff --git a/packages/errors/src/types.ts b/packages/errors/src/types.ts index abe9729e6..3de1a5956 100644 --- a/packages/errors/src/types.ts +++ b/packages/errors/src/types.ts @@ -26,14 +26,14 @@ export type MarshaledError = { [ErrorSentinel]: true; message: string; code?: ErrorCode; - data?: string; + data?: Json; stack?: string; cause?: MarshaledError | string; }; export type MarshaledOcapError = Omit & { code: ErrorCode; - data: string; + data: Json; }; const ErrorCodeStruct = union( diff --git a/packages/kernel/src/Supervisor.test.ts b/packages/kernel/src/Supervisor.test.ts index 4c5a43833..dbc885100 100644 --- a/packages/kernel/src/Supervisor.test.ts +++ b/packages/kernel/src/Supervisor.test.ts @@ -86,7 +86,7 @@ describe('Supervisor', () => { expect(replySpy).toHaveBeenCalledWith('v0:0', { method: VatCommandMethod.CapTpInit, - params: '~~~ CapTp Initialized ~~~', + params: '~~~ CapTP Initialized ~~~', }); }); diff --git a/packages/kernel/src/Supervisor.ts b/packages/kernel/src/Supervisor.ts index cea4d620e..f50039a88 100644 --- a/packages/kernel/src/Supervisor.ts +++ b/packages/kernel/src/Supervisor.ts @@ -101,7 +101,7 @@ export class Supervisor { ); await this.replyToMessage(id, { method: VatCommandMethod.CapTpInit, - params: '~~~ CapTp Initialized ~~~', + params: '~~~ CapTP Initialized ~~~', }); break; } diff --git a/packages/kernel/src/Vat.test.ts b/packages/kernel/src/Vat.test.ts index d6c94329f..c559afad8 100644 --- a/packages/kernel/src/Vat.test.ts +++ b/packages/kernel/src/Vat.test.ts @@ -203,7 +203,7 @@ describe('Vat', () => { ); expect(consoleLogSpy).toHaveBeenCalledWith( - 'CapTp from vat', + 'CapTP from vat', stringify(capTpQuestion), ); diff --git a/packages/kernel/src/Vat.ts b/packages/kernel/src/Vat.ts index 4f0a34b06..2514036e3 100644 --- a/packages/kernel/src/Vat.ts +++ b/packages/kernel/src/Vat.ts @@ -110,9 +110,9 @@ export class Vat { } /** - * Make a CapTp connection. + * Make a CapTP connection. * - * @returns A promise that resolves when the CapTp connection is made. + * @returns A promise that resolves when the CapTP connection is made. */ async makeCapTp(): Promise { if (this.capTp !== undefined) { @@ -129,7 +129,7 @@ export class Vat { this.streamEnvelopeReplyHandler.contentHandlers.capTp = async ( content: CapTpMessage, ) => { - this.logger.log('CapTp from vat', stringify(content)); + this.logger.log('CapTP from vat', stringify(content)); ctp.dispatch(content); }; @@ -140,10 +140,10 @@ export class Vat { } /** - * Call a CapTp method. + * Call a CapTP method. * - * @param payload - The CapTp payload. - * @returns A promise that resolves the result of the CapTp call. + * @param payload - The CapTP payload. + * @returns A promise that resolves the result of the CapTP call. */ async callCapTp(payload: CapTpPayload): Promise { if (!this.capTp) { diff --git a/packages/kernel/src/stream-envelope.ts b/packages/kernel/src/stream-envelope.ts index ec1bc34fc..2612ccbb9 100644 --- a/packages/kernel/src/stream-envelope.ts +++ b/packages/kernel/src/stream-envelope.ts @@ -8,7 +8,7 @@ import type { CapTpMessage, VatCommand, VatCommandReply } from './messages.js'; enum EnvelopeLabel { Command = 'command', - CapTp = 'capTp', + CapTP = 'capTp', } // makeStreamEnvelopeKit requires an enum of labels but typescript From ec0efd8c46875e7a52204f94e90f28374986c281 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Wed, 16 Oct 2024 16:44:13 +0200 Subject: [PATCH 15/24] Stricter rules for StreamReadError data structure --- .../errors/src/errors/StreamReadError.test.ts | 19 +++++++++++++++++++ packages/errors/src/errors/StreamReadError.ts | 18 +++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/errors/src/errors/StreamReadError.test.ts b/packages/errors/src/errors/StreamReadError.test.ts index 420711542..bf8623140 100644 --- a/packages/errors/src/errors/StreamReadError.test.ts +++ b/packages/errors/src/errors/StreamReadError.test.ts @@ -95,4 +95,23 @@ describe('StreamReadError', () => { 'Invalid StreamReadError structure', ); }); + + it('throws when both vatId and supervisorId are present in data', () => { + const marshaledError: MarshaledOcapError = { + [ErrorSentinel]: true, + message: 'Unexpected stream read error.', + stack: 'customStack', + code: ErrorCode.StreamReadError, + data: { supervisorId: mockSupervisorId, vatId: mockVatId }, + cause: { + [ErrorSentinel]: true, + message: 'Original error', + stack: 'bar', + }, + }; + + expect(() => StreamReadError.unmarshal(marshaledError)).toThrow( + 'Invalid StreamReadError structure', + ); + }); }); diff --git a/packages/errors/src/errors/StreamReadError.ts b/packages/errors/src/errors/StreamReadError.ts index 0f5f8ae78..b4b95b3a2 100644 --- a/packages/errors/src/errors/StreamReadError.ts +++ b/packages/errors/src/errors/StreamReadError.ts @@ -1,4 +1,12 @@ -import { is, literal, object, optional, string } from '@metamask/superstruct'; +import { + is, + literal, + never, + object, + optional, + string, + union, +} from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; import type { MarshaledOcapError } from '../types.js'; @@ -23,10 +31,10 @@ export class StreamReadError extends BaseError { [ErrorSentinel]: literal(true), message: string(), code: literal(ErrorCode.StreamReadError), - data: object({ - vatId: optional(string()), - supervisorId: optional(string()), - }), + data: union([ + object({ vatId: string(), supervisorId: optional(never()) }), + object({ supervisorId: string(), vatId: optional(never()) }), + ]), stack: optional(string()), cause: MarshaledErrorStruct, }); From defc1e07f017cd3629838459647167d6326e0301 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Wed, 16 Oct 2024 22:05:01 +0200 Subject: [PATCH 16/24] apply comments --- packages/errors/src/BaseError.test.ts | 2 +- packages/errors/src/BaseError.ts | 3 +- packages/errors/src/constants.ts | 34 +++++++++++++++++++ .../errors/src/errors/StreamReadError.test.ts | 2 +- packages/errors/src/errors/StreamReadError.ts | 6 +++- .../src/errors/VatAlreadyExistsError.test.ts | 2 +- .../src/errors/VatAlreadyExistsError.ts | 6 +++- .../VatCapTpConnectionExistsError.test.ts | 2 +- .../errors/VatCapTpConnectionExistsError.ts | 6 +++- .../VatCapTpConnectionNotFoundError.test.ts | 2 +- .../errors/VatCapTpConnectionNotFoundError.ts | 6 +++- .../errors/src/errors/VatDeletedError.test.ts | 2 +- packages/errors/src/errors/VatDeletedError.ts | 6 +++- .../src/errors/VatNotFoundError.test.ts | 2 +- .../errors/src/errors/VatNotFoundError.ts | 6 +++- packages/errors/src/errors/index.ts | 2 +- packages/errors/src/index.ts | 2 +- .../src/marshal/isMarshaledError.test.ts | 2 +- .../errors/src/marshal/isMarshaledError.ts | 2 +- .../src/marshal/isMarshaledOcapError.test.ts | 2 +- .../src/marshal/isMarshaledOcapError.ts | 2 +- .../errors/src/marshal/marshalError.test.ts | 2 +- packages/errors/src/marshal/marshalError.ts | 2 +- .../errors/src/marshal/unmarshalError.test.ts | 6 ++-- packages/errors/src/types.ts | 34 ++----------------- packages/errors/src/utils/isOcapError.test.ts | 2 +- packages/kernel/src/stream-envelope.ts | 2 +- 27 files changed, 88 insertions(+), 59 deletions(-) create mode 100644 packages/errors/src/constants.ts diff --git a/packages/errors/src/BaseError.test.ts b/packages/errors/src/BaseError.test.ts index 33a99d5db..d4a8719f3 100644 --- a/packages/errors/src/BaseError.test.ts +++ b/packages/errors/src/BaseError.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from 'vitest'; import { BaseError } from './BaseError.js'; +import { ErrorCode } from './constants.js'; import type { MarshaledOcapError } from './types.js'; -import { ErrorCode } from './types.js'; describe('BaseError', () => { const mockCode = ErrorCode.VatNotFound; diff --git a/packages/errors/src/BaseError.ts b/packages/errors/src/BaseError.ts index b05af1b07..5063fe4fd 100644 --- a/packages/errors/src/BaseError.ts +++ b/packages/errors/src/BaseError.ts @@ -1,6 +1,7 @@ import type { Json } from '@metamask/utils'; -import type { ErrorCode, MarshaledOcapError, OcapError } from './types.js'; +import type { ErrorCode } from './constants.js'; +import type { MarshaledOcapError, OcapError } from './types.js'; export class BaseError extends Error implements OcapError { public readonly code: ErrorCode; diff --git a/packages/errors/src/constants.ts b/packages/errors/src/constants.ts new file mode 100644 index 000000000..8b4e25bc5 --- /dev/null +++ b/packages/errors/src/constants.ts @@ -0,0 +1,34 @@ +import type { Struct } from '@metamask/superstruct'; +import { lazy, literal, optional, string, union } from '@metamask/superstruct'; +import { JsonStruct, object } from '@metamask/utils'; +import type { NonEmptyArray } from '@metamask/utils'; + +import type { MarshaledError } from './types.js'; + +export enum ErrorCode { + StreamReadError = 'STREAM_READ_ERROR', + VatAlreadyExists = 'VAT_ALREADY_EXISTS', + VatCapTpConnectionExists = 'VAT_CAPTP_CONNECTION_EXISTS', + VatCapTpConnectionNotFound = 'VAT_CAPTP_CONNECTION_NOT_FOUND', + VatDeleted = 'VAT_DELETED', + VatNotFound = 'VAT_NOT_FOUND', +} +/** + * A sentinel value to detect marshaled errors. + */ +export const ErrorSentinel = '@@MARSHALED_ERROR'; + +const ErrorCodeStruct = union( + Object.values(ErrorCode).map((code) => literal(code)) as NonEmptyArray< + Struct + >, +); + +export const MarshaledErrorStruct = object({ + [ErrorSentinel]: literal(true), + message: string(), + code: optional(ErrorCodeStruct), + data: optional(JsonStruct), + stack: optional(string()), + cause: optional(union([string(), lazy(() => MarshaledErrorStruct)])), +}) as Struct; diff --git a/packages/errors/src/errors/StreamReadError.test.ts b/packages/errors/src/errors/StreamReadError.test.ts index bf8623140..1ddd22ccc 100644 --- a/packages/errors/src/errors/StreamReadError.test.ts +++ b/packages/errors/src/errors/StreamReadError.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from 'vitest'; import { StreamReadError } from './StreamReadError.js'; +import { ErrorCode, ErrorSentinel } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; -import { ErrorCode, ErrorSentinel } from '../types.js'; describe('StreamReadError', () => { const mockVatId = 'mockVatId'; diff --git a/packages/errors/src/errors/StreamReadError.ts b/packages/errors/src/errors/StreamReadError.ts index b4b95b3a2..de3542f43 100644 --- a/packages/errors/src/errors/StreamReadError.ts +++ b/packages/errors/src/errors/StreamReadError.ts @@ -9,8 +9,12 @@ import { } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; +import { + ErrorCode, + ErrorSentinel, + MarshaledErrorStruct, +} from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; -import { ErrorCode, ErrorSentinel, MarshaledErrorStruct } from '../types.js'; type StreamReadErrorData = { vatId: string } | { supervisorId: string }; diff --git a/packages/errors/src/errors/VatAlreadyExistsError.test.ts b/packages/errors/src/errors/VatAlreadyExistsError.test.ts index 8baeceeb4..c86b4cf9d 100644 --- a/packages/errors/src/errors/VatAlreadyExistsError.test.ts +++ b/packages/errors/src/errors/VatAlreadyExistsError.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from 'vitest'; import { VatAlreadyExistsError } from './VatAlreadyExistsError.js'; +import { ErrorCode, ErrorSentinel } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; -import { ErrorCode, ErrorSentinel } from '../types.js'; describe('VatAlreadyExistsError', () => { const mockVatId = 'mockVatId'; diff --git a/packages/errors/src/errors/VatAlreadyExistsError.ts b/packages/errors/src/errors/VatAlreadyExistsError.ts index 10b7f6884..32a33b220 100644 --- a/packages/errors/src/errors/VatAlreadyExistsError.ts +++ b/packages/errors/src/errors/VatAlreadyExistsError.ts @@ -9,8 +9,12 @@ import { } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; +import { + ErrorCode, + ErrorSentinel, + MarshaledErrorStruct, +} from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; -import { ErrorCode, ErrorSentinel, MarshaledErrorStruct } from '../types.js'; export class VatAlreadyExistsError extends BaseError { constructor(vatId: string) { diff --git a/packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts b/packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts index 1355a24ae..c318db246 100644 --- a/packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts +++ b/packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from 'vitest'; import { VatCapTpConnectionExistsError } from './VatCapTpConnectionExistsError.js'; +import { ErrorCode, ErrorSentinel } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; -import { ErrorCode, ErrorSentinel } from '../types.js'; describe('VatCapTpConnectionExistsError', () => { const mockVatId = 'mockVatId'; diff --git a/packages/errors/src/errors/VatCapTpConnectionExistsError.ts b/packages/errors/src/errors/VatCapTpConnectionExistsError.ts index 75aee6d10..9a64d523a 100644 --- a/packages/errors/src/errors/VatCapTpConnectionExistsError.ts +++ b/packages/errors/src/errors/VatCapTpConnectionExistsError.ts @@ -9,8 +9,12 @@ import { } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; +import { + ErrorCode, + ErrorSentinel, + MarshaledErrorStruct, +} from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; -import { ErrorCode, ErrorSentinel, MarshaledErrorStruct } from '../types.js'; export class VatCapTpConnectionExistsError extends BaseError { constructor(vatId: string) { diff --git a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts index 220e8cd5b..75c5c0716 100644 --- a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts +++ b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from 'vitest'; import { VatCapTpConnectionNotFoundError } from './VatCapTpConnectionNotFoundError.js'; +import { ErrorCode, ErrorSentinel } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; -import { ErrorCode, ErrorSentinel } from '../types.js'; describe('VatCapTpConnectionNotFoundError', () => { const mockVatId = 'mockVatId'; diff --git a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts index 472abc8fe..b2622ed40 100644 --- a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts +++ b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts @@ -9,8 +9,12 @@ import { } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; +import { + ErrorCode, + ErrorSentinel, + MarshaledErrorStruct, +} from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; -import { ErrorCode, ErrorSentinel, MarshaledErrorStruct } from '../types.js'; export class VatCapTpConnectionNotFoundError extends BaseError { constructor(vatId: string) { diff --git a/packages/errors/src/errors/VatDeletedError.test.ts b/packages/errors/src/errors/VatDeletedError.test.ts index e77f469e2..f45d26d97 100644 --- a/packages/errors/src/errors/VatDeletedError.test.ts +++ b/packages/errors/src/errors/VatDeletedError.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from 'vitest'; import { VatDeletedError } from './VatDeletedError.js'; +import { ErrorCode, ErrorSentinel } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; -import { ErrorCode, ErrorSentinel } from '../types.js'; describe('VatDeletedError', () => { const mockVatId = 'mockVatId'; diff --git a/packages/errors/src/errors/VatDeletedError.ts b/packages/errors/src/errors/VatDeletedError.ts index 6783cd7dc..5c1619091 100644 --- a/packages/errors/src/errors/VatDeletedError.ts +++ b/packages/errors/src/errors/VatDeletedError.ts @@ -9,8 +9,12 @@ import { } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; +import { + ErrorCode, + ErrorSentinel, + MarshaledErrorStruct, +} from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; -import { ErrorCode, ErrorSentinel, MarshaledErrorStruct } from '../types.js'; export class VatDeletedError extends BaseError { constructor(vatId: string) { diff --git a/packages/errors/src/errors/VatNotFoundError.test.ts b/packages/errors/src/errors/VatNotFoundError.test.ts index ca483729e..dab8dd882 100644 --- a/packages/errors/src/errors/VatNotFoundError.test.ts +++ b/packages/errors/src/errors/VatNotFoundError.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from 'vitest'; import { VatNotFoundError } from './VatNotFoundError.js'; +import { ErrorCode, ErrorSentinel } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; -import { ErrorCode, ErrorSentinel } from '../types.js'; describe('VatNotFoundError', () => { const mockVatId = 'mockVatId'; diff --git a/packages/errors/src/errors/VatNotFoundError.ts b/packages/errors/src/errors/VatNotFoundError.ts index 8f3696598..ccf12e6bb 100644 --- a/packages/errors/src/errors/VatNotFoundError.ts +++ b/packages/errors/src/errors/VatNotFoundError.ts @@ -9,8 +9,12 @@ import { } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; +import { + ErrorCode, + ErrorSentinel, + MarshaledErrorStruct, +} from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; -import { ErrorCode, ErrorSentinel, MarshaledErrorStruct } from '../types.js'; export class VatNotFoundError extends BaseError { constructor(vatId: string) { diff --git a/packages/errors/src/errors/index.ts b/packages/errors/src/errors/index.ts index e2817b306..0c695511e 100644 --- a/packages/errors/src/errors/index.ts +++ b/packages/errors/src/errors/index.ts @@ -4,7 +4,7 @@ import { VatCapTpConnectionExistsError } from './VatCapTpConnectionExistsError.j import { VatCapTpConnectionNotFoundError } from './VatCapTpConnectionNotFoundError.js'; import { VatDeletedError } from './VatDeletedError.js'; import { VatNotFoundError } from './VatNotFoundError.js'; -import { ErrorCode } from '../types.js'; +import { ErrorCode } from '../constants.js'; export const errorClasses: { [K in ErrorCode]: unknown } = { [ErrorCode.StreamReadError]: StreamReadError, diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts index 8ca0c5775..c2aaf7186 100644 --- a/packages/errors/src/index.ts +++ b/packages/errors/src/index.ts @@ -5,7 +5,7 @@ export { VatAlreadyExistsError } from './errors/VatAlreadyExistsError.js'; export { VatDeletedError } from './errors/VatDeletedError.js'; export { VatNotFoundError } from './errors/VatNotFoundError.js'; export { StreamReadError } from './errors/StreamReadError.js'; -export { ErrorCode, ErrorSentinel } from './types.js'; +export { ErrorCode, ErrorSentinel } from './constants.js'; export { toError } from './utils/toError.js'; export { isOcapError } from './utils/isOcapError.js'; export { marshalError } from './marshal/marshalError.js'; diff --git a/packages/errors/src/marshal/isMarshaledError.test.ts b/packages/errors/src/marshal/isMarshaledError.test.ts index fcd7a22a9..2fe9c2acc 100644 --- a/packages/errors/src/marshal/isMarshaledError.test.ts +++ b/packages/errors/src/marshal/isMarshaledError.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { isMarshaledError } from './isMarshaledError.js'; -import { ErrorCode, ErrorSentinel } from '../types.js'; +import { ErrorCode, ErrorSentinel } from '../constants.js'; describe('isMarshaledError', () => { it.each([ diff --git a/packages/errors/src/marshal/isMarshaledError.ts b/packages/errors/src/marshal/isMarshaledError.ts index 9d433fe98..cd74916c2 100644 --- a/packages/errors/src/marshal/isMarshaledError.ts +++ b/packages/errors/src/marshal/isMarshaledError.ts @@ -1,7 +1,7 @@ import { is } from '@metamask/superstruct'; +import { MarshaledErrorStruct } from '../constants.js'; import type { MarshaledError } from '../types.js'; -import { MarshaledErrorStruct } from '../types.js'; /** * Checks if a value is a {@link MarshaledError}. diff --git a/packages/errors/src/marshal/isMarshaledOcapError.test.ts b/packages/errors/src/marshal/isMarshaledOcapError.test.ts index 20aceaf57..fe7c99313 100644 --- a/packages/errors/src/marshal/isMarshaledOcapError.test.ts +++ b/packages/errors/src/marshal/isMarshaledOcapError.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { isMarshaledOcapError } from './isMarshaledOcapError.js'; -import { ErrorCode, ErrorSentinel } from '../types.js'; +import { ErrorCode, ErrorSentinel } from '../constants.js'; describe('isMarshaledOcapError', () => { it.each([ diff --git a/packages/errors/src/marshal/isMarshaledOcapError.ts b/packages/errors/src/marshal/isMarshaledOcapError.ts index 432911124..845b14c6a 100644 --- a/packages/errors/src/marshal/isMarshaledOcapError.ts +++ b/packages/errors/src/marshal/isMarshaledOcapError.ts @@ -1,7 +1,7 @@ import { is } from '@metamask/superstruct'; +import { MarshaledErrorStruct } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; -import { MarshaledErrorStruct } from '../types.js'; /** * Checks if a value is a {@link MarshaledOcapError}. diff --git a/packages/errors/src/marshal/marshalError.test.ts b/packages/errors/src/marshal/marshalError.test.ts index 8d0d328fe..be7aa4765 100644 --- a/packages/errors/src/marshal/marshalError.test.ts +++ b/packages/errors/src/marshal/marshalError.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from 'vitest'; import { marshalError } from './marshalError.js'; +import { ErrorCode, ErrorSentinel } from '../constants.js'; import { VatNotFoundError } from '../errors/VatNotFoundError.js'; -import { ErrorCode, ErrorSentinel } from '../types.js'; describe('marshalError', () => { it('should marshal an error', () => { diff --git a/packages/errors/src/marshal/marshalError.ts b/packages/errors/src/marshal/marshalError.ts index c9b9b3ef6..bc91400f6 100644 --- a/packages/errors/src/marshal/marshalError.ts +++ b/packages/errors/src/marshal/marshalError.ts @@ -1,7 +1,7 @@ import { getSafeJson } from '@metamask/utils'; +import { ErrorSentinel } from '../constants.js'; import type { MarshaledError } from '../types.js'; -import { ErrorSentinel } from '../types.js'; import { isOcapError } from '../utils/isOcapError.js'; /** diff --git a/packages/errors/src/marshal/unmarshalError.test.ts b/packages/errors/src/marshal/unmarshalError.test.ts index 42914c9df..d3228517b 100644 --- a/packages/errors/src/marshal/unmarshalError.test.ts +++ b/packages/errors/src/marshal/unmarshalError.test.ts @@ -2,10 +2,10 @@ import { makeErrorMatcherFactory } from '@ocap/test-utils'; import { describe, it, expect } from 'vitest'; import { unmarshalError } from './unmarshalError.js'; +import { ErrorCode, ErrorSentinel } from '../constants.js'; import { StreamReadError } from '../errors/StreamReadError.js'; import { VatAlreadyExistsError } from '../errors/VatAlreadyExistsError.js'; import type { OcapError } from '../types.js'; -import { ErrorCode, ErrorSentinel } from '../types.js'; import { isOcapError } from '../utils/isOcapError.js'; const makeErrorMatcher = makeErrorMatcherFactory(expect); @@ -50,7 +50,7 @@ describe('unmarshalError', () => { ); }); - it('should unmarshal a custom error class', () => { + it('should unmarshal an ocap error class', () => { const data = { vatId: 'v123' }; const marshaledError = { [ErrorSentinel]: true, @@ -71,7 +71,7 @@ describe('unmarshalError', () => { expect(unmarshaledError.data).toStrictEqual(data); }); - it('should unmarshal a custom error class with a cause', () => { + it('should unmarshal an ocap error class with a cause', () => { const data = { vatId: 'v123' }; const marshaledError = { [ErrorSentinel]: true, diff --git a/packages/errors/src/types.ts b/packages/errors/src/types.ts index 3de1a5956..13c106b33 100644 --- a/packages/errors/src/types.ts +++ b/packages/errors/src/types.ts @@ -1,27 +1,12 @@ -import type { Struct } from '@metamask/superstruct'; -import { lazy, literal, optional, string, union } from '@metamask/superstruct'; -import { JsonStruct, object } from '@metamask/utils'; -import type { Json, NonEmptyArray } from '@metamask/utils'; +import type { Json } from '@metamask/utils'; -export enum ErrorCode { - StreamReadError = 'STREAM_READ_ERROR', - VatAlreadyExists = 'VAT_ALREADY_EXISTS', - VatCapTpConnectionExists = 'VAT_CAPTP_CONNECTION_EXISTS', - VatCapTpConnectionNotFound = 'VAT_CAPTP_CONNECTION_NOT_FOUND', - VatDeleted = 'VAT_DELETED', - VatNotFound = 'VAT_NOT_FOUND', -} +import type { ErrorCode, ErrorSentinel } from './constants.js'; export type OcapError = { code: ErrorCode; data: Json | undefined; } & Error; -/** - * A sentinel value to detect marshaled errors. - */ -export const ErrorSentinel = '@@MARSHALED_ERROR'; - export type MarshaledError = { [ErrorSentinel]: true; message: string; @@ -35,18 +20,3 @@ export type MarshaledOcapError = Omit & { code: ErrorCode; data: Json; }; - -const ErrorCodeStruct = union( - Object.values(ErrorCode).map((code) => literal(code)) as NonEmptyArray< - Struct - >, -); - -export const MarshaledErrorStruct = object({ - [ErrorSentinel]: literal(true), - message: string(), - code: optional(ErrorCodeStruct), - data: optional(JsonStruct), - stack: optional(string()), - cause: optional(union([string(), lazy(() => MarshaledErrorStruct)])), -}) as Struct; diff --git a/packages/errors/src/utils/isOcapError.test.ts b/packages/errors/src/utils/isOcapError.test.ts index 728211606..95f6b20b9 100644 --- a/packages/errors/src/utils/isOcapError.test.ts +++ b/packages/errors/src/utils/isOcapError.test.ts @@ -3,8 +3,8 @@ import { describe, it, expect } from 'vitest'; import { isOcapError } from './isOcapError.js'; import { BaseError } from '../BaseError.js'; +import { ErrorCode } from '../constants.js'; import { VatAlreadyExistsError } from '../errors/VatAlreadyExistsError.js'; -import { ErrorCode } from '../types.js'; class MockCodedError extends Error { code: string; diff --git a/packages/kernel/src/stream-envelope.ts b/packages/kernel/src/stream-envelope.ts index 2612ccbb9..ec1bc34fc 100644 --- a/packages/kernel/src/stream-envelope.ts +++ b/packages/kernel/src/stream-envelope.ts @@ -8,7 +8,7 @@ import type { CapTpMessage, VatCommand, VatCommandReply } from './messages.js'; enum EnvelopeLabel { Command = 'command', - CapTP = 'capTp', + CapTp = 'capTp', } // makeStreamEnvelopeKit requires an enum of labels but typescript From 3e14e995396c81d2eae79f5efd8c43ae1877f3b0 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 17 Oct 2024 12:36:03 +0200 Subject: [PATCH 17/24] use assert from superstruct --- packages/errors/src/errors/StreamReadError.test.ts | 4 ++-- packages/errors/src/errors/StreamReadError.ts | 6 ++---- packages/errors/src/errors/VatAlreadyExistsError.test.ts | 2 +- packages/errors/src/errors/VatAlreadyExistsError.ts | 6 ++---- .../errors/src/errors/VatCapTpConnectionExistsError.test.ts | 4 +++- packages/errors/src/errors/VatCapTpConnectionExistsError.ts | 6 ++---- .../src/errors/VatCapTpConnectionNotFoundError.test.ts | 4 +++- .../errors/src/errors/VatCapTpConnectionNotFoundError.ts | 6 ++---- packages/errors/src/errors/VatDeletedError.test.ts | 2 +- packages/errors/src/errors/VatDeletedError.ts | 6 ++---- packages/errors/src/errors/VatNotFoundError.test.ts | 2 +- packages/errors/src/errors/VatNotFoundError.ts | 6 ++---- packages/errors/src/marshal/unmarshalError.test.ts | 2 +- 13 files changed, 24 insertions(+), 32 deletions(-) diff --git a/packages/errors/src/errors/StreamReadError.test.ts b/packages/errors/src/errors/StreamReadError.test.ts index 1ddd22ccc..6069116e0 100644 --- a/packages/errors/src/errors/StreamReadError.test.ts +++ b/packages/errors/src/errors/StreamReadError.test.ts @@ -92,7 +92,7 @@ describe('StreamReadError', () => { }; expect(() => StreamReadError.unmarshal(marshaledError)).toThrow( - 'Invalid StreamReadError structure', + 'At path: data -- Expected the value to satisfy a union of `object | object`, but received: "invalid data"', ); }); @@ -111,7 +111,7 @@ describe('StreamReadError', () => { }; expect(() => StreamReadError.unmarshal(marshaledError)).toThrow( - 'Invalid StreamReadError structure', + 'At path: data -- Expected the value to satisfy a union of `object | object`, but received: [object Object]', ); }); }); diff --git a/packages/errors/src/errors/StreamReadError.ts b/packages/errors/src/errors/StreamReadError.ts index de3542f43..56ec45a2f 100644 --- a/packages/errors/src/errors/StreamReadError.ts +++ b/packages/errors/src/errors/StreamReadError.ts @@ -1,5 +1,5 @@ import { - is, + assert, literal, never, object, @@ -50,9 +50,7 @@ export class StreamReadError extends BaseError { * @returns The unmarshaled error. */ public static unmarshal(marshaledError: MarshaledOcapError): StreamReadError { - if (!is(marshaledError, this.struct)) { - throw new Error('Invalid StreamReadError structure'); - } + assert(marshaledError, this.struct); return new StreamReadError( marshaledError.data as StreamReadErrorData, // The cause will be properly unmarshaled during the parent call. diff --git a/packages/errors/src/errors/VatAlreadyExistsError.test.ts b/packages/errors/src/errors/VatAlreadyExistsError.test.ts index c86b4cf9d..3a7ec16aa 100644 --- a/packages/errors/src/errors/VatAlreadyExistsError.test.ts +++ b/packages/errors/src/errors/VatAlreadyExistsError.test.ts @@ -44,7 +44,7 @@ describe('VatAlreadyExistsError', () => { }; expect(() => VatAlreadyExistsError.unmarshal(marshaledError)).toThrow( - 'Invalid VatAlreadyExistsError structure', + 'At path: data -- Expected an object, but received: "{ vatId: mockVatId }"', ); }); }); diff --git a/packages/errors/src/errors/VatAlreadyExistsError.ts b/packages/errors/src/errors/VatAlreadyExistsError.ts index 32a33b220..aab281658 100644 --- a/packages/errors/src/errors/VatAlreadyExistsError.ts +++ b/packages/errors/src/errors/VatAlreadyExistsError.ts @@ -1,5 +1,5 @@ import { - is, + assert, lazy, literal, object, @@ -48,9 +48,7 @@ export class VatAlreadyExistsError extends BaseError { public static unmarshal( marshaledError: MarshaledOcapError, ): VatAlreadyExistsError { - if (!is(marshaledError, this.struct)) { - throw new Error('Invalid VatAlreadyExistsError structure'); - } + assert(marshaledError, this.struct); return new VatAlreadyExistsError(marshaledError.data.vatId); } } diff --git a/packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts b/packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts index c318db246..042eca480 100644 --- a/packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts +++ b/packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts @@ -48,6 +48,8 @@ describe('VatCapTpConnectionExistsError', () => { expect(() => VatCapTpConnectionExistsError.unmarshal(marshaledError), - ).toThrow('Invalid VatCapTpConnectionExistsError structure'); + ).toThrow( + 'At path: data -- Expected an object, but received: "{ vatId: mockVatId }"', + ); }); }); diff --git a/packages/errors/src/errors/VatCapTpConnectionExistsError.ts b/packages/errors/src/errors/VatCapTpConnectionExistsError.ts index 9a64d523a..bfd7acd15 100644 --- a/packages/errors/src/errors/VatCapTpConnectionExistsError.ts +++ b/packages/errors/src/errors/VatCapTpConnectionExistsError.ts @@ -1,5 +1,5 @@ import { - is, + assert, lazy, literal, object, @@ -52,9 +52,7 @@ export class VatCapTpConnectionExistsError extends BaseError { public static unmarshal( marshaledError: MarshaledOcapError, ): VatCapTpConnectionExistsError { - if (!is(marshaledError, this.struct)) { - throw new Error('Invalid VatCapTpConnectionExistsError structure'); - } + assert(marshaledError, this.struct); return new VatCapTpConnectionExistsError(marshaledError.data.vatId); } } diff --git a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts index 75c5c0716..309b62568 100644 --- a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts +++ b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts @@ -48,6 +48,8 @@ describe('VatCapTpConnectionNotFoundError', () => { expect(() => VatCapTpConnectionNotFoundError.unmarshal(marshaledError), - ).toThrow('Invalid VatCapTpConnectionNotFoundError structure'); + ).toThrow( + 'At path: data -- Expected an object, but received: "{ vatId: mockVatId }"', + ); }); }); diff --git a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts index b2622ed40..12e75cd06 100644 --- a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts +++ b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts @@ -1,5 +1,5 @@ import { - is, + assert, lazy, literal, object, @@ -50,9 +50,7 @@ export class VatCapTpConnectionNotFoundError extends BaseError { public static unmarshal( marshaledError: MarshaledOcapError, ): VatCapTpConnectionNotFoundError { - if (!is(marshaledError, this.struct)) { - throw new Error('Invalid VatCapTpConnectionNotFoundError structure'); - } + assert(marshaledError, this.struct); return new VatCapTpConnectionNotFoundError(marshaledError.data.vatId); } } diff --git a/packages/errors/src/errors/VatDeletedError.test.ts b/packages/errors/src/errors/VatDeletedError.test.ts index f45d26d97..301a0655d 100644 --- a/packages/errors/src/errors/VatDeletedError.test.ts +++ b/packages/errors/src/errors/VatDeletedError.test.ts @@ -44,7 +44,7 @@ describe('VatDeletedError', () => { }; expect(() => VatDeletedError.unmarshal(marshaledError)).toThrow( - 'Invalid VatDeletedError structure', + 'At path: data -- Expected an object, but received: "{ vatId: mockVatId }"', ); }); }); diff --git a/packages/errors/src/errors/VatDeletedError.ts b/packages/errors/src/errors/VatDeletedError.ts index 5c1619091..3e4dd5fc1 100644 --- a/packages/errors/src/errors/VatDeletedError.ts +++ b/packages/errors/src/errors/VatDeletedError.ts @@ -1,5 +1,5 @@ import { - is, + assert, lazy, literal, object, @@ -44,9 +44,7 @@ export class VatDeletedError extends BaseError { * @returns The unmarshaled error. */ public static unmarshal(marshaledError: MarshaledOcapError): VatDeletedError { - if (!is(marshaledError, this.struct)) { - throw new Error('Invalid VatDeletedError structure'); - } + assert(marshaledError, this.struct); return new VatDeletedError(marshaledError.data.vatId); } } diff --git a/packages/errors/src/errors/VatNotFoundError.test.ts b/packages/errors/src/errors/VatNotFoundError.test.ts index dab8dd882..ddcf0a4e4 100644 --- a/packages/errors/src/errors/VatNotFoundError.test.ts +++ b/packages/errors/src/errors/VatNotFoundError.test.ts @@ -44,7 +44,7 @@ describe('VatNotFoundError', () => { }; expect(() => VatNotFoundError.unmarshal(marshaledError)).toThrow( - 'Invalid VatNotFoundError structure', + 'At path: data -- Expected an object, but received: "{ vatId: mockVatId }"', ); }); }); diff --git a/packages/errors/src/errors/VatNotFoundError.ts b/packages/errors/src/errors/VatNotFoundError.ts index ccf12e6bb..818997d9c 100644 --- a/packages/errors/src/errors/VatNotFoundError.ts +++ b/packages/errors/src/errors/VatNotFoundError.ts @@ -1,5 +1,5 @@ import { - is, + assert, lazy, literal, object, @@ -46,9 +46,7 @@ export class VatNotFoundError extends BaseError { public static unmarshal( marshaledError: MarshaledOcapError, ): VatNotFoundError { - if (!is(marshaledError, this.struct)) { - throw new Error('Invalid VatNotFoundError structure'); - } + assert(marshaledError, this.struct); return new VatNotFoundError(marshaledError.data.vatId); } } diff --git a/packages/errors/src/marshal/unmarshalError.test.ts b/packages/errors/src/marshal/unmarshalError.test.ts index d3228517b..cecfa096c 100644 --- a/packages/errors/src/marshal/unmarshalError.test.ts +++ b/packages/errors/src/marshal/unmarshalError.test.ts @@ -110,7 +110,7 @@ describe('unmarshalError', () => { } as const; expect(() => unmarshalError(invalidMarshaledError)).toThrow( - 'Invalid VatAlreadyExistsError structure', + 'At path: data -- Expected an object, but received: "invalid data"', ); }); }); From 0e0a79170a10e2bd42e0c2bffdbb8f2002fba331 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 17 Oct 2024 12:40:07 +0200 Subject: [PATCH 18/24] harden classes --- packages/errors/src/errors/StreamReadError.ts | 2 ++ packages/errors/src/errors/VatAlreadyExistsError.ts | 2 ++ packages/errors/src/errors/VatCapTpConnectionExistsError.ts | 2 ++ packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts | 2 ++ packages/errors/src/errors/VatDeletedError.ts | 2 ++ packages/errors/src/errors/VatNotFoundError.ts | 2 ++ packages/errors/src/marshal/marshalError.ts | 2 +- packages/errors/vitest.config.ts | 1 + 8 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/errors/src/errors/StreamReadError.ts b/packages/errors/src/errors/StreamReadError.ts index 56ec45a2f..2aa030e95 100644 --- a/packages/errors/src/errors/StreamReadError.ts +++ b/packages/errors/src/errors/StreamReadError.ts @@ -26,6 +26,7 @@ export class StreamReadError extends BaseError { data, originalError, ); + harden(this); } /** @@ -58,3 +59,4 @@ export class StreamReadError extends BaseError { ); } } +harden(StreamReadError); diff --git a/packages/errors/src/errors/VatAlreadyExistsError.ts b/packages/errors/src/errors/VatAlreadyExistsError.ts index aab281658..e014ccb4e 100644 --- a/packages/errors/src/errors/VatAlreadyExistsError.ts +++ b/packages/errors/src/errors/VatAlreadyExistsError.ts @@ -21,6 +21,7 @@ export class VatAlreadyExistsError extends BaseError { super(ErrorCode.VatAlreadyExists, 'Vat already exists.', { vatId, }); + harden(this); } /** @@ -52,3 +53,4 @@ export class VatAlreadyExistsError extends BaseError { return new VatAlreadyExistsError(marshaledError.data.vatId); } } +harden(VatAlreadyExistsError); diff --git a/packages/errors/src/errors/VatCapTpConnectionExistsError.ts b/packages/errors/src/errors/VatCapTpConnectionExistsError.ts index bfd7acd15..915909bbc 100644 --- a/packages/errors/src/errors/VatCapTpConnectionExistsError.ts +++ b/packages/errors/src/errors/VatCapTpConnectionExistsError.ts @@ -25,6 +25,7 @@ export class VatCapTpConnectionExistsError extends BaseError { vatId, }, ); + harden(this); } /** @@ -56,3 +57,4 @@ export class VatCapTpConnectionExistsError extends BaseError { return new VatCapTpConnectionExistsError(marshaledError.data.vatId); } } +harden(VatCapTpConnectionExistsError); diff --git a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts index 12e75cd06..1c1307817 100644 --- a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts +++ b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts @@ -23,6 +23,7 @@ export class VatCapTpConnectionNotFoundError extends BaseError { 'Vat does not have a CapTP connection.', { vatId }, ); + harden(this); } /** @@ -54,3 +55,4 @@ export class VatCapTpConnectionNotFoundError extends BaseError { return new VatCapTpConnectionNotFoundError(marshaledError.data.vatId); } } +harden(VatCapTpConnectionNotFoundError); diff --git a/packages/errors/src/errors/VatDeletedError.ts b/packages/errors/src/errors/VatDeletedError.ts index 3e4dd5fc1..eefee1763 100644 --- a/packages/errors/src/errors/VatDeletedError.ts +++ b/packages/errors/src/errors/VatDeletedError.ts @@ -19,6 +19,7 @@ import type { MarshaledOcapError } from '../types.js'; export class VatDeletedError extends BaseError { constructor(vatId: string) { super(ErrorCode.VatDeleted, 'Vat was deleted.', { vatId }); + harden(this); } /** @@ -48,3 +49,4 @@ export class VatDeletedError extends BaseError { return new VatDeletedError(marshaledError.data.vatId); } } +harden(VatDeletedError); diff --git a/packages/errors/src/errors/VatNotFoundError.ts b/packages/errors/src/errors/VatNotFoundError.ts index 818997d9c..a7a175713 100644 --- a/packages/errors/src/errors/VatNotFoundError.ts +++ b/packages/errors/src/errors/VatNotFoundError.ts @@ -19,6 +19,7 @@ import type { MarshaledOcapError } from '../types.js'; export class VatNotFoundError extends BaseError { constructor(vatId: string) { super(ErrorCode.VatNotFound, 'Vat does not exist.', { vatId }); + harden(this); } /** @@ -50,3 +51,4 @@ export class VatNotFoundError extends BaseError { return new VatNotFoundError(marshaledError.data.vatId); } } +harden(VatNotFoundError); diff --git a/packages/errors/src/marshal/marshalError.ts b/packages/errors/src/marshal/marshalError.ts index bc91400f6..5b6a81fb3 100644 --- a/packages/errors/src/marshal/marshalError.ts +++ b/packages/errors/src/marshal/marshalError.ts @@ -34,5 +34,5 @@ export function marshalError(error: Error): MarshaledError { } } - return output; + return harden(output); } diff --git a/packages/errors/vitest.config.ts b/packages/errors/vitest.config.ts index d909288c9..a158b2c0e 100644 --- a/packages/errors/vitest.config.ts +++ b/packages/errors/vitest.config.ts @@ -12,6 +12,7 @@ const config = mergeConfig( defineConfig({ test: { pool: 'vmThreads', + setupFiles: '../test-utils/src/env/mock-endo.ts', }, }), ); From 5ff774d9e0067b5672b3e20ad04908bcb2811b59 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 17 Oct 2024 12:50:24 +0200 Subject: [PATCH 19/24] create a base error class struct schema --- packages/errors/src/constants.ts | 21 ++++++++++++++++- packages/errors/src/errors/StreamReadError.ts | 6 ++--- .../src/errors/VatAlreadyExistsError.ts | 23 +++---------------- .../errors/VatCapTpConnectionExistsError.ts | 23 +++---------------- .../errors/VatCapTpConnectionNotFoundError.ts | 23 +++---------------- packages/errors/src/errors/VatDeletedError.ts | 23 +++---------------- .../errors/src/errors/VatNotFoundError.ts | 23 +++---------------- 7 files changed, 37 insertions(+), 105 deletions(-) diff --git a/packages/errors/src/constants.ts b/packages/errors/src/constants.ts index 8b4e25bc5..a759bb47d 100644 --- a/packages/errors/src/constants.ts +++ b/packages/errors/src/constants.ts @@ -5,6 +5,9 @@ import type { NonEmptyArray } from '@metamask/utils'; import type { MarshaledError } from './types.js'; +/** + * Enum defining all error codes for Ocap errors. + */ export enum ErrorCode { StreamReadError = 'STREAM_READ_ERROR', VatAlreadyExists = 'VAT_ALREADY_EXISTS', @@ -13,8 +16,9 @@ export enum ErrorCode { VatDeleted = 'VAT_DELETED', VatNotFound = 'VAT_NOT_FOUND', } + /** - * A sentinel value to detect marshaled errors. + * A sentinel value used to identify marshaled errors. */ export const ErrorSentinel = '@@MARSHALED_ERROR'; @@ -24,6 +28,9 @@ const ErrorCodeStruct = union( >, ); +/** + * Struct to validate marshaled errors. + */ export const MarshaledErrorStruct = object({ [ErrorSentinel]: literal(true), message: string(), @@ -32,3 +39,15 @@ export const MarshaledErrorStruct = object({ stack: optional(string()), cause: optional(union([string(), lazy(() => MarshaledErrorStruct)])), }) as Struct; + +/** + * Base schema for validating Ocap error classes during error marshaling. + */ +export const baseErrorStructSchema = { + [ErrorSentinel]: literal(true), + message: string(), + code: ErrorCodeStruct, + data: JsonStruct, + stack: optional(string()), + cause: optional(union([string(), lazy(() => MarshaledErrorStruct)])), +}; diff --git a/packages/errors/src/errors/StreamReadError.ts b/packages/errors/src/errors/StreamReadError.ts index 2aa030e95..122dd66a7 100644 --- a/packages/errors/src/errors/StreamReadError.ts +++ b/packages/errors/src/errors/StreamReadError.ts @@ -10,8 +10,8 @@ import { import { BaseError } from '../BaseError.js'; import { + baseErrorStructSchema, ErrorCode, - ErrorSentinel, MarshaledErrorStruct, } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; @@ -33,14 +33,12 @@ export class StreamReadError extends BaseError { * A superstruct struct for validating marshaled {@link StreamReadError} instances. */ public static struct = object({ - [ErrorSentinel]: literal(true), - message: string(), + ...baseErrorStructSchema, code: literal(ErrorCode.StreamReadError), data: union([ object({ vatId: string(), supervisorId: optional(never()) }), object({ supervisorId: string(), vatId: optional(never()) }), ]), - stack: optional(string()), cause: MarshaledErrorStruct, }); diff --git a/packages/errors/src/errors/VatAlreadyExistsError.ts b/packages/errors/src/errors/VatAlreadyExistsError.ts index e014ccb4e..5ca2bd028 100644 --- a/packages/errors/src/errors/VatAlreadyExistsError.ts +++ b/packages/errors/src/errors/VatAlreadyExistsError.ts @@ -1,19 +1,7 @@ -import { - assert, - lazy, - literal, - object, - optional, - string, - union, -} from '@metamask/superstruct'; +import { assert, literal, object, string } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; -import { - ErrorCode, - ErrorSentinel, - MarshaledErrorStruct, -} from '../constants.js'; +import { baseErrorStructSchema, ErrorCode } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; export class VatAlreadyExistsError extends BaseError { @@ -28,16 +16,11 @@ export class VatAlreadyExistsError extends BaseError { * A superstruct struct for validating marshaled {@link VatAlreadyExistsError} instances. */ public static struct = object({ - [ErrorSentinel]: literal(true), - message: string(), + ...baseErrorStructSchema, code: literal(ErrorCode.VatAlreadyExists), data: object({ vatId: string(), }), - stack: optional(string()), - cause: optional( - union([string(), lazy(() => MarshaledErrorStruct), literal(undefined)]), - ), }); /** diff --git a/packages/errors/src/errors/VatCapTpConnectionExistsError.ts b/packages/errors/src/errors/VatCapTpConnectionExistsError.ts index 915909bbc..9d78c64c6 100644 --- a/packages/errors/src/errors/VatCapTpConnectionExistsError.ts +++ b/packages/errors/src/errors/VatCapTpConnectionExistsError.ts @@ -1,19 +1,7 @@ -import { - assert, - lazy, - literal, - object, - optional, - string, - union, -} from '@metamask/superstruct'; +import { assert, literal, object, string } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; -import { - ErrorCode, - ErrorSentinel, - MarshaledErrorStruct, -} from '../constants.js'; +import { baseErrorStructSchema, ErrorCode } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; export class VatCapTpConnectionExistsError extends BaseError { @@ -32,16 +20,11 @@ export class VatCapTpConnectionExistsError extends BaseError { * A superstruct struct for validating marshaled {@link VatCapTpConnectionExistsError} instances. */ public static struct = object({ - [ErrorSentinel]: literal(true), - message: string(), + ...baseErrorStructSchema, code: literal(ErrorCode.VatCapTpConnectionExists), data: object({ vatId: string(), }), - stack: optional(string()), - cause: optional( - union([string(), lazy(() => MarshaledErrorStruct), literal(undefined)]), - ), }); /** diff --git a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts index 1c1307817..2399a5c3b 100644 --- a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts +++ b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts @@ -1,19 +1,7 @@ -import { - assert, - lazy, - literal, - object, - optional, - string, - union, -} from '@metamask/superstruct'; +import { assert, literal, object, string } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; -import { - ErrorCode, - ErrorSentinel, - MarshaledErrorStruct, -} from '../constants.js'; +import { baseErrorStructSchema, ErrorCode } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; export class VatCapTpConnectionNotFoundError extends BaseError { @@ -30,16 +18,11 @@ export class VatCapTpConnectionNotFoundError extends BaseError { * A superstruct struct for validating marshaled {@link VatCapTpConnectionNotFoundError} instances. */ public static struct = object({ - [ErrorSentinel]: literal(true), - message: string(), + ...baseErrorStructSchema, code: literal(ErrorCode.VatCapTpConnectionNotFound), data: object({ vatId: string(), }), - stack: optional(string()), - cause: optional( - union([string(), lazy(() => MarshaledErrorStruct), literal(undefined)]), - ), }); /** diff --git a/packages/errors/src/errors/VatDeletedError.ts b/packages/errors/src/errors/VatDeletedError.ts index eefee1763..262d7cce2 100644 --- a/packages/errors/src/errors/VatDeletedError.ts +++ b/packages/errors/src/errors/VatDeletedError.ts @@ -1,19 +1,7 @@ -import { - assert, - lazy, - literal, - object, - optional, - string, - union, -} from '@metamask/superstruct'; +import { assert, literal, object, string } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; -import { - ErrorCode, - ErrorSentinel, - MarshaledErrorStruct, -} from '../constants.js'; +import { baseErrorStructSchema, ErrorCode } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; export class VatDeletedError extends BaseError { @@ -26,16 +14,11 @@ export class VatDeletedError extends BaseError { * A superstruct struct for validating marshaled {@link VatDeletedError} instances. */ public static struct = object({ - [ErrorSentinel]: literal(true), - message: string(), + ...baseErrorStructSchema, code: literal(ErrorCode.VatDeleted), data: object({ vatId: string(), }), - stack: optional(string()), - cause: optional( - union([string(), lazy(() => MarshaledErrorStruct), literal(undefined)]), - ), }); /** diff --git a/packages/errors/src/errors/VatNotFoundError.ts b/packages/errors/src/errors/VatNotFoundError.ts index a7a175713..458b0eeb4 100644 --- a/packages/errors/src/errors/VatNotFoundError.ts +++ b/packages/errors/src/errors/VatNotFoundError.ts @@ -1,19 +1,7 @@ -import { - assert, - lazy, - literal, - object, - optional, - string, - union, -} from '@metamask/superstruct'; +import { assert, literal, object, string } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; -import { - ErrorCode, - ErrorSentinel, - MarshaledErrorStruct, -} from '../constants.js'; +import { baseErrorStructSchema, ErrorCode } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; export class VatNotFoundError extends BaseError { @@ -26,16 +14,11 @@ export class VatNotFoundError extends BaseError { * A superstruct struct for validating marshaled {@link VatNotFoundError} instances. */ public static struct = object({ - [ErrorSentinel]: literal(true), - message: string(), + ...baseErrorStructSchema, code: literal(ErrorCode.VatNotFound), data: object({ vatId: string(), }), - stack: optional(string()), - cause: optional( - union([string(), lazy(() => MarshaledErrorStruct), literal(undefined)]), - ), }); /** From 1911b7a72f5bb0accfa37d88c3c53d1b41f1aaa6 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 17 Oct 2024 12:56:27 +0200 Subject: [PATCH 20/24] fix typo --- packages/errors/src/errors/StreamReadError.test.ts | 2 +- packages/errors/src/errors/VatAlreadyExistsError.test.ts | 2 +- .../errors/src/errors/VatCapTpConnectionExistsError.test.ts | 2 +- .../errors/src/errors/VatCapTpConnectionNotFoundError.test.ts | 2 +- packages/errors/src/errors/VatDeletedError.test.ts | 2 +- packages/errors/src/errors/VatNotFoundError.test.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/errors/src/errors/StreamReadError.test.ts b/packages/errors/src/errors/StreamReadError.test.ts index 6069116e0..e78c062d5 100644 --- a/packages/errors/src/errors/StreamReadError.test.ts +++ b/packages/errors/src/errors/StreamReadError.test.ts @@ -82,7 +82,7 @@ describe('StreamReadError', () => { expect((unmarshaledError.cause as Error).message).toBe('Original error'); }); - it('throws when an invalid messages is unmarshal marshaled', () => { + it('throws an error when an invalid message is unmarshaled', () => { const marshaledError: MarshaledOcapError = { [ErrorSentinel]: true, message: 'Unexpected stream read error.', diff --git a/packages/errors/src/errors/VatAlreadyExistsError.test.ts b/packages/errors/src/errors/VatAlreadyExistsError.test.ts index 3a7ec16aa..649c6e4a0 100644 --- a/packages/errors/src/errors/VatAlreadyExistsError.test.ts +++ b/packages/errors/src/errors/VatAlreadyExistsError.test.ts @@ -34,7 +34,7 @@ describe('VatAlreadyExistsError', () => { }); }); - it('throws when an invalid messages is unmarshal marshaled', () => { + it('throws an error when an invalid message is unmarshaled', () => { const marshaledError: MarshaledOcapError = { [ErrorSentinel]: true, message: 'Vat already exists.', diff --git a/packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts b/packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts index 042eca480..efd6d8f2b 100644 --- a/packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts +++ b/packages/errors/src/errors/VatCapTpConnectionExistsError.test.ts @@ -37,7 +37,7 @@ describe('VatCapTpConnectionExistsError', () => { }); }); - it('throws when an invalid messages is unmarshal marshaled', () => { + it('throws an error when an invalid message is unmarshaled', () => { const marshaledError: MarshaledOcapError = { [ErrorSentinel]: true, message: 'Vat already has a CapTP connection.', diff --git a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts index 309b62568..a76dcf74d 100644 --- a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts +++ b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.test.ts @@ -37,7 +37,7 @@ describe('VatCapTpConnectionNotFoundError', () => { }); }); - it('throws when an invalid messages is unmarshal marshaled', () => { + it('throws an error when an invalid message is unmarshaled', () => { const marshaledError: MarshaledOcapError = { [ErrorSentinel]: true, message: 'Vat does not have a CapTP connection.', diff --git a/packages/errors/src/errors/VatDeletedError.test.ts b/packages/errors/src/errors/VatDeletedError.test.ts index 301a0655d..28e478a57 100644 --- a/packages/errors/src/errors/VatDeletedError.test.ts +++ b/packages/errors/src/errors/VatDeletedError.test.ts @@ -34,7 +34,7 @@ describe('VatDeletedError', () => { }); }); - it('throws when an invalid messages is unmarshal marshaled', () => { + it('throws an error when an invalid message is unmarshaled', () => { const marshaledError: MarshaledOcapError = { [ErrorSentinel]: true, message: 'Vat was deleted.', diff --git a/packages/errors/src/errors/VatNotFoundError.test.ts b/packages/errors/src/errors/VatNotFoundError.test.ts index ddcf0a4e4..d61f74b89 100644 --- a/packages/errors/src/errors/VatNotFoundError.test.ts +++ b/packages/errors/src/errors/VatNotFoundError.test.ts @@ -34,7 +34,7 @@ describe('VatNotFoundError', () => { }); }); - it('throws when an invalid messages is unmarshal marshaled', () => { + it('throws an error when an invalid message is unmarshaled', () => { const marshaledError: MarshaledOcapError = { [ErrorSentinel]: true, message: 'Vat does not exist.', From 4a455f300c8cb3f71e44142f0ea7158bdac57727 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 17 Oct 2024 13:04:13 +0200 Subject: [PATCH 21/24] use a test instead of a lose type for errorClasses --- packages/errors/src/errors/index.test.ts | 12 ++++++++++++ packages/errors/src/errors/index.ts | 2 +- packages/errors/src/marshal/unmarshalError.ts | 5 +---- 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 packages/errors/src/errors/index.test.ts diff --git a/packages/errors/src/errors/index.test.ts b/packages/errors/src/errors/index.test.ts new file mode 100644 index 000000000..64e6fa444 --- /dev/null +++ b/packages/errors/src/errors/index.test.ts @@ -0,0 +1,12 @@ +import { describe, it, expect } from 'vitest'; + +import { errorClasses } from './index.js'; +import { ErrorCode } from '../constants.js'; + +describe('errorClasses', () => { + it('should contain all keys from ErrorCode', () => { + const errorCodes = Object.values(ErrorCode); + const errorClassKeys = Object.keys(errorClasses); + expect(errorClassKeys).toStrictEqual(errorCodes); + }); +}); diff --git a/packages/errors/src/errors/index.ts b/packages/errors/src/errors/index.ts index 0c695511e..c70b39a9f 100644 --- a/packages/errors/src/errors/index.ts +++ b/packages/errors/src/errors/index.ts @@ -6,7 +6,7 @@ import { VatDeletedError } from './VatDeletedError.js'; import { VatNotFoundError } from './VatNotFoundError.js'; import { ErrorCode } from '../constants.js'; -export const errorClasses: { [K in ErrorCode]: unknown } = { +export const errorClasses = { [ErrorCode.StreamReadError]: StreamReadError, [ErrorCode.VatAlreadyExists]: VatAlreadyExistsError, [ErrorCode.VatCapTpConnectionExists]: VatCapTpConnectionExistsError, diff --git a/packages/errors/src/marshal/unmarshalError.ts b/packages/errors/src/marshal/unmarshalError.ts index 3df426856..f2f6851b9 100644 --- a/packages/errors/src/marshal/unmarshalError.ts +++ b/packages/errors/src/marshal/unmarshalError.ts @@ -1,5 +1,4 @@ import { isMarshaledOcapError } from './isMarshaledOcapError.js'; -import type { BaseError } from '../BaseError.js'; import { errorClasses } from '../errors/index.js'; import type { MarshaledError, OcapError } from '../types.js'; @@ -15,9 +14,7 @@ export function unmarshalError( let error: Error | OcapError; if (isMarshaledOcapError(marshaledError)) { - error = (errorClasses[marshaledError.code] as typeof BaseError).unmarshal( - marshaledError, - ); + error = errorClasses[marshaledError.code].unmarshal(marshaledError); } else { error = new Error(marshaledError.message); } From ed6ed27938499df749931ec55c5d40b10ea55519 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 17 Oct 2024 18:20:54 +0200 Subject: [PATCH 22/24] fix test name --- packages/errors/src/marshal/unmarshalError.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/errors/src/marshal/unmarshalError.test.ts b/packages/errors/src/marshal/unmarshalError.test.ts index cecfa096c..d510b2d95 100644 --- a/packages/errors/src/marshal/unmarshalError.test.ts +++ b/packages/errors/src/marshal/unmarshalError.test.ts @@ -100,7 +100,7 @@ describe('unmarshalError', () => { expect(unmarshaledError.data).toStrictEqual(data); }); - it('should throw if the custom error class is malformed', () => { + it('should throw if the ocap error class is malformed', () => { const invalidMarshaledError = { [ErrorSentinel]: true, message: 'Vat already exists.', From 50deff46d0cd66882dfb8ec9bf3a9efe4f8babf7 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 17 Oct 2024 20:29:52 +0200 Subject: [PATCH 23/24] yarn build --- packages/errors/src/BaseError.ts | 4 ++- packages/errors/src/constants.ts | 26 ++++++++++--------- packages/errors/src/errors/StreamReadError.ts | 4 +-- .../src/errors/VatAlreadyExistsError.ts | 4 +-- .../errors/VatCapTpConnectionExistsError.ts | 4 +-- .../errors/VatCapTpConnectionNotFoundError.ts | 4 +-- packages/errors/src/errors/VatDeletedError.ts | 4 +-- .../errors/src/errors/VatNotFoundError.ts | 4 +-- .../src/marshal/isMarshaledOcapError.ts | 8 ++---- 9 files changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/errors/src/BaseError.ts b/packages/errors/src/BaseError.ts index 5063fe4fd..3817def11 100644 --- a/packages/errors/src/BaseError.ts +++ b/packages/errors/src/BaseError.ts @@ -6,7 +6,7 @@ import type { MarshaledOcapError, OcapError } from './types.js'; export class BaseError extends Error implements OcapError { public readonly code: ErrorCode; - public data: Json | undefined; + public readonly data: Json | undefined; constructor(code: ErrorCode, message: string, data?: Json, cause?: unknown) { super(message, { cause }); @@ -15,6 +15,7 @@ export class BaseError extends Error implements OcapError { this.code = code; this.data = data; this.cause = cause; + harden(this); } /** @@ -26,3 +27,4 @@ export class BaseError extends Error implements OcapError { throw new Error('Unmarshal method not implemented'); } } +harden(BaseError); diff --git a/packages/errors/src/constants.ts b/packages/errors/src/constants.ts index a759bb47d..cd5ca341a 100644 --- a/packages/errors/src/constants.ts +++ b/packages/errors/src/constants.ts @@ -3,7 +3,7 @@ import { lazy, literal, optional, string, union } from '@metamask/superstruct'; import { JsonStruct, object } from '@metamask/utils'; import type { NonEmptyArray } from '@metamask/utils'; -import type { MarshaledError } from './types.js'; +import type { MarshaledError, MarshaledOcapError } from './types.js'; /** * Enum defining all error codes for Ocap errors. @@ -28,26 +28,28 @@ const ErrorCodeStruct = union( >, ); -/** - * Struct to validate marshaled errors. - */ -export const MarshaledErrorStruct = object({ +export const marshaledErrorSchema = { [ErrorSentinel]: literal(true), message: string(), code: optional(ErrorCodeStruct), data: optional(JsonStruct), stack: optional(string()), +}; + +/** + * Struct to validate marshaled errors. + */ +export const MarshaledErrorStruct = object({ + ...marshaledErrorSchema, cause: optional(union([string(), lazy(() => MarshaledErrorStruct)])), }) as Struct; /** - * Base schema for validating Ocap error classes during error marshaling. + * Struct to validate marshaled ocap errors. */ -export const baseErrorStructSchema = { - [ErrorSentinel]: literal(true), - message: string(), +export const MarshaledOcapErrorStruct = object({ + ...marshaledErrorSchema, code: ErrorCodeStruct, data: JsonStruct, - stack: optional(string()), - cause: optional(union([string(), lazy(() => MarshaledErrorStruct)])), -}; + cause: optional(union([string(), lazy(() => MarshaledOcapErrorStruct)])), +}) as Struct; diff --git a/packages/errors/src/errors/StreamReadError.ts b/packages/errors/src/errors/StreamReadError.ts index 122dd66a7..73abe67f0 100644 --- a/packages/errors/src/errors/StreamReadError.ts +++ b/packages/errors/src/errors/StreamReadError.ts @@ -10,7 +10,7 @@ import { import { BaseError } from '../BaseError.js'; import { - baseErrorStructSchema, + marshaledErrorSchema, ErrorCode, MarshaledErrorStruct, } from '../constants.js'; @@ -33,7 +33,7 @@ export class StreamReadError extends BaseError { * A superstruct struct for validating marshaled {@link StreamReadError} instances. */ public static struct = object({ - ...baseErrorStructSchema, + ...marshaledErrorSchema, code: literal(ErrorCode.StreamReadError), data: union([ object({ vatId: string(), supervisorId: optional(never()) }), diff --git a/packages/errors/src/errors/VatAlreadyExistsError.ts b/packages/errors/src/errors/VatAlreadyExistsError.ts index 5ca2bd028..c8a9e9291 100644 --- a/packages/errors/src/errors/VatAlreadyExistsError.ts +++ b/packages/errors/src/errors/VatAlreadyExistsError.ts @@ -1,7 +1,7 @@ import { assert, literal, object, string } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; -import { baseErrorStructSchema, ErrorCode } from '../constants.js'; +import { marshaledErrorSchema, ErrorCode } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; export class VatAlreadyExistsError extends BaseError { @@ -16,7 +16,7 @@ export class VatAlreadyExistsError extends BaseError { * A superstruct struct for validating marshaled {@link VatAlreadyExistsError} instances. */ public static struct = object({ - ...baseErrorStructSchema, + ...marshaledErrorSchema, code: literal(ErrorCode.VatAlreadyExists), data: object({ vatId: string(), diff --git a/packages/errors/src/errors/VatCapTpConnectionExistsError.ts b/packages/errors/src/errors/VatCapTpConnectionExistsError.ts index 9d78c64c6..275bc35e7 100644 --- a/packages/errors/src/errors/VatCapTpConnectionExistsError.ts +++ b/packages/errors/src/errors/VatCapTpConnectionExistsError.ts @@ -1,7 +1,7 @@ import { assert, literal, object, string } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; -import { baseErrorStructSchema, ErrorCode } from '../constants.js'; +import { marshaledErrorSchema, ErrorCode } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; export class VatCapTpConnectionExistsError extends BaseError { @@ -20,7 +20,7 @@ export class VatCapTpConnectionExistsError extends BaseError { * A superstruct struct for validating marshaled {@link VatCapTpConnectionExistsError} instances. */ public static struct = object({ - ...baseErrorStructSchema, + ...marshaledErrorSchema, code: literal(ErrorCode.VatCapTpConnectionExists), data: object({ vatId: string(), diff --git a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts index 2399a5c3b..6500332f4 100644 --- a/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts +++ b/packages/errors/src/errors/VatCapTpConnectionNotFoundError.ts @@ -1,7 +1,7 @@ import { assert, literal, object, string } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; -import { baseErrorStructSchema, ErrorCode } from '../constants.js'; +import { marshaledErrorSchema, ErrorCode } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; export class VatCapTpConnectionNotFoundError extends BaseError { @@ -18,7 +18,7 @@ export class VatCapTpConnectionNotFoundError extends BaseError { * A superstruct struct for validating marshaled {@link VatCapTpConnectionNotFoundError} instances. */ public static struct = object({ - ...baseErrorStructSchema, + ...marshaledErrorSchema, code: literal(ErrorCode.VatCapTpConnectionNotFound), data: object({ vatId: string(), diff --git a/packages/errors/src/errors/VatDeletedError.ts b/packages/errors/src/errors/VatDeletedError.ts index 262d7cce2..b6fff0cf2 100644 --- a/packages/errors/src/errors/VatDeletedError.ts +++ b/packages/errors/src/errors/VatDeletedError.ts @@ -1,7 +1,7 @@ import { assert, literal, object, string } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; -import { baseErrorStructSchema, ErrorCode } from '../constants.js'; +import { marshaledErrorSchema, ErrorCode } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; export class VatDeletedError extends BaseError { @@ -14,7 +14,7 @@ export class VatDeletedError extends BaseError { * A superstruct struct for validating marshaled {@link VatDeletedError} instances. */ public static struct = object({ - ...baseErrorStructSchema, + ...marshaledErrorSchema, code: literal(ErrorCode.VatDeleted), data: object({ vatId: string(), diff --git a/packages/errors/src/errors/VatNotFoundError.ts b/packages/errors/src/errors/VatNotFoundError.ts index 458b0eeb4..10d699b48 100644 --- a/packages/errors/src/errors/VatNotFoundError.ts +++ b/packages/errors/src/errors/VatNotFoundError.ts @@ -1,7 +1,7 @@ import { assert, literal, object, string } from '@metamask/superstruct'; import { BaseError } from '../BaseError.js'; -import { baseErrorStructSchema, ErrorCode } from '../constants.js'; +import { marshaledErrorSchema, ErrorCode } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; export class VatNotFoundError extends BaseError { @@ -14,7 +14,7 @@ export class VatNotFoundError extends BaseError { * A superstruct struct for validating marshaled {@link VatNotFoundError} instances. */ public static struct = object({ - ...baseErrorStructSchema, + ...marshaledErrorSchema, code: literal(ErrorCode.VatNotFound), data: object({ vatId: string(), diff --git a/packages/errors/src/marshal/isMarshaledOcapError.ts b/packages/errors/src/marshal/isMarshaledOcapError.ts index 845b14c6a..cc15a0840 100644 --- a/packages/errors/src/marshal/isMarshaledOcapError.ts +++ b/packages/errors/src/marshal/isMarshaledOcapError.ts @@ -1,6 +1,6 @@ import { is } from '@metamask/superstruct'; -import { MarshaledErrorStruct } from '../constants.js'; +import { MarshaledOcapErrorStruct } from '../constants.js'; import type { MarshaledOcapError } from '../types.js'; /** @@ -12,9 +12,5 @@ import type { MarshaledOcapError } from '../types.js'; export function isMarshaledOcapError( value: unknown, ): value is MarshaledOcapError { - return ( - is(value, MarshaledErrorStruct) && - Boolean(value.data) && - Boolean(value.code) - ); + return is(value, MarshaledOcapErrorStruct); } From 9f7a94680618abd20ba9a53380ce7530a30c688e Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 17 Oct 2024 20:38:47 +0200 Subject: [PATCH 24/24] fix test --- packages/errors/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/errors/src/constants.ts b/packages/errors/src/constants.ts index cd5ca341a..0dc6f4e6c 100644 --- a/packages/errors/src/constants.ts +++ b/packages/errors/src/constants.ts @@ -51,5 +51,5 @@ export const MarshaledOcapErrorStruct = object({ ...marshaledErrorSchema, code: ErrorCodeStruct, data: JsonStruct, - cause: optional(union([string(), lazy(() => MarshaledOcapErrorStruct)])), + cause: optional(union([string(), lazy(() => MarshaledErrorStruct)])), }) as Struct;