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; } }