@agoric/swing-store 0.9.2-dev-bf1e645.0 → 0.9.2-dev-07f6885.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/transcriptstore.md +3 -3
- package/package.json +3 -3
- package/src/archiver.js +80 -0
- package/src/index.js +2 -0
- package/src/snapStore.js +55 -67
- package/src/swingStore.js +9 -1
- package/src/transcriptStore.js +167 -145
- package/test/deletion.test.js +182 -131
- package/test/export.test.js +4 -4
- package/test/exportImport.test.js +1 -1
- package/test/snapstore.test.js +47 -2
- package/test/state.test.js +2 -2
package/src/transcriptStore.js
CHANGED
|
@@ -15,10 +15,10 @@ import { createSHA256 } from './hasher.js';
|
|
|
15
15
|
*
|
|
16
16
|
* @typedef {{
|
|
17
17
|
* initTranscript: (vatID: string) => void,
|
|
18
|
-
* rolloverSpan: (vatID: string) => number
|
|
19
|
-
* rolloverIncarnation: (vatID: string) => number
|
|
18
|
+
* rolloverSpan: (vatID: string) => Promise<number>,
|
|
19
|
+
* rolloverIncarnation: (vatID: string) => Promise<number>,
|
|
20
20
|
* getCurrentSpanBounds: (vatID: string) => { startPos: number, endPos: number, hash: string, incarnation: number },
|
|
21
|
-
* stopUsingTranscript: (vatID: string) => void
|
|
21
|
+
* stopUsingTranscript: (vatID: string) => Promise<void>,
|
|
22
22
|
* deleteVatTranscripts: (vatID: string, budget?: number) => { done: boolean, cleanups: number },
|
|
23
23
|
* addItem: (vatID: string, item: string) => void,
|
|
24
24
|
* readSpan: (vatID: string, startPos?: number) => IterableIterator<string>,
|
|
@@ -61,14 +61,15 @@ function insistTranscriptPosition(position) {
|
|
|
61
61
|
* @param {() => void} ensureTxn
|
|
62
62
|
* @param {(key: string, value: string | undefined ) => void} noteExport
|
|
63
63
|
* @param {object} [options]
|
|
64
|
-
* @param {boolean
|
|
64
|
+
* @param {boolean} [options.keepTranscripts]
|
|
65
|
+
* @param {(spanName: string, entries: ReturnType<exportSpan>) => Promise<void>} [options.archiveTranscript]
|
|
65
66
|
* @returns { TranscriptStore & TranscriptStoreInternal & TranscriptStoreDebug }
|
|
66
67
|
*/
|
|
67
68
|
export function makeTranscriptStore(
|
|
68
69
|
db,
|
|
69
70
|
ensureTxn,
|
|
70
71
|
noteExport = () => {},
|
|
71
|
-
{ keepTranscripts = true } = {},
|
|
72
|
+
{ keepTranscripts = true, archiveTranscript } = {},
|
|
72
73
|
) {
|
|
73
74
|
db.exec(`
|
|
74
75
|
CREATE TABLE IF NOT EXISTS transcriptItems (
|
|
@@ -219,10 +220,12 @@ export function makeTranscriptStore(
|
|
|
219
220
|
*/
|
|
220
221
|
function initTranscript(vatID) {
|
|
221
222
|
ensureTxn();
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
223
|
+
const pos = 0;
|
|
224
|
+
const isCurrent = 1;
|
|
225
|
+
const incarnation = 0;
|
|
226
|
+
sqlWriteSpan.run(vatID, pos, pos, initialHash, isCurrent, incarnation);
|
|
227
|
+
const rec = spanRec(vatID, pos, pos, initialHash, isCurrent, incarnation);
|
|
228
|
+
noteExport(spanMetadataKey(rec), JSON.stringify(rec));
|
|
226
229
|
}
|
|
227
230
|
|
|
228
231
|
const sqlGetCurrentSpanBounds = db.prepare(`
|
|
@@ -244,6 +247,102 @@ export function makeTranscriptStore(
|
|
|
244
247
|
return bounds;
|
|
245
248
|
}
|
|
246
249
|
|
|
250
|
+
const sqlGetSpanEndPos = db.prepare(`
|
|
251
|
+
SELECT endPos
|
|
252
|
+
FROM transcriptSpans
|
|
253
|
+
WHERE vatID = ? AND startPos = ?
|
|
254
|
+
`);
|
|
255
|
+
sqlGetSpanEndPos.pluck(true);
|
|
256
|
+
|
|
257
|
+
const sqlReadSpanItems = db.prepare(`
|
|
258
|
+
SELECT item
|
|
259
|
+
FROM transcriptItems
|
|
260
|
+
WHERE vatID = ? AND ? <= position AND position < ?
|
|
261
|
+
ORDER BY position
|
|
262
|
+
`);
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Read the items in a transcript span
|
|
266
|
+
*
|
|
267
|
+
* @param {string} vatID The vat whose transcript is being read
|
|
268
|
+
* @param {number} [startPos] A start position identifying the span to be
|
|
269
|
+
* read; defaults to the current span, whatever it is
|
|
270
|
+
*
|
|
271
|
+
* @returns {IterableIterator<string>} An iterator over the items in the indicated span
|
|
272
|
+
*/
|
|
273
|
+
function readSpan(vatID, startPos) {
|
|
274
|
+
/** @type {number | undefined} */
|
|
275
|
+
let endPos;
|
|
276
|
+
if (startPos === undefined) {
|
|
277
|
+
({ startPos, endPos } = getCurrentSpanBounds(vatID));
|
|
278
|
+
} else {
|
|
279
|
+
insistTranscriptPosition(startPos);
|
|
280
|
+
endPos = sqlGetSpanEndPos.get(vatID, startPos);
|
|
281
|
+
if (typeof endPos !== 'number') {
|
|
282
|
+
throw Fail`no transcript span for ${q(vatID)} at ${q(startPos)}`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
startPos <= endPos || Fail`${q(startPos)} <= ${q(endPos)}}`;
|
|
286
|
+
const expectedCount = endPos - startPos;
|
|
287
|
+
|
|
288
|
+
function* reader() {
|
|
289
|
+
let count = 0;
|
|
290
|
+
for (const { item } of sqlReadSpanItems.iterate(
|
|
291
|
+
vatID,
|
|
292
|
+
startPos,
|
|
293
|
+
endPos,
|
|
294
|
+
)) {
|
|
295
|
+
yield item;
|
|
296
|
+
count += 1;
|
|
297
|
+
}
|
|
298
|
+
count === expectedCount ||
|
|
299
|
+
Fail`read ${q(count)} transcript entries (expected ${q(
|
|
300
|
+
expectedCount,
|
|
301
|
+
)})`;
|
|
302
|
+
}
|
|
303
|
+
harden(reader);
|
|
304
|
+
|
|
305
|
+
if (startPos === endPos) {
|
|
306
|
+
return empty();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return reader();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const sqlGetSpanIsCurrent = db.prepare(`
|
|
313
|
+
SELECT isCurrent
|
|
314
|
+
FROM transcriptSpans
|
|
315
|
+
WHERE vatID = ? AND startPos = ?
|
|
316
|
+
`);
|
|
317
|
+
sqlGetSpanIsCurrent.pluck(true);
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Read a transcript span and return it as a stream of data suitable for
|
|
321
|
+
* export to another store. Transcript items are terminated by newlines.
|
|
322
|
+
*
|
|
323
|
+
* Transcript span artifact names should be strings of the form:
|
|
324
|
+
* `transcript.${vatID}.${startPos}.${endPos}`
|
|
325
|
+
*
|
|
326
|
+
* @param {string} name The name of the transcript artifact to be read
|
|
327
|
+
* @returns {AsyncIterableIterator<Uint8Array>}
|
|
328
|
+
* @yields {Uint8Array}
|
|
329
|
+
*/
|
|
330
|
+
async function* exportSpan(name) {
|
|
331
|
+
typeof name === 'string' || Fail`artifact name must be a string`;
|
|
332
|
+
const parts = name.split('.');
|
|
333
|
+
const [type, vatID, pos] = parts;
|
|
334
|
+
// prettier-ignore
|
|
335
|
+
(parts.length === 4 && type === 'transcript') ||
|
|
336
|
+
Fail`expected artifact name of the form 'transcript.{vatID}.{startPos}.{endPos}', saw ${q(name)}`;
|
|
337
|
+
const isCurrent = sqlGetSpanIsCurrent.get(vatID, pos);
|
|
338
|
+
isCurrent !== undefined || Fail`transcript span ${q(name)} not available`;
|
|
339
|
+
const startPos = Number(pos);
|
|
340
|
+
for (const entry of readSpan(vatID, startPos)) {
|
|
341
|
+
yield Buffer.from(`${entry}\n`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
harden(exportSpan);
|
|
345
|
+
|
|
247
346
|
const sqlEndCurrentSpan = db.prepare(`
|
|
248
347
|
UPDATE transcriptSpans
|
|
249
348
|
SET isCurrent = null
|
|
@@ -255,32 +354,33 @@ export function makeTranscriptStore(
|
|
|
255
354
|
WHERE vatID = ? AND position >= ? AND position < ?
|
|
256
355
|
`);
|
|
257
356
|
|
|
258
|
-
|
|
357
|
+
/**
|
|
358
|
+
* Finalize a span, setting isCurrent to null, marking the resulting record
|
|
359
|
+
* for export, and effecting disk archival and database cleanup as
|
|
360
|
+
* configured.
|
|
361
|
+
* Note that creation of a new DB row and removal/replacement of the
|
|
362
|
+
* transcript.${vatID}.current export record are responsibility of the caller.
|
|
363
|
+
*
|
|
364
|
+
* @param {string} vatID
|
|
365
|
+
* @param {ReturnType<getCurrentSpanBounds>} bounds
|
|
366
|
+
*/
|
|
367
|
+
async function closeSpan(vatID, bounds) {
|
|
259
368
|
ensureTxn();
|
|
260
|
-
const {
|
|
369
|
+
const { startPos, endPos, hash, incarnation } = bounds;
|
|
261
370
|
const rec = spanRec(vatID, startPos, endPos, hash, false, incarnation);
|
|
262
371
|
|
|
263
|
-
// add a new record for the now-old span
|
|
372
|
+
// add a new export record for the now-old span
|
|
264
373
|
noteExport(spanMetadataKey(rec), JSON.stringify(rec));
|
|
265
374
|
|
|
266
|
-
// and change its DB row to isCurrent=
|
|
375
|
+
// and change its DB row to isCurrent=null
|
|
267
376
|
sqlEndCurrentSpan.run(vatID);
|
|
268
377
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
vatID,
|
|
276
|
-
endPos,
|
|
277
|
-
endPos,
|
|
278
|
-
initialHash,
|
|
279
|
-
true,
|
|
280
|
-
incarnationToUse,
|
|
281
|
-
);
|
|
282
|
-
noteExport(spanMetadataKey(newRec), JSON.stringify(newRec));
|
|
283
|
-
|
|
378
|
+
await null;
|
|
379
|
+
if (archiveTranscript) {
|
|
380
|
+
const spanName = spanArtifactName(rec);
|
|
381
|
+
const entries = exportSpan(spanName);
|
|
382
|
+
await archiveTranscript(spanName, entries);
|
|
383
|
+
}
|
|
284
384
|
if (!keepTranscripts) {
|
|
285
385
|
// Delete items of the previously-current span.
|
|
286
386
|
// There may still be items associated with even older spans, but we leave
|
|
@@ -290,7 +390,32 @@ export function makeTranscriptStore(
|
|
|
290
390
|
// that doesn't include them.
|
|
291
391
|
sqlDeleteOldItems.run(vatID, startPos, endPos);
|
|
292
392
|
}
|
|
293
|
-
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function doSpanRollover(vatID, isNewIncarnation) {
|
|
396
|
+
ensureTxn();
|
|
397
|
+
const bounds = getCurrentSpanBounds(vatID);
|
|
398
|
+
const { endPos, incarnation } = bounds;
|
|
399
|
+
|
|
400
|
+
// deal with the now-old span
|
|
401
|
+
await closeSpan(vatID, bounds);
|
|
402
|
+
|
|
403
|
+
// create a new (empty) DB row, with isCurrent=1
|
|
404
|
+
const newSpanIncarnation = isNewIncarnation ? incarnation + 1 : incarnation;
|
|
405
|
+
sqlWriteSpan.run(vatID, endPos, endPos, initialHash, 1, newSpanIncarnation);
|
|
406
|
+
|
|
407
|
+
// overwrite the transcript.${vatID}.current record with new span
|
|
408
|
+
const rec = spanRec(
|
|
409
|
+
vatID,
|
|
410
|
+
endPos,
|
|
411
|
+
endPos,
|
|
412
|
+
initialHash,
|
|
413
|
+
true,
|
|
414
|
+
newSpanIncarnation,
|
|
415
|
+
);
|
|
416
|
+
noteExport(spanMetadataKey(rec), JSON.stringify(rec));
|
|
417
|
+
|
|
418
|
+
return newSpanIncarnation;
|
|
294
419
|
}
|
|
295
420
|
|
|
296
421
|
/**
|
|
@@ -300,9 +425,9 @@ export function makeTranscriptStore(
|
|
|
300
425
|
* @param {string} vatID The vat whose transcript is to rollover to a new
|
|
301
426
|
* span.
|
|
302
427
|
*
|
|
303
|
-
* @returns {number} the new incarnation number
|
|
428
|
+
* @returns {Promise<number>} the new incarnation number
|
|
304
429
|
*/
|
|
305
|
-
function rolloverIncarnation(vatID) {
|
|
430
|
+
async function rolloverIncarnation(vatID) {
|
|
306
431
|
return doSpanRollover(vatID, true);
|
|
307
432
|
}
|
|
308
433
|
|
|
@@ -313,9 +438,9 @@ export function makeTranscriptStore(
|
|
|
313
438
|
* @param {string} vatID The vat whose transcript is to rollover to a new
|
|
314
439
|
* span.
|
|
315
440
|
*
|
|
316
|
-
* @returns {number} the incarnation number
|
|
441
|
+
* @returns {Promise<number>} the incarnation number
|
|
317
442
|
*/
|
|
318
|
-
function rolloverSpan(vatID) {
|
|
443
|
+
async function rolloverSpan(vatID) {
|
|
319
444
|
return doSpanRollover(vatID, false);
|
|
320
445
|
}
|
|
321
446
|
|
|
@@ -360,28 +485,21 @@ export function makeTranscriptStore(
|
|
|
360
485
|
`);
|
|
361
486
|
|
|
362
487
|
/**
|
|
363
|
-
* Prepare for vat deletion by marking the isCurrent span as not
|
|
364
|
-
*
|
|
488
|
+
* Prepare for vat deletion by marking the isCurrent=1 span as not current.
|
|
489
|
+
* Idempotent.
|
|
365
490
|
*
|
|
366
491
|
* @param {string} vatID The vat being terminated/deleted.
|
|
367
492
|
*/
|
|
368
|
-
function stopUsingTranscript(vatID) {
|
|
493
|
+
async function stopUsingTranscript(vatID) {
|
|
369
494
|
ensureTxn();
|
|
370
|
-
// this transforms the current span into a (short) historical one
|
|
371
|
-
// (basically doSpanRollover without adding replacement data)
|
|
372
495
|
const bounds = sqlGetCurrentSpanBounds.get(vatID);
|
|
373
|
-
if (bounds)
|
|
374
|
-
// add a new record for the now-old span
|
|
375
|
-
const { startPos, endPos, hash, incarnation } = bounds;
|
|
376
|
-
const rec = spanRec(vatID, startPos, endPos, hash, false, incarnation);
|
|
377
|
-
noteExport(spanMetadataKey(rec), JSON.stringify(rec));
|
|
496
|
+
if (!bounds) return;
|
|
378
497
|
|
|
379
|
-
|
|
380
|
-
|
|
498
|
+
// deal with the now-old span
|
|
499
|
+
await closeSpan(vatID, bounds);
|
|
381
500
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
501
|
+
// remove the transcript.${vatID}.current record
|
|
502
|
+
noteExport(spanMetadataKey({ vatID, isCurrent: true }), undefined);
|
|
385
503
|
}
|
|
386
504
|
|
|
387
505
|
/**
|
|
@@ -542,7 +660,7 @@ export function makeTranscriptStore(
|
|
|
542
660
|
}
|
|
543
661
|
}
|
|
544
662
|
} else if (artifactMode === 'archival') {
|
|
545
|
-
// every span for all vatIDs that have an isCurrent span (to
|
|
663
|
+
// every span for all vatIDs that have an isCurrent=1 span (to
|
|
546
664
|
// ignore terminated/partially-deleted vats)
|
|
547
665
|
const vatIDs = new Set();
|
|
548
666
|
for (const { vatID } of sqlGetCurrentSpanMetadata.iterate()) {
|
|
@@ -569,102 +687,6 @@ export function makeTranscriptStore(
|
|
|
569
687
|
}
|
|
570
688
|
harden(getArtifactNames);
|
|
571
689
|
|
|
572
|
-
const sqlGetSpanEndPos = db.prepare(`
|
|
573
|
-
SELECT endPos
|
|
574
|
-
FROM transcriptSpans
|
|
575
|
-
WHERE vatID = ? AND startPos = ?
|
|
576
|
-
`);
|
|
577
|
-
sqlGetSpanEndPos.pluck(true);
|
|
578
|
-
|
|
579
|
-
const sqlReadSpanItems = db.prepare(`
|
|
580
|
-
SELECT item
|
|
581
|
-
FROM transcriptItems
|
|
582
|
-
WHERE vatID = ? AND ? <= position AND position < ?
|
|
583
|
-
ORDER BY position
|
|
584
|
-
`);
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Read the items in a transcript span
|
|
588
|
-
*
|
|
589
|
-
* @param {string} vatID The vat whose transcript is being read
|
|
590
|
-
* @param {number} [startPos] A start position identifying the span to be
|
|
591
|
-
* read; defaults to the current span, whatever it is
|
|
592
|
-
*
|
|
593
|
-
* @returns {IterableIterator<string>} An iterator over the items in the indicated span
|
|
594
|
-
*/
|
|
595
|
-
function readSpan(vatID, startPos) {
|
|
596
|
-
/** @type {number | undefined} */
|
|
597
|
-
let endPos;
|
|
598
|
-
if (startPos === undefined) {
|
|
599
|
-
({ startPos, endPos } = getCurrentSpanBounds(vatID));
|
|
600
|
-
} else {
|
|
601
|
-
insistTranscriptPosition(startPos);
|
|
602
|
-
endPos = sqlGetSpanEndPos.get(vatID, startPos);
|
|
603
|
-
if (typeof endPos !== 'number') {
|
|
604
|
-
throw Fail`no transcript span for ${q(vatID)} at ${q(startPos)}`;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
startPos <= endPos || Fail`${q(startPos)} <= ${q(endPos)}}`;
|
|
608
|
-
const expectedCount = endPos - startPos;
|
|
609
|
-
|
|
610
|
-
function* reader() {
|
|
611
|
-
let count = 0;
|
|
612
|
-
for (const { item } of sqlReadSpanItems.iterate(
|
|
613
|
-
vatID,
|
|
614
|
-
startPos,
|
|
615
|
-
endPos,
|
|
616
|
-
)) {
|
|
617
|
-
yield item;
|
|
618
|
-
count += 1;
|
|
619
|
-
}
|
|
620
|
-
count === expectedCount ||
|
|
621
|
-
Fail`read ${q(count)} transcript entries (expected ${q(
|
|
622
|
-
expectedCount,
|
|
623
|
-
)})`;
|
|
624
|
-
}
|
|
625
|
-
harden(reader);
|
|
626
|
-
|
|
627
|
-
if (startPos === endPos) {
|
|
628
|
-
return empty();
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
return reader();
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const sqlGetSpanIsCurrent = db.prepare(`
|
|
635
|
-
SELECT isCurrent
|
|
636
|
-
FROM transcriptSpans
|
|
637
|
-
WHERE vatID = ? AND startPos = ?
|
|
638
|
-
`);
|
|
639
|
-
sqlGetSpanIsCurrent.pluck(true);
|
|
640
|
-
|
|
641
|
-
/**
|
|
642
|
-
* Read a transcript span and return it as a stream of data suitable for
|
|
643
|
-
* export to another store. Transcript items are terminated by newlines.
|
|
644
|
-
*
|
|
645
|
-
* Transcript span artifact names should be strings of the form:
|
|
646
|
-
* `transcript.${vatID}.${startPos}.${endPos}`
|
|
647
|
-
*
|
|
648
|
-
* @param {string} name The name of the transcript artifact to be read
|
|
649
|
-
* @returns {AsyncIterableIterator<Uint8Array>}
|
|
650
|
-
* @yields {Uint8Array}
|
|
651
|
-
*/
|
|
652
|
-
async function* exportSpan(name) {
|
|
653
|
-
typeof name === 'string' || Fail`artifact name must be a string`;
|
|
654
|
-
const parts = name.split('.');
|
|
655
|
-
const [type, vatID, pos] = parts;
|
|
656
|
-
// prettier-ignore
|
|
657
|
-
(parts.length === 4 && type === 'transcript') ||
|
|
658
|
-
Fail`expected artifact name of the form 'transcript.{vatID}.{startPos}.{endPos}', saw ${q(name)}`;
|
|
659
|
-
const isCurrent = sqlGetSpanIsCurrent.get(vatID, pos);
|
|
660
|
-
isCurrent !== undefined || Fail`transcript span ${q(name)} not available`;
|
|
661
|
-
const startPos = Number(pos);
|
|
662
|
-
for (const entry of readSpan(vatID, startPos)) {
|
|
663
|
-
yield Buffer.from(`${entry}\n`);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
harden(exportSpan);
|
|
667
|
-
|
|
668
690
|
const sqlAddItem = db.prepare(`
|
|
669
691
|
INSERT INTO transcriptItems (vatID, item, position, incarnation)
|
|
670
692
|
VALUES (?, ?, ?, ?)
|