@agoric/swingset-liveslots 0.10.3-u19.1 → 0.10.3-u20.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.
Files changed (38) hide show
  1. package/package.json +16 -16
  2. package/src/collectionManager.d.ts +1 -0
  3. package/src/collectionManager.d.ts.map +1 -1
  4. package/src/collectionManager.js +1 -0
  5. package/src/liveslots.js +2 -2
  6. package/src/message.d.ts +10 -6
  7. package/src/message.d.ts.map +1 -1
  8. package/src/message.js +7 -3
  9. package/src/types.d.ts +8 -3
  10. package/src/types.d.ts.map +1 -1
  11. package/src/types.js +6 -5
  12. package/src/virtualObjectManager.d.ts.map +1 -1
  13. package/src/virtualObjectManager.js +70 -14
  14. package/src/watchedPromises.d.ts.map +1 -1
  15. package/src/watchedPromises.js +10 -13
  16. package/test/gc-helpers.js +2 -2
  17. package/test/handled-promises.test.js +529 -163
  18. package/test/initial-vrefs.test.js +12 -18
  19. package/test/liveslots-helpers.d.ts +1 -0
  20. package/test/liveslots-helpers.d.ts.map +1 -1
  21. package/test/liveslots-helpers.js +1 -0
  22. package/test/liveslots-real-gc.test.js +2 -2
  23. package/test/liveslots.test.js +3 -3
  24. package/test/storeGC/lifecycle.test.js +13 -12
  25. package/test/util.d.ts +1 -1
  26. package/test/util.d.ts.map +1 -1
  27. package/test/util.js +2 -2
  28. package/test/virtual-objects/state-shape.test.js +312 -221
  29. package/test/virtual-objects/virtualObjectGC.test.js +37 -36
  30. package/test/virtual-objects/virtualObjectManager.test.js +41 -63
  31. package/test/vo-test-harness.test.js +13 -9
  32. package/tools/fakeVirtualSupport.d.ts.map +1 -1
  33. package/tools/setup-vat-data.d.ts.map +1 -1
  34. package/tools/setup-vat-data.js +0 -1
  35. package/tools/vo-test-harness.d.ts +31 -0
  36. package/tools/vo-test-harness.d.ts.map +1 -1
  37. package/tools/vo-test-harness.js +21 -0
  38. package/test/watch-promise.test.js +0 -42
