From 460e003fc1677183274153bd45578ccd9def86db Mon Sep 17 00:00:00 2001 From: Varun Dhananjaya Date: Mon, 19 Oct 2026 00:34:33 -0700 Subject: [PATCH] Ignore inherited Array.prototype properties in session tables for...in over the import, export, and onBroken tables also visits enumerable properties added to Array.prototype, so shutdown called abort() on them and threw. Use Object.keys(), which still skips holes and keeps negative indexes. Co-Authored-By: Claude Opus 5.5 (1M context) --- .changeset/array-prototype-session-tables.md | 5 +++++ __tests__/index.test.ts | 22 ++++++++++++++++++++ src/rpc.ts | 22 ++++++++------------ 3 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 .changeset/array-prototype-session-tables.md diff --git a/.changeset/array-prototype-session-tables.md b/.changeset/array-prototype-session-tables.md new file mode 100644 index 00000000..034205c6 --- /dev/null +++ b/.changeset/array-prototype-session-tables.md @@ -0,0 +1,5 @@ +--- +"capnweb": patch +--- + +Fix session shutdown and `getStats()` when `Array.prototype` has enumerable properties. diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index e7fe3743..e441ff72 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -1866,6 +1866,28 @@ describe("map() over RPC", () => { }); }); +describe("session tables", () => { + it("ignore enumerable properties added to Array.prototype", async () => { + Object.defineProperty(Array.prototype, "capnwebTestExtension", { + value: () => {}, + enumerable: true, + configurable: true, + writable: true, + }); + try { + let harness = new TestHarness(new TestTarget()); + expect(await harness.stub.square(3)).toBe(9); + await pumpMicrotasks(); + harness.checkAllDisposed(); + + harness.stub[Symbol.dispose](); + await pumpMicrotasks(); + } finally { + delete (Array.prototype as any).capnwebTestExtension; + } + }); +}); + describe("stub disposal over RPC", () => { it("disposes remote RpcTarget when stub is disposed", async () => { let targetDisposedCount = 0; diff --git a/src/rpc.ts b/src/rpc.ts index 8896fecc..f2982986 100644 --- a/src/rpc.ts +++ b/src/rpc.ts @@ -966,9 +966,9 @@ class RpcSessionImpl implements Importer, Exporter { } } - // WATCH OUT: these are sparse arrays. `for/let/of` will iterate only positive indexes - // including deleted indexes -- bad. We need to use `for/let/in` instead. - for (let i in this.onBrokenCallbacks) { + // WATCH OUT: these are sparse arrays. `Object.keys()` visits only their own entries, skipping + // deleted indexes and any enumerable properties added to `Array.prototype`. + for (let i of Object.keys(this.onBrokenCallbacks)) { try { this.onBrokenCallbacks[i](error); } catch (err) { @@ -976,11 +976,11 @@ class RpcSessionImpl implements Importer, Exporter { Promise.resolve(err); } } - for (let i in this.imports) { - this.imports[i].abort(error); + for (let i of Object.keys(this.imports)) { + this.imports[Number(i)].abort(error); } - for (let i in this.exports) { - this.exports[i].hook.dispose(); + for (let i of Object.keys(this.exports)) { + this.exports[Number(i)].hook.dispose(); } } @@ -1142,12 +1142,8 @@ class RpcSessionImpl implements Importer, Exporter { getStats(): {imports: number, exports: number} { let result = {imports: 0, exports: 0}; // We can't just use `.length` because the arrays can be sparse and can have negative indexes. - for (let i in this.imports) { - ++result.imports; - } - for (let i in this.exports) { - ++result.exports; - } + result.imports = Object.keys(this.imports).length; + result.exports = Object.keys(this.exports).length; return result; } }