Skip to content

Commit

Permalink
Merge pull request #4144 from Agoric/mfig-agoric-remoting
Browse files Browse the repository at this point in the history
feat(far): new package `@agoric/far`
mergify[bot] authored Dec 2, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents a995a75 + 8be558c commit 154da76
Showing 10 changed files with 493 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/test-all-packages.yml
Original file line number Diff line number Diff line change
@@ -162,6 +162,8 @@ jobs:
run: cd packages/ERTP && yarn test
- name: yarn test (eventual-send)
run: cd packages/eventual-send && yarn test
- name: yarn test (far)
run: cd packages/far && yarn test
- name: yarn test (governance)
run: cd packages/governance && yarn test
- name: yarn test (import-bundle)
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
"packages/transform-metering",
"packages/install-metering-and-ses",
"packages/marshal",
"packages/far",
"packages/same-structure",
"packages/captp",
"packages/stat-logger",
17 changes: 17 additions & 0 deletions packages/far/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Agoric Far Object helpers

The `@agoric/far` package provides a convenient way to use the Agoric
[distributed objects system](https://agoric.com/documentation/js-programming/far.html) without relying on the underlying messaging
implementation.

It exists to reduce the boilerplate in Hardened JavaScript vats that are running
in Agoric's SwingSet kernel,
[`@agoric/swingset-vat`](https://github.com/Agoric/agoric-sdk/tree/master/packages/SwingSet),
or arbitrary JS programs using Hardened JavaScript and communicating via
[`@agoric/captp`](https://github.com/Agoric/agoric-sdk/tree/master/packages/captp).

You can import any of the following from `@agoric/far`:

```js
import { E, Far, getInterfaceOf, passStyleOf } from '@agoric/far';
```
19 changes: 19 additions & 0 deletions packages/far/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// This file can contain .js-specific Typescript compiler config.
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",

"noEmit": true,
/*
// The following flags are for creating .d.ts files:
"noEmit": false,
"declaration": true,
"emitDeclarationOnly": true,
*/
"downlevelIteration": true,
"strictNullChecks": true,
"moduleResolution": "node",
},
"include": ["src/**/*.js", "exported.js"],
}
66 changes: 66 additions & 0 deletions packages/far/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"name": "@agoric/far",
"version": "0.1.0",
"description": "Helpers for Agoric distributed objects.",
"type": "module",
"main": "src/index.js",
"scripts": {
"test": "ava",
"test:c8": "c8 $C8_OPTIONS ava --config=ava-nesm.config.js",
"test:xs": "exit 0",
"build": "exit 0",
"lint-fix": "yarn lint:eslint --fix && yarn lint:types",
"lint-check": "yarn lint",
"lint": "yarn lint:types && yarn lint:eslint",
"lint:types": "tsc -p jsconfig.json",
"lint:eslint": "eslint '**/*.js'"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Agoric/agoric-sdk.git"
},
"author": "Agoric",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/Agoric/agoric-sdk/issues"
},
"homepage": "https://github.com/Agoric/agoric-sdk#readme",
"dependencies": {
"@agoric/eventual-send": "^0.13.30",
"@agoric/marshal": "^0.4.28"
},
"devDependencies": {
"@agoric/install-ses": "^0.5.28",
"@endo/ses-ava": "^0.2.8",
"ava": "^3.12.1",
"c8": "^7.7.2"
},
"keywords": [
"eventual send",
"wavy dot",
"remote objects",
"tildot",
"far"
],
"files": [
"src"
],
"eslintConfig": {
"extends": [
"@agoric"
]
},
"prettier": {
"trailingComma": "all",
"singleQuote": true
},
"publishConfig": {
"access": "public"
},
"ava": {
"files": [
"test/**/test-*.js"
],
"timeout": "2m"
}
}
7 changes: 7 additions & 0 deletions packages/far/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { E } from '@agoric/eventual-send';
export { Far, getInterfaceOf, passStyleOf } from '@agoric/marshal';

