@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.
@@ -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 | undefined} [options.keepTranscripts]
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 initialIncarnation = 0;
223
- sqlWriteSpan.run(vatID, 0, 0, initialHash, 1, initialIncarnation);
224
- const newRec = spanRec(vatID, 0, 0, initialHash, true, 0);
225
- noteExport(spanMetadataKey(newRec), JSON.stringify(newRec));
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
- function doSpanRollover(vatID, isNewIncarnation) {
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 { hash, startPos, endPos, incarnation } = getCurrentSpanBounds(vatID);
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=0
375
+ // and change its DB row to isCurrent=null
267
376
  sqlEndCurrentSpan.run(vatID);
268
377
 
269
- // create a new (empty) row, with isCurrent=1
270
- const incarnationToUse = isNewIncarnation ? incarnation + 1 : incarnation;
271
- sqlWriteSpan.run(vatID, endPos, endPos, initialHash, 1, incarnationToUse);
272
-
273
- // overwrite the transcript.${vatID}.current record with new span
274
- const newRec = spanRec(
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
- return incarnationToUse;
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
- * current. Idempotent.
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
- // and change its DB row to isCurrent=0
380
- sqlEndCurrentSpan.run(vatID);
498
+ // deal with the now-old span
499
+ await closeSpan(vatID, bounds);
381
500
 
382
- // remove the transcript.${vatID}.current record
383
- noteExport(spanMetadataKey({ vatID, isCurrent: true }), undefined);
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 (?, ?, ?, ?)