From b67d21aae7e66202e3a5a3f13c7bd5769061230e Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Thu, 30 Jul 2020 12:57:27 -0700 Subject: [PATCH] feat: adaptors between notifiers and async iterables (#1340) --- packages/notifier/exports.js | 1 + packages/notifier/package.json | 4 +- packages/notifier/src/asyncIterableAdaptor.js | 157 +++++++++++++ packages/notifier/src/notifier.js | 220 +++++++----------- packages/notifier/src/types.js | 69 ++++++ .../notifier/test/test-notifier-adaptor.js | 203 ++++++++++++++++ packages/notifier/test/test-notifier.js | 11 +- packages/zoe/src/contractFacet.js | 2 +- packages/zoe/src/external-types.js | 1 + packages/zoe/src/internal-types.js | 10 - packages/zoe/src/types.js | 5 - 11 files changed, 523 insertions(+), 160 deletions(-) create mode 100644 packages/notifier/exports.js create mode 100644 packages/notifier/src/asyncIterableAdaptor.js create mode 100644 packages/notifier/src/types.js create mode 100644 packages/notifier/test/test-notifier-adaptor.js create mode 100644 packages/zoe/src/external-types.js diff --git a/packages/notifier/exports.js b/packages/notifier/exports.js new file mode 100644 index 00000000000..bc6da56dd1e --- /dev/null +++ b/packages/notifier/exports.js @@ -0,0 +1 @@ +import './src/types'; diff --git a/packages/notifier/package.json b/packages/notifier/package.json index 59fb1a81a8f..e09d92ffef5 100644 --- a/packages/notifier/package.json +++ b/packages/notifier/package.json @@ -31,6 +31,7 @@ "homepage": "https://github.com/Agoric/agoric-sdk#readme", "dependencies": { "@agoric/assert": "^0.0.8", + "@agoric/eventual-send": "^0.9.3", "@agoric/produce-promise": "^0.1.3" }, "devDependencies": { @@ -77,7 +78,8 @@ "no-unused-expressions": "off", "no-loop-func": "off", "no-inner-declarations": "off", - "import/prefer-default-export": "off" + "import/prefer-default-export": "off", + "import/no-extraneous-dependencies": "off" } }, "eslintIgnore": [ diff --git a/packages/notifier/src/asyncIterableAdaptor.js b/packages/notifier/src/asyncIterableAdaptor.js new file mode 100644 index 00000000000..d6de2c84a97 --- /dev/null +++ b/packages/notifier/src/asyncIterableAdaptor.js @@ -0,0 +1,157 @@ +// @ts-check +// eslint-disable-next-line spaced-comment +/// + +import { E } from '@agoric/eventual-send'; +// eslint-disable-next-line import/no-cycle +import { makeNotifierKit } from './notifier'; + +import './types'; + +/** + * Adaptor from a notifierP to an async iterable. + * The notifierP can be any object that has an eventually invokable + * `getUpdateSince` method that behaves according to the notifier + * spec. This can be a notifier, a promise for a local or remote + * notfier, or a presence of a remote notifier. + * + * It is also used internally by notifier.js so that a notifier itself is an + * async iterable. + * + * An async iterable is an object with a `[Symbol.asyncIterator]()` method + * that returns an async iterator. The async iterator we return here has only + * a `next()` method, without the optional `return` and `throw` methods. The + * omitted methods, if present, would be used by the for/await/of loop to + * inform the iterator of early termination. But this adaptor would not do + * anything useful in reaction to this notification. + * + * An async iterator's `next()` method returns a promise for an iteration + * result. An iteration result is a record with `value` and `done` properties. + * + * The purpose of building on the notifier protocol is to have a lossy + * adaptor, where intermediate results can be missed in favor of more recent + * results which are therefore less stale. See + * https://github.com/Agoric/documentation/blob/master/main/distributed-programming.md#notifiers + * + * @template T + * @param {PromiseOrNot>} notifierP + * @returns {AsyncIterable} + */ +export const makeAsyncIterableFromNotifier = notifierP => { + return harden({ + [Symbol.asyncIterator]: () => { + /** @type {UpdateCount} */ + let localUpdateCount; + /** @type {Promise<{value: T, done: boolean}> | undefined} */ + let myIterationResultP; + return harden({ + next: () => { + if (!myIterationResultP) { + // In this adaptor, once `next()` is called and returns an + // unresolved promise, `myIterationResultP`, and until + // `myIterationResultP` is fulfilled with an + // iteration result, further `next()` calls will return the same + // `myIterationResultP` promise again without asking the notifier + // for more updates. If there's already an unanswered ask in the + // air, all further asks should just reuse the result of that one. + // + // This reuse behavior is only needed for code that uses the async + // iterator protocol explicitly. When this async iterator is + // consumed by a for/await/of loop, `next()` will only be called + // after the promise for the previous iteration result has + // fulfilled. If it fulfills with `done: true`, the for/await/of + // loop will never call `next()` again. + // + // See + // https://2ality.com/2016/10/asynchronous-iteration.html#queuing-next()-invocations + // for an explicit use that sends `next()` without waiting. + myIterationResultP = E(notifierP) + .getUpdateSince(localUpdateCount) + .then(({ value, updateCount }) => { + localUpdateCount = updateCount; + const done = localUpdateCount === undefined; + if (!done) { + // Once the outstanding question has been answered, stop + // using that answer, so any further `next()` questions + // cause a new `getUpdateSince` request. + // + // But only if more answers are expected. Once the notifier + // is `done`, that was the last answer so reuse it forever. + myIterationResultP = undefined; + } + return harden({ value, done }); + }); + } + return myIterationResultP; + }, + }); + }, + }); +}; + +/** + * This reads from `asyncIteratable` updating `updater` with each successive + * value. The `updater` the same API as the `updater` of a notifier kit, + * but can simply be an observer to react to these updates. As an observer, + * the `updater` may only be interested in certain occurrences (`updateState`, + * `finish`, `fail`), so for convenience, `updateFromIterable` feature + * tests for those methods before calling them. + * + * @template T + * @param {Partial>} updater + * @param {AsyncIterable} asyncIterable + * @returns {Promise} + */ +// See https://github.com/Agoric/agoric-sdk/issues/1345 for why +// `updateFromIterable` currently needs a local `asyncIterable` rather than +// a possibly remote `asyncIterableP`. +export const updateFromIterable = (updater, asyncIterable) => { + const iterator = asyncIterable[Symbol.asyncIterator](); + return new Promise(ack => { + const recur = () => { + E.when(iterator.next()).then( + ({ value, done }) => { + if (done) { + updater.finish && updater.finish(value); + ack(); + } else { + updater.updateState && updater.updateState(value); + recur(); + } + }, + reason => { + updater.fail && updater.fail(reason); + ack(); + }, + ); + }; + recur(); + }); +}; + +/** + * Adaptor from async iterable to notifier. + * + * @template T + * @param {AsyncIterable} asyncIterable + * @returns {Notifier} + */ +export const makeNotifierFromAsyncIterable = asyncIterable => { + const { notifier, updater } = makeNotifierKit(); + updateFromIterable(updater, asyncIterable); + return notifier; +}; + +/** + * As updates come in from the possibly remote `notifierP`, update + * the local `updater`. Since the updates come from a notifier, they + * are lossy, i.e., once a more recent state can be reported, less recent + * states are assumed irrelevant and dropped. + * + * @template T + * @param {Partial>} updater + * @param {PromiseOrNot>} notifierP + * @returns {Promise} + */ +export const updateFromNotifier = (updater, notifierP) => + updateFromIterable(updater, makeAsyncIterableFromNotifier(notifierP)); diff --git a/packages/notifier/src/notifier.js b/packages/notifier/src/notifier.js index 3832062ae89..a9873f66cf3 100644 --- a/packages/notifier/src/notifier.js +++ b/packages/notifier/src/notifier.js @@ -4,81 +4,28 @@ import { producePromise } from '@agoric/produce-promise'; import { assert } from '@agoric/assert'; +// eslint-disable-next-line import/no-cycle +import { makeAsyncIterableFromNotifier } from './asyncIterableAdaptor'; -/** - * @template T - * @typedef {import('@agoric/produce-promise').PromiseRecord} PromiseRecord - */ - -/** - * @typedef {number | undefined} UpdateCount a value used to mark the position - * in the update stream. For the last state, the updateCount is undefined. - */ - -/** - * @template T the type of the state value - * @typedef {Object} UpdateRecord - * @property {T} value is whatever state the service wants to publish - * @property {UpdateCount} updateCount is a value that identifies the update - */ - -/** - * @template T the type of the notifier state - * @callback GetUpdateSince Can be called repeatedly to get a sequence of - * update records - * @param {UpdateCount} [updateCount] return update record as of a handle - * If the handle argument is omitted or differs from the current handle, - * return the current record. - * Otherwise, after the next state change, the promise will resolve to the - * then-current value of the record. - * @returns {Promise|undefined>} resolves to the corresponding update - */ - -/** - * @template T the type of the notifier state - * @typedef {Object} Notifier an object that can be used to get the current - * state or updates - * @property {GetUpdateSince} getUpdateSince return update record as of a - * handle - */ - -/** - * @template T the type of the notifier state - * @typedef {Object} Updater an object that should be closely held, as - * anyone with access to - * it can provide updates - * @property {(state: T) => void} updateState sets the new state, and resolves - * the outstanding promise to send an update - * @property {(finalState: T) => void} finish sets the final state, sends a - * final update, and freezes the - * updater - * @property {(reason: T) => void} reject the stream becomes erroneously - * terminated, allegedly for the stated reason. - */ - -/** - * @template T the type of the notifier state - * @typedef {Object} NotifierRecord the produced notifier/updater pair - * @property {Notifier} notifier the (widely-held) notifier consumer - * @property {Updater} updater the (closely-held) notifier producer - */ +import './types'; /** * Produces a pair of objects, which allow a service to produce a stream of * update promises. * + * The initial state argument has to be truly optional even though it can + * be any first class value including `undefined`. We need to distinguish the + * presence vs the absence of it, which we cannot do with the optional argument + * syntax. Rather we use the arity of the arguments array. + * + * If no initial state is provided to `makeNotifierKit`, then it starts without + * an initial state. Its initial state will instead be the state of the first + * update. + * * @template T the type of the notifier state - * @param {T[]} args the first state to be returned + * @param {[] | [T]} args the first state to be returned * @returns {NotifierRecord} the notifier and updater */ -// The initial state argument has to be truly optional even though it can -// be any first class value including `undefined`. We need to distinguish the -// presence vs the absence of it, which we cannot do with the optional argument -// syntax. Rather we use the arity of the arguments array. -// -// If no initial state is provided to `makeNotifierKit`, then it starts without -// an initial state. Its initial state will instead be the state of the first -// update. export const makeNotifierKit = (...args) => { /** @type {PromiseRecord>|undefined} */ let nextPromiseKit = producePromise(); @@ -91,82 +38,83 @@ export const makeNotifierKit = (...args) => { const final = () => currentUpdateCount === undefined; - if (args.length >= 1) { - // start as hasState() && !final() - currentResponse = harden({ - value: args[0], - updateCount: currentUpdateCount, - }); - } - // else start as !hasState() && !final() - - // NaN matches nothing - function getUpdateSince(updateCount = NaN) { - if ( - hasState() && - (final() || - (currentResponse && currentResponse.updateCount !== updateCount)) - ) { - // If hasState() and either it is final() or it is - // not the state of updateCount, return the current state. - return Promise.resolve(currentResponse); - } - // otherwise return a promise for the next state. - assert(nextPromiseKit); - return nextPromiseKit.promise; - } + const baseNotifier = harden({ + // NaN matches nothing + getUpdateSince(updateCount = NaN) { + if ( + hasState() && + (final() || + (currentResponse && currentResponse.updateCount !== updateCount)) + ) { + // If hasState() and either it is final() or it is + // not the state of updateCount, return the current state. + assert(currentResponse !== undefined); + return Promise.resolve(currentResponse); + } + // otherwise return a promise for the next state. + assert(nextPromiseKit); + return nextPromiseKit.promise; + }, + }); - function updateState(state) { - if (final()) { - throw new Error('Cannot update state after termination.'); - } - - // become hasState() && !final() - assert(nextPromiseKit && currentUpdateCount); - currentUpdateCount += 1; - currentResponse = harden({ - value: state, - updateCount: currentUpdateCount, - }); - nextPromiseKit.resolve(currentResponse); - nextPromiseKit = producePromise(); - } + const asyncIterable = makeAsyncIterableFromNotifier(baseNotifier); - function finish(finalState) { - if (final()) { - throw new Error('Cannot finish after termination.'); - } - - // become hasState() && final() - assert(nextPromiseKit); - currentUpdateCount = undefined; - currentResponse = harden({ - value: finalState, - updateCount: currentUpdateCount, - }); - nextPromiseKit.resolve(currentResponse); - nextPromiseKit = undefined; - } + const notifier = harden({ + ...baseNotifier, + ...asyncIterable, + }); - function reject(reason) { - if (final()) { - throw new Error('Cannot reject after termination.'); - } + const updater = harden({ + updateState(state) { + if (final()) { + throw new Error('Cannot update state after termination.'); + } + + // become hasState() && !final() + assert(nextPromiseKit && currentUpdateCount); + currentUpdateCount += 1; + currentResponse = harden({ + value: state, + updateCount: currentUpdateCount, + }); + nextPromiseKit.resolve(currentResponse); + nextPromiseKit = producePromise(); + }, + + finish(finalState) { + if (final()) { + throw new Error('Cannot finish after termination.'); + } + + // become hasState() && final() + assert(nextPromiseKit); + currentUpdateCount = undefined; + currentResponse = harden({ + value: finalState, + updateCount: currentUpdateCount, + }); + nextPromiseKit.resolve(currentResponse); + nextPromiseKit = undefined; + }, + + fail(reason) { + if (final()) { + throw new Error('Cannot fail after termination.'); + } + + // become !hasState() && final() + assert(nextPromiseKit); + currentUpdateCount = undefined; + currentResponse = undefined; + nextPromiseKit.reject(reason); + }, + }); - // become !hasState() && final() - assert(nextPromiseKit); - currentUpdateCount = undefined; - currentResponse = undefined; - nextPromiseKit.reject(reason); + if (args.length >= 1) { + updater.updateState(args[0]); } - // notifier facet is separate so it can be handed out loosely while updater + // notifier facet is separate so it can be handed out while updater // is tightly held - const notifier = harden({ getUpdateSince }); - const updater = harden({ - updateState, - finish, - reject, - }); return harden({ notifier, updater }); }; diff --git a/packages/notifier/src/types.js b/packages/notifier/src/types.js new file mode 100644 index 00000000000..1859e259679 --- /dev/null +++ b/packages/notifier/src/types.js @@ -0,0 +1,69 @@ +/** + * @template T + * @typedef {T | PromiseLike} PromiseOrNot + */ + +/** + * @template T + * @typedef {import('@agoric/produce-promise').PromiseRecord} PromiseRecord + */ + +/** + * @typedef {number | undefined} UpdateCount a value used to mark the position + * in the update stream. For the last state, the updateCount is undefined. + */ + +/** + * @template T the type of the state value + * @typedef {Object} UpdateRecord + * @property {T} value is whatever state the service wants to publish + * @property {UpdateCount} updateCount is a value that identifies the update + */ + +/** + * @template T the type of the notifier state + * @callback GetUpdateSince Can be called repeatedly to get a sequence of + * update records + * @param {UpdateCount} [updateCount] return update record as of an update + * count. If the `updateCount` argument is omitted or differs from the current + * update count, return the current record. + * Otherwise, after the next state change, the promise will resolve to the + * then-current value of the record. + * @returns {Promise>} resolves to the corresponding + * update + */ + +/** + * @template T the type of the notifier state + * @typedef {Object} BaseNotifier an object that can be used to get the current + * state or updates + * @property {GetUpdateSince} getUpdateSince return update record as of an + * update count. + */ + +/** + * @template T the type of the notifier state + * @typedef {BaseNotifier & AsyncIterable} Notifier an object that can + * be used to get the current state or updates + */ + +/** + * @template T the type of the notifier state + * @typedef {Object} Updater an object that should be closely held, as + * anyone with access to + * it can provide updates + * @property {(state: T) => void} updateState sets the new state, and resolves + * the outstanding promise to send an update + * @property {(finalState: T) => void} finish sets the final state, sends a + * final update, and freezes the + * updater + * @property {(reason: T) => void} fail the stream becomes erroneously + * terminated, allegedly for the stated reason. + */ + +/** + * @template T the type of the notifier state + * @typedef {Object} NotifierRecord the produced notifier/updater pair + * @property {Notifier} notifier the (widely-held) notifier consumer + * @property {Updater} updater the (closely-held) notifier producer + */ diff --git a/packages/notifier/test/test-notifier-adaptor.js b/packages/notifier/test/test-notifier-adaptor.js new file mode 100644 index 00000000000..1ce30954ed0 --- /dev/null +++ b/packages/notifier/test/test-notifier-adaptor.js @@ -0,0 +1,203 @@ +// @ts-check +import '@agoric/install-ses'; +import test from 'tape-promise/tape'; +import { + makeAsyncIterableFromNotifier, + makeNotifierFromAsyncIterable, + updateFromIterable, + updateFromNotifier, +} from '../src/asyncIterableAdaptor'; + +const obj = harden({}); +const unresP = new Promise(_ => {}); +const rejP = Promise.reject(new Error('foo')); +rejP.catch(_ => {}); // Suppress Node UnhandledPromiseRejectionWarning +const payloads = harden([1, -0, undefined, NaN, obj, unresP, rejP, null]); + +const refReason = new Error('bar'); +const refResult = harden({}); + +const makeIterable = fails => { + return harden({ + [Symbol.asyncIterator]() { + let i = 0; + return harden({ + next() { + if (i < payloads.length) { + const value = payloads[i]; + i += 1; + return Promise.resolve(harden({ value, done: false })); + } + if (fails) { + return Promise.reject(refReason); + } + return Promise.resolve(harden({ value: refResult, done: true })); + }, + }); + }, + }); +}; + +const finiteStream = makeIterable(false); +const explodingStream = makeIterable(true); + +const testEnding = (t, p, fails) => { + return Promise.resolve(p).then( + result => { + t.equal(fails, false); + t.equal(result, refResult); + return t.end(); + }, + reason => { + t.equal(fails, true); + t.equal(reason, refReason); + return t.end(); + }, + ); +}; + +const skip = (i, value, lossy) => { + if (!lossy) { + return i; + } + while (i < payloads.length && !Object.is(value, payloads[i])) { + i += 1; + } + return i; +}; + +const testManualConsumer = (t, iterable, lossy) => { + const iterator = iterable[Symbol.asyncIterator](); + const testLoop = i => { + return iterator.next().then( + ({ value, done }) => { + if (done) { + t.equal(i, payloads.length); + return value; + } + i = skip(i, value, lossy); + t.assert(i < payloads.length); + // Need precise equality + t.assert(Object.is(value, payloads[i])); + return testLoop(i + 1); + }, + reason => { + t.equal(i, payloads.length); + throw reason; + }, + ); + }; + return testLoop(0); +}; + +const testAutoConsumer = async (t, iterable, lossy) => { + let i = 0; + try { + for await (const value of iterable) { + i = skip(i, value, lossy); + t.assert(i < payloads.length); + // Need precise equality + t.assert(Object.is(value, payloads[i])); + i += 1; + } + } finally { + t.equal(i, payloads.length); + } + // The for-await-of loop cannot observe the final value of the iterator + // so this consumer cannot test what that was. Just return what testEnding + // expects. + return refResult; +}; + +const makeTestUpdater = (t, lossy, fails) => { + let i = 0; + return harden({ + updateState(newState) { + i = skip(i, newState, lossy); + t.assert(i < payloads.length); + // Need precise equality + t.assert(Object.is(newState, payloads[i])); + i += 1; + }, + finish(finalState) { + t.equal(fails, false); + t.equal(finalState, refResult); + return t.end(); + }, + fail(reason) { + t.equal(fails, true); + t.equal(reason, refReason); + return t.end(); + }, + }); +}; + +test('async iterator - manual finishes', async t => { + const p = testManualConsumer(t, finiteStream, false); + return testEnding(t, p, false); +}); + +test('async iterator - manual fails', async t => { + const p = testManualConsumer(t, explodingStream, false); + return testEnding(t, p, true); +}); + +test('async iterator - for await finishes', async t => { + const p = testAutoConsumer(t, finiteStream, false); + return testEnding(t, p, false); +}); + +test('async iterator - for await fails', async t => { + const p = testAutoConsumer(t, explodingStream, false); + return testEnding(t, p, true); +}); + +test('notifier adaptor - manual finishes', async t => { + const n = makeNotifierFromAsyncIterable(finiteStream); + const finiteUpdates = makeAsyncIterableFromNotifier(n); + const p = testManualConsumer(t, finiteUpdates, true); + return testEnding(t, p, false); +}); + +test('notifier adaptor - manual fails', async t => { + const n = makeNotifierFromAsyncIterable(explodingStream); + const explodingUpdates = makeAsyncIterableFromNotifier(n); + const p = testManualConsumer(t, explodingUpdates, true); + return testEnding(t, p, true); +}); + +test('notifier adaptor - for await finishes', async t => { + const n = makeNotifierFromAsyncIterable(finiteStream); + const finiteUpdates = makeAsyncIterableFromNotifier(n); + const p = testAutoConsumer(t, finiteUpdates, true); + return testEnding(t, p, false); +}); + +test('notifier adaptor - for await fails', async t => { + const n = makeNotifierFromAsyncIterable(explodingStream); + const explodingUpdates = makeAsyncIterableFromNotifier(n); + const p = testAutoConsumer(t, explodingUpdates, true); + return testEnding(t, p, true); +}); + +test('notifier adaptor - update from iterator finishes', t => { + const u = makeTestUpdater(t, false, false); + return updateFromIterable(u, finiteStream); +}); + +test('notifier adaptor - update from iterator fails', t => { + const u = makeTestUpdater(t, false, true); + return updateFromIterable(u, explodingStream); +}); + +test('notifier adaptor - update from notifier finishes', t => { + const u = makeTestUpdater(t, true, false); + const n = makeNotifierFromAsyncIterable(finiteStream); + return updateFromNotifier(u, n); +}); + +test('notifier adaptor - update from notifier fails', t => { + const u = makeTestUpdater(t, true, true); + const n = makeNotifierFromAsyncIterable(explodingStream); + return updateFromNotifier(u, n); +}); diff --git a/packages/notifier/test/test-notifier.js b/packages/notifier/test/test-notifier.js index e0c11f6da32..d75e6dd182d 100644 --- a/packages/notifier/test/test-notifier.js +++ b/packages/notifier/test/test-notifier.js @@ -1,13 +1,10 @@ // @ts-check -// eslint-disable-next-line import/no-extraneous-dependencies import '@agoric/install-ses'; -import { test } from 'tape-promise/tape'; + +import test from 'tape-promise/tape'; import { makeNotifierKit } from '../src/notifier'; -/** - * @template T - * @typedef {import('../src/notifier').NotifierRecord} NotifierRecord - */ +import '../src/types'; test('notifier - initial state', async t => { /** @type {NotifierRecord<1>} */ @@ -15,7 +12,7 @@ test('notifier - initial state', async t => { updater.updateState(1); const updateDeNovo = await notifier.getUpdateSince(); - const updateFromNonExistent = await notifier.getUpdateSince({}); + const updateFromNonExistent = await notifier.getUpdateSince(); t.equals(updateDeNovo.value, 1, 'state is one'); t.deepEquals(updateDeNovo, updateFromNonExistent, 'no param same as unknown'); diff --git a/packages/zoe/src/contractFacet.js b/packages/zoe/src/contractFacet.js index 1a47f45bc42..8399c92f837 100644 --- a/packages/zoe/src/contractFacet.js +++ b/packages/zoe/src/contractFacet.js @@ -259,7 +259,7 @@ export function buildRootObject(_vatPowers) { const ignoringUpdater = harden({ updateState: _ => {}, finish: _ => {}, - reject: _ => {}, + fail: _ => {}, }); /** @type {Omit} */ diff --git a/packages/zoe/src/external-types.js b/packages/zoe/src/external-types.js new file mode 100644 index 00000000000..cb8b400c3bf --- /dev/null +++ b/packages/zoe/src/external-types.js @@ -0,0 +1 @@ +import '@agoric/notifier/exports'; diff --git a/packages/zoe/src/internal-types.js b/packages/zoe/src/internal-types.js index e16678d420a..2dd824161f3 100644 --- a/packages/zoe/src/internal-types.js +++ b/packages/zoe/src/internal-types.js @@ -13,16 +13,6 @@ * @typedef {import('@agoric/produce-promise').PromiseRecord} PromiseRecord */ -/** - * @template T - * @typedef {import('@agoric/notifier').Updater} Updater - */ - -/** - * @template T - * @typedef {import('@agoric/notifier').NotifierRecord} NotifierRecord - */ - /** * @typedef {Object} ZcfForZoe * The facet ZCF presents to Zoe. diff --git a/packages/zoe/src/types.js b/packages/zoe/src/types.js index e47465ef999..3413fe8e1c3 100644 --- a/packages/zoe/src/types.js +++ b/packages/zoe/src/types.js @@ -8,11 +8,6 @@ * actual value is just an empty object. */ -/** - * @template T - * @typedef {import('@agoric/notifier').Notifier} Notifier - */ - /** * @typedef {string} Keyword * @typedef {Handle<'InstallationHandle'>} InstallationHandle - an opaque handle for an bundle installation