-
Notifications
You must be signed in to change notification settings - Fork 229
/
Copy pathstorageWrapper.js
286 lines (261 loc) · 8.77 KB
/
storageWrapper.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
import { assert } from '@agoric/assert';
import { insistStorageAPI } from '../../lib/storageAPI.js';
// We wrap a provided object implementing StorageAPI methods { has, getKeys,
// get, set, delete } (cf. packages/SwingSet/docs/state.md#transactions) and
// add some convenience methods.
// NOTE: There's a lot of suspenders-and-belt paranoia here because we have to
// be vewy, vewy careful with host-realm objects. This raises a question
// whether ad hoc paranoia is the best engineering practice. Also, if it's
// important for users of a parameter to be aware of the potentially suspect
// nature of that parameter, perhaps we should establish some naming convention
// that signals that the object could be foreign and thus deserving of
// xenophobia.
/**
* Given two iterators over sequences of unique strings sorted in ascending
* order lexicographically by UTF-16 code unit, produce a new iterator that will
* output the ascending sequence of unique strings from their merged output.
*
* @param { Iterator } it1
* @param { Iterator } it2
*
* @yields any
*/
function* mergeUtf16SortedIterators(it1, it2) {
let v1 = it1.next();
let v2 = it2.next();
while (!v1.done && !v2.done) {
if (v1.value < v2.value) {
const result = v1.value;
v1 = it1.next();
yield result;
} else if (v1.value === v2.value) {
const result = v1.value;
v1 = it1.next();
v2 = it2.next();
yield result;
} else {
const result = v2.value;
v2 = it2.next();
yield result;
}
}
const itrest = v1.done ? it2 : it1;
let v = v1.done ? v2 : v1;
while (!v.done) {
const result = v.value;
v = itrest.next();
yield result;
}
}
/**
* Create and return a crank buffer, which wraps a storage object with logic
* that buffers any mutations until told to commit them.
*
* @param {KVStore} kvStore The StorageAPI object that this crank buffer will be based on.
* @param {CreateSHA256} createSHA256
* @param { (key: string) => boolean } isConsensusKey
* @returns {*} an object {
* crankBuffer, // crank buffer as described, wrapping `kvStore`
* commitCrank, // function to save buffered mutations to `kvStore`
* abortCrank, // function to discard buffered mutations
* }
*/
export function buildCrankBuffer(
kvStore,
createSHA256,
isConsensusKey = () => true,
) {
insistStorageAPI(kvStore);
let crankhasher;
function resetCrankhash() {
crankhasher = createSHA256();
}
// To avoid confusion, additions and deletions are prevented from sharing
// the same key at any given time.
const additions = new Map();
const deletions = new Set();
resetCrankhash();
const crankBuffer = {
has(key) {
if (additions.has(key)) {
return true;
}
if (deletions.has(key)) {
return false;
}
return kvStore.has(key);
},
*getKeys(start, end) {
// Warning: this function introduces a consistency risk that callers must
// take into account in their usage of it. If keys are added to the store
// within the range from `start` to `end` while iteration is in progress,
// these additions will not be visible to this iterator and thus not
// reflected in the stream of keys it returns. Callers who might be
// vulnerable to any resulting data inconsistencies thus introduced must
// take measures to protect themselves, either by avoiding making such
// changes while iterating or by arranging to invalidate and reconstruct
// the iterator when such changes are made. At this layer of abstraction,
// the store does not possess the necessary knowledge to protect the
// caller from these kinds of risks, as the nature of the risks themselves
// varies depending on what the caller is trying to do. This API should
// not be made available to user (i.e., vat) code. Rather, it is intended
// as a low-level mechanism to use in implementating higher level storage
// abstractions that are expected to provide their own consistency
// protections as appropriate to their own circumstances.
assert.typeof(start, 'string');
assert.typeof(end, 'string');
// Find additions within the query range for use during iteration.
const added = [];
for (const k of additions.keys()) {
if ((start === '' || start <= k) && (end === '' || k < end)) {
added.push(k);
}
}
added.sort();
for (const k of mergeUtf16SortedIterators(
added.values(),
kvStore.getKeys(start, end),
)) {
if ((start === '' || start <= k) && (end === '' || k < end)) {
if (!deletions.has(k)) {
yield k;
}
}
}
},
get(key) {
assert.typeof(key, 'string');
if (additions.has(key)) {
return additions.get(key);
}
if (deletions.has(key)) {
return undefined;
}
return kvStore.get(key);
},
set(key, value) {
assert.typeof(key, 'string');
assert.typeof(value, 'string');
additions.set(key, value);
deletions.delete(key);
if (isConsensusKey(key)) {
crankhasher.add('add');
crankhasher.add('\n');
crankhasher.add(key);
crankhasher.add('\n');
crankhasher.add(value);
crankhasher.add('\n');
}
},
delete(key) {
assert.typeof(key, 'string');
additions.delete(key);
deletions.add(key);
if (isConsensusKey(key)) {
crankhasher.add('delete');
crankhasher.add('\n');
crankhasher.add(key);
crankhasher.add('\n');
}
},
};
/**
* Flush any buffered mutations to the underlying storage, and update the
* activityhash.
*
* @returns { { crankhash: string, activityhash: string } }
*/
function commitCrank() {
for (const [key, value] of additions) {
kvStore.set(key, value);
}
for (const key of deletions) {
kvStore.delete(key);
}
additions.clear();
deletions.clear();
const crankhash = crankhasher.finish();
resetCrankhash();
let oldActivityhash = kvStore.get('activityhash');
if (oldActivityhash === undefined) {
oldActivityhash = '';
}
const hasher = createSHA256();
hasher.add('activityhash');
hasher.add('\n');
hasher.add(oldActivityhash);
hasher.add('\n');
hasher.add(crankhash);
hasher.add('\n');
const activityhash = hasher.finish();
kvStore.set('activityhash', activityhash);
return { crankhash, activityhash };
}
/**
* Discard any buffered mutations.
*/
function abortCrank() {
additions.clear();
deletions.clear();
resetCrankhash();
}
return harden({ crankBuffer, commitCrank, abortCrank });
}
/**
* @param {KVStore} kvStore
*/
export function addHelpers(kvStore) {
insistStorageAPI(kvStore);
// NOTE: awkward naming: the thing that returns a stream of keys is named
// "enumerate..." while the thing that returns a stream of values is named
// "get..."
function* enumeratePrefixedKeys(prefix, start = 0) {
// Return an iterator over all existing keys `${prefix}${N}`, for N
// starting at `start`, in numeric order. This is implemented with
// has/get rather than any hypothetical DB-specific getRange(start, end)
// to ensure that results are sorted numerically.
for (let i = start; true; i += 1) {
const key = `${prefix}${i}`;
if (kvStore.has(key)) {
yield key;
} else {
return;
}
}
}
function* getPrefixedValues(prefix, start = 0) {
for (const key of enumeratePrefixedKeys(prefix, start)) {
yield kvStore.get(key) || assert.fail('enumerate ensures get');
}
}
function deletePrefixedKeys(prefix, start = 0) {
// this is kind of like a deleteRange() would be, but can be implemented
// efficiently without backend DB support because it only looks at
// numeric suffixes, in sequential order.
for (const key of enumeratePrefixedKeys(prefix, start)) {
kvStore.delete(key);
}
}
return harden({
enumeratePrefixedKeys,
getPrefixedValues,
deletePrefixedKeys,
...kvStore,
});
}
// The "KeeperStorage" API is a set of functions { has, get, set, delete,
// enumeratePrefixedKeys, getPrefixedValues, deletePrefixedKeys }. The Kernel
// Keeper manipulates the saved kernel state through an object that
// implements the KeeperStorage API. That object is usually associated with a
// write-back buffer wrapper (the CrankBuffer), but the keeper is unaware of
// that.
export function wrapStorage(kvStore, createSHA256, isConsensusKey) {
insistStorageAPI(kvStore);
const { crankBuffer, commitCrank, abortCrank } = buildCrankBuffer(
kvStore,
createSHA256,
isConsensusKey,
);
const enhancedCrankBuffer = addHelpers(crankBuffer);
return { enhancedCrankBuffer, commitCrank, abortCrank };
}