diff --git a/.changeset/disposed-stub-error-stack.md b/.changeset/disposed-stub-error-stack.md new file mode 100644 index 0000000..73d58d3 --- /dev/null +++ b/.changeset/disposed-stub-error-stack.md @@ -0,0 +1,5 @@ +--- +"capnweb": patch +--- + +Disposed-stub errors now carry a stack trace pointing at the disposal site instead of at module initialization. diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index 00adc71..16a67f8 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -1647,6 +1647,30 @@ describe("map() over RPC", () => { }); }); +describe("using a disposed stub", () => { + it("rejects calls with an error saying the stub was disposed", async () => { + let stub = new RpcStub(new TestTarget()); + stub[Symbol.dispose](); + + await expect(stub.square(3)).rejects.toThrow("after it has been disposed"); + }); + + it("produces a distinct error for each disposal", async () => { + // Each disposal must create a fresh Error so that the stack trace points at the disposal + // site rather than at module initialization. + let stub1 = new RpcStub(new TestTarget()); + let stub2 = new RpcStub(new TestTarget()); + stub1[Symbol.dispose](); + stub2[Symbol.dispose](); + + let e1 = await stub1.square(1).then(() => undefined, (e: unknown) => e); + let e2 = await stub2.square(2).then(() => undefined, (e: unknown) => e); + expect(e1).toBeInstanceOf(Error); + expect(e2).toBeInstanceOf(Error); + expect(e1).not.toBe(e2); + }); +}); + describe("stub disposal over RPC", () => { it("disposes remote RpcTarget when stub is disposed", async () => { let targetDisposedCount = 0; diff --git a/src/core.ts b/src/core.ts index 86d8c03..b76d934 100644 --- a/src/core.ts +++ b/src/core.ts @@ -335,9 +335,6 @@ export class ErrorStubHook extends StubHook { } }; -const DISPOSED_HOOK: StubHook = new ErrorStubHook( - new Error("Attempted to use RPC stub after it has been disposed.")); - // A call interceptor can be used to intercept all RPC stub invocations within some synchronous // scope. This is used to implement record/replay type CallInterceptor = (hook: StubHook, path: PropertyPath, params: RpcPayload) => StubHook; @@ -391,7 +388,9 @@ const PROXY_HANDLERS: ProxyHandler<{raw: RpcStub}> = { // We only advertise Symbol.dispose on stubs and root promises, not properties. return () => { stub.hook.dispose(); - stub.hook = DISPOSED_HOOK; + // Created here so that its stack trace points at the disposal site. + stub.hook = new ErrorStubHook( + new Error("Attempted to use RPC stub after it has been disposed.")); }; } else { return undefined;