@agoric/swing-store 0.9.2-u11wf.0 → 0.9.2-u12.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/CHANGELOG.md +15 -0
- package/docs/data-export.md +10 -20
- package/package.json +3 -3
- package/src/importer.js +14 -6
- package/src/swingStore.js +31 -3
- package/test/test-bundles.js +5 -1
- package/test/test-exportImport.js +1 -0
- package/test/test-import.js +2 -0
- package/test/test-repair-metadata.js +23 -9
- package/test/test-state.js +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
### [0.9.2-u12.0](https://github.com/Agoric/agoric-sdk/compare/@agoric/swing-store@0.9.2-u11wf.0...@agoric/swing-store@0.9.2-u12.0) (2023-11-10)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **swing-store:** faster import of swing-store ([35aef87](https://github.com/Agoric/agoric-sdk/commit/35aef87ec0f10b7f0cdce462ac0509296e8bd752))
|
|
12
|
+
* **swing-store:** prevent SwingSet usage of imported swing-store ([03f642d](https://github.com/Agoric/agoric-sdk/commit/03f642d39f90ef9465a439723c3a69beef73bd61))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **swing-store:** ensure crank savepoint is wrapped in transaction ([8d738c6](https://github.com/Agoric/agoric-sdk/commit/8d738c65ed37b9159e94fbcf291ed7fe8478ee5a))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
### [0.9.2-u11wf.0](https://github.com/Agoric/agoric-sdk/compare/@agoric/swing-store@0.9.2-u11.0...@agoric/swing-store@0.9.2-u11wf.0) (2023-09-23)
|
|
7
22
|
|
|
8
23
|
**Note:** Version bump only for package @agoric/swing-store
|
package/docs/data-export.md
CHANGED
|
@@ -125,21 +125,9 @@ Then, on the few occasions when the application needs to build a full state-sync
|
|
|
125
125
|
|
|
126
126
|
## Import
|
|
127
127
|
|
|
128
|
-
On other end of the export process is an importer. This is a new host application
|
|
128
|
+
On the other end of the export process is an importer. This is used to restore kernel state, so that a new host application can simply continue mostly as if it had been previously executing. The expectation is that the import and the execution are 2 independent events, and the execution doesn't need to be aware it was imported.
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
```js
|
|
133
|
-
// this is done only the first time an instance is created:
|
|
134
|
-
|
|
135
|
-
import { openSwingStore } from '@agoric/swing-store';
|
|
136
|
-
import { initializeSwingset } from '@agoric/swingset-vat';
|
|
137
|
-
const dirPath = './swing-store';
|
|
138
|
-
const { hostStorage, kernelStorage } = openSwingStore(dirPath);
|
|
139
|
-
await initializeSwingset(config, argv, kernelStorage);
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
Once the initial state is created, each time the application is launched, it will build a controller around the existing state:
|
|
130
|
+
For reference, after the initial state is created, each time the application is launched, it builds a controller around the existing state:
|
|
143
131
|
|
|
144
132
|
```js
|
|
145
133
|
import { openSwingStore } from '@agoric/swing-store';
|
|
@@ -150,7 +138,7 @@ const controller = await makeSwingsetController(kernelStorage);
|
|
|
150
138
|
// ... now do things like controller.run(), etc
|
|
151
139
|
```
|
|
152
140
|
|
|
153
|
-
When cloning an existing kernel, the
|
|
141
|
+
When cloning an existing kernel, the host application first imports and commits the restored state using `importSwingStore`. The host application should feed the importer with the export data and artifacts, by passing an object that has the same API as the SwingStore's exporter:
|
|
154
142
|
|
|
155
143
|
```js
|
|
156
144
|
import { importSwingStore } from '@agoric/swing-store';
|
|
@@ -161,11 +149,13 @@ const exporter = {
|
|
|
161
149
|
getArtifact(name) { // return blob of artifact data },
|
|
162
150
|
};
|
|
163
151
|
const { hostStorage } = importSwingStore(exporter, dirPath);
|
|
164
|
-
hostStorage
|
|
165
|
-
|
|
152
|
+
// Update any hostStorage as needed
|
|
153
|
+
await hostStorage.commit();
|
|
154
|
+
await hostStorage.close();
|
|
155
|
+
// now the populated swingstore can be re-opened using `openSwingStore``
|
|
166
156
|
```
|
|
167
157
|
|
|
168
|
-
Once the new SwingStore is fully populated with the previously-exported data, the host application can
|
|
158
|
+
Once the new SwingStore is fully populated with the previously-exported data, the host application can update any host specific state before committing and closing the SwingStore. `importSwingStore` returns only the host facet of the SwingStore instance, as it is not suitable for immediate execution.
|
|
169
159
|
|
|
170
160
|
## Optional / Historical Data
|
|
171
161
|
|
|
@@ -196,14 +186,14 @@ Also note that when a vat is terminated, we delete all information about it, inc
|
|
|
196
186
|
|
|
197
187
|
When importing, the `importSwingStore()` function's options bag takes a property named `artifactMode`, with the same meanings as for export. Importing with the `operational` mode will ignore any artifacts other than those needed for current operations, and will fail unless all such artifacts were available. Importing with `replay` will ignore spans from old incarnations, but will fail unless all spans from current incarnations are present. Importing with `archival` will fail unless all spans from all incarnations are present. There is no `debug` option during import.
|
|
198
188
|
|
|
199
|
-
`importSwingStore()`
|
|
189
|
+
While `importSwingStore()`'s options bag accepts the same options as `openSwingStore()`, since it returns only the host facet of a SwingStore, some of these options might not be meaningful, such as `keepTranscripts`.
|
|
200
190
|
|
|
201
191
|
So, to avoid pruning current-incarnation historical transcript spans when exporting from one swingstore to another, you must set (or avoid overriding) the following options along the way:
|
|
202
192
|
|
|
203
193
|
* the original swingstore must not be opened with `{ keepTranscripts: false }`, otherwise the old spans will be pruned immediately
|
|
204
194
|
* the export must use `makeSwingStoreExporter(dirpath, { artifactMode: 'replay'})`, otherwise the export will omit the old spans
|
|
205
195
|
* the import must use `importSwingStore(exporter, dirPath, { artifactMode: 'replay'})`, otherwise the import will ignore the old spans
|
|
206
|
-
*
|
|
196
|
+
* subsequent `openSwingStore` calls must not use `keepTranscripts: false`, otherwise the new swingstore will prune historical spans as new ones are created (during `rolloverSpan`).
|
|
207
197
|
|
|
208
198
|
## Implementation Details
|
|
209
199
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agoric/swing-store",
|
|
3
|
-
"version": "0.9.2-
|
|
3
|
+
"version": "0.9.2-u12.0",
|
|
4
4
|
"description": "Persistent storage for SwingSet",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@agoric/assert": "^0.6.1-u11wf.0",
|
|
25
|
-
"@agoric/internal": "^0.
|
|
25
|
+
"@agoric/internal": "^0.4.0-u12.0",
|
|
26
26
|
"@endo/base64": "0.2.31",
|
|
27
27
|
"@endo/bundle-source": "2.5.2-upstream-rollup",
|
|
28
28
|
"@endo/check-bundle": "0.2.18",
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
],
|
|
46
46
|
"timeout": "2m"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "ee5a5fdad9187a6b1b7b26b772b6564c0db3062e"
|
|
49
49
|
}
|
package/src/importer.js
CHANGED
|
@@ -11,14 +11,16 @@ import { assertComplete } from './assertComplete.js';
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Function used to
|
|
14
|
+
* Function used to populate a swingStore from an object implementing the
|
|
15
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.
|
|
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.
|
|
17
19
|
*
|
|
18
20
|
* @param {import('./exporter').SwingStoreExporter} exporter
|
|
19
21
|
* @param {string | null} [dirPath]
|
|
20
22
|
* @param {ImportSwingStoreOptions} [options]
|
|
21
|
-
* @returns {Promise<import('./swingStore').SwingStore
|
|
23
|
+
* @returns {Promise<Pick<import('./swingStore').SwingStore, 'hostStorage' | 'debug'>>}
|
|
22
24
|
*/
|
|
23
25
|
export async function importSwingStore(exporter, dirPath = null, options = {}) {
|
|
24
26
|
if (dirPath && typeof dirPath !== 'string') {
|
|
@@ -27,8 +29,14 @@ export async function importSwingStore(exporter, dirPath = null, options = {}) {
|
|
|
27
29
|
const { artifactMode = 'operational', ...makeSwingStoreOptions } = options;
|
|
28
30
|
validateArtifactMode(artifactMode);
|
|
29
31
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
+
const { hostStorage, kernelStorage, internal, debug } = makeSwingStore(
|
|
33
|
+
dirPath,
|
|
34
|
+
true,
|
|
35
|
+
{
|
|
36
|
+
unsafeFastMode: true,
|
|
37
|
+
...makeSwingStoreOptions,
|
|
38
|
+
},
|
|
39
|
+
);
|
|
32
40
|
|
|
33
41
|
// For every exportData entry, we add a DB record. 'kv' entries are
|
|
34
42
|
// the "kvStore shadow table", and are not associated with any
|
|
@@ -121,5 +129,5 @@ export async function importSwingStore(exporter, dirPath = null, options = {}) {
|
|
|
121
129
|
assertComplete(internal, checkMode);
|
|
122
130
|
|
|
123
131
|
await exporter.close();
|
|
124
|
-
return
|
|
132
|
+
return { hostStorage, debug };
|
|
125
133
|
}
|
package/src/swingStore.js
CHANGED
|
@@ -203,9 +203,29 @@ export function makeSwingStore(dirPath, forceReset, options = {}) {
|
|
|
203
203
|
// mode that defers merge work for a later attempt rather than block any
|
|
204
204
|
// potential readers or writers. See https://sqlite.org/wal.html for details.
|
|
205
205
|
|
|
206
|
+
// However we also allow opening the DB with journaling off, which is unsafe
|
|
207
|
+
// and doesn't support rollback, but avoids any overhead for large
|
|
208
|
+
// transactions like for during an import.
|
|
209
|
+
|
|
210
|
+
function setUnsafeFastMode(enabled) {
|
|
211
|
+
const journalMode = enabled ? 'off' : 'wal';
|
|
212
|
+
const synchronousMode = enabled ? 'normal' : 'full';
|
|
213
|
+
!db.inTransaction || Fail`must not be in a transaction`;
|
|
214
|
+
|
|
215
|
+
db.unsafeMode(!!enabled);
|
|
216
|
+
// The WAL mode is persistent so it's not possible to switch to a different
|
|
217
|
+
// mode for an existing DB.
|
|
218
|
+
const actualMode = db.pragma(`journal_mode=${journalMode}`, {
|
|
219
|
+
simple: true,
|
|
220
|
+
});
|
|
221
|
+
actualMode === journalMode ||
|
|
222
|
+
filePath === ':memory:' ||
|
|
223
|
+
Fail`Couldn't set swing-store DB to ${journalMode} mode (is ${actualMode})`;
|
|
224
|
+
db.pragma(`synchronous=${synchronousMode}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
206
227
|
// PRAGMAs have to happen outside a transaction
|
|
207
|
-
|
|
208
|
-
db.exec(`PRAGMA synchronous=FULL`);
|
|
228
|
+
setUnsafeFastMode(options.unsafeFastMode);
|
|
209
229
|
|
|
210
230
|
// We use IMMEDIATE because the kernel is supposed to be the sole writer of
|
|
211
231
|
// the DB, and if some other process is holding a write lock, we want to find
|
|
@@ -370,6 +390,10 @@ export function makeSwingStore(dirPath, forceReset, options = {}) {
|
|
|
370
390
|
inCrank || Fail`establishCrankSavepoint outside of crank`;
|
|
371
391
|
const savepointOrdinal = savepoints.length;
|
|
372
392
|
savepoints.push(savepoint);
|
|
393
|
+
// We must be in a transaction when creating the savepoint or releasing it
|
|
394
|
+
// later will cause an autocommit.
|
|
395
|
+
// See https://github.com/Agoric/agoric-sdk/issues/8423
|
|
396
|
+
ensureTxn();
|
|
373
397
|
const sql = db.prepare(`SAVEPOINT t${savepointOrdinal}`);
|
|
374
398
|
sql.run();
|
|
375
399
|
}
|
|
@@ -477,7 +501,11 @@ export function makeSwingStore(dirPath, forceReset, options = {}) {
|
|
|
477
501
|
}
|
|
478
502
|
|
|
479
503
|
/** @type {import('./internal.js').SwingStoreInternal} */
|
|
480
|
-
const internal = harden({
|
|
504
|
+
const internal = harden({
|
|
505
|
+
snapStore,
|
|
506
|
+
transcriptStore,
|
|
507
|
+
bundleStore,
|
|
508
|
+
});
|
|
481
509
|
|
|
482
510
|
async function repairMetadata(exporter) {
|
|
483
511
|
return doRepairMetadata(internal, exporter);
|
package/test/test-bundles.js
CHANGED
|
@@ -119,7 +119,11 @@ test('b0 import', async t => {
|
|
|
119
119
|
},
|
|
120
120
|
close: async () => undefined,
|
|
121
121
|
};
|
|
122
|
-
const
|
|
122
|
+
const ss = await importSwingStore(exporter);
|
|
123
|
+
t.teardown(ss.hostStorage.close);
|
|
124
|
+
await ss.hostStorage.commit();
|
|
125
|
+
const serialized = ss.debug.serialize();
|
|
126
|
+
const { kernelStorage } = initSwingStore(null, { serialized });
|
|
123
127
|
const { bundleStore } = kernelStorage;
|
|
124
128
|
t.truthy(bundleStore.hasBundle(idA));
|
|
125
129
|
t.deepEqual(bundleStore.getBundle(idA), b0A);
|
package/test/test-import.js
CHANGED
|
@@ -81,6 +81,7 @@ test('import empty', async t => {
|
|
|
81
81
|
t.teardown(cleanup);
|
|
82
82
|
const exporter = makeExporter(new Map(), new Map());
|
|
83
83
|
const ss = await importSwingStore(exporter, dbDir);
|
|
84
|
+
t.teardown(ss.hostStorage.close);
|
|
84
85
|
await ss.hostStorage.commit();
|
|
85
86
|
const data = convert(ss.debug.dump());
|
|
86
87
|
t.deepEqual(data, {
|
|
@@ -164,6 +165,7 @@ const importTest = test.macro(async (t, mode) => {
|
|
|
164
165
|
|
|
165
166
|
// now import
|
|
166
167
|
const ss = await importSwingStore(exporter, dbDir, { artifactMode });
|
|
168
|
+
t.teardown(ss.hostStorage.close);
|
|
167
169
|
await ss.hostStorage.commit();
|
|
168
170
|
const data = convert(ss.debug.dump());
|
|
169
171
|
|
|
@@ -6,7 +6,7 @@ import path from 'path';
|
|
|
6
6
|
import test from 'ava';
|
|
7
7
|
import sqlite3 from 'better-sqlite3';
|
|
8
8
|
|
|
9
|
-
import { importSwingStore } from '../src/index.js';
|
|
9
|
+
import { importSwingStore, openSwingStore } from '../src/index.js';
|
|
10
10
|
|
|
11
11
|
import { makeExporter, buildData } from './test-import.js';
|
|
12
12
|
import { tmpDir } from './util.js';
|
|
@@ -21,8 +21,9 @@ test('repair metadata', async t => {
|
|
|
21
21
|
// then manually deleting the historical metadata entries from the
|
|
22
22
|
// DB
|
|
23
23
|
const exporter = makeExporter(exportData, artifacts);
|
|
24
|
-
const
|
|
25
|
-
await
|
|
24
|
+
const ssi = await importSwingStore(exporter, dbDir);
|
|
25
|
+
await ssi.hostStorage.commit();
|
|
26
|
+
await ssi.hostStorage.close();
|
|
26
27
|
|
|
27
28
|
const filePath = path.join(dbDir, 'swingstore.sqlite');
|
|
28
29
|
const db = sqlite3(filePath);
|
|
@@ -53,6 +54,8 @@ test('repair metadata', async t => {
|
|
|
53
54
|
t.deepEqual(ss2, [7]);
|
|
54
55
|
|
|
55
56
|
// now fix it
|
|
57
|
+
const ss = openSwingStore(dbDir);
|
|
58
|
+
t.teardown(ss.hostStorage.close);
|
|
56
59
|
await ss.hostStorage.repairMetadata(exporter);
|
|
57
60
|
await ss.hostStorage.commit();
|
|
58
61
|
|
|
@@ -64,6 +67,7 @@ test('repair metadata', async t => {
|
|
|
64
67
|
|
|
65
68
|
// repair should be idempotent
|
|
66
69
|
await ss.hostStorage.repairMetadata(exporter);
|
|
70
|
+
await ss.hostStorage.commit();
|
|
67
71
|
|
|
68
72
|
const ts4 = getTS.all('v1');
|
|
69
73
|
t.deepEqual(ts4, [0, 2, 5, 8]); // still there
|
|
@@ -78,11 +82,15 @@ test('repair metadata ignores kvStore entries', async t => {
|
|
|
78
82
|
const { exportData, artifacts } = buildData();
|
|
79
83
|
|
|
80
84
|
const exporter = makeExporter(exportData, artifacts);
|
|
81
|
-
const
|
|
82
|
-
await
|
|
85
|
+
const ssi = await importSwingStore(exporter, dbDir);
|
|
86
|
+
await ssi.hostStorage.commit();
|
|
87
|
+
await ssi.hostStorage.close();
|
|
83
88
|
|
|
84
89
|
// perform the repair with spurious kv entries
|
|
85
90
|
exportData.set('kv.key2', 'value2');
|
|
91
|
+
|
|
92
|
+
const ss = openSwingStore(dbDir);
|
|
93
|
+
t.teardown(ss.hostStorage.close);
|
|
86
94
|
await ss.hostStorage.repairMetadata(exporter);
|
|
87
95
|
await ss.hostStorage.commit();
|
|
88
96
|
|
|
@@ -97,14 +105,17 @@ test('repair metadata rejects mismatched snapshot entries', async t => {
|
|
|
97
105
|
const { exportData, artifacts } = buildData();
|
|
98
106
|
|
|
99
107
|
const exporter = makeExporter(exportData, artifacts);
|
|
100
|
-
const
|
|
101
|
-
await
|
|
108
|
+
const ssi = await importSwingStore(exporter, dbDir);
|
|
109
|
+
await ssi.hostStorage.commit();
|
|
110
|
+
await ssi.hostStorage.close();
|
|
102
111
|
|
|
103
112
|
// perform the repair with mismatched snapshot entry
|
|
104
113
|
const old = JSON.parse(exportData.get('snapshot.v1.4'));
|
|
105
114
|
const wrong = { ...old, hash: 'wrong' };
|
|
106
115
|
exportData.set('snapshot.v1.4', JSON.stringify(wrong));
|
|
107
116
|
|
|
117
|
+
const ss = openSwingStore(dbDir);
|
|
118
|
+
t.teardown(ss.hostStorage.close);
|
|
108
119
|
await t.throwsAsync(async () => ss.hostStorage.repairMetadata(exporter), {
|
|
109
120
|
message: /repairSnapshotRecord metadata mismatch/,
|
|
110
121
|
});
|
|
@@ -117,14 +128,17 @@ test('repair metadata rejects mismatched transcript span', async t => {
|
|
|
117
128
|
const { exportData, artifacts } = buildData();
|
|
118
129
|
|
|
119
130
|
const exporter = makeExporter(exportData, artifacts);
|
|
120
|
-
const
|
|
121
|
-
await
|
|
131
|
+
const ssi = await importSwingStore(exporter, dbDir);
|
|
132
|
+
await ssi.hostStorage.commit();
|
|
133
|
+
await ssi.hostStorage.close();
|
|
122
134
|
|
|
123
135
|
// perform the repair with mismatched transcript span entry
|
|
124
136
|
const old = JSON.parse(exportData.get('transcript.v1.0'));
|
|
125
137
|
const wrong = { ...old, hash: 'wrong' };
|
|
126
138
|
exportData.set('transcript.v1.0', JSON.stringify(wrong));
|
|
127
139
|
|
|
140
|
+
const ss = openSwingStore(dbDir);
|
|
141
|
+
t.teardown(ss.hostStorage.close);
|
|
128
142
|
await t.throwsAsync(async () => ss.hostStorage.repairMetadata(exporter), {
|
|
129
143
|
message: /repairTranscriptSpanRecord metadata mismatch/,
|
|
130
144
|
});
|
package/test/test-state.js
CHANGED
|
@@ -316,3 +316,37 @@ test('close will abort transaction', async t => {
|
|
|
316
316
|
t.is(kvStore.get('key2'), undefined);
|
|
317
317
|
t.falsy(kvStore.has('key2'));
|
|
318
318
|
});
|
|
319
|
+
|
|
320
|
+
test('savepoints', async t => {
|
|
321
|
+
const [dbDir, cleanup] = await tmpDir('testdb');
|
|
322
|
+
t.teardown(cleanup);
|
|
323
|
+
const ss1 = initSwingStore(dbDir);
|
|
324
|
+
ss1.kernelStorage.startCrank();
|
|
325
|
+
ss1.kernelStorage.kvStore.set('key', 'value1');
|
|
326
|
+
ss1.kernelStorage.establishCrankSavepoint('sp1');
|
|
327
|
+
ss1.kernelStorage.kvStore.set('key', 'value2');
|
|
328
|
+
ss1.kernelStorage.establishCrankSavepoint('sp2');
|
|
329
|
+
ss1.kernelStorage.kvStore.set('key', 'value3');
|
|
330
|
+
ss1.kernelStorage.rollbackCrank('sp1');
|
|
331
|
+
ss1.kernelStorage.endCrank();
|
|
332
|
+
await ss1.hostStorage.commit();
|
|
333
|
+
await ss1.hostStorage.close();
|
|
334
|
+
|
|
335
|
+
const ss2 = openSwingStore(dbDir);
|
|
336
|
+
t.is(ss2.kernelStorage.kvStore.get('key'), 'value1');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('savepoints do not automatically commit', async t => {
|
|
340
|
+
const [dbDir, cleanup] = await tmpDir('testdb');
|
|
341
|
+
t.teardown(cleanup);
|
|
342
|
+
const ss1 = initSwingStore(dbDir);
|
|
343
|
+
ss1.kernelStorage.startCrank();
|
|
344
|
+
ss1.kernelStorage.establishCrankSavepoint('sp1');
|
|
345
|
+
ss1.kernelStorage.kvStore.set('key', 'value1');
|
|
346
|
+
// #8423 meant this .endCrank() accidentally did a commit()
|
|
347
|
+
ss1.kernelStorage.endCrank();
|
|
348
|
+
await ss1.hostStorage.close();
|
|
349
|
+
|
|
350
|
+
const ss2 = openSwingStore(dbDir);
|
|
351
|
+
t.false(ss2.kernelStorage.kvStore.has('key'));
|
|
352
|
+
});
|