diff --git a/packages/pass-style/index.js b/packages/pass-style/index.js index ef54b29c9f..db1d3b50cb 100644 --- a/packages/pass-style/index.js +++ b/packages/pass-style/index.js @@ -35,5 +35,7 @@ export { isCopyArray, } from './src/typeGuards.js'; +export { FarBaseClass } from './src/far-class-instances.js'; + // eslint-disable-next-line import/export export * from './src/types.js'; diff --git a/packages/pass-style/package.json b/packages/pass-style/package.json index 8759556307..35f261add4 100644 --- a/packages/pass-style/package.json +++ b/packages/pass-style/package.json @@ -37,6 +37,7 @@ "@fast-check/ava": "^1.1.5" }, "devDependencies": { + "@endo/eventual-send": "^0.17.5", "@endo/init": "^0.5.60", "@endo/ses-ava": "^0.2.44", "ava": "^5.3.0", diff --git a/packages/pass-style/src/far-class-instances.js b/packages/pass-style/src/far-class-instances.js new file mode 100644 index 0000000000..cea0cf7483 --- /dev/null +++ b/packages/pass-style/src/far-class-instances.js @@ -0,0 +1,19 @@ +import { Far } from './make-far.js'; + +/** + * Classes whose instances should be Far objects should inherit from + * this convenient base class. Note that the constructor of this base class + * freezes the instance in an empty state, so all is interesting attributes + * can only depend on its identity and what it inherits from. + * This includes private fields, as those are keyed only on + * this object's identity. However, we discourage (but cannot prevent) such + * use of private fields, as they cannot easily be refactored into exo state. + */ +export const FarBaseClass = class FarBaseClass { + constructor() { + harden(this); + } +}; + +Far('FarBaseClass', FarBaseClass.prototype); +harden(FarBaseClass); diff --git a/packages/pass-style/test/test-far-class-instances.js b/packages/pass-style/test/test-far-class-instances.js new file mode 100644 index 0000000000..788e5cd7a6 --- /dev/null +++ b/packages/pass-style/test/test-far-class-instances.js @@ -0,0 +1,80 @@ +/* eslint-disable class-methods-use-this */ +/* eslint-disable max-classes-per-file */ +import { test } from './prepare-test-env-ava.js'; + +// TODO enable import of getMethodNames without deep import +// eslint-disable-next-line import/order +import { getMethodNames } from '@endo/eventual-send/src/local.js'; +import { passStyleOf } from '../src/passStyleOf.js'; +import { FarBaseClass } from '../src/far-class-instances.js'; + +class FarSubclass1 extends FarBaseClass { + double(x) { + return x + x; + } +} + +class FarSubclass2 extends FarSubclass1 { + #y = 0; + + constructor(y) { + super(); + this.#y = y; + } + + doubleAdd(x) { + return this.double(x) + this.#y; + } +} + +test('far class instances', t => { + const fb = new FarBaseClass(); + t.is(passStyleOf(fb), 'remotable'); + t.deepEqual(getMethodNames(fb), ['constructor']); + + t.assert(new fb.constructor() instanceof FarBaseClass); + t.throws(() => fb.constructor(), { + // TODO message depends on JS engine, and so is a fragile golden test + message: "Class constructor FarBaseClass cannot be invoked without 'new'", + }); + + const fs1 = new FarSubclass1(); + t.is(passStyleOf(fs1), 'remotable'); + t.is(fs1.double(4), 8); + t.assert(new fs1.constructor() instanceof FarSubclass1); + t.deepEqual(getMethodNames(fs1), ['constructor', 'double']); + + const fs2 = new FarSubclass2(3); + t.is(passStyleOf(fs2), 'remotable'); + t.is(fs2.double(4), 8); + t.is(fs2.doubleAdd(4), 11); + t.deepEqual(getMethodNames(fs2), ['constructor', 'double', 'doubleAdd']); + + const yField = new WeakMap(); + class FarSubclass3 extends FarSubclass1 { + constructor(y) { + super(); + yField.set(this, y); + } + + doubleAdd(x) { + return this.double(x) + yField.get(this); + } + } + + const fs3 = new FarSubclass3(3); + t.is(passStyleOf(fs3), 'remotable'); + t.is(fs3.double(4), 8); + t.is(fs3.doubleAdd(4), 11); + t.deepEqual(getMethodNames(fs3), ['constructor', 'double', 'doubleAdd']); +}); + +test('far class instance hardened empty', t => { + class FarClass4 extends FarBaseClass { + z = 0; + } + t.throws(() => new FarClass4(), { + // TODO message depends on JS engine, and so is a fragile golden test + message: 'Cannot define property z, object is not extensible', + }); +});