@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.
@@ -7,7 +7,6 @@ import { checkBundle } from '@endo/check-bundle/lite.js';
7
7
  import { Nat } from '@endo/nat';
8
8
  import { Fail, q } from '@agoric/assert';
9
9
  import { createSHA256 } from './hasher.js';
10
- import { buffer } from './util.js';
11
10
 
12
11
  /**
13
12
  * @typedef { { moduleFormat: 'getExport', source: string, sourceMap?: string } } GetExportBundle
@@ -16,7 +15,8 @@ import { buffer } from './util.js';
16
15
  * @typedef { EndoZipBase64Bundle | GetExportBundle | NestedEvaluateBundle } Bundle
17
16
  */
18
17
  /**
19
- * @typedef { import('./swingStore').SwingStoreExporter } SwingStoreExporter
18
+ * @typedef { import('./exporter.js').SwingStoreExporter } SwingStoreExporter
19
+ * @typedef { import('./internal.js').ArtifactMode } ArtifactMode
20
20
  *
21
21
  * @typedef {{
22
22
  * addBundle: (bundleID: string, bundle: Bundle) => void;
@@ -27,7 +27,10 @@ import { buffer } from './util.js';
27
27
  *
28
28
  * @typedef {{
29
29
  * exportBundle: (name: string) => AsyncIterableIterator<Uint8Array>,
30
- * importBundle: (artifactName: string, exporter: SwingStoreExporter, bundleID: string) => void,
30
+ * repairBundleRecord: (key: string, value: string) => void,
31
+ * importBundleRecord: (key: string, value: string) => void,
32
+ * importBundle: (name: string, dataProvider: () => Promise<Buffer>) => Promise<void>,
33
+ * assertComplete: (checkMode: Omit<ArtifactMode, 'debug'>) => void,
31
34
  * getExportRecords: () => IterableIterator<readonly [key: string, value: string]>,
32
35
  * getArtifactNames: () => AsyncIterableIterator<string>,
33
36
  * getBundleIDs: () => IterableIterator<string>,
@@ -39,6 +42,18 @@ import { buffer } from './util.js';
39
42
  *
40
43
  */
41
44
 
