@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.
@@ -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: API.UnknownLink }>} items
457
+ * @param {Array<{ type?: string, key: string, value?: API.UnknownLink }>} ops
233
458
  */
234
- async putBatch(items) {
459
+ async applyBatch(ops) {
235
460
  const batch = await Batch.create(this.blocks, this.head);
236
- for (const { key, value } of items) {
237
- await batch.put(key, value);
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)}`;