Skip to content

Commit

Permalink
feat(far): new package @agoric/far
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfig committed Dec 2, 2021
1 parent a995a75 commit 8be558c
Show file tree
Hide file tree
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
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"packages/transform-metering",
"packages/install-metering-and-ses",
"packages/marshal",
"packages/far",
"packages/same-structure",
"packages/captp",
"packages/stat-logger",
Expand Down
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/,
});
});
Loading

0 comments on commit 8be558c

Please sign in to comment.