@agoric/swingset-liveslots 0.10.3-dev-ee2126b.0 → 0.10.3-dev-c7eca9b.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agoric/swingset-liveslots",
3
- "version": "0.10.3-dev-ee2126b.0+ee2126b",
3
+ "version": "0.10.3-dev-c7eca9b.0+c7eca9b",
4
4
  "description": "SwingSet ocap support layer",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -17,8 +17,8 @@
17
17
  "lint:eslint": "eslint ."
18
18
  },
19
19
  "dependencies": {
20
- "@agoric/internal": "0.3.3-dev-ee2126b.0+ee2126b",
21
- "@agoric/store": "0.9.3-dev-ee2126b.0+ee2126b",
20
+ "@agoric/internal": "0.3.3-dev-c7eca9b.0+c7eca9b",
21
+ "@agoric/store": "0.9.3-dev-c7eca9b.0+c7eca9b",
22
22
  "@endo/env-options": "^1.1.4",
23
23
  "@endo/errors": "^1.2.2",
24
24
  "@endo/eventual-send": "^1.2.2",
@@ -32,7 +32,7 @@
32
32
  "@endo/promise-kit": "^1.1.2"
33
33
  },
34
34
  "devDependencies": {
35
- "@agoric/kmarshal": "0.1.1-dev-ee2126b.0+ee2126b",
35
+ "@agoric/kmarshal": "0.1.1-dev-c7eca9b.0+c7eca9b",
36
36
  "ava": "^5.3.0"
37
37
  },
38
38
  "files": [
@@ -68,5 +68,5 @@
68
68
  "typeCoverage": {
69
69
  "atLeast": 75.35
70
70
  },
71
- "gitHead": "ee2126bcb2267010a840679a9cc52c848ed07b70"
71
+ "gitHead": "c7eca9bba1bb4ca361c716570ef2ee4bc496beb0"
72
72
  }
