@agoric/swing-store 0.9.1 → 0.9.2-calypso-dev-84eb287.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.
@@ -6,7 +6,12 @@ import BufferLineTransform from '@agoric/internal/src/node/buffer-line-transform
6
6
  import { createSHA256 } from './hasher.js';
7
7
 
8
8
  /**
9
- * @typedef { import('./swingStore').SwingStoreExporter } SwingStoreExporter
9
+ * @template T
10
+ * @typedef { IterableIterator<T> | AsyncIterableIterator<T> } AnyIterableIterator<T>
11
+ */
12
+
13
+ /**
14
+ * @typedef { import('./internal.js').ArtifactMode } ArtifactMode
10
15
  *
11
16
  * @typedef {{
12
17
  * initTranscript: (vatID: string) => void,
@@ -19,10 +24,13 @@ import { createSHA256 } from './hasher.js';
19
24
  * }} TranscriptStore
20
25
  *
21
26
  * @typedef {{
22
- * exportSpan: (name: string, includeHistorical: boolean) => AsyncIterableIterator<Uint8Array>
23
- * importSpan: (artifactName: string, exporter: SwingStoreExporter, artifactMetadata: Map) => Promise<void>,
27
+ * exportSpan: (name: string) => AsyncIterableIterator<Uint8Array>
24
28
  * getExportRecords: (includeHistorical: boolean) => IterableIterator<readonly [key: string, value: string]>,
25
- * getArtifactNames: (includeHistorical: boolean) => AsyncIterableIterator<string>,
29
+ * getArtifactNames: (artifactMode: ArtifactMode) => AsyncIterableIterator<string>,
30
+ * importTranscriptSpanRecord: (key: string, value: string) => void,
31
+ * populateTranscriptSpan: (name: string, makeChunkIterator: () => AnyIterableIterator<Uint8Array>, options: { artifactMode: ArtifactMode }) => Promise<void>,
32
+ * assertComplete: (checkMode: Omit<ArtifactMode, 'debug'>) => void,
33
+ * repairTranscriptSpanRecord: (key: string, value: string) => void,
26
34
  * readFullVatTranscript: (vatID: string) => Iterable<{position: number, item: string}>
27
35
  * }} TranscriptStoreInternal
28
36
  *
@@ -35,6 +43,7 @@ import { createSHA256 } from './hasher.js';
35
43
  function* empty() {
36
44
  // Yield nothing
37
45
  }
46
+ harden(empty);
38
47
 
39
48
  /**
40
49
  * @param {number} position
@@ -82,7 +91,7 @@ export function makeTranscriptStore(
82
91
  //
83
92
  // The transcriptItems associated with historical spans may or may not exist,
84
93
  // depending on pruning. However, the items associated with the current span
85
- // must always be present
94
+ // must always be present.
86
95
 
87
96
  db.exec(`
88
97
  CREATE TABLE IF NOT EXISTS transcriptSpans (
@@ -150,6 +159,7 @@ export function makeTranscriptStore(
150
159
  }
151
160
  }
152
161
  }
162
+ harden(readFullVatTranscript);
153
163
 
154
164
  function spanArtifactName(rec) {
155
165
  return `transcript.${rec.vatID}.${rec.startPos}.${rec.endPos}`;
@@ -325,6 +335,13 @@ export function makeTranscriptStore(
325
335
  ORDER BY vatID, startPos
326
336
  `);
327
337
 
338
+ const sqlGetIncarnationSpanMetadata = db.prepare(`
339
+ SELECT vatID, startPos, endPos, hash, isCurrent, incarnation
340
+ FROM transcriptSpans
341
+ WHERE vatID=? AND incarnation=?
342
+ ORDER BY vatID, startPos
343
+ `);
344
+
328
345
  const sqlGetCurrentSpanMetadata = db.prepare(`
329
346
  SELECT vatID, startPos, endPos, hash, isCurrent, incarnation
330
347
  FROM transcriptSpans
@@ -332,6 +349,11 @@ export function makeTranscriptStore(
332
349
  ORDER BY vatID, startPos
333
350
  `);
334
351
 
352
+ function dbRecToExportRec(dbRec) {
353
+ const { vatID, startPos, endPos, hash, isCurrent, incarnation } = dbRec;
354
+ return spanRec(vatID, startPos, endPos, hash, isCurrent, incarnation);
355
+ }
356
+
335
357
  /**
336
358
  * Obtain artifact metadata records for spans contained in this store.
337
359
  *
@@ -354,47 +376,81 @@ export function makeTranscriptStore(
354
376
  * replay will never be required or because such replay would be prohibitively
355
377
  * expensive regardless of need and therefor other repair strategies employed.
356
378
  *
379
+ * The only code path which could use 'false' would be `swingstore.dump()`,
380
+ * which takes the same flag.
381
+ *
357
382
  * @yields {readonly [key: string, value: string]}
358
383
  * @returns {IterableIterator<readonly [key: string, value: string]>}
359
384
  * An iterator over pairs of [spanMetadataKey, rec], where `rec` is a
360
385
  * JSON-encoded metadata record for the span named by `spanMetadataKey`.
361
386
  */