/**
* @template T
* @typedef {import('@agoric/eventual-send').EOnly<T>} EOnly
*/
7 changes: 7 additions & 0 deletions packages/far/test/prepare-test-env-ava.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import '@agoric/install-ses/debug.js';

import { wrapTest } from '@endo/ses-ava';
import rawTest from 'ava';

/** @type {typeof rawTest} */
export const test = wrapTest(rawTest);
164 changes: 164 additions & 0 deletions packages/far/test/test-e.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/* global HandledPromise */
// eslint-disable-next-line import/no-extraneous-dependencies
import { test } from './prepare-test-env-ava.js';

import { E } from '../src/index.js';

test('E reexports', async t => {
t.is(E.resolve, HandledPromise.resolve, 'E reexports resolve');
});

test('E.when', async t => {
let stash;
await E.when(123, val => (stash = val));
t.is(stash, 123, `onfulfilled handler fires`);
let raised;
// eslint-disable-next-line prefer-promise-reject-errors
await E.when(Promise.reject('foo'), undefined, val => (raised = val));
t.assert(raised, 'foo', 'onrejected handler fires');

let ret;
let exc;
await E.when(
Promise.resolve('foo'),
val => (ret = val),
val => (exc = val),
);
t.is(ret, 'foo', 'onfulfilled option fires');
t.is(exc, undefined, 'onrejected option does not fire');

let ret2;
let exc2;
await E.when(
// eslint-disable-next-line prefer-promise-reject-errors
Promise.reject('foo'),
val => (ret2 = val),
val => (exc2 = val),
);
t.is(ret2, undefined, 'onfulfilled option does not fire');
t.is(exc2, 'foo', 'onrejected option fires');
});

test('E method calls', async t => {
const x = {
double(n) {
return 2 * n;
},
};
const d = E(x).double(6);
t.is(typeof d.then, 'function', 'return is a thenable');
t.is(await d, 12, 'method call works');
});

test('E sendOnly method calls', async t => {
let testIncrDoneResolve;
const testIncrDone = new Promise(resolve => {
testIncrDoneResolve = resolve;
});

let count = 0;
const counter = {
incr(n) {
count += n;
testIncrDoneResolve(); // only here for the test.
return count;
},
};
const result = E.sendOnly(counter).incr(42);
t.is(typeof result, 'undefined', 'return is undefined as expected');
await testIncrDone;
t.is(count, 42, 'sendOnly method call variant works');
});

test('E call missing method', async t => {
const x = {
double(n) {
return 2 * n;
},
};
await t.throwsAsync(() => E(x).triple(6), {
message: 'target has no method "triple", has ["double"]',
});
});

test('E sendOnly call missing method', async t => {
let count = 279;
const counter = {
incr(n) {
count += n;
return count;
},
};

const result = E.sendOnly(counter).decr(210);
t.is(result, undefined, 'return is undefined as expected');
await null;
t.is(count, 279, `sendOnly method call doesn't change count`);
});

test('E call undefined method', async t => {
const x = {
double(n) {
return 2 * n;
},
};
await t.throwsAsync(() => E(x)(6), {
message: 'Cannot invoke target as a function; typeof target is "object"',
});
});

test('E invoke a non-method', async t => {
const x = { double: 24 };
await t.throwsAsync(() => E(x).double(6), {
message: 'invoked method "double" is not a function; it is a "number"',
});
});

test('E method call undefined receiver', async t => {
await t.throwsAsync(() => E(undefined).double(6), {
message: 'Cannot deliver "double" to target; typeof target is "undefined"',
});
});

test('E shortcuts', async t => {
const x = {
name: 'buddy',
val: 123,
y: Object.freeze({
val2: 456,
name2: 'holly',
fn: n => 2 * n,
}),
hello(greeting) {
return `${greeting}, ${this.name}!`;
},
};
t.is(await E(x).hello('Hello'), 'Hello, buddy!', 'method call works');
t.is(
await E(await E.get(await E.get(x).y).fn)(4),
8,
'anonymous method works',
);
t.is(await E.get(x).val, 123, 'property get');
});

