@web3-storage/pail 0.6.2 → 0.6.3-rc.1
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/dist/src/batch/api.d.ts +5 -0
- package/dist/src/batch/api.d.ts.map +1 -1
- package/dist/src/batch/index.d.ts +1 -0
- package/dist/src/batch/index.d.ts.map +1 -1
- package/dist/src/batch/index.js +34 -0
- package/dist/src/crdt/batch/index.d.ts.map +1 -1
- package/dist/src/crdt/batch/index.js +10 -0
- package/dist/src/crdt/index.d.ts +1 -1
- package/dist/src/crdt/index.d.ts.map +1 -1
- package/dist/src/crdt/index.js +182 -78
- package/dist/test/batch.test.js +92 -0
- package/dist/test/crdt.test.js +241 -5
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/dist/test/crdt.test.js
CHANGED
|
@@ -2,7 +2,7 @@ import { expect } from 'vitest';
|
|
|
2
2
|
// eslint-disable-next-line no-unused-vars
|
|
3
3
|
import * as API from '../src/crdt/api.js';
|
|
4
4
|
import { advance, vis } from '../src/clock/index.js';
|
|
5
|
-
import { put, get, root, entries } from '../src/crdt/index.js';
|
|
5
|
+
import { put, del, get, root, entries } from '../src/crdt/index.js';
|
|
6
6
|
import * as Batch from '../src/crdt/batch/index.js';
|
|
7
7
|
import { Blockstore, clockVis, randomCID, randomString } from './helpers.js';
|
|
8
8
|
describe('CRDT', () => {
|
|
@@ -196,6 +196,221 @@ describe('CRDT batch', () => {
|
|
|
196
196
|
assert.equal(res1.toString(), value1.toString());
|
|
197
197
|
});
|
|
198
198
|
});
|
|
199
|
+
describe('CRDT batch del', () => {
|
|
200
|
+
it('batch with del operations', async () => {
|
|
201
|
+
const blocks = new Blockstore();
|
|
202
|
+
const alice = new TestPail(blocks, []);
|
|
203
|
+
await alice.put('apple', await randomCID(32));
|
|
204
|
+
await alice.put('banana', await randomCID(32));
|
|
205
|
+
await alice.put('cherry', await randomCID(32));
|
|
206
|
+
await alice.applyBatch([
|
|
207
|
+
{ type: 'del', key: 'apple' },
|
|
208
|
+
{ type: 'del', key: 'banana' }
|
|
209
|
+
]);
|
|
210
|
+
assert.equal(await alice.get('apple'), undefined);
|
|
211
|
+
assert.equal(await alice.get('banana'), undefined);
|
|
212
|
+
assert(await alice.get('cherry'));
|
|
213
|
+
});
|
|
214
|
+
it('batch with mixed put and del', async () => {
|
|
215
|
+
const blocks = new Blockstore();
|
|
216
|
+
const alice = new TestPail(blocks, []);
|
|
217
|
+
await alice.put('apple', await randomCID(32));
|
|
218
|
+
await alice.put('banana', await randomCID(32));
|
|
219
|
+
const mangoValue = await randomCID(32);
|
|
220
|
+
await alice.applyBatch([
|
|
221
|
+
{ type: 'del', key: 'apple' },
|
|
222
|
+
{ type: 'put', key: 'mango', value: mangoValue }
|
|
223
|
+
]);
|
|
224
|
+
assert.equal(await alice.get('apple'), undefined);
|
|
225
|
+
assert(await alice.get('banana'));
|
|
226
|
+
const mango = await alice.get('mango');
|
|
227
|
+
assert(mango);
|
|
228
|
+
assert.equal(mango.toString(), mangoValue.toString());
|
|
229
|
+
});
|
|
230
|
+
it('batch with internal put then delete of same key simplifies correctly on merge', async () => {
|
|
231
|
+
const blocks = new Blockstore();
|
|
232
|
+
const alice = new TestPail(blocks, []);
|
|
233
|
+
await alice.put('apple', await randomCID(32));
|
|
234
|
+
await alice.put('banana', await randomCID(32));
|
|
235
|
+
const bob = new TestPail(blocks, alice.head);
|
|
236
|
+
// alice does an unrelated put
|
|
237
|
+
const { event: aEvent } = await alice.put('cherry', await randomCID(32));
|
|
238
|
+
// bob does a batch: put apple (new value), put date, then delete apple
|
|
239
|
+
// net effect of the batch should be: put date, delete apple
|
|
240
|
+
const { event: bEvent } = await bob.applyBatch([
|
|
241
|
+
{ type: 'put', key: 'apple', value: await randomCID(32) },
|
|
242
|
+
{ type: 'put', key: 'date', value: await randomCID(32) },
|
|
243
|
+
{ type: 'del', key: 'apple' }
|
|
244
|
+
]);
|
|
245
|
+
assert(aEvent);
|
|
246
|
+
assert(bEvent);
|
|
247
|
+
// merge
|
|
248
|
+
await alice.advance(bEvent.cid);
|
|
249
|
+
await bob.advance(aEvent.cid);
|
|
250
|
+
// apple should be deleted — the batch's internal delete comes after its put
|
|
251
|
+
assert.equal(await alice.get('apple'), undefined);
|
|
252
|
+
assert.equal(await bob.get('apple'), undefined);
|
|
253
|
+
// date from batch should exist
|
|
254
|
+
assert(await alice.get('date'));
|
|
255
|
+
assert(await bob.get('date'));
|
|
256
|
+
// cherry and banana should still exist
|
|
257
|
+
assert(await alice.get('cherry'));
|
|
258
|
+
assert(await alice.get('banana'));
|
|
259
|
+
// roots should converge
|
|
260
|
+
assert(alice.root);
|
|
261
|
+
assert(bob.root);
|
|
262
|
+
assert.equal(alice.root.toString(), bob.root.toString());
|
|
263
|
+
});
|
|
264
|
+
it('batch internal delete overridden by concurrent external put', async () => {
|
|
265
|
+
const blocks = new Blockstore();
|
|
266
|
+
const alice = new TestPail(blocks, []);
|
|
267
|
+
await alice.put('apple', await randomCID(32));
|
|
268
|
+
const bob = new TestPail(blocks, alice.head);
|
|
269
|
+
// alice puts a new value for apple
|
|
270
|
+
const value1 = await randomCID(32);
|
|
271
|
+
const { event: aEvent } = await alice.put('apple', value1);
|
|
272
|
+
// bob does a batch: put apple, put banana, delete apple
|
|
273
|
+
// net effect of batch: put banana, delete apple
|
|
274
|
+
// but alice's concurrent put should win over the batch's net delete
|
|
275
|
+
const { event: bEvent } = await bob.applyBatch([
|
|
276
|
+
{ type: 'put', key: 'apple', value: await randomCID(32) },
|
|
277
|
+
{ type: 'put', key: 'banana', value: await randomCID(32) },
|
|
278
|
+
{ type: 'del', key: 'apple' }
|
|
279
|
+
]);
|
|
280
|
+
assert(aEvent);
|
|
281
|
+
assert(bEvent);
|
|
282
|
+
// merge
|
|
283
|
+
await alice.advance(bEvent.cid);
|
|
284
|
+
await bob.advance(aEvent.cid);
|
|
285
|
+
// put wins: alice's concurrent put for apple should win over batch's net delete
|
|
286
|
+
const aValue = await alice.get('apple');
|
|
287
|
+
assert(aValue);
|
|
288
|
+
assert.equal(aValue.toString(), value1.toString());
|
|
289
|
+
// banana from batch should exist
|
|
290
|
+
assert(await alice.get('banana'));
|
|
291
|
+
// roots should converge
|
|
292
|
+
assert(alice.root);
|
|
293
|
+
assert(bob.root);
|
|
294
|
+
assert.equal(alice.root.toString(), bob.root.toString());
|
|
295
|
+
});
|
|
296
|
+
it('concurrent batch del and put converge', async () => {
|
|
297
|
+
const blocks = new Blockstore();
|
|
298
|
+
const alice = new TestPail(blocks, []);
|
|
299
|
+
await alice.put('apple', await randomCID(32));
|
|
300
|
+
await alice.put('banana', await randomCID(32));
|
|
301
|
+
const bob = new TestPail(blocks, alice.head);
|
|
302
|
+
// alice puts new value for apple
|
|
303
|
+
const value1 = await randomCID(32);
|
|
304
|
+
const { event: aEvent } = await alice.put('apple', value1);
|
|
305
|
+
// bob batch-deletes apple and puts cherry
|
|
306
|
+
const cherryValue = await randomCID(32);
|
|
307
|
+
const { event: bEvent } = await bob.applyBatch([
|
|
308
|
+
{ type: 'del', key: 'apple' },
|
|
309
|
+
{ type: 'put', key: 'cherry', value: cherryValue }
|
|
310
|
+
]);
|
|
311
|
+
assert(aEvent);
|
|
312
|
+
assert(bEvent);
|
|
313
|
+
await alice.advance(bEvent.cid);
|
|
314
|
+
await bob.advance(aEvent.cid);
|
|
315
|
+
// put wins: apple should have alice's value
|
|
316
|
+
const aValue = await alice.get('apple');
|
|
317
|
+
assert(aValue);
|
|
318
|
+
assert.equal(aValue.toString(), value1.toString());
|
|
319
|
+
// cherry from batch should exist
|
|
320
|
+
const aCherry = await alice.get('cherry');
|
|
321
|
+
assert(aCherry);
|
|
322
|
+
assert.equal(aCherry.toString(), cherryValue.toString());
|
|
323
|
+
// roots should converge
|
|
324
|
+
assert(alice.root);
|
|
325
|
+
assert(bob.root);
|
|
326
|
+
assert.equal(alice.root.toString(), bob.root.toString());
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
describe('CRDT delete', () => {
|
|
330
|
+
it('delete a key', async () => {
|
|
331
|
+
const blocks = new Blockstore();
|
|
332
|
+
const alice = new TestPail(blocks, []);
|
|
333
|
+
await alice.put('apple', await randomCID(32));
|
|
334
|
+
await alice.put('banana', await randomCID(32));
|
|
335
|
+
const { event } = await alice.del('apple');
|
|
336
|
+
assert(event);
|
|
337
|
+
assert.equal(event.value.data.type, 'del');
|
|
338
|
+
// @ts-expect-error TS can't narrow discriminated union through assert.equal
|
|
339
|
+
assert.equal(event.value.data.key, 'apple');
|
|
340
|
+
const value = await alice.get('apple');
|
|
341
|
+
assert.equal(value, undefined);
|
|
342
|
+
// other key unaffected
|
|
343
|
+
const banana = await alice.get('banana');
|
|
344
|
+
assert(banana);
|
|
345
|
+
});
|
|
346
|
+
it('delete non-existent key is a no-op', async () => {
|
|
347
|
+
const blocks = new Blockstore();
|
|
348
|
+
const alice = new TestPail(blocks, []);
|
|
349
|
+
const { root: r0 } = await alice.put('apple', await randomCID(32));
|
|
350
|
+
const { root: r1, additions, removals } = await alice.del('nonexistent');
|
|
351
|
+
assert.equal(r1.toString(), r0.toString());
|
|
352
|
+
assert.equal(additions.length, 0);
|
|
353
|
+
assert.equal(removals.length, 0);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
describe('CRDT concurrent deletes', () => {
|
|
357
|
+
it('put wins over concurrent delete of same key', async () => {
|
|
358
|
+
const blocks = new Blockstore();
|
|
359
|
+
const alice = new TestPail(blocks, []);
|
|
360
|
+
const value0 = await randomCID(32);
|
|
361
|
+
await alice.put('apple', value0);
|
|
362
|
+
await alice.put('banana', await randomCID(32));
|
|
363
|
+
const bob = new TestPail(blocks, alice.head);
|
|
364
|
+
// alice puts new value for apple
|
|
365
|
+
const value1 = await randomCID(32);
|
|
366
|
+
const { event: aEvent } = await alice.put('apple', value1);
|
|
367
|
+
// bob deletes apple
|
|
368
|
+
const { event: bEvent } = await bob.del('apple');
|
|
369
|
+
assert(aEvent);
|
|
370
|
+
assert(bEvent);
|
|
371
|
+
// merge both directions
|
|
372
|
+
await alice.advance(bEvent.cid);
|
|
373
|
+
await bob.advance(aEvent.cid);
|
|
374
|
+
// put wins: apple should have alice's new value
|
|
375
|
+
const aValue = await alice.get('apple');
|
|
376
|
+
assert(aValue);
|
|
377
|
+
assert.equal(aValue.toString(), value1.toString());
|
|
378
|
+
const bValue = await bob.get('apple');
|
|
379
|
+
assert(bValue);
|
|
380
|
+
assert.equal(bValue.toString(), value1.toString());
|
|
381
|
+
// roots should converge
|
|
382
|
+
assert(alice.root);
|
|
383
|
+
assert(bob.root);
|
|
384
|
+
assert.equal(alice.root.toString(), bob.root.toString());
|
|
385
|
+
});
|
|
386
|
+
it('concurrent duplicate deletes collapse to single delete', async () => {
|
|
387
|
+
const blocks = new Blockstore();
|
|
388
|
+
const alice = new TestPail(blocks, []);
|
|
389
|
+
await alice.put('apple', await randomCID(32));
|
|
390
|
+
await alice.put('banana', await randomCID(32));
|
|
391
|
+
const bob = new TestPail(blocks, alice.head);
|
|
392
|
+
// both delete apple
|
|
393
|
+
const { event: aEvent } = await alice.del('apple');
|
|
394
|
+
const { event: bEvent } = await bob.del('apple');
|
|
395
|
+
assert(aEvent);
|
|
396
|
+
assert(bEvent);
|
|
397
|
+
// merge
|
|
398
|
+
await alice.advance(bEvent.cid);
|
|
399
|
+
await bob.advance(aEvent.cid);
|
|
400
|
+
// apple should be gone
|
|
401
|
+
const aValue = await alice.get('apple');
|
|
402
|
+
assert.equal(aValue, undefined);
|
|
403
|
+
const bValue = await bob.get('apple');
|
|
404
|
+
assert.equal(bValue, undefined);
|
|
405
|
+
// banana should still exist
|
|
406
|
+
assert(await alice.get('banana'));
|
|
407
|
+
assert(await bob.get('banana'));
|
|
408
|
+
// roots should converge
|
|
409
|
+
assert(alice.root);
|
|
410
|
+
assert(bob.root);
|
|
411
|
+
assert.equal(alice.root.toString(), bob.root.toString());
|
|
412
|
+
});
|
|
413
|
+
});
|
|
199
414
|
class TestPail {
|
|
200
415
|
/**
|
|
201
416
|
* @param {Blockstore} blocks
|
|
@@ -215,6 +430,16 @@ class TestPail {
|
|
|
215
430
|
this.root = result.root;
|
|
216
431
|
return this.head;
|
|
217
432
|
}
|
|
433
|
+
/** @param {string} key */
|
|
434
|
+
async del(key) {
|
|
435
|
+
const result = await del(this.blocks, this.head, key);
|
|
436
|
+
if (result.event)
|
|
437
|
+
this.blocks.putSync(result.event.cid, result.event.bytes);
|
|
438
|
+
result.additions.forEach(a => this.blocks.putSync(a.cid, a.bytes));
|
|
439
|
+
this.head = result.head;
|
|
440
|
+
this.root = (await root(this.blocks, this.head)).root;
|
|
441
|
+
return result;
|
|
442
|
+
}
|
|
218
443
|
/**
|
|
219
444
|
* @param {string} key
|
|
220
445
|
* @param {API.UnknownLink} value
|
|
@@ -229,12 +454,17 @@ class TestPail {
|
|
|
229
454
|
return result;
|
|
230
455
|
}
|
|
231
456
|
/**
|
|
232
|
-
* @param {Array<{ key: string, value
|
|
457
|
+
* @param {Array<{ type?: string, key: string, value?: API.UnknownLink }>} ops
|
|
233
458
|
*/
|
|
234
|
-
async
|
|
459
|
+
async applyBatch(ops) {
|
|
235
460
|
const batch = await Batch.create(this.blocks, this.head);
|
|
236
|
-
for (const
|
|
237
|
-
|
|
461
|
+
for (const op of ops) {
|
|
462
|
+
if (!op.type || op.type === 'put') {
|
|
463
|
+
await batch.put(op.key, /** @type {API.UnknownLink} */ (op.value));
|
|
464
|
+
}
|
|
465
|
+
else if (op.type === 'del') {
|
|
466
|
+
await batch.del(op.key);
|
|
467
|
+
}
|
|
238
468
|
}
|
|
239
469
|
const result = await batch.commit();
|
|
240
470
|
if (result.event)
|
|
@@ -244,6 +474,12 @@ class TestPail {
|
|
|
244
474
|
this.root = (await root(this.blocks, this.head)).root;
|
|
245
475
|
return result;
|
|
246
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* @param {Array<{ key: string, value: API.UnknownLink }>} items
|
|
479
|
+
*/
|
|
480
|
+
async putBatch(items) {
|
|
481
|
+
return this.applyBatch(items.map(i => ({ type: 'put', ...i })));
|
|
482
|
+
}
|
|
247
483
|
async vis() {
|
|
248
484
|
/** @param {API.UnknownLink} l */
|
|
249
485
|
const shortLink = l => `${String(l).slice(0, 4)}..${String(l).slice(-4)}`;
|