45
+ function bundleIDFromName(name) {
46
+ typeof name === 'string' || Fail`artifact name must be a string`;
47
+ const [tag, ...pieces] = name.split('.');
48
+ if (tag !== 'bundle' || pieces.length !== 1) {
49
+ Fail`expected artifact name of the form 'bundle.{bundleID}', saw ${q(
50
+ name,
51
+ )}`;
52
+ }
53
+ const bundleID = pieces[0];
54
+ return bundleID;
55
+ }
56
+
42
57
  /**
43
58
  * @param {*} db
44
59
  * @param {() => void} ensureTxn
@@ -54,6 +69,9 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
54
69
  )
55
70
  `);
56
71
 
72
+ // A populated record contains both bundleID and bundle, while a
73
+ // pruned record has a bundle of NULL.
74
+
57
75
  function bundleArtifactName(bundleID) {
58
76
  return `bundle.${bundleID}`;
59
77
  }
@@ -62,20 +80,36 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
62
80
  return `b${Nat(version)}-${hash}`;
63
81
  }
64
82
 
65
- const sqlAddBundle = db.prepare(`
66
- INSERT OR REPLACE INTO bundles
67
- (bundleID, bundle)
68
- VALUES (?, ?)
83
+ // the PRIMARY KEY constraint requires the bundleID not already
84
+ // exist
85
+ const sqlAddBundleRecord = db.prepare(`
86
+ INSERT INTO bundles (bundleID, bundle) VALUES (?, NULL)
69
87
  `);
70
88
 
71
- /**
72
- * Store a bundle. Here the bundle itself is presumed valid.
73
- *
74
- * @param {string} bundleID
75
- * @param {Bundle} bundle
76
- */
77
- function addBundle(bundleID, bundle) {
89
+ // this sees both populated and pruned (not-yet-populated) records
90
+ const sqlHasBundleRecord = db.prepare(`
91
+ SELECT count(*)
92
+ FROM bundles
93
+ WHERE bundleID = ?
94
+ `);
95
+ sqlHasBundleRecord.pluck();
96
+
97
+ const sqlPopulateBundleRecord = db.prepare(`
98
+ UPDATE bundles SET bundle = $serialized WHERE bundleID = $bundleID
99
+ `);
100
+
101
+ function addBundleRecord(bundleID) {
102
+ ensureTxn();
103
+ sqlAddBundleRecord.run(bundleID);
104
+ }
105
+
106
+ function populateBundle(bundleID, serialized) {
78
107
  ensureTxn();
108
+ sqlHasBundleRecord.get(bundleID) || Fail`missing ${bundleID}`;
109
+ sqlPopulateBundleRecord.run({ bundleID, serialized });
110
+ }
111
+
112
+ function serializeBundle(bundleID, bundle) {
79
113
  const { moduleFormat } = bundle;
80
114
  let serialized;
81
115
  if (bundleID.startsWith('b0-')) {
@@ -98,19 +132,55 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
98
132
  } else {
99
133
  throw Fail`unsupported BundleID ${bundleID}`;
100
134
  }
101
- sqlAddBundle.run(bundleID, serialized);
135
+ return serialized;
136
+ }
137
+
138
+ /**
139
+ * Store a complete bundle in a single operation, used by runtime
140
+ * (i.e. not an import). We rely upon the caller to provide a
141
+ * correct bundle (e.g. no unexpected properties), but we still
142
+ * check the ID against the contents.
143
+ *
144
+ * @param {string} bundleID
145
+ * @param {Bundle} bundle
146
+ */
147
+ function addBundle(bundleID, bundle) {
148
+ const serialized = serializeBundle(bundleID, bundle);
149
+ addBundleRecord(bundleID);
150
+ populateBundle(bundleID, serialized);
102
151
  noteExport(bundleArtifactName(bundleID), bundleID);
103
152
  }
104
153
 
105
- const sqlHasBundle = db.prepare(`
154
+ const sqlGetPrunedBundles = db.prepare(`
155
+ SELECT bundleID
156
+ FROM bundles
157
+ WHERE bundle IS NULL
158
+ ORDER BY bundleID
159
+ `);
160
+ sqlGetPrunedBundles.pluck();
161
+
162
+ function getPrunedBundles() {
163
+ return sqlGetPrunedBundles.all();
164
+ }
165
+
166
+ function assertComplete(checkMode) {
167
+ assert(checkMode !== 'debug', checkMode);
168
+ const pruned = getPrunedBundles();
169
+ if (pruned.length) {
170
+ throw Fail`missing bundles for: ${pruned.join(',')}`;
171
+ }
172
+ }
173
+
174
+ const sqlHasPopulatedBundle = db.prepare(`
106
175
  SELECT count(*)
107
176
  FROM bundles
108
177
  WHERE bundleID = ?
178
+ AND bundle IS NOT NULL
109
179
  `);
110
- sqlHasBundle.pluck(true);
180
+ sqlHasPopulatedBundle.pluck(true);
111
181
 
112
182
  function hasBundle(bundleID) {
113
- const count = sqlHasBundle.get(bundleID);
183
+ const count = sqlHasPopulatedBundle.get(bundleID);
114
184
  return count !== 0;
115
185
  }
116
186
 
@@ -119,15 +189,15 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
119
189
  FROM bundles
120
190
  WHERE bundleID = ?
121
191
  `);
122
- sqlGetBundle.pluck(true);
123
192
 
124
193
  /**
125
194
  * @param {string} bundleID
126
195
  * @returns {Bundle}
127
196
  */
128
197
  function getBundle(bundleID) {
129
- const rawBundle = sqlGetBundle.get(bundleID);
130
- rawBundle || Fail`bundle ${q(bundleID)} not found`;
198
+ const row =
199
+ sqlGetBundle.get(bundleID) || Fail`bundle ${q(bundleID)} not found`;
200
+ const rawBundle = row.bundle || Fail`bundle ${q(bundleID)} pruned`;
131
201
  if (bundleID.startsWith('b0-')) {
132
202
  return harden(JSON.parse(rawBundle));
133
203
  } else if (bundleID.startsWith('b1-')) {
@@ -153,6 +223,31 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
153
223
  }
154
224
  }
155
225
 
226
+ // take an export-data record (id/hash but not bundle contents) and
227
+ // insert something in the DB
228
+ function importBundleRecord(key, value) {
229
+ const bundleID = bundleIDFromName(key);
230
+ assert.equal(bundleID, value);
231
+ addBundleRecord(bundleID);
232
+ }
233
+
234
+ function repairBundleRecord(key, value) {
235
+ // Bundle records have no metadata, and all bundles must be
236
+ // present (there's no notion of "historical bundle"). So there's
237
+ // no "repair", and if the repair process supplies a bundle record
238
+ // that isn't already present, we throw an error. The repair
239
+ // process doesn't get artifacts, so adding a new record here
240
+ // would fail the subsequent completeness check anyways.
241
+
242
+ const bundleID = bundleIDFromName(key);
243
+ assert.equal(bundleID, value);
244
+ if (sqlHasBundleRecord.get(bundleID)) {
245
+ // record is present, there's no metadata to mismatch, so ignore quietly
246
+ return;
247
+ }
248
+ throw Fail`unexpected new bundle record for ${bundleID} during repair`;
249
+ }
250
+
156
251
  /**
157
252
  * Read a bundle and return it as a stream of data suitable for export to
158
253
  * another store.
@@ -166,16 +261,13 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
166
261
  * @returns {AsyncIterableIterator<Uint8Array>}
167
262
  */
168
263
  async function* exportBundle(name) {
169
- typeof name === 'string' || Fail`artifact name must be a string`;
170
- const parts = name.split('.');
171
- const [type, bundleID] = parts;
172
- // prettier-ignore
173
- (parts.length === 2 && type === 'bundle') ||
174
- Fail`expected artifact name of the form 'bundle.{bundleID}', saw ${q(name)}`;
175
- const rawBundle = sqlGetBundle.get(bundleID);
176
- rawBundle || Fail`bundle ${q(name)} not available`;
264
+ const bundleID = bundleIDFromName(name);
265
+ const row =
266
+ sqlGetBundle.get(bundleID) || Fail`bundle ${q(bundleID)} not found`;
267
+ const rawBundle = row.bundle || Fail`bundle ${q(bundleID)} pruned`;
177
268
  yield* Readable.from(Buffer.from(rawBundle));
178
269
  }
270
+ harden(exportBundle);
179
271
 
180
272
  const sqlGetBundleIDs = db.prepare(`
181
273
  SELECT bundleID
@@ -195,12 +287,14 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
195
287
  yield [bundleArtifactName(bundleID), bundleID];
196
288
  }
197
289
  }
290
+ harden(getExportRecords);
198
291
 
199
292
  async function* getArtifactNames() {
200
293
  for (const bundleID of sqlGetBundleIDs.iterate()) {
201
294
  yield bundleArtifactName(bundleID);
202
295
  }
203
296
  }
297
+ harden(getArtifactNames);
204
298
 
205
299
  function computeSha512(bytes) {
206
300
  const hash = createHash('sha512');
@@ -209,23 +303,17 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
209
303
  }
210
304
 
211
305
  /**
212
- * @param {string} name Artifact name of the bundle
213
- * @param {SwingStoreExporter} exporter Whence to get the bits
214
- * @param {string} bundleID Bundle ID of the bundle
306
+ * Call addBundleRecord() first, then this importBundle() will
307
+ * populate the record.
308
+ *
309
+ * @param {string} name Artifact name, `bundle.${bundleID}`
310
+ * @param {() => Promise<Buffer>} dataProvider Function to get bundle bytes
215
311
  * @returns {Promise<void>}
216
312
  */
217
- async function importBundle(name, exporter, bundleID) {
313
+ async function importBundle(name, dataProvider) {
218
314
  await 0; // no synchronous prefix
219
- const parts = name.split('.');
220
- const [type, bundleIDkey] = parts;
221
- // prettier-ignore
222
- parts.length === 2 && type === 'bundle' ||
223
- Fail`expected artifact name of the form 'bundle.{bundleID}', saw '${q(name)}'`;
224
- bundleIDkey === bundleID ||
225
- Fail`bundle artifact name ${name} doesn't match bundleID ${bundleID}`;
226
- const artifactChunks = exporter.getArtifact(name);
227
- const inStream = Readable.from(artifactChunks);
228
- const data = await buffer(inStream);
315
+ const bundleID = bundleIDFromName(name);
316
+ const data = await dataProvider();
229
317
  if (bundleID.startsWith('b0-')) {
230
318
  // we dissect and reassemble the bundle, to exclude unexpected properties
231
319
  const { moduleFormat, source, sourceMap } = JSON.parse(data.toString());
@@ -234,7 +322,7 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
234
322
  const serialized = JSON.stringify(bundle);
235
323
  bundleID === bundleIdFromHash(0, createSHA256(serialized).finish()) ||
236
324
  Fail`bundleID ${q(bundleID)} does not match bundle artifact`;
237
- addBundle(bundleID, bundle);
325
+ populateBundle(bundleID, serialized);
238
326
  } else if (bundleID.startsWith('b1-')) {
239
327
  /** @type {EndoZipBase64Bundle} */
240
328
  const bundle = harden({
@@ -243,9 +331,8 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
243
331
  endoZipBase64: encodeBase64(data),
244
332
  });
245
333
  // Assert that the bundle contents match the ID and hash
246
- // eslint-disable-next-line @jessie.js/no-nested-await
247
334
  await checkBundle(bundle, computeSha512, bundleID);
248
- addBundle(bundleID, bundle);
335
+ populateBundle(bundleID, serializeBundle(bundleID, bundle));
249
336
  } else {
250
337
  Fail`unsupported BundleID ${q(bundleID)}`;
251
338
  }
@@ -265,7 +352,7 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
265
352
  const dump = {};
266
353
  for (const row of sql.iterate()) {
267
354
  const { bundleID, bundle } = row;
268
- dump[bundleID] = encodeBase64(bundle);
355
+ dump[bundleID] = encodeBase64(Buffer.from(bundle, 'utf-8'));
269
356
  }
270
357
  return dump;
271
358
  }
@@ -280,17 +367,23 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
280
367
  function* getBundleIDs() {
281
368
  yield* sqlListBundleIDs.iterate();
282
369
  }
370
+ harden(getBundleIDs);
283
371
 
284
372
  return harden({
373
+ importBundleRecord,
374
+ importBundle,
375
+ assertComplete,
376
+
285
377
  addBundle,
286
378
  hasBundle,
287
379
  getBundle,
288
380
  deleteBundle,
381
+
289
382
  getExportRecords,
290
383
  getArtifactNames,
291
384
  exportBundle,
292
- importBundle,
293
385
  getBundleIDs,
386
+ repairBundleRecord,
294
387
 
295
388
  dumpBundles,
296
389
  });
@@ -0,0 +1,221 @@
1
+ import sqlite3 from 'better-sqlite3';
2
+
3
+ import { Fail, q } from '@agoric/assert';
4
+
5
+ import { dbFileInDirectory } from './util.js';
6
+ import { getKeyType } from './kvStore.js';
7
+ import { makeBundleStore } from './bundleStore.js';
8
+ import { makeSnapStore } from './snapStore.js';
9
+ import { makeSnapStoreIO } from './snapStoreIO.js';
10
+ import { makeTranscriptStore } from './transcriptStore.js';
11
+ import { assertComplete } from './assertComplete.js';
12
+ import { validateArtifactMode } from './internal.js';
13
+
14
+ /**
15
+ * @template T
16
+ * @typedef { Iterable<T> | AsyncIterable<T> } AnyIterable
17
+ */
18
+ /**
19
+ * @template T
20
+ * @typedef { IterableIterator<T> | AsyncIterableIterator<T> } AnyIterableIterator
21
+ */
22
+
23
+ /**
24
+ *
25
+ * @typedef {readonly [
26
+ * key: string,
27
+ * value?: string | null | undefined,
28
+ * ]} KVPair
29
+ *
30
+ * @typedef {object} SwingStoreExporter
31
+ *
32
+ * Allows export of data from a swingStore as a fixed view onto the content as
33
+ * of the most recent commit point at the time the exporter was created. The
34
+ * exporter may be used while another SwingStore instance is active for the same
35
+ * DB, possibly in another thread or process. It guarantees that regardless of
36
+ * the concurrent activity of other swingStore instances, the data representing
37
+ * the commit point will stay consistent and available.
38
+ *
39
+ * @property {(key: string) => string | undefined} getHostKV
40
+ *
41
+ * Retrieve a value from the "host" portion of the kvStore, just like
42
+ * hostStorage.hostKVStore.get() would do.
43
+ *
44
+ * @property {() => AnyIterableIterator<KVPair>} getExportData
45
+ *
46
+ * Get a full copy of the first-stage export data (key-value pairs) from the
47
+ * swingStore. This represents both the contents of the KVStore (excluding host
48
+ * and local prefixes), as well as any data needed to validate all artifacts,
49
+ * both current and historical. As such it represents the root of trust for the
50
+ * application.
51
+ *
52
+ * Content of validation data (with supporting entries for indexing):
53
+ * - kv.${key} = ${value} // ordinary kvStore data entry
54
+ * - snapshot.${vatID}.${snapPos} = ${{ vatID, snapPos, hash }};
55
+ * - snapshot.${vatID}.current = `snapshot.${vatID}.${snapPos}`
56
+ * - transcript.${vatID}.${startPos} = ${{ vatID, startPos, endPos, hash }}
57
+ * - transcript.${vatID}.current = ${{ vatID, startPos, endPos, hash }}
58
+ *
59
+ * @property {() => AnyIterableIterator<string>} getArtifactNames
60
+ *
61
+ * Get a list of name of artifacts available from the swingStore. A name
62
+ * returned by this method guarantees that a call to `getArtifact` on the same
63
+ * exporter instance will succeed. The `artifactMode` option to
64
+ * `makeSwingStoreExporter` controls the filtering of the artifact names
65
+ * yielded.
66
+ *
67
+ * Artifact names:
68
+ * - transcript.${vatID}.${startPos}.${endPos}
69
+ * - snapshot.${vatID}.${snapPos}
70
+ * - bundle.${bundleID}
71
+ *
72
+ * @property {(name: string) => AnyIterableIterator<Uint8Array>} getArtifact
73
+ *
74
+ * Retrieve an artifact by name as a sequence of binary chunks. May throw if
75
+ * the artifact is not available, which can occur if the artifact is historical
76
+ * and wasn't preserved.
77
+ *
78
+ * @property {() => Promise<void>} close
79
+ *
80
+ * Dispose of all resources held by this exporter. Any further operation on this
81
+ * exporter or its outstanding iterators will fail.
82
+ */
83
+
84
+ /**
85
+ * @typedef { object } ExportSwingStoreOptions
86
+ * @property { import('./internal.js').ArtifactMode } [artifactMode] What artifacts should/must the exporter provide?
87
+ */
88
+
89
+ /**
90
+ * @param {string} dirPath
91
+ * @param { ExportSwingStoreOptions } [options]
92
+ * @returns {SwingStoreExporter}
93
+ */
94
+ export function makeSwingStoreExporter(dirPath, options = {}) {
95
+ typeof dirPath === 'string' || Fail`dirPath must be a string`;
96
+ const { artifactMode = 'operational' } = options;
97
+ validateArtifactMode(artifactMode);
98
+
99
+ const filePath = dbFileInDirectory(dirPath);
100
+ const db = sqlite3(filePath);
101
+
102
+ // Execute the data export in a (read) transaction, to ensure that we are
103
+ // capturing the state of the database at a single point in time. Our close()
104
+ // will ROLLBACK the txn just in case some bug tried to change the DB.
105
+ const sqlBeginTransaction = db.prepare('BEGIN TRANSACTION');
106
+ sqlBeginTransaction.run();
107
+
108
+ // ensureTxn can be a dummy, we just started one
109
+ const ensureTxn = () => {};
110
+ const snapStore = makeSnapStore(db, ensureTxn, makeSnapStoreIO());
111
+ const bundleStore = makeBundleStore(db, ensureTxn);
112
+ const transcriptStore = makeTranscriptStore(db, ensureTxn, () => {});
113
+
114
+ const sqlKVGet = db.prepare(`
115
+ SELECT value
116
+ FROM kvStore
117
+ WHERE key = ?
118
+ `);
119
+ sqlKVGet.pluck(true);
120
+
121
+ /**
122
+ * Obtain the value stored for a given host key. This is for the
123
+ * benefit of clients who need to briefly query the DB to ensure
124
+ * they are exporting the right thing, and need to avoid modifying
125
+ * anything (or creating a read-write DB lock) in the process.
126
+ *
127
+ * @param {string} key The key whose value is sought.
128
+ *
129
+ * @returns {string | undefined} the (string) value for the given key, or
130
+ * undefined if there is no such value.
131
+ *
132
+ * @throws if key is not a string, or the key is not in the host
133
+ * section
134
+ */
135
+ function getHostKV(key) {
136
+ typeof key === 'string' || Fail`key must be a string`;
137
+ getKeyType(key) === 'host' || Fail`getHostKV requires host keys`;
138
+ // @ts-expect-error unknown
139
+ return sqlKVGet.get(key);
140
+ }
141
+
142
+ /** @type {any} */
143
+ const sqlGetAllKVData = db.prepare(`
144
+ SELECT key, value
145
+ FROM kvStore
146
+ ORDER BY key
147
+ `);
148
+
149
+ /**
150
+ * @returns {AsyncIterableIterator<KVPair>}
151
+ * @yields {KVPair}
152
+ */
153
+ async function* getExportData() {
154
+ for (const { key, value } of sqlGetAllKVData.iterate()) {
155
+ if (getKeyType(key) === 'consensus') {
156
+ yield [`kv.${key}`, value];
157
+ }
158
+ }
159
+ yield* snapStore.getExportRecords(true);
160
+ yield* transcriptStore.getExportRecords(true);
161
+ yield* bundleStore.getExportRecords();
162
+ }
163
+ harden(getExportData);
164
+
165
+ /** @yields {string} */
166
+ async function* artifactNames() {
167
+ yield* snapStore.getArtifactNames(artifactMode);
168
+ yield* transcriptStore.getArtifactNames(artifactMode);
169
+ yield* bundleStore.getArtifactNames();
170
+ }
171
+ harden(artifactNames);
172
+
173
+ /**
174
+ * @returns {AsyncIterableIterator<string>}
175
+ */
176
+ function getArtifactNames() {
177
+ if (artifactMode !== 'debug') {
178
+ // throw if this DB will not be able to yield all the desired artifacts
179
+ const internal = { snapStore, bundleStore, transcriptStore };
180
+ assertComplete(internal, artifactMode);
181
+ }
182
+ return artifactNames();
183
+ }
184
+
185
+ /**
186
+ * @param {string} name
187
+ * @returns {AsyncIterableIterator<Uint8Array>}
188
+ */
189
+ function getArtifact(name) {
190
+ typeof name === 'string' || Fail`artifact name must be a string`;
191
+ const [type] = name.split('.', 1);
192
+
193
+ if (type === 'snapshot') {
194
+ return snapStore.exportSnapshot(name);
195
+ } else if (type === 'transcript') {
196
+ return transcriptStore.exportSpan(name);
197
+ } else if (type === 'bundle') {
198
+ return bundleStore.exportBundle(name);
199
+ } else {
200
+ throw Fail`invalid type in artifact name ${q(name)}`;
201
+ }
202
+ }
203
+
204
+ const sqlAbort = db.prepare('ROLLBACK');
205
+
206
+ async function close() {
207
+ // After all the data has been extracted, always abort the export
208
+ // transaction to ensure that the export was read-only (i.e., that no bugs
209
+ // inadvertantly modified the database).
210
+ sqlAbort.run();
211
+ db.close();
212
+ }
213
+
214
+ return harden({
215
+ getHostKV,
216
+ getExportData,
217
+ getArtifactNames,
218
+ getArtifact,
219
+ close,
220
+ });
221
+ }
@@ -0,0 +1,133 @@
1
+ import { Fail, q } from '@agoric/assert';
2
+
3
+ import { makeSwingStore } from './swingStore.js';
4
+ import { buffer } from './util.js';
5
+ import { validateArtifactMode } from './internal.js';
6
+ import { assertComplete } from './assertComplete.js';
7
+
8
+ /**
9
+ * @typedef { object } ImportSwingStoreOptions
10
+ * @property { import('./internal.js').ArtifactMode } [artifactMode] What artifacts should the importer use and require?
11
+ */
12
+
13
+ /**
14
+ * Function used to populate a swingStore from an object implementing the
15
+ * exporter API. The exporter API may be provided by a swingStore instance, or
16
+ * implemented by a host to restore data that was previously exported. The
17
+ * returned swingStore is not suitable for execution, and thus only contains
18
+ * the host facet for committing the populated swingStore.
19
+ *
20
+ * @param {import('./exporter.js').SwingStoreExporter} exporter
21
+ * @param {string | null} [dirPath]
22
+ * @param {ImportSwingStoreOptions} [options]
23
+ * @returns {Promise<Pick<import('./swingStore.js').SwingStore, 'hostStorage' | 'debug'>>}
24
+ */
25
+ export async function importSwingStore(exporter, dirPath = null, options = {}) {
26
+ if (dirPath && typeof dirPath !== 'string') {
27
+ Fail`dirPath must be a string`;
28
+ }
29
+ const { artifactMode = 'operational', ...makeSwingStoreOptions } = options;
30
+ validateArtifactMode(artifactMode);
31
+
32
+ const { hostStorage, kernelStorage, internal, debug } = makeSwingStore(
33
+ dirPath,
34
+ true,
35
+ {
36
+ unsafeFastMode: true,
37
+ ...makeSwingStoreOptions,
38
+ },
39
+ );
40
+
41
+ // For every exportData entry, we add a DB record. 'kv' entries are
42
+ // the "kvStore shadow table", and are not associated with any
43
+ // artifacts. All other entries are associated with an artifact,
44
+ // however the import may or may not contain that artifact (the
45
+ // dataset can be incomplete: either the original DB was pruned at
46
+ // some point, or the exporter did not choose to include
47
+ // everything). The DB records we add are marked as incomplete (as
48
+ // if they had been pruned locally), and can be populated later when
49
+ // the artifact is retrieved.
50
+
51
+ // While unlikely, the getExportData() protocol *is* allowed to
52
+ // deliver multiple values for the same key (last one wins), or use
53
+ // 'null' to delete a previously-defined key. So our first pass both
54
+ // installs the kvStore shadow records, and de-dups/deletes the
55
+ // metadata records into this Map.
56
+
57
+ const allMetadata = new Map();
58
+
59
+ for await (const [key, value] of exporter.getExportData()) {
60
+ const [tag] = key.split('.', 1);
61
+ if (tag === 'kv') {
62
+ // 'kv' keys contain individual kvStore entries
63
+ const subKey = key.substring(tag.length + 1);
64
+ if (value == null) {
65
+ // Note '==' rather than '===': any nullish value implies deletion
66
+ kernelStorage.kvStore.delete(subKey);
67
+ } else {
68
+ kernelStorage.kvStore.set(subKey, value);
69
+ }
70
+ } else if (value == null) {
71
+ allMetadata.delete(key);
72
+ } else {
73
+ allMetadata.set(key, value);
74
+ }
75
+ }
76
+
77
+ // Now take each metadata record and install the stub/pruned entry
78
+ // into the DB.
79
+
80
+ for (const [key, value] of allMetadata.entries()) {
81
+ const [tag] = key.split('.', 1);
82
+ if (tag === 'bundle') {
83
+ internal.bundleStore.importBundleRecord(key, value);
84
+ } else if (tag === 'snapshot') {
85
+ internal.snapStore.importSnapshotRecord(key, value);
86
+ } else if (tag === 'transcript') {
87
+ internal.transcriptStore.importTranscriptSpanRecord(key, value);
88
+ } else {
89
+ Fail`unknown export-data type ${q(tag)} on import`;
90
+ }
91
+ }
92
+
93
+ // All the metadata is now installed, and we're prepared for
94
+ // artifacts. We walk `getArtifactNames()` and offer each one to the
95
+ // submodule, which may ignore it according to `artifactMode`, but
96
+ // otherwise validates and accepts it. This is an initial import, so
97
+ // we don't need to check if we already have the data, but the
98
+ // submodule function is free to do such checks.
99
+
100
+ for await (const name of exporter.getArtifactNames()) {
101
+ const makeChunkIterator = () => exporter.getArtifact(name);
102
+ const dataProvider = async () => buffer(makeChunkIterator());
103
+ const [tag] = name.split('.', 1);
104
+ // TODO: pass the same args to all artifact importers, and let
105
+ // stores register their functions by
106
+ // 'type'. https://github.com/Agoric/agoric-sdk/pull/8075#discussion_r1285265453
107
+ if (tag === 'bundle') {
108
+ await internal.bundleStore.importBundle(name, dataProvider);
109
+ } else if (tag === 'snapshot') {
110
+ await internal.snapStore.populateSnapshot(name, makeChunkIterator, {
111
+ artifactMode,
112
+ });
113
+ } else if (tag === 'transcript') {
114
+ await internal.transcriptStore.populateTranscriptSpan(
115
+ name,
116
+ makeChunkIterator,
117
+ { artifactMode },
118
+ );
119
+ } else {
120
+ Fail`unknown artifact type ${q(tag)} on import`;
121
+ }
122
+ }
123
+
124
+ // We've installed all the artifacts that we could, now do a
125
+ // completeness check. Enforce at least 'operational' completeness,
126
+ // even if the given mode was 'debug'.
127
+
128
+ const checkMode = artifactMode === 'debug' ? 'operational' : artifactMode;
129
+ assertComplete(internal, checkMode);
130
+
131
+ await exporter.close();
132
+ return { hostStorage, debug };
133
+ }