test('E.get', async t => {
const x = {
name: 'buddy',
val: 123,
y: Object.freeze({
val2: 456,
name2: 'holly',
fn: n => 2 * n,
}),
hello(greeting) {
return `${greeting}, ${this.name}!`;
},
};
t.is(
await E(await E.get(await E.get(x).y).fn)(4),
8,
'anonymous method works',
);
t.is(await E.get(x).val, 123, 'property get');
});
68 changes: 68 additions & 0 deletions packages/far/test/test-marshal-far-function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// @ts-check

import { test } from './prepare-test-env-ava.js';

import { Far, getInterfaceOf, passStyleOf } from '../src/index.js';

const { freeze, setPrototypeOf } = Object;

test('Far functions', t => {
t.notThrows(() => Far('arrow', a => a + 1), 'Far function');
const arrow = Far('arrow', a => a + 1);
t.is(passStyleOf(arrow), 'remotable');
t.is(getInterfaceOf(arrow), 'Alleged: arrow');
});

test('Acceptable far functions', t => {
t.is(passStyleOf(Far('asyncArrow', async a => a + 1)), 'remotable');
// Even though concise methods start as methods, they can be
// made into far functions *instead*.
const concise = { doFoo() {} }.doFoo;
t.is(passStyleOf(Far('concise', concise)), 'remotable');
});

test('Unacceptable far functions', t => {
t.throws(
() =>
Far(
'alreadyFrozen',
freeze(a => a + 1),
),
{
message: /is already frozen/,
},
);
t.throws(() => Far('keywordFunc', function keyword() {}), {
message: /unexpected properties besides \.name and \.length/,
});
});

test('Far functions cannot be methods', t => {
const doFoo = Far('doFoo', a => a + 1);
t.throws(
() =>
Far('badMethod', {
doFoo,
}),
{
message: /Remotables with non-methods/,
},
);
});

test('Data can contain far functions', t => {
const arrow = Far('arrow', a => a + 1);
t.is(passStyleOf(harden({ x: 8, foo: arrow })), 'copyRecord');
const mightBeMethod = a => a + 1;
t.throws(() => passStyleOf(freeze({ x: 8, foo: mightBeMethod })), {
message: /Remotables with non-methods like "x" /,
});
});

test('function without prototype', t => {
const arrow = a => a;
setPrototypeOf(arrow, null);
t.throws(() => Far('arrow', arrow), {
message: /must not inherit from null/,
});
});
142 changes: 142 additions & 0 deletions packages/far/test/test-marshal-far-obj.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// @ts-check

import { test } from './prepare-test-env-ava.js';

import { Far, passStyleOf, getInterfaceOf } from '../src/index.js';

const { quote: q } = assert;
const { create, getPrototypeOf } = Object;

// this only includes the tests that do not use liveSlots

test('Remotable/getInterfaceOf', t => {
t.throws(
() => Far('MyHandle', { foo: 123 }),
{ message: /cannot serialize/ },
'non-function props are not implemented',
);

t.is(getInterfaceOf('foo'), undefined, 'string, no interface');
t.is(getInterfaceOf(null), undefined, 'null, no interface');
t.is(
getInterfaceOf(a => a + 1),
undefined,
'function, no interface',
);
t.is(getInterfaceOf(123), undefined, 'number, no interface');

// Check that a handle can be created.
const p = Far('MyHandle');
harden(p);
// console.log(p);
t.is(getInterfaceOf(p), 'Alleged: MyHandle', `interface MyHandle`);
t.is(`${p}`, '[object Alleged: MyHandle]', 'stringify [MyHandle]');
t.is(`${q(p)}`, '"[Alleged: MyHandle]"', 'quotify [MyHandle]');

const p2 = Far('Thing', {
name() {
return 'cretin';
},
birthYear(now) {
return now - 64;
},
});
t.is(getInterfaceOf(p2), 'Alleged: Thing', `interface is Thing`);
t.is(p2.name(), 'cretin', `name() method is presence`);
t.is(p2.birthYear(2020), 1956, `birthYear() works`);
});