362
387
  function* getExportRecords(includeHistorical = true) {
363
- const sql = includeHistorical
364
- ? sqlGetAllSpanMetadata
365
- : sqlGetCurrentSpanMetadata;
366
- for (const rec of sql.iterate()) {
367
- const { vatID, startPos, endPos, hash, isCurrent, incarnation } = rec;
368
- const exportRec = spanRec(
369
- vatID,
370
- startPos,
371
- endPos,
372
- hash,
373
- isCurrent,
374
- incarnation,
375
- );
376
- yield [spanMetadataKey(rec), JSON.stringify(exportRec)];
388
+ if (includeHistorical) {
389
+ for (const rec of sqlGetAllSpanMetadata.iterate()) {
390
+ yield [spanMetadataKey(rec), JSON.stringify(dbRecToExportRec(rec))];
391
+ }
392
+ } else {
393
+ for (const rec of sqlGetCurrentSpanMetadata.iterate()) {
394
+ yield [spanMetadataKey(rec), JSON.stringify(dbRecToExportRec(rec))];
395
+ }
377
396
  }
378
397
  }
398
+ harden(getExportRecords);
399
+
400
+ const sqlCountSpanItems = db.prepare(`
401
+ SELECT COUNT(*) FROM transcriptItems
402
+ WHERE vatID = ? AND position >= ? AND position < ?
403
+ `);
404
+ sqlCountSpanItems.pluck();
379
405
 
380
406
  /**
381
407
  * Obtain artifact names for spans contained in this store.
382
408
  *
383
- * @param {boolean} includeHistorical If true, include all spans that are
384
- * present in the store regardless of their currency; if false, only include
385
- * the current span for each vat.
386
- *
409
+ * @param {ArtifactMode} artifactMode Control which artifacts should be exported.
410
+ * At 'operational', only include current spans. At 'replay',
411
+ * include all spans of the current incarnation for each vat. At
412
+ * 'archival' and 'debug', include all spans.
387
413
  * @yields {string}
388
414
  * @returns {AsyncIterableIterator<string>} An iterator over the names of all the artifacts requested
389
415
  */
390
- async function* getArtifactNames(includeHistorical) {
391
- const sql = includeHistorical
392
- ? sqlGetAllSpanMetadata
393
- : sqlGetCurrentSpanMetadata;
394
- for (const rec of sql.iterate()) {
395
- yield spanArtifactName(rec);
416
+ async function* getArtifactNames(artifactMode) {
417
+ // for all non-'debug' modes, the exporter asserts that all
418
+ // requested items are present (i.e. the artifacts will be
419
+ // complete), so we don't need to check that ourselves
420
+ if (artifactMode === 'operational') {
421
+ for (const rec of sqlGetCurrentSpanMetadata.iterate()) {
422
+ yield spanArtifactName(rec);
423
+ }
424
+ } else if (artifactMode === 'replay') {
425
+ for (const curRec of sqlGetCurrentSpanMetadata.iterate()) {
426
+ const { vatID, incarnation } = curRec;
427
+ for (const rec of sqlGetIncarnationSpanMetadata.iterate(
428
+ vatID,
429
+ incarnation,
430
+ )) {
431
+ yield spanArtifactName(rec);
432
+ }
433
+ }
434
+ } else if (artifactMode === 'archival') {
435
+ // everything
436
+ for (const rec of sqlGetAllSpanMetadata.iterate()) {
437
+ yield spanArtifactName(rec);
438
+ }
439
+ } else if (artifactMode === 'debug') {
440
+ // everything that is a complete span
441
+ for (const rec of sqlGetAllSpanMetadata.iterate()) {
442
+ const { vatID, startPos, endPos } = rec;
443
+ const count = sqlCountSpanItems.get(vatID, startPos, endPos);
444
+ if (count !== endPos - startPos) {
445
+ // skip incomplete spans, because the exporter did not
446
+ // already do a completeness check in 'debug' mode
447
+ continue;
448
+ }
449
+ yield spanArtifactName(rec);
450
+ }
396
451
  }
397
452
  }
453
+ harden(getArtifactNames);
398
454
 
399
455
  const sqlGetSpanEndPos = db.prepare(`
400
456
  SELECT endPos
@@ -432,16 +488,24 @@ export function makeTranscriptStore(
432
488
  }
433
489
  }
434
490
  startPos <= endPos || Fail`${q(startPos)} <= ${q(endPos)}}`;
491
+ const expectedCount = endPos - startPos;
435
492
 
436
493
  function* reader() {
494
+ let count = 0;
437
495
  for (const { item } of sqlReadSpanItems.iterate(
438
496
  vatID,
439
497
  startPos,
440
498
  endPos,
441
499
  )) {
442
500
  yield item;
501
+ count += 1;
443
502
  }
503
+ count === expectedCount ||
504
+ Fail`read ${q(count)} transcript entries (expected ${q(
505
+ expectedCount,
506
+ )})`;
444
507
  }
508
+ harden(reader);
445
509
 
446
510
  if (startPos === endPos) {
447
511
  return empty();
@@ -465,12 +529,10 @@ export function makeTranscriptStore(
465
529
  * `transcript.${vatID}.${startPos}.${endPos}`
466
530
  *
467
531
  * @param {string} name The name of the transcript artifact to be read
468
- * @param {boolean} includeHistorical If true, allow non-current spans to be fetched
469
- *
470
532
  * @returns {AsyncIterableIterator<Uint8Array>}
471
533
  * @yields {Uint8Array}
472
534
  */
473
- async function* exportSpan(name, includeHistorical) {
535
+ async function* exportSpan(name) {
474
536
  typeof name === 'string' || Fail`artifact name must be a string`;
475
537
  const parts = name.split('.');
476
538
  const [type, vatID, pos] = parts;
@@ -479,14 +541,12 @@ export function makeTranscriptStore(
479
541
  Fail`expected artifact name of the form 'transcript.{vatID}.{startPos}.{endPos}', saw ${q(name)}`;
480
542
  const isCurrent = sqlGetSpanIsCurrent.get(vatID, pos);
481
543
  isCurrent !== undefined || Fail`transcript span ${q(name)} not available`;
482
- isCurrent ||
483
- includeHistorical ||
484
- Fail`transcript span ${q(name)} not available`;
485
544
  const startPos = Number(pos);
486
545
  for (const entry of readSpan(vatID, startPos)) {
487
546
  yield Buffer.from(`${entry}\n`);
488
547
  }
489
548
  }
549
+ harden(exportSpan);
490
550
 
491
551
  const sqlAddItem = db.prepare(`
492
552
  INSERT INTO transcriptItems (vatID, item, position, incarnation)
@@ -516,33 +576,104 @@ export function makeTranscriptStore(
516
576
  noteExport(spanMetadataKey(rec), JSON.stringify(rec));
517
577
  };
518
578
 
579
+ function importTranscriptSpanRecord(key, value) {
580
+ ensureTxn();
581
+ const [tag, keyVatID, keyStartPos] = key.split('.');
582
+ assert.equal(tag, 'transcript');
583
+ const metadata = JSON.parse(value);
584
+ if (key.endsWith('.current') !== Boolean(metadata.isCurrent)) {
585
+ throw Fail`transcript key ${key} mismatches metadata ${metadata}`;
586
+ }
587
+ const { vatID, startPos, endPos, hash, isCurrent, incarnation } = metadata;
588
+ vatID || Fail`transcript metadata missing vatID: ${metadata}`;
589
+ startPos !== undefined ||
590
+ Fail`transcript metadata missing startPos: ${metadata}`;
591
+ endPos !== undefined ||
592
+ Fail`transcript metadata missing endPos: ${metadata}`;
593
+ hash || Fail`transcript metadata missing hash: ${metadata}`;
594
+ isCurrent !== undefined ||
595
+ Fail`transcript metadata missing isCurrent: ${metadata}`;
596
+ incarnation !== undefined ||
597
+ Fail`transcript metadata missing incarnation: ${metadata}`;
598
+ if (keyStartPos !== 'current') {
599
+ if (Number(keyStartPos) !== startPos) {
600
+ Fail`transcript key ${key} mismatches metadata ${metadata}`;
601
+ }
602
+ }
603
+ keyVatID === vatID ||
604
+ Fail`transcript key ${key} mismatches metadata ${metadata}`;
605
+
606
+ // sqlWriteSpan is an INSERT, so the PRIMARY KEY (vatID, position)
607
+ // constraint will catch broken export-data errors like trying to
608
+ // add two different versions of the same span (e.g. one holding
609
+ // items 4..8, a second holding 4..9)
610
+
611
+ sqlWriteSpan.run(
612
+ vatID,
613
+ startPos,
614
+ endPos,
615
+ hash,
616
+ isCurrent ? 1 : null,
617
+ incarnation,
618
+ );
619
+ }
620
+
621
+ const sqlGetSpanMetadataFor = db.prepare(`
622
+ SELECT hash, isCurrent, incarnation, endPos
623
+ FROM transcriptSpans
624
+ WHERE vatID = ? AND startPos = ?
625
+ `);
626
+
627
+ const sqlGetStartOfIncarnation = db.prepare(`
628
+ SELECT startPos
629
+ FROM transcriptSpans
630
+ WHERE vatID=? AND incarnation=?
631
+ ORDER BY startPos ASC LIMIT 1
632
+ `);
633
+ sqlGetStartOfIncarnation.pluck();
634
+
519
635
  /**
520
636
  * Import a transcript span from another store.
521
637
  *
522
638
  * @param {string} name Artifact Name of the transcript span
523
- * @param {SwingStoreExporter} exporter Exporter from which to get the span data
524
- * @param {object} info Metadata describing the span
639
+ * @param {() => AnyIterableIterator<Uint8Array>} makeChunkIterator get an iterator of transcript byte chunks
640
+ * @param {object} options
641
+ * @param {ArtifactMode} options.artifactMode
525
642
  *
526
643
  * @returns {Promise<void>}
527
644
  */
528
- async function importSpan(name, exporter, info) {
645
+ async function populateTranscriptSpan(name, makeChunkIterator, options) {
646
+ ensureTxn();
647
+ const { artifactMode } = options;
529
648
  const parts = name.split('.');
530
649
  const [type, vatID, rawStartPos, rawEndPos] = parts;
531
650
  // prettier-ignore
532
651
  parts.length === 4 && type === 'transcript' ||
533
652
  Fail`expected artifact name of the form 'transcript.{vatID}.{startPos}.{endPos}', saw '${q(name)}'`;
534
- // prettier-ignore
535
- info.vatID === vatID ||
536
- Fail`artifact name says vatID ${q(vatID)}, metadata says ${q(info.vatID)}`;
537
653
  const startPos = Number(rawStartPos);
538
- // prettier-ignore
539
- info.startPos === startPos ||
540
- Fail`artifact name says startPos ${q(startPos)}, metadata says ${q(info.startPos)}`;
541
654
  const endPos = Number(rawEndPos);
542
- // prettier-ignore
543
- info.endPos === endPos ||
544
- Fail`artifact name says endPos ${q(endPos)}, metadata says ${q(info.endPos)}`;
545
- const artifactChunks = exporter.getArtifact(name);
655
+
656
+ const metadata =
657
+ sqlGetSpanMetadataFor.get(vatID, startPos) ||
658
+ Fail`no metadata for transcript span ${name}`;
659
+ assert.equal(metadata.endPos, endPos);
660
+
661
+ if (artifactMode === 'operational') {
662
+ if (!metadata.isCurrent) {
663
+ return; // ignore old spans
664
+ }
665
+ }
666
+ if (artifactMode === 'replay') {
667
+ // ignore spans that aren't for the current incarnation
668
+ const { incarnation } = sqlGetCurrentSpanBounds.get(vatID);
669
+ const incStart = sqlGetStartOfIncarnation.get(vatID, incarnation);
670
+ if (startPos < incStart) {
671
+ return;
672
+ }
673
+ }
674
+ // 'archival' and 'debug' modes accept all spans
675
+
676
+ const artifactChunks = await makeChunkIterator();
546
677
  const inStream = Readable.from(artifactChunks);
547
678
  const lineTransform = new BufferLineTransform();
548
679
  const lineStream = inStream.pipe(lineTransform).setEncoding('utf8');
@@ -550,21 +681,90 @@ export function makeTranscriptStore(
550
681
  let pos = startPos;
551
682
  for await (const line of lineStream) {
552
683
  const item = line.trimEnd();
553
- sqlAddItem.run(vatID, item, pos, info.incarnation);
684
+ sqlAddItem.run(vatID, item, pos, metadata.incarnation);
554
685
  hash = updateSpanHash(hash, item);
555
686
  pos += 1;
556
687
  }
557
- pos === endPos || Fail`artifact ${name} is not available`;
558
- info.hash === hash ||
559
- Fail`artifact ${name} hash is ${q(hash)}, metadata says ${q(info.hash)}`;
560
- sqlWriteSpan.run(
561
- info.vatID,
562
- info.startPos,
563
- info.endPos,
564
- info.hash,
565
- info.isCurrent ? 1 : null,
566
- info.incarnation,
567
- );
688
+ pos === endPos || Fail`artifact ${name} is not complete`;
689
+
690
+ // validate against the previously-established metadata
691
+
692
+ // prettier-ignore
693
+ metadata.hash === hash ||
694
+ Fail`artifact ${name} hash is ${q(hash)}, metadata says ${q(metadata.hash)}`;
695
+
696
+ // If that passes, the not-yet-committed data is good. If it
697
+ // fails, the thrown error will flunk the import and inhibit a
698
+ // commit. So we're done.
699
+ }
700
+
701
+ function repairTranscriptSpanRecord(key, value) {
702
+ ensureTxn();
703
+ const [tag, keyVatID, keyStartPos] = key.split('.');
704
+ assert.equal(tag, 'transcript');
705
+ const metadata = JSON.parse(value);
706
+ const { vatID, startPos, endPos, hash, isCurrent, incarnation } = metadata;
707
+ assert.equal(keyVatID, vatID);
708
+ if (keyStartPos !== 'current') {
709
+ if (Number(keyStartPos) !== startPos) {
710
+ Fail`transcript key ${key} mismatches metadata ${metadata}`;
711
+ }
712
+ }
713
+
714
+ const existing = sqlGetSpanMetadataFor.get(vatID, startPos);
715
+ if (existing) {
716
+ if (
717
+ Boolean(existing.isCurrent) !== Boolean(isCurrent) ||
718
+ existing.hash !== hash ||
719
+ existing.incarnation !== incarnation ||
720
+ existing.endPos !== endPos
721
+ ) {
722
+ throw Fail`repairTranscriptSpanRecord metadata mismatch: ${existing} vs ${metadata}`;
723
+ }
724
+ } else {
725
+ sqlWriteSpan.run(
726
+ vatID,
727
+ startPos,
728
+ endPos,
729
+ hash,
730
+ isCurrent ? 1 : null,
731
+ incarnation,
732
+ );
733
+ }
734
+ }
735
+
736
+ function assertComplete(checkMode) {
737
+ assert(checkMode !== 'debug', checkMode);
738
+ for (const rec of sqlGetCurrentSpanMetadata.iterate()) {
739
+ const { vatID, startPos, endPos, incarnation } = rec;
740
+
741
+ if (checkMode === 'operational') {
742
+ // at 'operational', every 'isCurrent' transcript span must
743
+ // have all items
744
+ const count = sqlCountSpanItems.get(vatID, startPos, endPos);
745
+ if (count !== endPos - startPos) {
746
+ throw Fail`incomplete current transcript span: ${count} items, ${rec}`;
747
+ }
748
+ } else if (checkMode === 'replay') {
749
+ // at 'replay', every vat's current incarnation must be fully
750
+ // populated (which implies 'operational')
751
+ const incStart = sqlGetStartOfIncarnation.get(vatID, incarnation);
752
+ const incCount = sqlCountSpanItems.get(vatID, incStart, endPos);
753
+ if (incCount !== endPos - incStart) {
754
+ throw Fail`incomplete current incarnation transcript: ${incCount} items`;
755
+ }
756
+ } else if (checkMode === 'archival') {
757
+ // at 'archival', every incarnation must be fully populated,
758
+ // which means position=0 up through endPos-1 (which implies
759
+ // 'replay')
760
+ const arcCount = sqlCountSpanItems.get(vatID, 0, endPos);
761
+ if (arcCount !== endPos) {
762
+ throw Fail`incomplete archival transcript: ${arcCount} vs ${endPos}`;
763
+ }
764
+ } else {
765
+ throw Fail`unknown checkMode ${checkMode}`;
766
+ }
767
+ }
568
768
  }
569
769
 
570
770
  return harden({
@@ -577,10 +777,14 @@ export function makeTranscriptStore(
577
777
  deleteVatTranscripts,
578
778
 
579
779
  exportSpan,
580
- importSpan,
581
780
  getExportRecords,
582
781
  getArtifactNames,
583
782
 
783
+ importTranscriptSpanRecord,
784
+ populateTranscriptSpan,
785
+ assertComplete,
786
+ repairTranscriptSpanRecord,
787
+
584
788
  dumpTranscripts,
585
789
  readFullVatTranscript,
586
790
  });
package/src/types.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ export type {
2
+ SwingStore,
3
+ SwingStoreKernelStorage,
4
+ SwingStoreHostStorage,
5
+ } from './swingStore.js';
6
+ export type { KVStore } from './kvStore.js';
7
+ export type { BundleStore } from './bundleStore.js';
8
+ export type { SnapStore, SnapshotResult, SnapshotInfo } from './snapStore.js';
9
+ export type { TranscriptStore } from './transcriptStore.js';
10
+ export type { ArtifactMode } from './internal.js';
11
+ export type { ImportSwingStoreOptions } from './importer.js';
12
+ export type {
13
+ SwingStoreExporter,
14
+ ExportSwingStoreOptions,
15
+ } from './exporter.js';
package/src/types.js ADDED
@@ -0,0 +1,6 @@
1
+ // Types for the public API
2
+
3
+ // Everything this "exports" actually comes from the neighboring types.d.ts file
4
+
5
+ // Ensure this is a module.
6
+ export {};
package/src/util.js CHANGED
@@ -1,3 +1,4 @@
1
+ import path from 'path';
1
2
  import { Buffer } from 'buffer';
2
3
 
3
4
  /**
@@ -5,7 +6,7 @@ import { Buffer } from 'buffer';
5
6
  * 'stream/consumers' package, which unfortunately only exists in newer versions
6
7
  * of Node.
7
8
  *
8
- * @param {import('./swingStore').AnyIterable<Uint8Array>} inStream
9
+ * @param {import('./exporter.js').AnyIterable<Uint8Array>} inStream
9
10
  */
10
11
  export const buffer = async inStream => {
11
12
  const chunks = [];
@@ -14,3 +15,8 @@ export const buffer = async inStream => {
14
15
  }
15
16
  return Buffer.concat(chunks);
16
17
  };
18
+
19
+ export function dbFileInDirectory(dirPath) {
20
+ const filePath = path.resolve(dirPath, 'swingstore.sqlite');
21
+ return filePath;
22
+ }
@@ -1,14 +1,11 @@
1
1
  // @ts-check
2
- import '@endo/init/debug.js';
3
2
  import test from 'ava';
4
3
  import tmp from 'tmp';
5
4
  import { Buffer } from 'buffer';
6
5
  import { createSHA256 } from '../src/hasher.js';
7
- import {
8
- importSwingStore,
9
- initSwingStore,
10
- makeSwingStoreExporter,
11
- } from '../src/swingStore.js';
6
+ import { initSwingStore } from '../src/swingStore.js';
7
+ import { makeSwingStoreExporter } from '../src/exporter.js';
8
+ import { importSwingStore } from '../src/importer.js';
12
9
  import { buffer } from '../src/util.js';
13
10
 
14
11
  function makeB0ID(bundle) {
@@ -109,6 +106,9 @@ test('b0 import', async t => {
109
106
  const idA = makeB0ID(b0A);
110
107
  const nameA = `bundle.${idA}`;
111
108
  const exporter = {
109
+ getHostKV(_key) {
110
+ return undefined;
111
+ },
112
112
  async *getExportData() {
113
113
  yield /** @type {const} */ ([nameA, idA]);
114
114
  },
@@ -116,10 +116,16 @@ test('b0 import', async t => {
116
116
  t.is(name, nameA);
117
117
  yield Buffer.from(JSON.stringify(b0A));
118
118
  },
119
- getArtifactNames: () => assert.fail('import should not query all names'),
119
+ async *getArtifactNames() {
120
+ yield* [nameA];
121
+ },
120
122
  close: async () => undefined,
121
123
  };
122
- const { kernelStorage } = await importSwingStore(exporter);
124
+ const ss = await importSwingStore(exporter);
125
+ t.teardown(ss.hostStorage.close);
126
+ await ss.hostStorage.commit();
127
+ const serialized = ss.debug.serialize();
128
+ const { kernelStorage } = initSwingStore(null, { serialized });
123
129
  const { bundleStore } = kernelStorage;
124
130
  t.truthy(bundleStore.hasBundle(idA));
125
131
  t.deepEqual(bundleStore.getBundle(idA), b0A);
@@ -131,6 +137,9 @@ test('b0 bad import', async t => {
131
137
  const idA = makeB0ID(b0A);
132
138
  const nameA = `bundle.${idA}`;
133
139
  const exporter = {
140
+ getHostKV(_key) {
141
+ return undefined;
142
+ },
134
143
  async *getExportData() {
135
144
  yield /** @type {const} */ ([nameA, idA]);
136
145
  },
@@ -138,7 +147,9 @@ test('b0 bad import', async t => {
138
147
  t.is(name, nameA);
139
148
  yield Buffer.from(JSON.stringify(b0Abogus));
140
149
  },
141
- getArtifactNames: () => assert.fail('import should not query all names'),
150
+ async *getArtifactNames() {
151
+ yield* [nameA];
152
+ },
142
153
  close: async () => undefined,
143
154
  };
144
155
  await t.throwsAsync(async () => importSwingStore(exporter), {
@@ -150,7 +161,7 @@ test('unknown format', t => {
150
161
  const { kernelStorage } = initSwingStore();
151
162
  const { bundleStore } = kernelStorage;
152
163
  const unknownID = 'b1999-whoa-futuristic';
153
- /** @typedef {import('../src/bundleStore.js').Bundle} Bundle */
164
+ /** @import {Bundle} from '../src/bundleStore.js' */
154
165
  t.throws(() => bundleStore.addBundle(unknownID, /** @type {Bundle} */ ({})), {
155
166
  message: /unsupported BundleID/,
156
167
  });
@@ -1,12 +1,12 @@
1
1
  // @ts-check
2
2
  import test from 'ava';
3
- import '@endo/init/debug.js';
4
3
  import { Buffer } from 'node:buffer';
5
4
  import { initSwingStore } from '../src/swingStore.js';
6
5
 
7
6
  async function* getSnapshotStream() {
8
7
  yield Buffer.from('abc');
9
8
  }
9
+ harden(getSnapshotStream);
10
10
 
11
11
  test('delete snapshots with export callback', async t => {
12
12
  const exportLog = [];