package/src/liveslots.js CHANGED
@@ -752,32 +752,43 @@ function build(
752
752
  if (facet !== undefined) {
753
753
  result = vrm.getFacet(id, val, facet);
754
754
  }
755
- } else {
755
+ } else if (type === 'object') {
756
+ // Note: an abandonned (e.g. by an upgrade) exported ephemeral or virtual
757
+ // object would appear as an import if re-introduced. In the future we
758
+ // may need to change that if we want to keep recognizing such references
759
+ // In that case we'd need to create an imported presence for these
760
+ // unknown vrefs allocated by the vat.
761
+ // See https://github.com/Agoric/agoric-sdk/issues/9746
756
762
  !allocatedByVat || Fail`I don't remember allocating ${slot}`;
757
- if (type === 'object') {
758
- // this is a new import value
759
- val = makeImportedPresence(slot, iface);
760
- } else if (type === 'promise') {
761
- const pRec = makePipelinablePromise(slot);
762
- importedVPIDs.set(slot, pRec);
763
- val = pRec.promise;
764
- // ideally we'd wait until .then is called on p before subscribing,
765
- // but the current Promise API doesn't give us a way to discover
766
- // this, so we must subscribe right away. If we were using Vows or
767
- // some other then-able, we could just hook then() to notify us.
768
- if (importedPromises) {
769
- // leave the subscribe() up to dispatch.notify()
770
- importedPromises.add(slot);
771
- } else {
772
- // probably in dispatch.deliver(), so subscribe now
773
- syscall.subscribe(slot);
774
- }
775
- } else if (type === 'device') {
776
- val = makeDeviceNode(slot, iface);
777
- importedDevices.add(val);
763
+ // this is a new import value
764
+ val = makeImportedPresence(slot, iface);
765
+ } else if (type === 'promise') {
766
+ // We unconditionally create a promise record, even if the promise looks
767
+ // like it was allocated by us. This can happen when re-importing a
768
+ // promise created by the previous incarnation. We may or may not have
769
+ // been the decider of the promise. If we were, the kernel will be
770
+ // rejecting the promise on our behalf. We may have previously been
771
+ // subscribed to that promise, but subscription is idempotent.
772
+ const pRec = makePipelinablePromise(slot);
773
+ importedVPIDs.set(slot, pRec);
774
+ val = pRec.promise;
775
+ // ideally we'd wait until .then is called on p before subscribing,
776
+ // but the current Promise API doesn't give us a way to discover
777
+ // this, so we must subscribe right away. If we were using Vows or
778
+ // some other then-able, we could just hook then() to notify us.
779
+ if (importedPromises) {
780
+ // leave the subscribe() up to dispatch.notify()
781
+ importedPromises.add(slot);
778
782
  } else {
779
- Fail`unrecognized slot type '${type}'`;
783
+ // probably in dispatch.deliver(), so subscribe now
784
+ syscall.subscribe(slot);
780
785
  }
786
+ } else if (type === 'device') {
787
+ !allocatedByVat || Fail`unexpected device ${slot} allocated by vat`;
788
+ val = makeDeviceNode(slot, iface);
789
+ importedDevices.add(val);
790
+ } else {
791
+ Fail`unrecognized slot type '${type}'`;
781
792
  }
782
793
  registerValue(baseRef, val, facet !== undefined);
783
794
  if (!result) {
@@ -790,7 +801,25 @@ function build(
790
801
  meterControl.assertNotMetered();
791
802
  const { type } = parseVatSlot(slot);
792
803
  type === 'promise' || Fail`revivePromise called on non-promise ${slot}`;
793
- !getValForSlot(slot) || Fail`revivePromise called on pre-existing ${slot}`;
804
+ const val = getValForSlot(slot);
805
+ if (val) {
806
+ // revivePromise is only called by loadWatchedPromiseTable, which runs
807
+ // after buildRootObject(), which is given the deserialized vatParameters.
808
+ // The only way revivePromise() might encounter a pre-existing vpid is if
809
+ // these vatParameters include a promise that the previous incarnation
810
+ // watched, but that `buildRootObject` in the new incarnation didn't
811
+ // explicitly watch again. This can be either a previously imported
812
+ // promise, or a promise the previous incarnation exported, regardless of
813
+ // who the decider now is.
814
+ //
815
+ // In that case convertSlotToVal() has already deserialized the vpid, but
816
+ // since `buildRootObject` didn't explicitely call watchPromise on it, no
817
+ // registration exists so loadWatchedPromiseTable attempts to revive the
818
+ // promise.
819
+ return val;
820
+ }
821
+ // NOTE: it is important that this code not do anything *more*
822
+ // than what convertSlotToVal(vpid) would do
794
823
  const pRec = makePipelinablePromise(slot);
795
824
  importedVPIDs.set(slot, pRec);
796
825
  const p = pRec.promise;
@@ -1593,12 +1622,10 @@ function build(
1593
1622
  } else if (delivery[0] === 'stopVat') {
1594
1623
  return meterControl.runWithoutMeteringAsync(() => stopVat(delivery[1]));
1595
1624
  } else {
1596
- let complete = false;
1597
1625
  // Start user code running, record any internal liveslots errors. We do
1598
1626
  // *not* directly wait for the userspace function to complete, nor for
1599
1627
  // any promise it returns to fire.
1600
1628
  const p = Promise.resolve(delivery).then(unmeteredDispatch);
1601
- void p.finally(() => (complete = true));
1602
1629
 
1603
1630
  // Instead, we wait for userspace to become idle by draining the
1604
1631
  // promise queue. We clean up and then examine/return 'p' so any
@@ -1609,10 +1636,11 @@ function build(
1609
1636
  return gcTools.waitUntilQuiescent().then(() => {
1610
1637
  afterDispatchActions();
1611
1638
  // eslint-disable-next-line prefer-promise-reject-errors
1612
- return complete ? p : Promise.reject('buildRootObject unresolved');
1639
+ return Promise.race([p, Promise.reject('buildRootObject unresolved')]);
1613
1640
  // the only delivery that pays attention to a user-provided
1614
1641
  // Promise is startVat, so the error message is specialized to
1615
- // the only user problem that could cause complete===false
1642
+ // the only user problem that could cause the promise to not be
1643
+ // settled.
1616
1644
  });
1617
1645
  }
1618
1646
  }
@@ -139,6 +139,12 @@ export function makeWatchedPromiseManager({
139
139
  */
140
140
  function loadWatchedPromiseTable(revivePromise) {
141
141
  for (const vpid of watchedPromiseTable.keys()) {
142
+ if (promiseRegistrations.has(vpid)) {
143
+ // We're only interested in reconnecting the promises from the previous
144
+ // incarnation. Any promise watched during buildRootObject would have
145
+ // already created a registration.
146
+ continue;
147
+ }
142
148
  const p = revivePromise(vpid);
143
149
  promiseRegistrations.init(vpid, p);
144
150
  pseudoThen(p, vpid);
@@ -84,7 +84,7 @@ const makeExoUtils = VatData => {
84
84
  };
85
85
 
86
86
  // cf. packages/SwingSet/test/vat-durable-promise-watcher.js
87
- const buildPromiseWatcherRootObject = (vatPowers, _vatParameters, baggage) => {
87
+ const buildPromiseWatcherRootObject = (vatPowers, vatParameters, baggage) => {
88
88
  const { VatData } = vatPowers;
89
89
  const { watchPromise } = VatData;
90
90
  const { prepareExo } = makeExoUtils(VatData);
@@ -93,37 +93,62 @@ const buildPromiseWatcherRootObject = (vatPowers, _vatParameters, baggage) => {
93
93
  onFulfilled: M.call(M.any(), M.string()).returns(),
94
94
  onRejected: M.call(M.any(), M.string()).returns(),
95
95
  });
96
+ const watchResolutions = new Map();
96
97
  const watcher = prepareExo(
97
98
  baggage,
99
+ // No longer ignoring, but the name is set in stone in `kvStoreDataV1`
98
100
  'DurablePromiseIgnorer',
99
101
  PromiseWatcherI,
100
102
  {
101
- onFulfilled(_value, _name) {},
102
- onRejected(_reason, _name) {},
103
+ onFulfilled(value, name) {
104
+ watchResolutions.set(name, { status: 'fulfilled', value });
105
+ },
106
+ onRejected(reason, name) {
107
+ watchResolutions.set(name, { status: 'rejected', reason });
108
+ },
103
109
  },
104
110
  );
105
111
 
106
- const localPromises = new Map();
112
+ const knownPromises = new Map();
107
113
 
108
- return Far('root', {
109
- exportPromise: () => [Promise.resolve()],
114
+ const root = Far('root', {
115
+ getPromise: name => {
116
+ const promise = knownPromises.get(name);
117
+ promise || Fail`promise doesn't exists: ${name}`;
118
+ return { promise };
119
+ },
120
+ importPromise: (name, promise) => {
121
+ !knownPromises.has(name) || Fail`promise already exists: ${name}`;
122
+ knownPromises.set(name, promise);
123
+ return `imported promise: ${name}`;
124
+ },
110
125
  createLocalPromise: (name, fulfillment, rejection) => {
111
- !localPromises.has(name) || Fail`local promise already exists: ${name}`;
126
+ !knownPromises.has(name) || Fail`promise already exists: ${name}`;
112
127
  const { promise, resolve, reject } = makePromiseKit();
113
128
  if (fulfillment !== undefined) {
114
129
  resolve(fulfillment);
115
130
  } else if (rejection !== undefined) {
116
131
  reject(rejection);
117
132
  }
118
- localPromises.set(name, promise);
133
+ knownPromises.set(name, promise);
119
134
  return `created local promise: ${name}`;
120
135
  },
121
- watchLocalPromise: name => {
122
- localPromises.has(name) || Fail`local promise not found: ${name}`;
123
- watchPromise(localPromises.get(name), watcher, name);
124
- return `watched local promise: ${name}`;
136
+ watchPromise: name => {
137
+ knownPromises.has(name) || Fail`promise not found: ${name}`;
138
+ watchPromise(knownPromises.get(name), watcher, name);
139
+ return `watched promise: ${name}`;
140
+ },
141
+ getWatchResolution: name => {
142
+ return watchResolutions.get(name);
125
143
  },
126
144
  });
145
+
146
+ const startOperations = vatParameters?.startOperations || [];
147
+ for (const [method, ...args] of startOperations) {
148
+ root[method](...args);
149
+ }
150
+
151
+ return root;
127
152
  };
128
153
  const kvStoreDataV1 = Object.entries({
129
154
  baggageID: 'o+d6/1',
@@ -176,23 +201,55 @@ const kvStoreDataV1VpidsToKeep = ['p-8'];
176
201
  const kvStoreDataV1KeysToKeep = ['vc.4.sp-8'];
177
202
 
178
203
  test('past-incarnation watched promises', async t => {
204
+ const S = 'settlement';
205
+ // Anchor promise counters upon which the other assertions depend.
206
+ const firstPImport = 9;
207
+ // cf. src/liveslots.js:initialIDCounters
208
+ const firstPExport = 5;
209
+
210
+ const startImportedP = firstPImport - 2;
211
+
212
+ const v1StartOperations = [
213
+ ['createLocalPromise', 'start orphaned'],
214
+ ['watchPromise', 'start orphaned'],
215
+ ['createLocalPromise', 'start fulfilled', S],
216
+ ['watchPromise', 'start fulfilled'],
217
+ ['importPromise', 'start imported', kslot(`p-${startImportedP}`)],
218
+ ['watchPromise', 'start imported'],
219
+ ];
179
220
  const kvStore = new Map();
180
- let { v, dispatch, dispatchMessage } = await setupTestLiveslots(
221
+ let {
222
+ v,
223
+ dispatch,
224
+ dispatchMessage: rawDispatch,
225
+ } = await setupTestLiveslots(
181
226
  t,
182
227
  buildPromiseWatcherRootObject,
183
228
  'durable-promise-watcher',
184
- { kvStore },
229
+ {
230
+ kvStore,
231
+ nextPromiseImportNumber: firstPImport,
232
+ vatParameters: { startOperations: v1StartOperations },
233
+ },
185
234
  );
186
235
  let vatLogs = v.log;
187
236
 
188
- // Anchor promise counters upon which the other assertions depend.
189
- const firstPImport = 1;
190
- // cf. src/liveslots.js:initialIDCounters
191
- const firstPExport = 5;
192
237
  let lastPImport = firstPImport - 1;
193
238
  let lastPExport = firstPExport - 1;
239
+ let dispatches = 0;
240
+ const exportedPromises = new Set();
241
+ const v1OrphanedPExports = [];
194
242
  const nextPImport = () => (lastPImport += 1);
195
243
  const nextPExport = () => (lastPExport += 1);
244
+ /** @type {typeof rawDispatch} */
245
+ const dispatchMessage = (...args) => {
246
+ dispatches += 1;
247
+ return rawDispatch(...args);
248
+ };
249
+ const recordExportedPromise = name => {
250
+ exportedPromises.add(name);
251
+ return name;
252
+ };
196
253
  // Ignore vatstore syscalls.
197
254
  const getDispatchLogs = () =>
198
255
  vatLogs.splice(0).filter(m => !m.type.startsWith('vatstore'));
@@ -208,18 +265,55 @@ test('past-incarnation watched promises', async t => {
208
265
  type: 'subscribe',
209
266
  target: vpid,
210
267
  });
211
- vatLogs.length = 0;
212
- await dispatchMessage('exportPromise');
268
+
269
+ // startVat logs
270
+ v1OrphanedPExports.push(nextPExport());
271
+ recordExportedPromise('start orphaned');
272
+ v1OrphanedPExports.push(nextPExport());
273
+ recordExportedPromise('start fulfilled');
213
274
  t.deepEqual(getDispatchLogs(), [
214
- fulfillmentMessage(`p-${nextPImport()}`, [kslot(`p+${nextPExport()}`)]),
215
- fulfillmentMessage(`p+${lastPExport}`, undefined),
275
+ subscribeMessage(`p-${startImportedP}`),
276
+ subscribeMessage(`p+${lastPExport - 1}`),
277
+ subscribeMessage(`p+${lastPExport}`),
278
+ fulfillmentMessage(`p+${lastPExport}`, S),
279
+ ]);
280
+ await dispatchMessage('createLocalPromise', 'exported', S);
281
+ t.deepEqual(getDispatchLogs(), [
282
+ fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: exported'),
283
+ ]);
284
+ await dispatchMessage('getPromise', recordExportedPromise('exported'));
285
+ t.deepEqual(getDispatchLogs(), [
286
+ fulfillmentMessage(`p-${nextPImport()}`, {
287
+ promise: kslot(`p+${nextPExport()}`),
288
+ }),
289
+ fulfillmentMessage(`p+${lastPExport}`, S),
290
+ ]);
291
+ const importedP = firstPImport - 1;
292
+ await dispatchMessage('importPromise', 'imported', kslot(`p-${importedP}`));
293
+ t.deepEqual(getDispatchLogs(), [
294
+ subscribeMessage(`p-${importedP}`),
295
+ fulfillmentMessage(`p-${nextPImport()}`, 'imported promise: imported'),
216
296
  ]);
217
-
218
- const S = 'settlement';
219
297
  await dispatchMessage('createLocalPromise', 'orphaned');
220
298
  t.deepEqual(getDispatchLogs(), [
221
299
  fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: orphaned'),
222
300
  ]);
301
+ await dispatchMessage('createLocalPromise', 'orphaned exported');
302
+ await dispatchMessage(
303
+ 'getPromise',
304
+ recordExportedPromise('orphaned exported'),
305
+ );
306
+ const orphanedExportedP = nextPExport();
307
+ v1OrphanedPExports.push(orphanedExportedP);
308
+ t.deepEqual(getDispatchLogs(), [
309
+ fulfillmentMessage(
310
+ `p-${nextPImport()}`,
311
+ 'created local promise: orphaned exported',
312
+ ),
313
+ fulfillmentMessage(`p-${nextPImport()}`, {
314
+ promise: kslot(`p+${orphanedExportedP}`),
315
+ }),
316
+ ]);
223
317
  await dispatchMessage('createLocalPromise', 'fulfilled', S);
224
318
  t.deepEqual(getDispatchLogs(), [
225
319
  fulfillmentMessage(
@@ -233,68 +327,185 @@ test('past-incarnation watched promises', async t => {
233
327
  ]);
234
328
  t.is(
235
329
  lastPImport - firstPImport + 1,
236
- 4,
237
- 'imported 4 promises (1 per dispatch)',
330
+ dispatches,
331
+ `imported ${dispatches} promises (1 per dispatch)`,
332
+ );
333
+ t.is(
334
+ lastPExport - firstPExport + 1,
335
+ exportedPromises.size,
336
+ `exported ${exportedPromises.size} promises: ${[...exportedPromises].join(', ')}`,
238
337
  );
239
- t.is(lastPExport - firstPExport + 1, 1, 'exported 1 promise: first');
240
338
 
241
- await dispatchMessage('watchLocalPromise', 'orphaned');
339
+ await dispatchMessage('watchPromise', recordExportedPromise('orphaned'));
340
+ v1OrphanedPExports.push(nextPExport());
242
341
  t.deepEqual(getDispatchLogs(), [
243
- subscribeMessage(`p+${nextPExport()}`),
244
- fulfillmentMessage(`p-${nextPImport()}`, 'watched local promise: orphaned'),
342
+ subscribeMessage(`p+${lastPExport}`),
343
+ fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: orphaned'),
245
344
  ]);
246
- await dispatchMessage('watchLocalPromise', 'fulfilled');
345
+ await dispatchMessage('watchPromise', recordExportedPromise('fulfilled'));
247
346
  t.deepEqual(getDispatchLogs(), [
248
347
  subscribeMessage(`p+${nextPExport()}`),
249
- fulfillmentMessage(
250
- `p-${nextPImport()}`,
251
- 'watched local promise: fulfilled',
252
- ),
348
+ fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: fulfilled'),
253
349
  fulfillmentMessage(`p+${lastPExport}`, S),
254
350
  ]);
255
- await dispatchMessage('watchLocalPromise', 'rejected');
351
+ await dispatchMessage('watchPromise', recordExportedPromise('rejected'));
256
352
  t.deepEqual(getDispatchLogs(), [
257
353
  subscribeMessage(`p+${nextPExport()}`),
258
- fulfillmentMessage(`p-${nextPImport()}`, 'watched local promise: rejected'),
354
+ fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: rejected'),
259
355
  rejectionMessage(`p+${lastPExport}`, S),
260
356
  ]);
357
+ await dispatchMessage('watchPromise', 'imported');
358
+ t.deepEqual(getDispatchLogs(), [
359
+ // no subscribe, we already did at import
360
+ fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: imported'),
361
+ ]);
362
+ await dispatchMessage('getWatchResolution', 'fulfilled');
363
+ t.deepEqual(getDispatchLogs(), [
364
+ fulfillmentMessage(`p-${nextPImport()}`, {
365
+ status: 'fulfilled',
366
+ value: S,
367
+ }),
368
+ ]);
369
+ await dispatchMessage('getWatchResolution', 'rejected');
370
+ t.deepEqual(getDispatchLogs(), [
371
+ fulfillmentMessage(`p-${nextPImport()}`, {
372
+ status: 'rejected',
373
+ reason: S,
374
+ }),
375
+ ]);
376
+ await dispatchMessage('getWatchResolution', 'start fulfilled');
377
+ t.deepEqual(getDispatchLogs(), [
378
+ fulfillmentMessage(`p-${nextPImport()}`, {
379
+ status: 'fulfilled',
380
+ value: S,
381
+ }),
382
+ ]);
383
+
261
384
  t.is(
262
385
  lastPImport - firstPImport + 1,
263
- 7,
264
- 'imported 7 promises (1 per dispatch)',
386
+ dispatches,
387
+ `imported ${dispatches} promises (1 per dispatch)`,
265
388
  );
266
389
  t.is(
267
390
  lastPExport - firstPExport + 1,
268
- 4,
269
- 'exported 4 promises: first, orphaned, fulfilled, rejected',
391
+ exportedPromises.size,
392
+ `exported ${exportedPromises.size} promises: ${[...exportedPromises].join(', ')}`,
270
393
  );
271
394
 
272
395
  // Simulate upgrade by starting from the non-empty kvStore.
273
396
  // t.log(Object.fromEntries([...kvStore.entries()].sort(compareEntriesByKey)));
274
397
  const clonedStore = new Map(kvStore);
275
- ({ v, dispatch, dispatchMessage } = await setupTestLiveslots(
398
+ const startImported2P = firstPImport - 3;
399
+ const v2StartOperations = [
400
+ ['importPromise', 'start imported 2', kslot(`p-${startImported2P}`)], // import of new promise
401
+ ['importPromise', 'imported', kslot(`p-${importedP}`)], // import previously imported and watched promise
402
+ ['importPromise', 'orphaned exported', kslot(`p+${orphanedExportedP}`)], // import previously exported but unwatched promise
403
+ ['watchPromise', 'orphaned exported'],
404
+ ];
405
+ ({
406
+ v,
407
+ dispatch,
408
+ dispatchMessage: rawDispatch,
409
+ } = await setupTestLiveslots(
276
410
  t,
277
411
  buildPromiseWatcherRootObject,
278
412
  'durable-promise-watcher-v2',
279
- { kvStore: clonedStore, nextPromiseImportNumber: lastPImport + 1 },
413
+ {
414
+ kvStore: clonedStore,
415
+ nextPromiseImportNumber: lastPImport + 1,
416
+ vatParameters: { startOperations: v2StartOperations },
417
+ },
280
418
  ));
281
419
  vatLogs = v.log;
282
420
 
421
+ // startVat logs
422
+ t.deepEqual(getDispatchLogs(), [
423
+ subscribeMessage(`p-${startImported2P}`),
424
+ subscribeMessage(`p-${importedP}`),
425
+ subscribeMessage(`p+${orphanedExportedP}`),
426
+ ]);
283
427
  // Simulate kernel rejection of promises orphaned by termination/upgrade of their decider vat.
284
428
  const expectedDeletions = [...clonedStore.entries()].filter(entry =>
285
429
  entry[1].includes('orphaned'),
286
430
  );
287
431
  t.true(expectedDeletions.length >= 1);
288
- await dispatch(
289
- makeReject(`p+${firstPExport + 1}`, kser('tomorrow never came')),
290
- );
432
+ for (const orphanedPExport of v1OrphanedPExports) {
433
+ await dispatch(
434
+ makeReject(`p+${orphanedPExport}`, kser('tomorrow never came')),
435
+ );
436
+ }
437
+ await dispatchMessage('getWatchResolution', 'orphaned');
438
+ t.deepEqual(getDispatchLogs(), [
439
+ fulfillmentMessage(`p-${nextPImport()}`, {
440
+ status: 'rejected',
441
+ reason: 'tomorrow never came',
442
+ }),
443
+ ]);
444
+ await dispatchMessage('getWatchResolution', 'start orphaned');
445
+ t.deepEqual(getDispatchLogs(), [
446
+ fulfillmentMessage(`p-${nextPImport()}`, {
447
+ status: 'rejected',
448
+ reason: 'tomorrow never came',
449
+ }),
450
+ ]);
451
+ await dispatchMessage('getWatchResolution', 'orphaned exported');
452
+ t.deepEqual(getDispatchLogs(), [
453
+ fulfillmentMessage(`p-${nextPImport()}`, {
454
+ status: 'rejected',
455
+ reason: 'tomorrow never came',
456
+ }),
457
+ ]);
291
458
  for (const [key, value] of expectedDeletions) {
292
459
  t.false(clonedStore.has(key), `entry should be removed: ${key}: ${value}`);
293
460
  }
461
+ // Simulate resolution of imported promises watched in previous incarnation
462
+ await dispatch(makeResolve(`p-${importedP}`, kser(undefined)));
463
+ await dispatchMessage('getWatchResolution', 'imported');
464
+ t.deepEqual(getDispatchLogs(), [
465
+ fulfillmentMessage(`p-${nextPImport()}`, {
466
+ status: 'fulfilled',
467
+ value: undefined,
468
+ }),
469
+ ]);
470
+ await dispatch(makeResolve(`p-${startImportedP}`, kser(undefined)));
471
+ await dispatchMessage('getWatchResolution', 'start imported');
472
+ t.deepEqual(getDispatchLogs(), [
473
+ fulfillmentMessage(`p-${nextPImport()}`, {
474
+ status: 'fulfilled',
475
+ value: undefined,
476
+ }),
477
+ ]);
478
+ await dispatch(makeResolve(`p-${startImported2P}`, kser(undefined)));
479
+ await dispatchMessage('getWatchResolution', 'start imported 2');
480
+ t.deepEqual(getDispatchLogs(), [
481
+ fulfillmentMessage(`p-${nextPImport()}`, undefined),
482
+ ]);
483
+ // simulate resolution of imported promise watched after resolution
484
+ await dispatchMessage('watchPromise', 'start imported 2');
485
+ t.deepEqual(getDispatchLogs(), [
486
+ // Promise was previously resolved, so it is re-exported
487
+ subscribeMessage(`p+${nextPExport()}`),
488
+ fulfillmentMessage(
489
+ `p-${nextPImport()}`,
490
+ 'watched promise: start imported 2',
491
+ ),
492
+ fulfillmentMessage(`p+${lastPExport}`, undefined),
493
+ ]);
494
+ await dispatchMessage('getWatchResolution', 'start imported 2');
495
+ t.deepEqual(getDispatchLogs(), [
496
+ fulfillmentMessage(`p-${nextPImport()}`, {
497
+ status: 'fulfilled',
498
+ value: undefined,
499
+ }),
500
+ ]);
294
501
 
295
502
  // Verify that the data is still in loadable condition.
296
503
  const finalClonedStore = new Map(clonedStore);
297
- ({ v, dispatch, dispatchMessage } = await setupTestLiveslots(
504
+ ({
505
+ v,
506
+ dispatch,
507
+ dispatchMessage: rawDispatch,
508
+ } = await setupTestLiveslots(
298
509
  t,
299
510
  buildPromiseWatcherRootObject,
300
511
  'durable-promise-watcher-final',
@@ -303,12 +514,17 @@ test('past-incarnation watched promises', async t => {
303
514
  vatLogs = v.log;
304
515
  vatLogs.length = 0;
305
516
  await dispatchMessage('createLocalPromise', 'final', S);
306
- await dispatchMessage('watchLocalPromise', 'final');
517
+ await dispatchMessage('watchPromise', 'final');
518
+ await dispatchMessage('getWatchResolution', 'final');
307
519
  t.deepEqual(getDispatchLogs(), [
308
520
  fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: final'),
309
521
  subscribeMessage(`p+${nextPExport()}`),
310
- fulfillmentMessage(`p-${nextPImport()}`, 'watched local promise: final'),
522
+ fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: final'),
311
523
  fulfillmentMessage(`p+${lastPExport}`, S),
524
+ fulfillmentMessage(`p-${nextPImport()}`, {
525
+ status: 'fulfilled',
526
+ value: S,
527
+ }),
312
528
  ]);
313
529
  });
314
530
 
@@ -131,6 +131,7 @@ export async function makeDispatch(
131
131
  build,
132
132
  vatID = 'vatA',
133
133
  liveSlotsOptions = {},
134
+ vatParameters = undefined,
134
135
  ) {
135
136
  const gcTools = harden({
136
137
  WeakRef,
@@ -150,7 +151,7 @@ export async function makeDispatch(
150
151
  return { buildRootObject: build };
151
152
  },
152
153
  );
153
- await dispatch(['startVat', kser()]);
154
+ await dispatch(['startVat', kser(vatParameters)]);
154
155
  return { dispatch, testHooks };
155
156
  }
156
157
 
@@ -171,6 +172,7 @@ function makeRPMaker(nextNumber = 1) {
171
172
  * @param {Map<string, string>} [options.kvStore]
172
173
  * @param {number} [options.nextPromiseImportNumber]
173
174
  * @param {boolean} [options.skipLogging]
175
+ * @param {any} [options.vatParameters]
174
176
  */
175
177
  export async function setupTestLiveslots(
176
178
  t,
@@ -183,6 +185,7 @@ export async function setupTestLiveslots(
183
185
  kvStore = new Map(),
184
186
  nextPromiseImportNumber,
185
187
  skipLogging = false,
188
+ vatParameters,
186
189
  } = options;
187
190
  const { log, syscall, fakestore } = buildSyscall({ skipLogging, kvStore });
188
191
  const nextRP = makeRPMaker(nextPromiseImportNumber);
@@ -190,6 +193,8 @@ export async function setupTestLiveslots(
190
193
  syscall,
191
194
  buildRootObject,
192
195
  vatName,
196
+ {},
197
+ vatParameters,
193
198
  );
194
199
 
195
200
  async function dispatchMessage(message, ...args) {
@@ -898,3 +898,104 @@ test('inter-vat circular promise references', async t => {
898
898
  // });
899
899
  // t.deepEqual(log, []);
900
900
  });
901
+
902
+ test('accept previously allocated promise', async t => {
903
+ function build(vatPowers, vatParameters) {
904
+ const { target } = vatParameters;
905
+ return Far('root', {
906
+ call() {
907
+ const promise = E(target).foo();
908
+ // wrap to avoid adoption
909
+ return { promise };
910
+ },
911
+ async waitFor(promise2) {
912
+ // if we're in the same incarnation as the `E(target).foo()`, this
913
+ // `promise2` will be the same JS Promise as the `promise` above
914
+ const v = await promise2;
915
+ return v;
916
+ },
917
+ });
918
+ }
919
+
920
+ let log;
921
+ let syscall;
922
+ let dispatch;
923
+
924
+ const kvStore = new Map();
925
+ ({ log, syscall } = buildSyscall({ kvStore }));
926
+
927
+ const target = 'o-1';
928
+ ({ dispatch } = await makeDispatch(
929
+ syscall,
930
+ build,
931
+ 'reimport-promise',
932
+ {},
933
+ { target: kslot(target) },
934
+ ));
935
+ log.length = 0; // assume pre-build vatstore operations are correct
936
+
937
+ const root = 'o+0';
938
+ const callResultP = 'p-1';
939
+ const waitForResultP = 'p-2';
940
+ const expectedP1 = 'p+5';
941
+
942
+ await dispatch(makeMessage(root, 'call', [], callResultP));
943
+
944
+ // The vat should send 'foo', subscribe to the result promise, and resolve with that promise
945
+ t.deepEqual(log.splice(0, 3), [
946
+ {
947
+ type: 'send',
948
+ targetSlot: target,
949
+ methargs: kser(['foo', []]),
950
+ resultSlot: expectedP1,
951
+ },
952
+ { type: 'subscribe', target: expectedP1 },
953
+ {
954
+ type: 'resolve',
955
+ resolutions: [[callResultP, false, kser({ promise: kslot(expectedP1) })]],
956
+ },
957
+ ]);
958
+ matchIDCounterSet(t, log);
959
+ t.deepEqual(log, []);
960
+
961
+ // snapshot the store at this point
962
+ const clonedStore = new Map(kvStore);
963
+
964
+ const verifyPromiseReImport = async shouldSubscribe => {
965
+ await dispatch(
966
+ makeMessage(root, 'waitFor', [kslot(expectedP1)], waitForResultP),
967
+ );
968
+
969
+ // The vat will only subscribe if it was upgraded; if the vat still
970
+ // remembers it, receiving the vref will merely look it up in slotToVal and
971
+ // not create a new Promise (and trigger a subscribe)
972
+ if (shouldSubscribe) {
973
+ t.deepEqual(log.shift(), { type: 'subscribe', target: expectedP1 });
974
+ }
975
+
976
+ // waitFor will suspend until promise is resolved
977
+ t.deepEqual(log, []);
978
+
979
+ // Resolution propagates the value to the waitFor result
980
+ await dispatch(makeResolve(expectedP1, kser('success')));
981
+ t.deepEqual(log.shift(), {
982
+ type: 'resolve',
983
+ resolutions: [[waitForResultP, false, kser('success')]],
984
+ });
985
+ t.deepEqual(log, []);
986
+ };
987
+
988
+ await verifyPromiseReImport(false);
989
+ ({ log, syscall } = buildSyscall({ kvStore: clonedStore }));
990
+ ({ dispatch } = await makeDispatch(
991
+ syscall,
992
+ build,
993
+ 'reimport-promise-v2',
994
+ {},
995
+ {},
996
+ ));
997
+ log.length = 0;
998
+
999
+ // verify this works the same in the restarted vat
1000
+ await verifyPromiseReImport(true);
1001
+ });