@@ -45,34 +45,50 @@ const buildPromiseWatcherRootObject = (vatPowers, vatParameters, baggage) => {
45
45
 
46
46
  const root = Far('root', {
47
47
  getPromise: name => {
48
- const promise = knownPromises.get(name);
49
- promise || Fail`promise doesn't exists: ${name}`;
48
+ knownPromises.has(name) || Fail`promise not found: ${name}`;
49
+ const { promise } = knownPromises.get(name);
50
50
  return { promise };
51
51
  },
52
52
  importPromise: (name, promise) => {
53
53
  !knownPromises.has(name) || Fail`promise already exists: ${name}`;
54
- knownPromises.set(name, promise);
54
+ knownPromises.set(name, { promise });
55
55
  return `imported promise: ${name}`;
56
56
  },
57
57
  createLocalPromise: (name, fulfillment, rejection) => {
58
58
  !knownPromises.has(name) || Fail`promise already exists: ${name}`;
59
59
  const { promise, resolve, reject } = makePromiseKit();
60
+ let resolvers = {};
60
61
  if (fulfillment !== undefined) {
61
62
  resolve(fulfillment);
62
63
  } else if (rejection !== undefined) {
63
64
  reject(rejection);
65
+ } else {
66
+ resolvers = { resolve, reject };
64
67
  }
65
- knownPromises.set(name, promise);
68
+ knownPromises.set(name, { promise, ...resolvers });
66
69
  return `created local promise: ${name}`;
67
70
  },
71
+ resolveLocalPromise: (name, rejection, value) => {
72
+ knownPromises.has(name) || Fail`promise not found: ${name}`;
73
+ const { resolve, reject, promise } = knownPromises.get(name);
74
+ (resolve && reject) || Fail`promise not resolvable: ${name}`;
75
+ (rejection ? reject : resolve)(value);
76
+ knownPromises.set(name, { promise });
77
+ return `resolved promise: ${name}`;
78
+ },
68
79
  watchPromise: name => {
69
80
  knownPromises.has(name) || Fail`promise not found: ${name}`;
70
- watchPromise(knownPromises.get(name), watcher, name);
81
+ watchPromise(knownPromises.get(name).promise, watcher, name);
71
82
  return `watched promise: ${name}`;
72
83
  },
73
84
  getWatchResolution: name => {
74
85
  return watchResolutions.get(name);
75
86
  },
87
+ sendToPromise: (name, method, ...args) => {
88
+ knownPromises.has(name) || Fail`promise not found: ${name}`;
89
+ const { promise } = knownPromises.get(name);
90
+ return HandledPromise.applyMethod(promise, method, args);
91
+ },
76
92
  });
77
93
 
78
94
  const startOperations = vatParameters?.startOperations || [];
@@ -132,6 +148,29 @@ const kvStoreDataV1KeysToDelete = ['vc.4.sp+6', 'vc.4.sp-9'];
132
148
  const kvStoreDataV1VpidsToKeep = ['p-8'];
133
149
  const kvStoreDataV1KeysToKeep = ['vc.4.sp-8'];
134
150
 
151
+ // Ignore vatstore syscalls.
152
+ const extractDispatchLogs = vatLogs =>
153
+ vatLogs.splice(0).filter(m => !m.type.startsWith('vatstore'));
154
+
155
+ const settlementMessage = (vpid, rejected, value) => ({
156
+ type: 'resolve',
157
+ resolutions: [[vpid, rejected, kser(value)]],
158
+ });
159
+ const fulfillmentMessage = (vpid, value) =>
160
+ settlementMessage(vpid, false, value);
161
+ const rejectionMessage = (vpid, value) => settlementMessage(vpid, true, value);
162
+ const subscribeMessage = vpid => ({
163
+ type: 'subscribe',
164
+ target: vpid,
165
+ });
166
+
167
+ /** @param {`p-${number}` | `p+${number}`} p */
168
+ const extractPNum = p => {
169
+ const r = /^p[-+](\d+)$/.exec(p);
170
+ if (!r) throw Fail`Invalid promise ${p}`;
171
+ return parseInt(r[1], 10);
172
+ };
173
+
135
174
  test('past-incarnation watched promises', async t => {
136
175
  const S = 'settlement';
137
176
  // Anchor promise counters upon which the other assertions depend.
@@ -139,14 +178,14 @@ test('past-incarnation watched promises', async t => {
139
178
  // cf. src/liveslots.js:initialIDCounters
140
179
  const firstPExport = 5;
141
180
 
142
- const startImportedP = firstPImport - 2;
181
+ const startImportedP = `p-${firstPImport - 2}`;
143
182
 
144
183
  const v1StartOperations = [
145
184
  ['createLocalPromise', 'start orphaned'],
146
185
  ['watchPromise', 'start orphaned'],
147
186
  ['createLocalPromise', 'start fulfilled', S],
148
187
  ['watchPromise', 'start fulfilled'],
149
- ['importPromise', 'start imported', kslot(`p-${startImportedP}`)],
188
+ ['importPromise', 'start imported', kslot(startImportedP)],
150
189
  ['watchPromise', 'start imported'],
151
190
  ];
152
191
  const kvStore = new Map();
@@ -166,172 +205,133 @@ test('past-incarnation watched promises', async t => {
166
205
  );
167
206
  let vatLogs = v.log;
168
207
 
169
- let lastPImport = firstPImport - 1;
170
- let lastPExport = firstPExport - 1;
171
- let dispatches = 0;
172
- const exportedPromises = new Set();
173
- const v1OrphanedPExports = [];
174
- const nextPImport = () => (lastPImport += 1);
175
- const nextPExport = () => (lastPExport += 1);
208
+ /** @type {`p-${number}`} */
209
+ let rp = `p-0`;
176
210
  /** @type {typeof rawDispatch} */
177
- const dispatchMessage = (...args) => {
178
- dispatches += 1;
179
- return rawDispatch(...args);
211
+ const dispatchMessage = async (...args) => {
212
+ rp = /** @type {`p-${number}`} */ (await rawDispatch(...args));
213
+ return rp;
180
214
  };
181
- const recordExportedPromise = name => {
182
- exportedPromises.add(name);
183
- return name;
215
+ let lastPExport = firstPExport - 1;
216
+ const exportedPromises = new Map();
217
+ const recordNextExportedPromise = name => {
218
+ const p = `p+${(lastPExport += 1)}`;
219
+ exportedPromises.set(p, name);
220
+ return p;
221
+ };
222
+ const recordExportedPromiseNotification = p => {
223
+ t.true(exportedPromises.delete(p));
224
+ return p;
184
225
  };
185
- // Ignore vatstore syscalls.
186
- const getDispatchLogs = () =>
187
- vatLogs.splice(0).filter(m => !m.type.startsWith('vatstore'));
188
- const settlementMessage = (vpid, rejected, value) => ({
189
- type: 'resolve',
190
- resolutions: [[vpid, rejected, kser(value)]],
191
- });
192
- const fulfillmentMessage = (vpid, value) =>
193
- settlementMessage(vpid, false, value);
194
- const rejectionMessage = (vpid, value) =>
195
- settlementMessage(vpid, true, value);
196
- const subscribeMessage = vpid => ({
197
- type: 'subscribe',
198
- target: vpid,
199
- });
200
226
 
201
227
  // startVat logs
202
- v1OrphanedPExports.push(nextPExport());
203
- recordExportedPromise('start orphaned');
204
- v1OrphanedPExports.push(nextPExport());
205
- recordExportedPromise('start fulfilled');
206
- t.deepEqual(getDispatchLogs(), [
207
- subscribeMessage(`p-${startImportedP}`),
208
- subscribeMessage(`p+${lastPExport - 1}`),
209
- subscribeMessage(`p+${lastPExport}`),
210
- fulfillmentMessage(`p+${lastPExport}`, S),
228
+ const startOrphanedP = recordNextExportedPromise('start orphaned');
229
+ const startFulfilledP = recordNextExportedPromise('start fulfilled');
230
+ t.deepEqual(extractDispatchLogs(vatLogs), [
231
+ subscribeMessage(startImportedP),
232
+ subscribeMessage(startOrphanedP),
233
+ subscribeMessage(startFulfilledP),
234
+ fulfillmentMessage(startFulfilledP, S),
211
235
  ]);
212
236
  await dispatchMessage('createLocalPromise', 'exported', S);
213
- t.deepEqual(getDispatchLogs(), [
214
- fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: exported'),
237
+ t.deepEqual(extractDispatchLogs(vatLogs), [
238
+ fulfillmentMessage(rp, 'created local promise: exported'),
215
239
  ]);
216
- await dispatchMessage('getPromise', recordExportedPromise('exported'));
217
- t.deepEqual(getDispatchLogs(), [
218
- fulfillmentMessage(`p-${nextPImport()}`, {
219
- promise: kslot(`p+${nextPExport()}`),
240
+ await dispatchMessage('getPromise', 'exported');
241
+ const exportedP = recordNextExportedPromise('exported');
242
+ t.deepEqual(extractDispatchLogs(vatLogs), [
243
+ fulfillmentMessage(rp, {
244
+ promise: kslot(exportedP),
220
245
  }),
221
- fulfillmentMessage(`p+${lastPExport}`, S),
246
+ fulfillmentMessage(recordExportedPromiseNotification(exportedP), S),
222
247
  ]);
223
- const importedP = firstPImport - 1;
224
- await dispatchMessage('importPromise', 'imported', kslot(`p-${importedP}`));
225
- t.deepEqual(getDispatchLogs(), [
226
- subscribeMessage(`p-${importedP}`),
227
- fulfillmentMessage(`p-${nextPImport()}`, 'imported promise: imported'),
248
+ const importedP = `p-${firstPImport - 1}`;
249
+ await dispatchMessage('importPromise', 'imported', kslot(importedP));
250
+ t.deepEqual(extractDispatchLogs(vatLogs), [
251
+ subscribeMessage(importedP),
252
+ fulfillmentMessage(rp, 'imported promise: imported'),
228
253
  ]);
229
254
  await dispatchMessage('createLocalPromise', 'orphaned');
230
- t.deepEqual(getDispatchLogs(), [
231
- fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: orphaned'),
255
+ t.deepEqual(extractDispatchLogs(vatLogs), [
256
+ fulfillmentMessage(rp, 'created local promise: orphaned'),
232
257
  ]);
233
258
  await dispatchMessage('createLocalPromise', 'orphaned exported');
234
- await dispatchMessage(
235
- 'getPromise',
236
- recordExportedPromise('orphaned exported'),
237
- );
238
- const orphanedExportedP = nextPExport();
239
- v1OrphanedPExports.push(orphanedExportedP);
240
- t.deepEqual(getDispatchLogs(), [
241
- fulfillmentMessage(
242
- `p-${nextPImport()}`,
243
- 'created local promise: orphaned exported',
244
- ),
245
- fulfillmentMessage(`p-${nextPImport()}`, {
246
- promise: kslot(`p+${orphanedExportedP}`),
259
+ t.deepEqual(extractDispatchLogs(vatLogs), [
260
+ fulfillmentMessage(rp, 'created local promise: orphaned exported'),
261
+ ]);
262
+ await dispatchMessage('getPromise', 'orphaned exported');
263
+ const orphanedExportedP = recordNextExportedPromise('orphaned exported');
264
+ t.deepEqual(extractDispatchLogs(vatLogs), [
265
+ fulfillmentMessage(rp, {
266
+ promise: kslot(orphanedExportedP),
247
267
  }),
248
268
  ]);
249
269
  await dispatchMessage('createLocalPromise', 'fulfilled', S);
250
- t.deepEqual(getDispatchLogs(), [
251
- fulfillmentMessage(
252
- `p-${nextPImport()}`,
253
- 'created local promise: fulfilled',
254
- ),
270
+ t.deepEqual(extractDispatchLogs(vatLogs), [
271
+ fulfillmentMessage(rp, 'created local promise: fulfilled'),
255
272
  ]);
256
273
  await dispatchMessage('createLocalPromise', 'rejected', undefined, S);
257
- t.deepEqual(getDispatchLogs(), [
258
- fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: rejected'),
274
+ t.deepEqual(extractDispatchLogs(vatLogs), [
275
+ fulfillmentMessage(rp, 'created local promise: rejected'),
259
276
  ]);
260
- t.is(
261
- lastPImport - firstPImport + 1,
262
- dispatches,
263
- `imported ${dispatches} promises (1 per dispatch)`,
264
- );
265
- t.is(
266
- lastPExport - firstPExport + 1,
267
- exportedPromises.size,
268
- `exported ${exportedPromises.size} promises: ${[...exportedPromises].join(', ')}`,
269
- );
270
277
 
271
- await dispatchMessage('watchPromise', recordExportedPromise('orphaned'));
272
- v1OrphanedPExports.push(nextPExport());
273
- t.deepEqual(getDispatchLogs(), [
274
- subscribeMessage(`p+${lastPExport}`),
275
- fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: orphaned'),
278
+ await dispatchMessage('watchPromise', 'orphaned');
279
+ const orphanedP = recordNextExportedPromise('orphaned');
280
+ t.deepEqual(extractDispatchLogs(vatLogs), [
281
+ subscribeMessage(orphanedP),
282
+ fulfillmentMessage(rp, 'watched promise: orphaned'),
276
283
  ]);
277
- await dispatchMessage('watchPromise', recordExportedPromise('fulfilled'));
278
- t.deepEqual(getDispatchLogs(), [
279
- subscribeMessage(`p+${nextPExport()}`),
280
- fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: fulfilled'),
281
- fulfillmentMessage(`p+${lastPExport}`, S),
284
+ await dispatchMessage('watchPromise', 'fulfilled');
285
+ const fulfilledP = recordNextExportedPromise('fulfilled');
286
+ t.deepEqual(extractDispatchLogs(vatLogs), [
287
+ subscribeMessage(fulfilledP),
288
+ fulfillmentMessage(rp, 'watched promise: fulfilled'),
289
+ fulfillmentMessage(recordExportedPromiseNotification(fulfilledP), S),
282
290
  ]);
283
- await dispatchMessage('watchPromise', recordExportedPromise('rejected'));
284
- t.deepEqual(getDispatchLogs(), [
285
- subscribeMessage(`p+${nextPExport()}`),
286
- fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: rejected'),
287
- rejectionMessage(`p+${lastPExport}`, S),
291
+ await dispatchMessage('watchPromise', 'rejected');
292
+ const rejectedP = recordNextExportedPromise('rejected');
293
+ t.deepEqual(extractDispatchLogs(vatLogs), [
294
+ subscribeMessage(rejectedP),
295
+ fulfillmentMessage(rp, 'watched promise: rejected'),
296
+ rejectionMessage(recordExportedPromiseNotification(rejectedP), S),
288
297
  ]);
289
298
  await dispatchMessage('watchPromise', 'imported');
290
- t.deepEqual(getDispatchLogs(), [
299
+ t.deepEqual(extractDispatchLogs(vatLogs), [
291
300
  // no subscribe, we already did at import
292
- fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: imported'),
301
+ fulfillmentMessage(rp, 'watched promise: imported'),
293
302
  ]);
294
303
  await dispatchMessage('getWatchResolution', 'fulfilled');
295
- t.deepEqual(getDispatchLogs(), [
296
- fulfillmentMessage(`p-${nextPImport()}`, {
304
+ t.deepEqual(extractDispatchLogs(vatLogs), [
305
+ fulfillmentMessage(rp, {
297
306
  status: 'fulfilled',
298
307
  value: S,
299
308
  }),
300
309
  ]);
301
310
  await dispatchMessage('getWatchResolution', 'rejected');
302
- t.deepEqual(getDispatchLogs(), [
303
- fulfillmentMessage(`p-${nextPImport()}`, {
311
+ t.deepEqual(extractDispatchLogs(vatLogs), [
312
+ fulfillmentMessage(rp, {
304
313
  status: 'rejected',
305
314
  reason: S,
306
315
  }),
307
316
  ]);
308
317
  await dispatchMessage('getWatchResolution', 'start fulfilled');
309
- t.deepEqual(getDispatchLogs(), [
310
- fulfillmentMessage(`p-${nextPImport()}`, {
318
+ t.deepEqual(extractDispatchLogs(vatLogs), [
319
+ fulfillmentMessage(rp, {
311
320
  status: 'fulfilled',
312
321
  value: S,
313
322
  }),
314
323
  ]);
315
324
 
316
- t.is(
317
- lastPImport - firstPImport + 1,
318
- dispatches,
319
- `imported ${dispatches} promises (1 per dispatch)`,
320
- );
321
- t.is(
322
- lastPExport - firstPExport + 1,
323
- exportedPromises.size,
324
- `exported ${exportedPromises.size} promises: ${[...exportedPromises].join(', ')}`,
325
- );
325
+ const v2FirstPromise = extractPNum(rp) + 1;
326
326
 
327
327
  // Simulate upgrade by starting from the non-empty kvStore.
328
328
  // t.log(Object.fromEntries([...kvStore.entries()].sort(compareEntriesByKey)));
329
329
  const clonedStore = new Map(kvStore);
330
- const startImported2P = firstPImport - 3;
330
+ const startImported2P = `p-${firstPImport - 3}`;
331
331
  const v2StartOperations = [
332
- ['importPromise', 'start imported 2', kslot(`p-${startImported2P}`)], // import of new promise
333
- ['importPromise', 'imported', kslot(`p-${importedP}`)], // import previously imported and watched promise
334
- ['importPromise', 'orphaned exported', kslot(`p+${orphanedExportedP}`)], // import previously exported but unwatched promise
332
+ ['importPromise', 'start imported 2', kslot(startImported2P)], // import of new promise
333
+ ['importPromise', 'imported', kslot(importedP)], // import previously imported and watched promise
334
+ ['importPromise', 'orphaned exported', kslot(orphanedExportedP)], // import previously exported but unwatched promise
335
335
  ['watchPromise', 'orphaned exported'],
336
336
  ];
337
337
  ({
@@ -344,45 +344,44 @@ test('past-incarnation watched promises', async t => {
344
344
  'durable-promise-watcher-v2',
345
345
  {
346
346
  kvStore: clonedStore,
347
- nextPromiseImportNumber: lastPImport + 1,
347
+ nextPromiseImportNumber: v2FirstPromise,
348
348
  vatParameters: { startOperations: v2StartOperations },
349
349
  },
350
350
  ));
351
351
  vatLogs = v.log;
352
352
 
353
353
  // startVat logs
354
- t.deepEqual(getDispatchLogs(), [
355
- subscribeMessage(`p-${startImported2P}`),
356
- subscribeMessage(`p-${importedP}`),
357
- subscribeMessage(`p+${orphanedExportedP}`),
354
+ t.deepEqual(extractDispatchLogs(vatLogs), [
355
+ subscribeMessage(startImported2P),
356
+ subscribeMessage(importedP),
357
+ subscribeMessage(orphanedExportedP),
358
358
  ]);
359
359
  // Simulate kernel rejection of promises orphaned by termination/upgrade of their decider vat.
360
360
  const expectedDeletions = [...clonedStore.entries()].filter(entry =>
361
361
  entry[1].includes('orphaned'),
362
362
  );
363
363
  t.true(expectedDeletions.length >= 1);
364
- for (const orphanedPExport of v1OrphanedPExports) {
365
- await dispatch(
366
- makeReject(`p+${orphanedPExport}`, kser('tomorrow never came')),
367
- );
364
+ for (const [orphanedPExport] of exportedPromises) {
365
+ await dispatch(makeReject(orphanedPExport, kser('tomorrow never came')));
368
366
  }
367
+ exportedPromises.clear();
369
368
  await dispatchMessage('getWatchResolution', 'orphaned');
370
- t.deepEqual(getDispatchLogs(), [
371
- fulfillmentMessage(`p-${nextPImport()}`, {
369
+ t.deepEqual(extractDispatchLogs(vatLogs), [
370
+ fulfillmentMessage(rp, {
372
371
  status: 'rejected',
373
372
  reason: 'tomorrow never came',
374
373
  }),
375
374
  ]);
376
375
  await dispatchMessage('getWatchResolution', 'start orphaned');
377
- t.deepEqual(getDispatchLogs(), [
378
- fulfillmentMessage(`p-${nextPImport()}`, {
376
+ t.deepEqual(extractDispatchLogs(vatLogs), [
377
+ fulfillmentMessage(rp, {
379
378
  status: 'rejected',
380
379
  reason: 'tomorrow never came',
381
380
  }),
382
381
  ]);
383
382
  await dispatchMessage('getWatchResolution', 'orphaned exported');
384
- t.deepEqual(getDispatchLogs(), [
385
- fulfillmentMessage(`p-${nextPImport()}`, {
383
+ t.deepEqual(extractDispatchLogs(vatLogs), [
384
+ fulfillmentMessage(rp, {
386
385
  status: 'rejected',
387
386
  reason: 'tomorrow never came',
388
387
  }),
@@ -391,46 +390,50 @@ test('past-incarnation watched promises', async t => {
391
390
  t.false(clonedStore.has(key), `entry should be removed: ${key}: ${value}`);
392
391
  }
393
392
  // Simulate resolution of imported promises watched in previous incarnation
394
- await dispatch(makeResolve(`p-${importedP}`, kser(undefined)));
393
+ await dispatch(makeResolve(importedP, kser(undefined)));
395
394
  await dispatchMessage('getWatchResolution', 'imported');
396
- t.deepEqual(getDispatchLogs(), [
397
- fulfillmentMessage(`p-${nextPImport()}`, {
395
+ t.deepEqual(extractDispatchLogs(vatLogs), [
396
+ fulfillmentMessage(rp, {
398
397
  status: 'fulfilled',
399
398
  value: undefined,
400
399
  }),
401
400
  ]);
402
- await dispatch(makeResolve(`p-${startImportedP}`, kser(undefined)));
401
+ await dispatch(makeResolve(startImportedP, kser(undefined)));
403
402
  await dispatchMessage('getWatchResolution', 'start imported');
404
- t.deepEqual(getDispatchLogs(), [
405
- fulfillmentMessage(`p-${nextPImport()}`, {
403
+ t.deepEqual(extractDispatchLogs(vatLogs), [
404
+ fulfillmentMessage(rp, {
406
405
  status: 'fulfilled',
407
406
  value: undefined,
408
407
  }),
409
408
  ]);
410
- await dispatch(makeResolve(`p-${startImported2P}`, kser(undefined)));
409
+ await dispatch(makeResolve(startImported2P, kser(undefined)));
411
410
  await dispatchMessage('getWatchResolution', 'start imported 2');
412
- t.deepEqual(getDispatchLogs(), [
413
- fulfillmentMessage(`p-${nextPImport()}`, undefined),
411
+ t.deepEqual(extractDispatchLogs(vatLogs), [
412
+ fulfillmentMessage(rp, undefined),
414
413
  ]);
415
414
  // simulate resolution of imported promise watched after resolution
416
415
  await dispatchMessage('watchPromise', 'start imported 2');
417
- t.deepEqual(getDispatchLogs(), [
416
+ const startImported2ReexportedP =
417
+ recordNextExportedPromise('start imported 2');
418
+ t.deepEqual(extractDispatchLogs(vatLogs), [
418
419
  // Promise was previously resolved, so it is re-exported
419
- subscribeMessage(`p+${nextPExport()}`),
420
+ subscribeMessage(startImported2ReexportedP),
421
+ fulfillmentMessage(rp, 'watched promise: start imported 2'),
420
422
  fulfillmentMessage(
421
- `p-${nextPImport()}`,
422
- 'watched promise: start imported 2',
423
+ recordExportedPromiseNotification(startImported2ReexportedP),
424
+ undefined,
423
425
  ),
424
- fulfillmentMessage(`p+${lastPExport}`, undefined),
425
426
  ]);
426
427
  await dispatchMessage('getWatchResolution', 'start imported 2');
427
- t.deepEqual(getDispatchLogs(), [
428
- fulfillmentMessage(`p-${nextPImport()}`, {
428
+ t.deepEqual(extractDispatchLogs(vatLogs), [
429
+ fulfillmentMessage(rp, {
429
430
  status: 'fulfilled',
430
431
  value: undefined,
431
432
  }),
432
433
  ]);
433
434
 
435
+ const finalFirstPromise = extractPNum(rp) + 1;
436
+
434
437
  // Verify that the data is still in loadable condition.
435
438
  const finalClonedStore = new Map(clonedStore);
436
439
  ({
@@ -441,19 +444,25 @@ test('past-incarnation watched promises', async t => {
441
444
  t,
442
445
  buildPromiseWatcherRootObject,
443
446
  'durable-promise-watcher-final',
444
- { kvStore: finalClonedStore, nextPromiseImportNumber: lastPImport + 1 },
447
+ { kvStore: finalClonedStore, nextPromiseImportNumber: finalFirstPromise },
445
448
  ));
446
449
  vatLogs = v.log;
447
450
  vatLogs.length = 0;
451
+ t.deepEqual([...exportedPromises], [], 'exportedPromises is empty');
448
452
  await dispatchMessage('createLocalPromise', 'final', S);
453
+ t.deepEqual(extractDispatchLogs(vatLogs), [
454
+ fulfillmentMessage(rp, 'created local promise: final'),
455
+ ]);
449
456
  await dispatchMessage('watchPromise', 'final');
457
+ const finalP = recordNextExportedPromise('final');
458
+ t.deepEqual(extractDispatchLogs(vatLogs), [
459
+ subscribeMessage(finalP),
460
+ fulfillmentMessage(rp, 'watched promise: final'),
461
+ fulfillmentMessage(recordExportedPromiseNotification(finalP), S),
462
+ ]);
450
463
  await dispatchMessage('getWatchResolution', 'final');
451
- t.deepEqual(getDispatchLogs(), [
452
- fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: final'),
453
- subscribeMessage(`p+${nextPExport()}`),
454
- fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: final'),
455
- fulfillmentMessage(`p+${lastPExport}`, S),
456
- fulfillmentMessage(`p-${nextPImport()}`, {
464
+ t.deepEqual(extractDispatchLogs(vatLogs), [
465
+ fulfillmentMessage(rp, {
457
466
  status: 'fulfilled',
458
467
  value: S,
459
468
  }),
@@ -504,3 +513,360 @@ test('past-incarnation watched promises from original-format kvStore', async t =
504
513
  t.false(finalClonedStore.has(key), `key should be removed: ${key}`);
505
514
  }
506
515
  });
516
+
517
+ test('watched local promises should not leak slotToVal entries', async t => {
518
+ const S = 'settlement';
519
+ // cf. src/liveslots.js:initialIDCounters
520
+ const firstPExport = 5;
521
+
522
+ const {
523
+ v: { log: vatLogs },
524
+ dispatchMessage,
525
+ testHooks,
526
+ } = await setupTestLiveslots(t, buildPromiseWatcherRootObject, 'vatA');
527
+ const { slotToVal } = testHooks;
528
+ const initial = slotToVal.size;
529
+
530
+ let lastPExport = firstPExport - 1;
531
+ const nextPExport = () => `p+${(lastPExport += 1)}`;
532
+
533
+ let rp;
534
+
535
+ // Watch already resolved promise
536
+ rp = await dispatchMessage('createLocalPromise', 'p1', S);
537
+ t.deepEqual(extractDispatchLogs(vatLogs), [
538
+ fulfillmentMessage(rp, 'created local promise: p1'),
539
+ ]);
540
+ rp = await dispatchMessage('watchPromise', 'p1');
541
+ const p1 = nextPExport();
542
+ t.deepEqual(extractDispatchLogs(vatLogs), [
543
+ subscribeMessage(p1),
544
+ fulfillmentMessage(rp, 'watched promise: p1'),
545
+ fulfillmentMessage(p1, S),
546
+ ]);
547
+ t.is(slotToVal.size, initial); // exported promise did not leak
548
+ rp = await dispatchMessage('getWatchResolution', 'p1');
549
+ t.deepEqual(extractDispatchLogs(vatLogs), [
550
+ fulfillmentMessage(rp, {
551
+ status: 'fulfilled',
552
+ value: S,
553
+ }),
554
+ ]);
555
+
556
+ // Watch subsequently resolved promise
557
+ rp = await dispatchMessage('createLocalPromise', 'p2');
558
+ t.deepEqual(extractDispatchLogs(vatLogs), [
559
+ fulfillmentMessage(rp, 'created local promise: p2'),
560
+ ]);
561
+ t.is(slotToVal.size, initial);
562
+
563
+ rp = await dispatchMessage('watchPromise', 'p2');
564
+ const p2 = nextPExport();
565
+ t.deepEqual(extractDispatchLogs(vatLogs), [
566
+ subscribeMessage(p2),
567
+ fulfillmentMessage(rp, 'watched promise: p2'),
568
+ ]);
569
+ t.is(slotToVal.size, initial + 1); // exported promise
570
+
571
+ rp = await dispatchMessage('resolveLocalPromise', 'p2', false, S);
572
+ t.deepEqual(extractDispatchLogs(vatLogs), [
573
+ fulfillmentMessage(p2, S),
574
+ fulfillmentMessage(rp, 'resolved promise: p2'),
575
+ ]);
576
+ t.is(slotToVal.size, initial); // exported promise did not leak
577
+
578
+ rp = await dispatchMessage('getWatchResolution', 'p2');
579
+ t.deepEqual(extractDispatchLogs(vatLogs), [
580
+ fulfillmentMessage(rp, {
581
+ status: 'fulfilled',
582
+ value: S,
583
+ }),
584
+ ]);
585
+ });
586
+
587
+ // Remaining case for https://github.com/Agoric/agoric-sdk/issues/10756
588
+ // The workaround doesn't handle this case because it learns about the
589
+ // settlement before the virtual object system
590
+ // See https://github.com/Agoric/agoric-sdk/issues/10757
591
+ test('watched imported promises should not leak slotToVal entries', async t => {
592
+ const S = 'settlement';
593
+ // cf. src/liveslots.js:initialIDCounters
594
+ const firstPExport = 5;
595
+
596
+ const {
597
+ v: { log: vatLogs },
598
+ dispatch,
599
+ dispatchMessage,
600
+ nextPImport,
601
+ testHooks,
602
+ } = await setupTestLiveslots(t, buildPromiseWatcherRootObject, 'vatA');
603
+ const { slotToVal } = testHooks;
604
+ const initial = slotToVal.size;
605
+
606
+ let lastPExport = firstPExport - 1;
607
+ const nextPExport = () => `p+${(lastPExport += 1)}`;
608
+
609
+ let rp;
610
+
611
+ // Watch already imported promise
612
+ const importedP = nextPImport();
613
+ rp = await dispatchMessage('importPromise', 'importedP', kslot(importedP));
614
+ t.deepEqual(extractDispatchLogs(vatLogs), [
615
+ subscribeMessage(importedP),
616
+ fulfillmentMessage(rp, 'imported promise: importedP'),
617
+ ]);
618
+ t.is(slotToVal.size, initial + 1); // imported promise
619
+
620
+ rp = await dispatchMessage('watchPromise', 'importedP');
621
+ t.deepEqual(extractDispatchLogs(vatLogs), [
622
+ fulfillmentMessage(rp, 'watched promise: importedP'),
623
+ ]);
624
+ t.is(slotToVal.size, initial + 1); // imported promise
625
+
626
+ await dispatch(makeResolve(importedP, kser(S)));
627
+ t.deepEqual(extractDispatchLogs(vatLogs), []);
628
+ t.is(slotToVal.size, initial); // should not leak
629
+
630
+ rp = await dispatchMessage('getPromise', 'importedP');
631
+ const reexportedP = nextPExport(); // Should allocate a new exported promise
632
+ t.deepEqual(extractDispatchLogs(vatLogs), [
633
+ fulfillmentMessage(rp, { promise: kslot(reexportedP) }),
634
+ fulfillmentMessage(reexportedP, S),
635
+ ]);
636
+ t.is(slotToVal.size, initial); // reexported promise did not leak
637
+ });
638
+
639
+ test('known imported promises in resolutions should not leak slotToVal entries', async t => {
640
+ const S = 'settlement';
641
+ // cf. src/liveslots.js:initialIDCounters
642
+ const firstPExport = 5;
643
+
644
+ const {
645
+ v: { log: vatLogs },
646
+ dispatch,
647
+ dispatchMessage,
648
+ nextPImport,
649
+ testHooks,
650
+ } = await setupTestLiveslots(t, buildPromiseWatcherRootObject, 'vatA');
651
+ const { slotToVal } = testHooks;
652
+ const initial = slotToVal.size;
653
+
654
+ let lastPExport = firstPExport - 1;
655
+ const nextPExport = () => `p+${(lastPExport += 1)}`;
656
+
657
+ let rp;
658
+
659
+ const importedP = nextPImport();
660
+ rp = await dispatchMessage('importPromise', 'importedP', kslot(importedP));
661
+ t.deepEqual(extractDispatchLogs(vatLogs), [
662
+ subscribeMessage(importedP),
663
+ fulfillmentMessage(rp, 'imported promise: importedP'),
664
+ ]);
665
+ t.is(slotToVal.size, initial + 1); // imported promise
666
+
667
+ rp = await dispatchMessage('getPromise', 'importedP');
668
+ t.deepEqual(extractDispatchLogs(vatLogs), [
669
+ fulfillmentMessage(rp, { promise: kslot(importedP) }),
670
+ ]);
671
+ t.is(slotToVal.size, initial + 1); // imported promise
672
+
673
+ await dispatch(makeResolve(importedP, kser(S)));
674
+ t.deepEqual(extractDispatchLogs(vatLogs), []);
675
+ t.is(slotToVal.size, initial); // promise no longer imported
676
+
677
+ // The first getPromise causes liveslots to learn about the resolution value
678
+ // of importedP since liveslots currently only tracks known promise
679
+ // resolutions on export.
680
+ rp = await dispatchMessage('getPromise', 'importedP');
681
+ const reexportedP = nextPExport();
682
+ t.deepEqual(extractDispatchLogs(vatLogs), [
683
+ fulfillmentMessage(rp, { promise: kslot(reexportedP) }),
684
+ fulfillmentMessage(reexportedP, S), // liveslots learns about the resolution
685
+ ]);
686
+ t.is(slotToVal.size, initial);
687
+
688
+ // The second getPromise verifies that liveslots does not leak when notifying
689
+ // a known resolved promise. It allocates a new exported promise because the
690
+ // resolution was previously notified
691
+ rp = await dispatchMessage('getPromise', 'importedP');
692
+ const reexportedP2 = nextPExport(); // allocates a new export promise
693
+ t.deepEqual(extractDispatchLogs(vatLogs), [
694
+ {
695
+ type: 'resolve',
696
+ resolutions: [
697
+ [rp, false, kser({ promise: kslot(reexportedP2) })],
698
+ [reexportedP2, false, kser(S)], // one shot resolution notification
699
+ ],
700
+ },
701
+ ]);
702
+ t.is(slotToVal.size, initial); // did not leak reexportedP2
703
+
704
+ // The 3rd getPromise ensures that the 2nd fully cleaned up, and that liveslots
705
+ // doesn't attempt to re-use a the previously exported promise
706
+ rp = await dispatchMessage('getPromise', 'importedP');
707
+ const reexportedP3 = nextPExport(); // allocates a new export promise
708
+ t.deepEqual(extractDispatchLogs(vatLogs), [
709
+ {
710
+ type: 'resolve',
711
+ resolutions: [
712
+ [rp, false, kser({ promise: kslot(reexportedP3) })],
713
+ [reexportedP3, false, kser(S)], // still one shot resolution
714
+ ],
715
+ },
716
+ ]);
717
+ t.is(slotToVal.size, initial); // did not leak reexportedP3
718
+ });
719
+
720
+ test('known exported promises in resolutions should not leak slotToVal entries', async t => {
721
+ const S = 'settlement';
722
+ // cf. src/liveslots.js:initialIDCounters
723
+ const firstPExport = 5;
724
+
725
+ const {
726
+ v: { log: vatLogs },
727
+ dispatchMessage,
728
+ testHooks,
729
+ } = await setupTestLiveslots(t, buildPromiseWatcherRootObject, 'vatA');
730
+ const { slotToVal } = testHooks;
731
+ const initial = slotToVal.size;
732
+
733
+ let lastPExport = firstPExport - 1;
734
+ const nextPExport = () => `p+${(lastPExport += 1)}`;
735
+
736
+ let rp;
737
+
738
+ rp = await dispatchMessage('createLocalPromise', 'localP');
739
+ t.deepEqual(extractDispatchLogs(vatLogs), [
740
+ fulfillmentMessage(rp, 'created local promise: localP'),
741
+ ]);
742
+ t.is(slotToVal.size, initial);
743
+
744
+ rp = await dispatchMessage('getPromise', 'localP');
745
+ const localP = nextPExport();
746
+ t.deepEqual(extractDispatchLogs(vatLogs), [
747
+ fulfillmentMessage(rp, { promise: kslot(localP) }),
748
+ ]);
749
+ t.is(slotToVal.size, initial + 1); // exported promise
750
+
751
+ rp = await dispatchMessage('resolveLocalPromise', 'localP', false, S);
752
+ t.deepEqual(extractDispatchLogs(vatLogs), [
753
+ fulfillmentMessage(localP, S),
754
+ fulfillmentMessage(rp, 'resolved promise: localP'),
755
+ ]);
756
+ t.is(slotToVal.size, initial); // promise no longer exported
757
+
758
+ // The first getPromise verifies that liveslots does not leak when notifying
759
+ // a known resolved promise. It allocates a new exported promise because the
760
+ // resolution was previously notified
761
+ rp = await dispatchMessage('getPromise', 'localP');
762
+ const reexportedP = nextPExport();
763
+ t.deepEqual(extractDispatchLogs(vatLogs), [
764
+ {
765
+ type: 'resolve',
766
+ resolutions: [
767
+ [rp, false, kser({ promise: kslot(reexportedP) })],
768
+ [reexportedP, false, kser(S)], // one shot resolution notification
769
+ ],
770
+ },
771
+ ]);
772
+ t.is(slotToVal.size, initial);
773
+
774
+ // The second getPromise ensures that previous fully cleaned up, and that
775
+ // liveslots doesn't attempt to re-use a the previously exported promise
776
+ rp = await dispatchMessage('getPromise', 'localP');
777
+ const reexportedP2 = nextPExport(); // allocates a new export promise
778
+ t.deepEqual(extractDispatchLogs(vatLogs), [
779
+ {
780
+ type: 'resolve',
781
+ resolutions: [
782
+ [rp, false, kser({ promise: kslot(reexportedP2) })],
783
+ [reexportedP2, false, kser(S)], // still one shot resolution
784
+ ],
785
+ },
786
+ ]);
787
+ t.is(slotToVal.size, initial); // did not leak reexportedP2
788
+ });
789
+
790
+ test('known promises in message sends should not leak slotToVal entries', async t => {
791
+ const S = 'settlement';
792
+ // cf. src/liveslots.js:initialIDCounters
793
+ const firstPExport = 5;
794
+
795
+ const targetO = `o-1`;
796
+
797
+ const {
798
+ v: { log: vatLogs },
799
+ dispatch,
800
+ dispatchMessage,
801
+ testHooks,
802
+ } = await setupTestLiveslots(t, buildPromiseWatcherRootObject, 'vatA', {
803
+ vatParameters: {
804
+ startOperations: [['createLocalPromise', 'target', kslot(targetO)]],
805
+ },
806
+ });
807
+ const { slotToVal } = testHooks;
808
+ const initial = slotToVal.size;
809
+
810
+ let lastPExport = firstPExport - 1;
811
+ const nextPExport = () => `p+${(lastPExport += 1)}`;
812
+
813
+ let rp;
814
+
815
+ rp = await dispatchMessage('createLocalPromise', 'targetP');
816
+ t.deepEqual(extractDispatchLogs(vatLogs), [
817
+ fulfillmentMessage(rp, 'created local promise: targetP'),
818
+ ]);
819
+ t.is(slotToVal.size, initial);
820
+
821
+ rp = await dispatchMessage('createLocalPromise', 'localP');
822
+ t.deepEqual(extractDispatchLogs(vatLogs), [
823
+ fulfillmentMessage(rp, 'created local promise: localP'),
824
+ ]);
825
+ t.is(slotToVal.size, initial);
826
+
827
+ rp = await dispatchMessage('getPromise', 'localP');
828
+ const localP = nextPExport();
829
+ t.deepEqual(extractDispatchLogs(vatLogs), [
830
+ fulfillmentMessage(rp, { promise: kslot(localP) }),
831
+ ]);
832
+ t.is(slotToVal.size, initial + 1); // localP promise
833
+
834
+ rp = await dispatchMessage('sendToPromise', 'targetP', 'foo', kslot(localP));
835
+ const sendToPromiseResultP = rp;
836
+ t.deepEqual(extractDispatchLogs(vatLogs), []);
837
+ t.is(slotToVal.size, initial + 2); // localP and sendToPromiseResultP promises
838
+
839
+ rp = await dispatchMessage('resolveLocalPromise', 'localP', false, S);
840
+ t.deepEqual(extractDispatchLogs(vatLogs), [
841
+ fulfillmentMessage(localP, S),
842
+ fulfillmentMessage(rp, 'resolved promise: localP'),
843
+ ]);
844
+ t.is(slotToVal.size, initial + 1); // sendToPromiseResultP promise
845
+
846
+ rp = await dispatchMessage(
847
+ 'resolveLocalPromise',
848
+ 'targetP',
849
+ false,
850
+ kslot(targetO),
851
+ );
852
+ const reexportedP = nextPExport(); // Allocate a new promise for importedP
853
+ const sendResultP = nextPExport();
854
+ t.deepEqual(extractDispatchLogs(vatLogs), [
855
+ fulfillmentMessage(rp, 'resolved promise: targetP'), // no await so first syscall
856
+ {
857
+ type: 'send',
858
+ targetSlot: targetO,
859
+ methargs: kser(['foo', [kslot(reexportedP)]]),
860
+ resultSlot: sendResultP,
861
+ },
862
+ fulfillmentMessage(reexportedP, S), // known resolutions comes before subscribe of send result
863
+ subscribeMessage(sendResultP),
864
+ ]);
865
+ t.is(slotToVal.size, initial + 2); // sendToPromiseResultP and sendResultP promises
866
+
867
+ await dispatch(makeResolve(sendResultP, kser(S)));
868
+ t.deepEqual(extractDispatchLogs(vatLogs), [
869
+ fulfillmentMessage(sendToPromiseResultP, S),
870
+ ]);
871
+ t.is(slotToVal.size, initial);
872
+ });