@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.
- package/docs/data-export.md +116 -74
- package/docs/swingstore.md +52 -0
- package/package.json +25 -16
- package/src/assertComplete.js +21 -0
- package/src/bundleStore.js +141 -48
- package/src/exporter.js +221 -0
- package/src/importer.js +133 -0
- package/src/index.js +11 -0
- package/src/internal.js +22 -0
- package/src/kvStore.js +172 -0
- package/src/repairMetadata.js +67 -0
- package/src/snapStore.js +173 -38
- package/src/snapStoreIO.js +8 -0
- package/src/swingStore.js +94 -597
- package/src/transcriptStore.js +265 -61
- package/src/types.d.ts +15 -0
- package/src/types.js +6 -0
- package/src/util.js +7 -1
- package/test/{test-bundles.js → bundles.test.js} +21 -10
- package/test/{test-deletion.js → deletion.test.js} +1 -1
- package/test/export.test.js +350 -0
- package/test/{test-exportImport.js → exportImport.test.js} +87 -67
- package/test/exports.js +102 -0
- package/test/{test-hasher.js → hasher.test.js} +0 -2
- package/test/import.test.js +413 -0
- package/test/repair-metadata.test.js +143 -0
- package/test/{test-snapstore.js → snapstore.test.js} +2 -5
- package/test/{test-state.js → state.test.js} +36 -3
- package/test/util.js +27 -0
- package/{jsconfig.json → tsconfig.json} +1 -0
package/src/transcriptStore.js
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
|
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: (
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
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 {
|
|
384
|
-
*
|
|
385
|
-
* the current
|
|
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(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
|
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 {
|
|
524
|
-
* @param {object}
|
|
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
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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,
|
|
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
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
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('./
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
119
|
+
async *getArtifactNames() {
|
|
120
|
+
yield* [nameA];
|
|
121
|
+
},
|
|
120
122
|
close: async () => undefined,
|
|
121
123
|
};
|
|
122
|
-
const
|
|
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
|
|
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
|
-
/** @
|
|
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 = [];
|