Skip to content

Commit

Permalink
Merge branch 'master' into leotm-versioned-lockdown-bundle
Browse files Browse the repository at this point in the history
  • Loading branch information
leotm authored Nov 20, 2023
2 parents c14ff1c + e1c63bf commit 56a9f59
Show file tree
Hide file tree
Showing 19 changed files with 848 additions and 56 deletions.
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Everything else is wired up thanks to workspaces, so no need to run installs in

# Making a Release

* Review the [next release](
https://github.com/endojs/endo/labels/next-release
) label for additional tasks or pending changes particular to this release.

* Do not release from a Git workspace.
In a Git workspace, `.git` is a file and not a directory.
At time of writing, Lerna does not account for Git workspaces when it looks
Expand Down
6 changes: 6 additions & 0 deletions packages/eventual-send/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
"lint:types": "tsc",
"lint:eslint": "eslint '**/*.js'"
},
"exports": {
"./package.json": "./package.json",
".": "./src/no-shim.js",
"./shim.js": "./shim.js",
"./utils.js": "./utils.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/endojs/endo.git"
Expand Down
1 change: 1 addition & 0 deletions packages/eventual-send/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getMethodNames } from './src/local.js';
1 change: 1 addition & 0 deletions packages/exo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
},
"dependencies": {
"@endo/env-options": "^0.1.4",
"@endo/eventual-send": "^0.17.6",
"@endo/far": "^0.2.22",
"@endo/pass-style": "^0.1.7",
"@endo/patterns": "^0.2.6"
Expand Down
103 changes: 65 additions & 38 deletions packages/exo/src/exo-tools.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { E, Far } from '@endo/far';
import { getMethodNames } from '@endo/eventual-send/utils.js';
import { hasOwnPropertyOf } from '@endo/pass-style';
import { E, Far } from '@endo/far';
import {
listDifference,
objectMap,
Expand All @@ -12,7 +13,6 @@ import {
getInterfaceGuardPayload,
getCopyMapEntries,
} from '@endo/patterns';

import { GET_INTERFACE_GUARD } from './get-interface.js';

/** @typedef {import('@endo/patterns').Method} Method */
Expand Down Expand Up @@ -182,6 +182,9 @@ const defendSyncMethod = (method, methodGuardPayload, label) => {
return syncMethod;
};

/**
* @param {MethodGuardPayload} methodGuardPayload
*/
const desync = methodGuardPayload => {
const {
argGuards,
Expand Down Expand Up @@ -210,22 +213,43 @@ const desync = methodGuardPayload => {
};
};

/**
* @param {(...args: unknown[]) => any} method
* @param {MethodGuardPayload} methodGuardPayload
* @param {string} label
*/
const defendAsyncMethod = (method, methodGuardPayload, label) => {
const { returnGuard } = methodGuardPayload;
const isRawReturn = isRawGuard(returnGuard);

const { awaitIndexes, rawMethodGuardPayload } = desync(methodGuardPayload);
const matchConfig = buildMatchConfig(rawMethodGuardPayload);

const { asyncMethod } = {
// Note purposeful use of `this` and concise method syntax
asyncMethod(...args) {
const awaitList = awaitIndexes.map(i => args[i]);
const awaitList = [];
for (const i of awaitIndexes) {
if (i >= args.length) {
break;
}
awaitList.push(args[i]);
}
const p = Promise.all(awaitList);
const syncArgs = [...args];
const resultP = E.when(p, awaitedArgs => {
for (let j = 0; j < awaitIndexes.length; j += 1) {
syncArgs[awaitIndexes[j]] = awaitedArgs[j];
}
const realArgs = defendSyncArgs(syncArgs, rawMethodGuardPayload, label);
return apply(method, this, realArgs);
});
const resultP = E.when(
p,
/** @param {any[]} awaitedArgs */ awaitedArgs => {
for (let j = 0; j < awaitedArgs.length; j += 1) {
syncArgs[awaitIndexes[j]] = awaitedArgs[j];
}
const realArgs = defendSyncArgs(syncArgs, matchConfig, label);
return apply(method, this, realArgs);
},
);
if (isRawReturn) {
return resultP;
}
return E.when(resultP, result => {
mustMatch(harden(result), returnGuard, `${label}: result`);
return result;
Expand Down Expand Up @@ -353,21 +377,6 @@ const bindMethod = (
return method;
};

/**
*
* @template {Record<PropertyKey, CallableFunction>} T
* @param {T} behaviorMethods
* @param {InterfaceGuard<{ [M in keyof T]: MethodGuard }>} interfaceGuard
* @returns {T & import('./get-interface.js').GetInterfaceGuard<T>}
*/
const withGetInterfaceGuardMethod = (behaviorMethods, interfaceGuard) =>
harden({
[GET_INTERFACE_GUARD]() {
return interfaceGuard;
},
...behaviorMethods,
});

/**
* @template {Record<PropertyKey, CallableFunction>} T
* @param {string} tag
Expand All @@ -384,13 +393,20 @@ export const defendPrototype = (
interfaceGuard = undefined,
) => {
const prototype = {};
if (hasOwnPropertyOf(behaviorMethods, 'constructor')) {
// By ignoring any method named "constructor", we can use a
const methodNames = getMethodNames(behaviorMethods).filter(
// By ignoring any method that seems to be a constructor, we can use a
// class.prototype as a behaviorMethods.
const { constructor: _, ...methods } = behaviorMethods;
// @ts-expect-error TS misses that hasOwn check makes this safe
behaviorMethods = harden(methods);
}
key => {
if (key !== 'constructor') {
return true;
}
const constructor = behaviorMethods.constructor;
return !(
constructor.prototype &&
constructor.prototype.constructor === constructor
);
},
);
/** @type {Record<PropertyKey, MethodGuard> | undefined} */
let methodGuards;
/** @type {import('@endo/patterns').DefaultGuardType} */
Expand All @@ -410,8 +426,6 @@ export const defendPrototype = (
});
defaultGuards = dg;
{
const methodNames = ownKeys(behaviorMethods);
assert(methodGuards);
const methodGuardNames = ownKeys(methodGuards);
const unimplemented = listDifference(methodGuardNames, methodNames);
unimplemented.length === 0 ||
Expand All @@ -422,12 +436,9 @@ export const defendPrototype = (
Fail`methods ${q(unguarded)} not guarded by ${q(interfaceName)}`;
}
}
behaviorMethods = withGetInterfaceGuardMethod(
behaviorMethods,
interfaceGuard,
);
}
for (const prop of ownKeys(behaviorMethods)) {

for (const prop of methodNames) {
prototype[prop] = bindMethod(
`In ${q(prop)} method of (${tag})`,
contextProvider,
Expand All @@ -439,6 +450,22 @@ export const defendPrototype = (
);
}

if (!hasOwnPropertyOf(prototype, GET_INTERFACE_GUARD)) {
const getInterfaceGuardMethod = {
[GET_INTERFACE_GUARD]() {
// Note: May be `undefined`
return interfaceGuard;
},
}[GET_INTERFACE_GUARD];
prototype[GET_INTERFACE_GUARD] = bindMethod(
`In ${q(GET_INTERFACE_GUARD)} method of (${tag})`,
contextProvider,
getInterfaceGuardMethod,
thisfulMethods,
undefined,
);
}

return Far(
tag,
/** @type {T & import('./get-interface.js').GetInterfaceGuard<T>} */ (
Expand Down
14 changes: 9 additions & 5 deletions packages/exo/src/get-interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@
* The name of the automatically added default meta-method for
* obtaining an exo's interface, if it has one.
*
* Intended to be similar to `GET_METHOD_NAMES` from `@endo/pass-style`.
*
* TODO Name to be bikeshed. Perhaps even whether it is a
* string or symbol to be bikeshed.
* string or symbol to be bikeshed. See
* https://github.com/endojs/endo/pull/1809#discussion_r1388052454
*
* TODO Beware that an exo's interface can change across an upgrade,
* so remotes that cache it can become stale.
*/
export const GET_INTERFACE_GUARD = Symbol.for('getInterfaceGuard');
export const GET_INTERFACE_GUARD = '__getInterfaceGuard__';

/**
* @template {Record<PropertyKey, CallableFunction>} M
* @typedef {{
* [GET_INTERFACE_GUARD]: () => import('@endo/patterns').InterfaceGuard<{
* [K in keyof M]: import('@endo/patterns').MethodGuard
* }>
* [GET_INTERFACE_GUARD]: () =>
* import('@endo/patterns').InterfaceGuard<{
* [K in keyof M]: import('@endo/patterns').MethodGuard
* }> | undefined
* }} GetInterfaceGuard
*/
77 changes: 77 additions & 0 deletions packages/exo/test/test-exo-class-js-class.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* eslint-disable max-classes-per-file */
/* eslint-disable class-methods-use-this */
// eslint-disable-next-line import/order
import { test } from './prepare-test-env-ava.js';

import { passStyleOf } from '@endo/pass-style';
import { M, getInterfaceGuardPayload } from '@endo/patterns';
import { makeExo, defineExoClass } from '../src/exo-makers.js';

// Based on FarSubclass1 in test-far-class-instances.js
class DoublerBehaviorClass {
double(x) {
return x + x;
}
}

const DoublerI = M.interface('Doubler', {
double: M.call(M.lte(10)).returns(M.number()),
});

const doubler = makeExo('doubler', DoublerI, DoublerBehaviorClass.prototype);

test('exo doubler using js classes', t => {
t.is(passStyleOf(doubler), 'remotable');
t.is(doubler.double(3), 6);
t.throws(() => doubler.double('x'), {
message: 'In "double" method of (doubler): arg 0: "x" - Must be <= 10',
});
t.throws(() => doubler.double(), {
message:
'In "double" method of (doubler): Expected at least 1 arguments: []',
});
t.throws(() => doubler.double(12), {
message: 'In "double" method of (doubler): arg 0: 12 - Must be <= 10',
});
});

// Based on FarSubclass2 in test-far-class-instances.js
class DoubleAdderBehaviorClass extends DoublerBehaviorClass {
doubleAddSelfCall(x) {
const {
state: { y },
self,
} = this;
return self.double(x) + y;
}

doubleAddSuperCall(x) {
const {
state: { y },
} = this;
return super.double(x) + y;
}
}

const DoubleAdderI = M.interface('DoubleAdder', {
...getInterfaceGuardPayload(DoublerI).methodGuards,
doubleAddSelfCall: M.call(M.number()).returns(M.number()),
doubleAddSuperCall: M.call(M.number()).returns(M.number()),
});

const makeDoubleAdder = defineExoClass(
'doubleAdderClass',
DoubleAdderI,
y => ({ y }),
DoubleAdderBehaviorClass.prototype,
);

test('exo inheritance self vs super call', t => {
const da = makeDoubleAdder(5);
t.is(da.doubleAddSelfCall(3), 11);
t.throws(() => da.doubleAddSelfCall(12), {
message:
'In "double" method of (doubleAdderClass): arg 0: 12 - Must be <= 10',
});
t.is(da.doubleAddSuperCall(12), 29);
});
Loading

0 comments on commit 56a9f59

Please sign in to comment.