const GOOD_PASS_STYLE = Symbol.for('passStyle');
const BAD_PASS_STYLE = Symbol('passStyle');

const testRecord = ({
styleSymbol = GOOD_PASS_STYLE,
styleString = 'remotable',
styleEnumerable = false,
tagSymbol = Symbol.toStringTag,
tagString = 'Alleged: Good remotable proto',
tagEnumerable = false,
extras = {},
} = {}) =>
harden(
create(Object.prototype, {
[styleSymbol]: { value: styleString, enumerable: styleEnumerable },
[tagSymbol]: { value: tagString, enumerable: tagEnumerable },
...extras,
}),
);

const goodRemotableProto = testRecord();

// @ts-ignore We're testing bad things anyway
const badRemotableProto1 = testRecord({ styleSymbol: BAD_PASS_STYLE });

const badRemotableProto2 = testRecord({ styleString: 'string' });

const badRemotableProto3 = testRecord({
extras: {
toString: {
value: Object, // Any function will do
enumerable: true,
},
},
});

const badRemotableProto4 = testRecord({ tagString: 'Bad remotable proto' });

const sub = sup => harden({ __proto__: sup });

test('getInterfaceOf validation', t => {
t.is(getInterfaceOf(goodRemotableProto), undefined);
t.is(getInterfaceOf(badRemotableProto1), undefined);
t.is(getInterfaceOf(badRemotableProto2), undefined);
t.is(getInterfaceOf(badRemotableProto3), undefined);
t.is(getInterfaceOf(badRemotableProto4), undefined);

t.is(
getInterfaceOf(sub(goodRemotableProto)),
'Alleged: Good remotable proto',
);
t.is(getInterfaceOf(sub(badRemotableProto1)), undefined);
t.is(getInterfaceOf(sub(badRemotableProto2)), undefined);
t.is(getInterfaceOf(sub(badRemotableProto3)), undefined);
t.is(getInterfaceOf(sub(badRemotableProto4)), undefined);
});

const NON_METHOD = {
message: /cannot serialize Remotables with non-methods like .* in .*/,
};
const IFACE_ALLEGED = {
message: /For now, iface "Bad remotable proto" must be "Remotable" or begin with "Alleged: "; unimplemented/,
};
const UNEXPECTED_PROPS = {
message: /Unexpected properties on Remotable Proto .*/,
};
const UNEXPECTED_PASS_STYLE = {
message: /Unrecognized PassStyle/,
};
const EXPECTED_PASS_STYLE = {
message: /\[Symbol\(passStyle\)\]" property expected/,
};

// Parallels the getInterfaceOf validation cases, explaining why
// each failure failed.
test('passStyleOf validation of remotables', t => {
t.throws(() => passStyleOf(goodRemotableProto), NON_METHOD);
t.throws(() => passStyleOf(badRemotableProto1), NON_METHOD);
t.throws(() => passStyleOf(badRemotableProto2), UNEXPECTED_PASS_STYLE);
t.throws(() => passStyleOf(badRemotableProto3), NON_METHOD);
t.throws(() => passStyleOf(badRemotableProto4), NON_METHOD);

t.is(passStyleOf(sub(goodRemotableProto)), 'remotable');

t.throws(() => passStyleOf(sub(badRemotableProto1)), EXPECTED_PASS_STYLE);
t.throws(() => passStyleOf(sub(badRemotableProto2)), UNEXPECTED_PASS_STYLE);
t.throws(() => passStyleOf(sub(badRemotableProto3)), UNEXPECTED_PROPS);
t.throws(() => passStyleOf(sub(badRemotableProto4)), IFACE_ALLEGED);
});

test('object without prototype', t => {
const base = Far('base', { __proto__: null });
t.is(getPrototypeOf(getPrototypeOf(base)), Object.prototype);
});

0 comments on commit 154da76

Please sign in to comment.