@web3-storage/pail 0.6.0 → 0.6.2
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/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +208 -0
- package/dist/scripts/randomcid.d.ts +2 -0
- package/dist/scripts/randomcid.d.ts.map +1 -0
- package/dist/scripts/randomcid.js +10 -0
- package/dist/scripts/words/gen.d.ts +2 -0
- package/dist/scripts/words/gen.d.ts.map +1 -0
- package/dist/scripts/words/gen.js +51 -0
- package/dist/src/api.d.ts +4 -2
- package/dist/src/api.d.ts.map +1 -1
- package/dist/src/api.js +1 -0
- package/dist/src/batch/api.d.ts +2 -2
- package/dist/src/batch/api.d.ts.map +1 -1
- package/dist/src/batch/api.js +1 -0
- package/dist/src/batch/index.d.ts +1 -53
- package/dist/src/batch/index.d.ts.map +1 -1
- package/dist/src/batch/index.js +241 -0
- package/dist/src/batch/shard.d.ts +1 -1
- package/dist/src/batch/shard.d.ts.map +1 -1
- package/dist/src/batch/shard.js +12 -0
- package/dist/src/block.d.ts +2 -2
- package/dist/src/block.d.ts.map +1 -1
- package/dist/src/block.js +66 -0
- package/dist/src/clock/api.d.ts +1 -1
- package/dist/src/clock/api.d.ts.map +1 -1
- package/dist/src/clock/api.js +1 -0
- package/dist/src/clock/index.d.ts +2 -2
- package/dist/src/clock/index.d.ts.map +1 -1
- package/dist/src/clock/index.js +199 -0
- package/dist/src/crdt/api.d.ts +1 -1
- package/dist/src/crdt/api.d.ts.map +1 -1
- package/dist/src/crdt/api.js +1 -0
- package/dist/src/crdt/batch/api.d.ts +1 -1
- package/dist/src/crdt/batch/api.d.ts.map +1 -1
- package/dist/src/crdt/batch/api.js +1 -0
- package/dist/src/crdt/batch/index.d.ts.map +1 -1
- package/dist/src/crdt/batch/index.js +140 -0
- package/dist/src/crdt/index.d.ts +2 -2
- package/dist/src/crdt/index.d.ts.map +1 -1
- package/dist/src/crdt/index.js +344 -0
- package/dist/src/diff.d.ts +3 -3
- package/dist/src/diff.d.ts.map +1 -1
- package/dist/src/diff.js +151 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +356 -0
- package/dist/src/merge.d.ts.map +1 -1
- package/dist/src/merge.js +42 -0
- package/dist/src/shard.d.ts +4 -4
- package/dist/src/shard.d.ts.map +1 -1
- package/dist/src/shard.js +166 -0
- package/dist/test/batch.test.d.ts +2 -0
- package/dist/test/batch.test.d.ts.map +1 -0
- package/dist/test/batch.test.js +126 -0
- package/dist/test/clock.test.d.ts +2 -0
- package/dist/test/clock.test.d.ts.map +1 -0
- package/dist/test/clock.test.js +244 -0
- package/dist/test/crdt.test.d.ts +2 -0
- package/dist/test/crdt.test.d.ts.map +1 -0
- package/dist/test/crdt.test.js +271 -0
- package/dist/test/del.test.d.ts +2 -0
- package/dist/test/del.test.d.ts.map +1 -0
- package/dist/test/del.test.js +21 -0
- package/dist/test/diff.test.d.ts +2 -0
- package/dist/test/diff.test.d.ts.map +1 -0
- package/dist/test/diff.test.js +38 -0
- package/dist/test/entries.test.d.ts +2 -0
- package/dist/test/entries.test.d.ts.map +1 -0
- package/dist/test/entries.test.js +210 -0
- package/dist/test/get.test.d.ts +2 -0
- package/dist/test/get.test.d.ts.map +1 -0
- package/dist/test/get.test.js +26 -0
- package/dist/test/helpers.d.ts +35 -0
- package/dist/test/helpers.d.ts.map +1 -0
- package/dist/test/helpers.js +168 -0
- package/dist/test/put.test.d.ts +2 -0
- package/dist/test/put.test.d.ts.map +1 -0
- package/dist/test/put.test.js +165 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +13 -0
- package/package.json +49 -94
- package/scripts/propernames/gen.sh +3 -0
- package/cli.js +0 -218
- package/src/api.js +0 -1
- package/src/api.ts +0 -90
- package/src/batch/api.js +0 -1
- package/src/batch/api.ts +0 -59
- package/src/batch/index.js +0 -258
- package/src/batch/shard.js +0 -13
- package/src/block.js +0 -75
- package/src/clock/api.js +0 -1
- package/src/clock/api.ts +0 -12
- package/src/clock/index.js +0 -182
- package/src/crdt/api.js +0 -1
- package/src/crdt/api.ts +0 -33
- package/src/crdt/batch/api.js +0 -1
- package/src/crdt/batch/api.ts +0 -30
- package/src/crdt/batch/index.js +0 -155
- package/src/crdt/index.js +0 -354
- package/src/diff.js +0 -151
- package/src/index.js +0 -406
- package/src/merge.js +0 -43
- package/src/shard.js +0 -180
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { expect } from 'vitest';
|
|
2
|
+
// eslint-disable-next-line no-unused-vars
|
|
3
|
+
import * as API from '../src/api.js';
|
|
4
|
+
import { ShardBlock } from '../src/shard.js';
|
|
5
|
+
import * as Pail from '../src/index.js';
|
|
6
|
+
import * as Batch from '../src/batch/index.js';
|
|
7
|
+
import { Blockstore, randomCID, randomString, vis } from './helpers.js';
|
|
8
|
+
describe('batch', () => {
|
|
9
|
+
it('batches puts', async () => {
|
|
10
|
+
const rootblk = await ShardBlock.create();
|
|
11
|
+
const blocks = new Blockstore();
|
|
12
|
+
await blocks.put(rootblk.cid, rootblk.bytes);
|
|
13
|
+
const ops = [];
|
|
14
|
+
for (let i = 0; i < 1000; i++) {
|
|
15
|
+
ops.push({ type: 'put', key: `test${randomString(10)}`, value: await randomCID() });
|
|
16
|
+
}
|
|
17
|
+
const batch = await Batch.create(blocks, rootblk.cid);
|
|
18
|
+
for (const op of ops) {
|
|
19
|
+
await batch.put(op.key, op.value);
|
|
20
|
+
}
|
|
21
|
+
const { root, additions, removals } = await batch.commit();
|
|
22
|
+
for (const b of removals) {
|
|
23
|
+
blocks.deleteSync(b.cid);
|
|
24
|
+
}
|
|
25
|
+
for (const b of additions) {
|
|
26
|
+
blocks.putSync(b.cid, b.bytes);
|
|
27
|
+
}
|
|
28
|
+
assert.equal(removals.length, 1);
|
|
29
|
+
assert.equal(removals[0].cid.toString(), rootblk.cid.toString());
|
|
30
|
+
for (const o of ops) {
|
|
31
|
+
const value = await Pail.get(blocks, root, o.key);
|
|
32
|
+
assert(value);
|
|
33
|
+
assert.equal(value.toString(), o.value.toString());
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
it('create the same DAG as non-batched puts', async () => {
|
|
37
|
+
const root = await ShardBlock.create();
|
|
38
|
+
const blocks = new Blockstore();
|
|
39
|
+
await blocks.put(root.cid, root.bytes);
|
|
40
|
+
const ops = [];
|
|
41
|
+
for (let i = 0; i < 1000; i++) {
|
|
42
|
+
ops.push({ type: 'put', key: `test${randomString(10)}`, value: await randomCID() });
|
|
43
|
+
}
|
|
44
|
+
const batch = await Batch.create(blocks, root.cid);
|
|
45
|
+
for (const op of ops) {
|
|
46
|
+
await batch.put(op.key, op.value);
|
|
47
|
+
}
|
|
48
|
+
const { root: batchedRoot } = await batch.commit();
|
|
49
|
+
/** @type {API.ShardLink} */
|
|
50
|
+
let nonBatchedRoot = root.cid;
|
|
51
|
+
for (const op of ops) {
|
|
52
|
+
const { root, additions } = await Pail.put(blocks, nonBatchedRoot, op.key, op.value);
|
|
53
|
+
nonBatchedRoot = root;
|
|
54
|
+
for (const b of additions) {
|
|
55
|
+
blocks.putSync(b.cid, b.bytes);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
assert.equal(batchedRoot.toString(), nonBatchedRoot.toString());
|
|
59
|
+
});
|
|
60
|
+
it('error when put after commit', async () => {
|
|
61
|
+
const root = await ShardBlock.create();
|
|
62
|
+
const blocks = new Blockstore();
|
|
63
|
+
await blocks.put(root.cid, root.bytes);
|
|
64
|
+
const ops = [];
|
|
65
|
+
for (let i = 0; i < 5; i++) {
|
|
66
|
+
ops.push({ type: 'put', key: `test${randomString(10)}`, value: await randomCID() });
|
|
67
|
+
}
|
|
68
|
+
const batch = await Batch.create(blocks, root.cid);
|
|
69
|
+
for (const op of ops) {
|
|
70
|
+
await batch.put(op.key, op.value);
|
|
71
|
+
}
|
|
72
|
+
await batch.commit();
|
|
73
|
+
await expect(async () => batch.put('test', await randomCID())).rejects.toThrowError(/batch already committed/);
|
|
74
|
+
});
|
|
75
|
+
it('error when commit after commit', async () => {
|
|
76
|
+
const root = await ShardBlock.create();
|
|
77
|
+
const blocks = new Blockstore();
|
|
78
|
+
await blocks.put(root.cid, root.bytes);
|
|
79
|
+
const ops = [];
|
|
80
|
+
for (let i = 0; i < 5; i++) {
|
|
81
|
+
ops.push({ type: 'put', key: `test${randomString(10)}`, value: await randomCID() });
|
|
82
|
+
}
|
|
83
|
+
const batch = await Batch.create(blocks, root.cid);
|
|
84
|
+
for (const op of ops) {
|
|
85
|
+
await batch.put(op.key, op.value);
|
|
86
|
+
}
|
|
87
|
+
await batch.commit();
|
|
88
|
+
await expect(async () => batch.commit()).rejects.toThrowError(/batch already committed/);
|
|
89
|
+
});
|
|
90
|
+
it('traverses existing shards to put values', async () => {
|
|
91
|
+
const rootblk = await ShardBlock.create();
|
|
92
|
+
const blocks = new Blockstore();
|
|
93
|
+
await blocks.put(rootblk.cid, rootblk.bytes);
|
|
94
|
+
const ops0 = [];
|
|
95
|
+
for (let i = 0; i < 5; i++) {
|
|
96
|
+
ops0.push({ type: 'put', key: `test${randomString(10)}`, value: await randomCID() });
|
|
97
|
+
}
|
|
98
|
+
ops0.push({ type: 'put', key: 'test', value: await randomCID() });
|
|
99
|
+
const ops1 = [];
|
|
100
|
+
for (const op of ops0) {
|
|
101
|
+
ops1.push({ ...op, value: await randomCID() });
|
|
102
|
+
}
|
|
103
|
+
const batch0 = await Batch.create(blocks, rootblk.cid);
|
|
104
|
+
for (const op of ops0) {
|
|
105
|
+
await batch0.put(op.key, op.value);
|
|
106
|
+
}
|
|
107
|
+
const { root: root0, additions: additions0 } = await batch0.commit();
|
|
108
|
+
for (const b of additions0) {
|
|
109
|
+
blocks.putSync(b.cid, b.bytes);
|
|
110
|
+
}
|
|
111
|
+
const batch1 = await Batch.create(blocks, root0);
|
|
112
|
+
for (const op of ops1) {
|
|
113
|
+
await batch1.put(op.key, op.value);
|
|
114
|
+
}
|
|
115
|
+
const { root: root1, additions: additions1 } = await batch1.commit();
|
|
116
|
+
for (const b of additions1) {
|
|
117
|
+
blocks.putSync(b.cid, b.bytes);
|
|
118
|
+
}
|
|
119
|
+
vis(blocks, root1);
|
|
120
|
+
for (const o of ops1) {
|
|
121
|
+
const value = await Pail.get(blocks, root1, o.key);
|
|
122
|
+
assert(value);
|
|
123
|
+
assert.equal(value.toString(), o.value.toString());
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.test.d.ts","sourceRoot":"","sources":["../../test/clock.test.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// eslint-disable-next-line no-unused-vars
|
|
2
|
+
import * as API from '../src/clock/api.js';
|
|
3
|
+
import { advance, EventBlock, vis } from '../src/clock/index.js';
|
|
4
|
+
import { Blockstore, randomCID, clockVis } from './helpers.js';
|
|
5
|
+
async function randomEventData() {
|
|
6
|
+
return {
|
|
7
|
+
type: 'put',
|
|
8
|
+
key: `test-${Date.now()}`,
|
|
9
|
+
value: await randomCID(32)
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
describe('clock', () => {
|
|
13
|
+
it('create a new clock', async () => {
|
|
14
|
+
const blocks = new Blockstore();
|
|
15
|
+
const event = await EventBlock.create({});
|
|
16
|
+
await blocks.put(event.cid, event.bytes);
|
|
17
|
+
const head = await advance(blocks, [], event.cid);
|
|
18
|
+
await clockVis(blocks, head);
|
|
19
|
+
assert.equal(head.length, 1);
|
|
20
|
+
assert.equal(head[0].toString(), event.cid.toString());
|
|
21
|
+
});
|
|
22
|
+
it('add an event', async () => {
|
|
23
|
+
const blocks = new Blockstore();
|
|
24
|
+
const root = await EventBlock.create(await randomEventData());
|
|
25
|
+
await blocks.put(root.cid, root.bytes);
|
|
26
|
+
/** @type {API.EventLink<any>[]} */
|
|
27
|
+
let head = [root.cid];
|
|
28
|
+
const event = await EventBlock.create(await randomEventData(), head);
|
|
29
|
+
await blocks.put(event.cid, event.bytes);
|
|
30
|
+
head = await advance(blocks, head, event.cid);
|
|
31
|
+
await clockVis(blocks, head);
|
|
32
|
+
assert.equal(head.length, 1);
|
|
33
|
+
assert.equal(head[0].toString(), event.cid.toString());
|
|
34
|
+
});
|
|
35
|
+
it('add two events with shared parents', async () => {
|
|
36
|
+
const blocks = new Blockstore();
|
|
37
|
+
const root = await EventBlock.create(await randomEventData());
|
|
38
|
+
await blocks.put(root.cid, root.bytes);
|
|
39
|
+
/** @type {API.EventLink<any>[]} */
|
|
40
|
+
let head = [root.cid];
|
|
41
|
+
const parents = head;
|
|
42
|
+
const event0 = await EventBlock.create(await randomEventData(), parents);
|
|
43
|
+
await blocks.put(event0.cid, event0.bytes);
|
|
44
|
+
head = await advance(blocks, parents, event0.cid);
|
|
45
|
+
const event1 = await EventBlock.create(await randomEventData(), parents);
|
|
46
|
+
await blocks.put(event1.cid, event1.bytes);
|
|
47
|
+
head = await advance(blocks, head, event1.cid);
|
|
48
|
+
await clockVis(blocks, head);
|
|
49
|
+
assert.equal(head.length, 2);
|
|
50
|
+
assert.equal(head[0].toString(), event0.cid.toString());
|
|
51
|
+
assert.equal(head[1].toString(), event1.cid.toString());
|
|
52
|
+
});
|
|
53
|
+
it('add two events with some shared parents', async () => {
|
|
54
|
+
const blocks = new Blockstore();
|
|
55
|
+
const root = await EventBlock.create(await randomEventData());
|
|
56
|
+
await blocks.put(root.cid, root.bytes);
|
|
57
|
+
/** @type {API.EventLink<any>[]} */
|
|
58
|
+
let head = [root.cid];
|
|
59
|
+
const parents0 = head;
|
|
60
|
+
const event0 = await EventBlock.create(await randomEventData(), parents0);
|
|
61
|
+
await blocks.put(event0.cid, event0.bytes);
|
|
62
|
+
head = await advance(blocks, head, event0.cid);
|
|
63
|
+
const event1 = await EventBlock.create(await randomEventData(), parents0);
|
|
64
|
+
await blocks.put(event1.cid, event1.bytes);
|
|
65
|
+
head = await advance(blocks, head, event1.cid);
|
|
66
|
+
const event2 = await EventBlock.create(await randomEventData(), parents0);
|
|
67
|
+
await blocks.put(event2.cid, event2.bytes);
|
|
68
|
+
head = await advance(blocks, head, event2.cid);
|
|
69
|
+
const event3 = await EventBlock.create(await randomEventData(), [event0.cid, event1.cid]);
|
|
70
|
+
await blocks.put(event3.cid, event3.bytes);
|
|
71
|
+
head = await advance(blocks, head, event3.cid);
|
|
72
|
+
const event4 = await EventBlock.create(await randomEventData(), [event2.cid]);
|
|
73
|
+
await blocks.put(event4.cid, event4.bytes);
|
|
74
|
+
head = await advance(blocks, head, event4.cid);
|
|
75
|
+
await clockVis(blocks, head);
|
|
76
|
+
assert.equal(head.length, 2);
|
|
77
|
+
assert.equal(head[0].toString(), event3.cid.toString());
|
|
78
|
+
assert.equal(head[1].toString(), event4.cid.toString());
|
|
79
|
+
});
|
|
80
|
+
it('converge when multi-root', async () => {
|
|
81
|
+
const blocks = new Blockstore();
|
|
82
|
+
const root = await EventBlock.create(await randomEventData());
|
|
83
|
+
await blocks.put(root.cid, root.bytes);
|
|
84
|
+
/** @type {API.EventLink<any>[]} */
|
|
85
|
+
let head = [root.cid];
|
|
86
|
+
const parents0 = head;
|
|
87
|
+
const event0 = await EventBlock.create(await randomEventData(), parents0);
|
|
88
|
+
await blocks.put(event0.cid, event0.bytes);
|
|
89
|
+
head = await advance(blocks, head, event0.cid);
|
|
90
|
+
const event1 = await EventBlock.create(await randomEventData(), parents0);
|
|
91
|
+
await blocks.put(event1.cid, event1.bytes);
|
|
92
|
+
head = await advance(blocks, head, event1.cid);
|
|
93
|
+
const parents1 = head;
|
|
94
|
+
const event2 = await EventBlock.create(await randomEventData(), parents1);
|
|
95
|
+
await blocks.put(event2.cid, event2.bytes);
|
|
96
|
+
head = await advance(blocks, head, event2.cid);
|
|
97
|
+
const event3 = await EventBlock.create(await randomEventData(), parents1);
|
|
98
|
+
await blocks.put(event3.cid, event3.bytes);
|
|
99
|
+
head = await advance(blocks, head, event3.cid);
|
|
100
|
+
const event4 = await EventBlock.create(await randomEventData(), parents1);
|
|
101
|
+
await blocks.put(event4.cid, event4.bytes);
|
|
102
|
+
head = await advance(blocks, head, event4.cid);
|
|
103
|
+
const parents2 = head;
|
|
104
|
+
const event5 = await EventBlock.create(await randomEventData(), parents2);
|
|
105
|
+
await blocks.put(event5.cid, event5.bytes);
|
|
106
|
+
head = await advance(blocks, head, event5.cid);
|
|
107
|
+
await clockVis(blocks, head);
|
|
108
|
+
assert.equal(head.length, 1);
|
|
109
|
+
assert.equal(head[0].toString(), event5.cid.toString());
|
|
110
|
+
});
|
|
111
|
+
it('add an old event', async () => {
|
|
112
|
+
const blocks = new Blockstore();
|
|
113
|
+
const blockGet = blocks.get.bind(blocks);
|
|
114
|
+
let count = 0;
|
|
115
|
+
blocks.get = async (cid) => {
|
|
116
|
+
count++;
|
|
117
|
+
return blockGet(cid);
|
|
118
|
+
};
|
|
119
|
+
const root = await EventBlock.create(await randomEventData());
|
|
120
|
+
await blocks.put(root.cid, root.bytes);
|
|
121
|
+
/** @type {API.EventLink<any>[]} */
|
|
122
|
+
let head = [root.cid];
|
|
123
|
+
const parents0 = head;
|
|
124
|
+
const event0 = await EventBlock.create(await randomEventData(), parents0);
|
|
125
|
+
await blocks.put(event0.cid, event0.bytes);
|
|
126
|
+
head = await advance(blocks, head, event0.cid);
|
|
127
|
+
const event1 = await EventBlock.create(await randomEventData(), parents0);
|
|
128
|
+
await blocks.put(event1.cid, event1.bytes);
|
|
129
|
+
head = await advance(blocks, head, event1.cid);
|
|
130
|
+
const parents1 = head;
|
|
131
|
+
const event2 = await EventBlock.create(await randomEventData(), parents1);
|
|
132
|
+
await blocks.put(event2.cid, event2.bytes);
|
|
133
|
+
head = await advance(blocks, head, event2.cid);
|
|
134
|
+
const event3 = await EventBlock.create(await randomEventData(), parents1);
|
|
135
|
+
await blocks.put(event3.cid, event3.bytes);
|
|
136
|
+
head = await advance(blocks, head, event3.cid);
|
|
137
|
+
const event4 = await EventBlock.create(await randomEventData(), parents1);
|
|
138
|
+
await blocks.put(event4.cid, event4.bytes);
|
|
139
|
+
head = await advance(blocks, head, event4.cid);
|
|
140
|
+
const parents2 = head;
|
|
141
|
+
const event5 = await EventBlock.create(await randomEventData(), parents2);
|
|
142
|
+
await blocks.put(event5.cid, event5.bytes);
|
|
143
|
+
head = await advance(blocks, head, event5.cid);
|
|
144
|
+
// now very old one
|
|
145
|
+
const event6 = await EventBlock.create(await randomEventData(), parents0);
|
|
146
|
+
await blocks.put(event6.cid, event6.bytes);
|
|
147
|
+
const before = count;
|
|
148
|
+
head = await advance(blocks, head, event6.cid);
|
|
149
|
+
assert.equal(count - before, 10);
|
|
150
|
+
await clockVis(blocks, head);
|
|
151
|
+
assert.equal(head.length, 2);
|
|
152
|
+
assert.equal(head[0].toString(), event5.cid.toString());
|
|
153
|
+
assert.equal(head[1].toString(), event6.cid.toString());
|
|
154
|
+
});
|
|
155
|
+
it('add an event with missing parents', async () => {
|
|
156
|
+
const blocks = new Blockstore();
|
|
157
|
+
const root = await EventBlock.create(await randomEventData());
|
|
158
|
+
await blocks.put(root.cid, root.bytes);
|
|
159
|
+
/** @type {API.EventLink<any>[]} */
|
|
160
|
+
let head = [root.cid];
|
|
161
|
+
const event0 = await EventBlock.create(await randomEventData(), head);
|
|
162
|
+
await blocks.put(event0.cid, event0.bytes);
|
|
163
|
+
const event1 = await EventBlock.create(await randomEventData(), [event0.cid]);
|
|
164
|
+
await blocks.put(event1.cid, event1.bytes);
|
|
165
|
+
head = await advance(blocks, head, event1.cid);
|
|
166
|
+
await clockVis(blocks, head);
|
|
167
|
+
assert.equal(head.length, 1);
|
|
168
|
+
assert.equal(head[0].toString(), event1.cid.toString());
|
|
169
|
+
});
|
|
170
|
+
/*
|
|
171
|
+
```mermaid
|
|
172
|
+
flowchart TB
|
|
173
|
+
event3 --> event1
|
|
174
|
+
event3 --> event2
|
|
175
|
+
event2 --> event0
|
|
176
|
+
event1 --> event0
|
|
177
|
+
event0 --> genesis
|
|
178
|
+
event4 --> genesis
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
All we want to do is prove that `event0` and `genesis` are not fetched
|
|
182
|
+
multiple times, since there are multiple paths to it in the tree.
|
|
183
|
+
|
|
184
|
+
We arrive at 8, because when we advance with `head: [event3], event: event4`
|
|
185
|
+
we firstly check if the head is in the event:
|
|
186
|
+
|
|
187
|
+
* get event4 (1)
|
|
188
|
+
* get event3 (2)
|
|
189
|
+
* get genesis (3)
|
|
190
|
+
|
|
191
|
+
Then we check if the event is in the head, with de-duping:
|
|
192
|
+
|
|
193
|
+
* get event3 (4)
|
|
194
|
+
* get event4 (5)
|
|
195
|
+
* then get each of the nodes back to parent(s) of `event4` (`genesis`):
|
|
196
|
+
* event1 (6)
|
|
197
|
+
* event2 (7)
|
|
198
|
+
* event0 (8)
|
|
199
|
+
* (we don't fetch genesis due to existing cycle detection)
|
|
200
|
+
|
|
201
|
+
Without deduping, we expect 9 node fetches, since we traverse across `event0`
|
|
202
|
+
again, since it is linked to by 2 nodes.
|
|
203
|
+
*/
|
|
204
|
+
it('contains only traverses history once', async () => {
|
|
205
|
+
const blocks = new Blockstore();
|
|
206
|
+
const genesis = await EventBlock.create(await randomEventData());
|
|
207
|
+
await blocks.put(genesis.cid, genesis.bytes);
|
|
208
|
+
/** @type {API.EventLink<any>[]} */
|
|
209
|
+
let head = [genesis.cid];
|
|
210
|
+
const blockGet = blocks.get.bind(blocks);
|
|
211
|
+
let count = 0;
|
|
212
|
+
blocks.get = async (cid) => {
|
|
213
|
+
count++;
|
|
214
|
+
return blockGet(cid);
|
|
215
|
+
};
|
|
216
|
+
const event0 = await EventBlock.create(await randomEventData(), [genesis.cid]);
|
|
217
|
+
await blocks.put(event0.cid, event0.bytes);
|
|
218
|
+
head = await advance(blocks, head, event0.cid);
|
|
219
|
+
const event1 = await EventBlock.create(await randomEventData(), [event0.cid]);
|
|
220
|
+
await blocks.put(event1.cid, event1.bytes);
|
|
221
|
+
head = await advance(blocks, head, event1.cid);
|
|
222
|
+
const event2 = await EventBlock.create(await randomEventData(), [event0.cid]);
|
|
223
|
+
await blocks.put(event2.cid, event2.bytes);
|
|
224
|
+
head = await advance(blocks, head, event2.cid);
|
|
225
|
+
const event3 = await EventBlock.create(await randomEventData(), [event1.cid, event2.cid]);
|
|
226
|
+
await blocks.put(event3.cid, event3.bytes);
|
|
227
|
+
head = await advance(blocks, head, event3.cid);
|
|
228
|
+
const before = count;
|
|
229
|
+
const event4 = await EventBlock.create(await randomEventData(), [genesis.cid]);
|
|
230
|
+
await blocks.put(event4.cid, event4.bytes);
|
|
231
|
+
head = await advance(blocks, head, event4.cid);
|
|
232
|
+
assert.equal(head.length, 2);
|
|
233
|
+
assert.equal(head[1].toString(), event4.cid.toString());
|
|
234
|
+
assert.equal(head[0].toString(), event3.cid.toString());
|
|
235
|
+
assert.equal(count - before, 8, 'The number of traversals should be 8 with optimization');
|
|
236
|
+
});
|
|
237
|
+
it('sorts event links', async () => {
|
|
238
|
+
const parent0 = await EventBlock.create(await randomEventData());
|
|
239
|
+
const parent1 = await EventBlock.create(await randomEventData());
|
|
240
|
+
const child0 = await EventBlock.create({}, [parent0.cid, parent1.cid]);
|
|
241
|
+
const child1 = await EventBlock.create({}, [parent1.cid, parent0.cid]);
|
|
242
|
+
assert.deepEqual(child0.bytes, child1.bytes);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crdt.test.d.ts","sourceRoot":"","sources":["../../test/crdt.test.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { expect } from 'vitest';
|
|
2
|
+
// eslint-disable-next-line no-unused-vars
|
|
3
|
+
import * as API from '../src/crdt/api.js';
|
|
4
|
+
import { advance, vis } from '../src/clock/index.js';
|
|
5
|
+
import { put, get, root, entries } from '../src/crdt/index.js';
|
|
6
|
+
import * as Batch from '../src/crdt/batch/index.js';
|
|
7
|
+
import { Blockstore, clockVis, randomCID, randomString } from './helpers.js';
|
|
8
|
+
describe('CRDT', () => {
|
|
9
|
+
it('put a value to a new clock', async () => {
|
|
10
|
+
const blocks = new Blockstore();
|
|
11
|
+
const alice = new TestPail(blocks, []);
|
|
12
|
+
const key = 'key';
|
|
13
|
+
const value = await randomCID(32);
|
|
14
|
+
const { event, head } = await alice.put(key, value);
|
|
15
|
+
await alice.vis();
|
|
16
|
+
assert(event);
|
|
17
|
+
assert(event?.value.data.type === 'put');
|
|
18
|
+
assert.equal(event?.value.data.key, key);
|
|
19
|
+
assert.equal(event?.value.data.value.toString(), value.toString());
|
|
20
|
+
assert.equal(head.length, 1);
|
|
21
|
+
assert.equal(head[0].toString(), event.cid.toString());
|
|
22
|
+
});
|
|
23
|
+
it('linear put multiple values', async () => {
|
|
24
|
+
const blocks = new Blockstore();
|
|
25
|
+
const alice = new TestPail(blocks, []);
|
|
26
|
+
const key0 = 'key0';
|
|
27
|
+
const value0 = await randomCID(32);
|
|
28
|
+
await alice.put(key0, value0);
|
|
29
|
+
const key1 = 'key1';
|
|
30
|
+
const value1 = await randomCID(32);
|
|
31
|
+
const result = await alice.put(key1, value1);
|
|
32
|
+
await alice.vis();
|
|
33
|
+
assert(result.event);
|
|
34
|
+
assert(result.event.value.data.type === 'put');
|
|
35
|
+
assert.equal(result.event.value.data.key, key1);
|
|
36
|
+
assert.equal(result.event.value.data.value.toString(), value1.toString());
|
|
37
|
+
assert.equal(result.head.length, 1);
|
|
38
|
+
assert.equal(result.head[0].toString(), result.event.cid.toString());
|
|
39
|
+
});
|
|
40
|
+
it('simple parallel put multiple values', async () => {
|
|
41
|
+
const blocks = new Blockstore();
|
|
42
|
+
const alice = new TestPail(blocks, []);
|
|
43
|
+
await alice.put('apple', await randomCID(32));
|
|
44
|
+
const bob = new TestPail(blocks, alice.head);
|
|
45
|
+
/** @type {Array<[string, API.UnknownLink]>} */
|
|
46
|
+
const data = [
|
|
47
|
+
['banana', await randomCID(32)],
|
|
48
|
+
['kiwi', await randomCID(32)],
|
|
49
|
+
['mango', await randomCID(32)],
|
|
50
|
+
['orange', await randomCID(32)],
|
|
51
|
+
['pear', await randomCID(32)]
|
|
52
|
+
];
|
|
53
|
+
const { event: aevent0 } = await alice.put(data[0][0], data[0][1]);
|
|
54
|
+
const { event: bevent0 } = await bob.put(data[1][0], data[1][1]);
|
|
55
|
+
assert(aevent0);
|
|
56
|
+
assert(bevent0);
|
|
57
|
+
const carol = new TestPail(blocks, bob.head);
|
|
58
|
+
const { event: bevent1 } = await bob.put(data[2][0], data[2][1]);
|
|
59
|
+
const { event: cevent1 } = await carol.put(data[3][0], data[3][1]);
|
|
60
|
+
assert(bevent1);
|
|
61
|
+
assert(cevent1);
|
|
62
|
+
await alice.advance(cevent1.cid);
|
|
63
|
+
await alice.advance(bevent0.cid);
|
|
64
|
+
await alice.advance(bevent1.cid);
|
|
65
|
+
await bob.advance(aevent0.cid);
|
|
66
|
+
const { event: aevent1 } = await alice.put(data[4][0], data[4][1]);
|
|
67
|
+
await alice.vis();
|
|
68
|
+
assert(aevent1);
|
|
69
|
+
await bob.advance(aevent1.cid);
|
|
70
|
+
await carol.advance(aevent1.cid);
|
|
71
|
+
assert(alice.root);
|
|
72
|
+
assert(bob.root);
|
|
73
|
+
assert(carol.root);
|
|
74
|
+
assert.equal(bob.root.toString(), alice.root.toString());
|
|
75
|
+
assert.equal(carol.root.toString(), alice.root.toString());
|
|
76
|
+
// get item put to bob from alice
|
|
77
|
+
const avalue = await alice.get(data[1][0]);
|
|
78
|
+
assert(avalue);
|
|
79
|
+
assert.equal(avalue.toString(), data[1][1].toString());
|
|
80
|
+
// get item put to alice from bob
|
|
81
|
+
const bvalue = await bob.get(data[0][0]);
|
|
82
|
+
assert(bvalue);
|
|
83
|
+
assert.equal(bvalue.toString(), data[0][1].toString());
|
|
84
|
+
// get item put to alice from carol
|
|
85
|
+
const cvalue = await bob.get(data[4][0]);
|
|
86
|
+
assert(cvalue);
|
|
87
|
+
assert.equal(cvalue.toString(), data[4][1].toString());
|
|
88
|
+
});
|
|
89
|
+
it('get from multi event head', async () => {
|
|
90
|
+
const blocks = new Blockstore();
|
|
91
|
+
const alice = new TestPail(blocks, []);
|
|
92
|
+
await alice.put('apple', await randomCID(32));
|
|
93
|
+
const bob = new TestPail(blocks, alice.head);
|
|
94
|
+
/** @type {Array<[string, API.UnknownLink]>} */
|
|
95
|
+
const data = [
|
|
96
|
+
['banana', await randomCID(32)],
|
|
97
|
+
['kiwi', await randomCID(32)]
|
|
98
|
+
];
|
|
99
|
+
await alice.put(data[0][0], data[0][1]);
|
|
100
|
+
const { event } = await bob.put(data[1][0], data[1][1]);
|
|
101
|
+
assert(event);
|
|
102
|
+
await alice.advance(event.cid);
|
|
103
|
+
const value = await alice.get(data[1][0]);
|
|
104
|
+
assert(value);
|
|
105
|
+
assert.equal(value.toString(), data[1][1].toString());
|
|
106
|
+
});
|
|
107
|
+
it('entries from multi event head', async () => {
|
|
108
|
+
const blocks = new Blockstore();
|
|
109
|
+
const alice = new TestPail(blocks, []);
|
|
110
|
+
await alice.put('apple', await randomCID(32));
|
|
111
|
+
const bob = new TestPail(blocks, alice.head);
|
|
112
|
+
/** @type {Array<[string, API.UnknownLink]>} */
|
|
113
|
+
const data = [
|
|
114
|
+
['banana', await randomCID(32)],
|
|
115
|
+
['kiwi', await randomCID(32)]
|
|
116
|
+
];
|
|
117
|
+
await alice.put(data[0][0], data[0][1]);
|
|
118
|
+
const { event } = await bob.put(data[1][0], data[1][1]);
|
|
119
|
+
assert(event);
|
|
120
|
+
await alice.advance(event.cid);
|
|
121
|
+
for await (const [k, v] of alice.entries()) {
|
|
122
|
+
assert(v.toString(), new Map(data).get(k)?.toString());
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
it('put same value to existing key', async () => {
|
|
126
|
+
const blocks = new Blockstore();
|
|
127
|
+
const alice = new TestPail(blocks, []);
|
|
128
|
+
const key0 = 'key0';
|
|
129
|
+
const value0 = await randomCID(32);
|
|
130
|
+
const r0 = await alice.put(key0, value0);
|
|
131
|
+
assert(r0.additions.map(s => s.cid.toString()).includes(r0.root.toString()));
|
|
132
|
+
assert(!r0.removals.map(s => s.cid.toString()).includes(r0.root.toString()));
|
|
133
|
+
const r1 = await alice.put(key0, value0);
|
|
134
|
+
// nothing was added or removed
|
|
135
|
+
assert.equal(r1.root.toString(), r0.root.toString());
|
|
136
|
+
assert.equal(r1.additions.length, 0);
|
|
137
|
+
assert.equal(r1.removals.length, 0);
|
|
138
|
+
// no event should have been added to the clock
|
|
139
|
+
assert.deepEqual(r1.head.map(cid => cid.toString()), r0.head.map(cid => cid.toString()));
|
|
140
|
+
assert.equal(r1.event, undefined);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe('CRDT batch', () => {
|
|
144
|
+
it('error when put after commit', async () => {
|
|
145
|
+
const blocks = new Blockstore();
|
|
146
|
+
const ops = [];
|
|
147
|
+
for (let i = 0; i < 5; i++) {
|
|
148
|
+
ops.push({ type: 'put', key: `test${randomString(10)}`, value: await randomCID() });
|
|
149
|
+
}
|
|
150
|
+
const batch = await Batch.create(blocks, []);
|
|
151
|
+
for (const op of ops) {
|
|
152
|
+
await batch.put(op.key, op.value);
|
|
153
|
+
}
|
|
154
|
+
await batch.commit();
|
|
155
|
+
await expect(async () => batch.put('test', await randomCID())).rejects.toThrow(/batch already committed/);
|
|
156
|
+
});
|
|
157
|
+
it('error when commit after commit', async () => {
|
|
158
|
+
const blocks = new Blockstore();
|
|
159
|
+
const ops = [];
|
|
160
|
+
for (let i = 0; i < 5; i++) {
|
|
161
|
+
ops.push({ type: 'put', key: `test${randomString(10)}`, value: await randomCID() });
|
|
162
|
+
}
|
|
163
|
+
const batch = await Batch.create(blocks, []);
|
|
164
|
+
for (const op of ops) {
|
|
165
|
+
await batch.put(op.key, op.value);
|
|
166
|
+
}
|
|
167
|
+
await batch.commit();
|
|
168
|
+
await expect(async () => batch.commit()).rejects.toThrow(/batch already committed/);
|
|
169
|
+
});
|
|
170
|
+
it('linear put with batch', async () => {
|
|
171
|
+
const blocks = new Blockstore();
|
|
172
|
+
const alice = new TestPail(blocks, []);
|
|
173
|
+
const key0 = 'test0';
|
|
174
|
+
const value0 = await randomCID(32);
|
|
175
|
+
await alice.put(key0, value0);
|
|
176
|
+
const ops = [];
|
|
177
|
+
for (let i = 0; i < 25; i++) {
|
|
178
|
+
ops.push({ type: 'put', key: `test${randomString(10)}`, value: await randomCID() });
|
|
179
|
+
}
|
|
180
|
+
await alice.putBatch(ops);
|
|
181
|
+
// put a new value for the first batch key
|
|
182
|
+
const key1 = ops[0].key;
|
|
183
|
+
const value1 = await randomCID(32);
|
|
184
|
+
await alice.put(key1, value1);
|
|
185
|
+
await alice.vis();
|
|
186
|
+
const res0 = await alice.get(key0);
|
|
187
|
+
assert(res0);
|
|
188
|
+
assert.equal(res0.toString(), value0.toString());
|
|
189
|
+
for (const op of ops.slice(1)) {
|
|
190
|
+
const res = await alice.get(op.key);
|
|
191
|
+
assert(res);
|
|
192
|
+
assert.equal(res.toString(), op.value.toString());
|
|
193
|
+
}
|
|
194
|
+
const res1 = await alice.get(key1);
|
|
195
|
+
assert(res1);
|
|
196
|
+
assert.equal(res1.toString(), value1.toString());
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
class TestPail {
|
|
200
|
+
/**
|
|
201
|
+
* @param {Blockstore} blocks
|
|
202
|
+
* @param {API.EventLink<API.Operation>[]} head
|
|
203
|
+
*/
|
|
204
|
+
constructor(blocks, head) {
|
|
205
|
+
this.blocks = blocks;
|
|
206
|
+
this.head = head;
|
|
207
|
+
/** @type {API.ShardLink?} */
|
|
208
|
+
this.root = null;
|
|
209
|
+
}
|
|
210
|
+
/** @param {API.EventLink<API.Operation>} event */
|
|
211
|
+
async advance(event) {
|
|
212
|
+
this.head = await advance(this.blocks, this.head, event);
|
|
213
|
+
const result = await root(this.blocks, this.head);
|
|
214
|
+
result.additions.forEach(a => this.blocks.putSync(a.cid, a.bytes));
|
|
215
|
+
this.root = result.root;
|
|
216
|
+
return this.head;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* @param {string} key
|
|
220
|
+
* @param {API.UnknownLink} value
|
|
221
|
+
*/
|
|
222
|
+
async put(key, value) {
|
|
223
|
+
const result = await put(this.blocks, this.head, key, value);
|
|
224
|
+
if (result.event)
|
|
225
|
+
this.blocks.putSync(result.event.cid, result.event.bytes);
|
|
226
|
+
result.additions.forEach(a => this.blocks.putSync(a.cid, a.bytes));
|
|
227
|
+
this.head = result.head;
|
|
228
|
+
this.root = (await root(this.blocks, this.head)).root;
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* @param {Array<{ key: string, value: API.UnknownLink }>} items
|
|
233
|
+
*/
|
|
234
|
+
async putBatch(items) {
|
|
235
|
+
const batch = await Batch.create(this.blocks, this.head);
|
|
236
|
+
for (const { key, value } of items) {
|
|
237
|
+
await batch.put(key, value);
|
|
238
|
+
}
|
|
239
|
+
const result = await batch.commit();
|
|
240
|
+
if (result.event)
|
|
241
|
+
this.blocks.putSync(result.event.cid, result.event.bytes);
|
|
242
|
+
result.additions.forEach(a => this.blocks.putSync(a.cid, a.bytes));
|
|
243
|
+
this.head = result.head;
|
|
244
|
+
this.root = (await root(this.blocks, this.head)).root;
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
async vis() {
|
|
248
|
+
/** @param {API.UnknownLink} l */
|
|
249
|
+
const shortLink = l => `${String(l).slice(0, 4)}..${String(l).slice(-4)}`;
|
|
250
|
+
/**
|
|
251
|
+
* @param {API.PutOperation|API.DeleteOperation|API.BatchOperation} o
|
|
252
|
+
* @returns {string}
|
|
253
|
+
**/
|
|
254
|
+
const renderOp = o => o.type === 'batch'
|
|
255
|
+
? `${o.ops.slice(0, 10).map(renderOp).join('\\n')}${o.ops.length > 10 ? `\\n...${o.ops.length - 10} more` : ''}`
|
|
256
|
+
: `${o.type}(${o.key}${o.type === 'put'
|
|
257
|
+
? `, ${shortLink(o.value)}`
|
|
258
|
+
: ''})`;
|
|
259
|
+
/** @type {(e: API.EventBlockView<API.Operation>) => string} */
|
|
260
|
+
const renderNodeLabel = event => `${shortLink(event.cid)}\\n${renderOp(event.value.data)}`;
|
|
261
|
+
await clockVis(this.blocks, this.head, { renderNodeLabel });
|
|
262
|
+
}
|
|
263
|
+
/** @param {string} key */
|
|
264
|
+
async get(key) {
|
|
265
|
+
return get(this.blocks, this.head, key);
|
|
266
|
+
}
|
|
267
|
+
/** @param {API.EntriesOptions} [options] */
|
|
268
|
+
async *entries(options) {
|
|
269
|
+
yield* entries(this.blocks, this.head, options);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"del.test.d.ts","sourceRoot":"","sources":["../../test/del.test.js"],"names":[],"mappings":""}
|