@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/src/batch/api.d.ts
CHANGED
|
@@ -20,6 +20,11 @@ export interface Batcher {
|
|
|
20
20
|
* overwritten.
|
|
21
21
|
*/
|
|
22
22
|
put: (key: string, value: UnknownLink) => Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Delete the value for the given key. If the key is not found no operation
|
|
25
|
+
* occurs.
|
|
26
|
+
*/
|
|
27
|
+
del: (key: string) => Promise<void>;
|
|
23
28
|
/**
|
|
24
29
|
* Encode all altered shards in the batch and return the new root CID and
|
|
25
30
|
* difference blocks.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../src/batch/api.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,SAAS,EACT,SAAS,EACT,UAAU,EACV,oBAAoB,EACpB,mBAAmB,EACnB,2BAA2B,EAC3B,WAAW,EACX,YAAY,EACZ,cAAc,EACd,YAAY,EACb,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,WAAW,EACX,SAAS,EACT,SAAS,EACT,UAAU,EACV,oBAAoB,EACpB,mBAAmB,EACnB,2BAA2B,EAC3B,WAAW,EACX,YAAY,EACZ,cAAc,EACd,YAAY,EACb,CAAA;AAED,MAAM,WAAW,YAAa,SAAQ,WAAW;IAC/C,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,OAAO,EAAE,iBAAiB,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,OAAO,CAAC,EAAE,iBAAiB,EAAE,CAAA;CAC9B;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM;IACX,KAAK,EAAE,oBAAoB,GAAG,mBAAmB,GAAG,2BAA2B,GAAG,oBAAoB,GAAG,4BAA4B;CACtI,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG,CAAC,YAAY,CAAC,CAAA;AAEjD,MAAM,MAAM,4BAA4B,GAAG,CAAC,YAAY,EAAE,WAAW,CAAC,CAAA;AAEtE,MAAM,WAAW,OAAO;IACtB;;;OAGG;IACH,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACvD;;;OAGG;IACH,MAAM,EAAE,MAAM,OAAO,CAAC;QAAE,IAAI,EAAE,SAAS,CAAA;KAAE,GAAG,SAAS,CAAC,CAAA;CACvD"}
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../src/batch/api.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,SAAS,EACT,SAAS,EACT,UAAU,EACV,oBAAoB,EACpB,mBAAmB,EACnB,2BAA2B,EAC3B,WAAW,EACX,YAAY,EACZ,cAAc,EACd,YAAY,EACb,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,WAAW,EACX,SAAS,EACT,SAAS,EACT,UAAU,EACV,oBAAoB,EACpB,mBAAmB,EACnB,2BAA2B,EAC3B,WAAW,EACX,YAAY,EACZ,cAAc,EACd,YAAY,EACb,CAAA;AAED,MAAM,WAAW,YAAa,SAAQ,WAAW;IAC/C,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,OAAO,EAAE,iBAAiB,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,OAAO,CAAC,EAAE,iBAAiB,EAAE,CAAA;CAC9B;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM;IACX,KAAK,EAAE,oBAAoB,GAAG,mBAAmB,GAAG,2BAA2B,GAAG,oBAAoB,GAAG,4BAA4B;CACtI,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG,CAAC,YAAY,CAAC,CAAA;AAEjD,MAAM,MAAM,4BAA4B,GAAG,CAAC,YAAY,EAAE,WAAW,CAAC,CAAA;AAEtE,MAAM,WAAW,OAAO;IACtB;;;OAGG;IACH,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACvD;;;OAGG;IACH,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACnC;;;OAGG;IACH,MAAM,EAAE,MAAM,OAAO,CAAC;QAAE,IAAI,EAAE,SAAS,CAAA;KAAE,GAAG,SAAS,CAAC,CAAA;CACvD"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export function put(blocks: API.BlockFetcher, shard: API.BatcherShard, key: string, value: API.UnknownLink): Promise<void>;
|
|
2
|
+
export function del(blocks: API.BlockFetcher, shard: API.BatcherShard, key: string): Promise<void>;
|
|
2
3
|
export function traverse(shards: ShardFetcher, shard: API.BatcherShard, key: string): Promise<{
|
|
3
4
|
shard: API.BatcherShard;
|
|
4
5
|
key: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/batch/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/batch/index.js"],"names":[],"mappings":"AA0EO,4BANI,GAAG,CAAC,YAAY,SAChB,GAAG,CAAC,YAAY,OAChB,MAAM,SACN,GAAG,CAAC,WAAW,GACb,OAAO,CAAC,IAAI,CAAC,CAsGzB;AAQM,4BALI,GAAG,CAAC,YAAY,SAChB,GAAG,CAAC,YAAY,OAChB,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAmBzB;AAWM,iCALI,YAAY,SACZ,GAAG,CAAC,YAAY,OAChB,MAAM,GACJ,OAAO,CAAC;IAAE,KAAK,EAAE,GAAG,CAAC,YAAY,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAiB7D;AAQM,8BAFI,GAAG,CAAC,YAAY;;;;GAkC1B;AAaM,+BAJI,GAAG,CAAC,YAAY,QAChB,GAAG,CAAC,SAAS,GACX,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAE6C;AAE9E;IAUE,oBAAmC;IATnC;;;OAGG;IACH,sBAHW,MAAM,YACN,YAAY,EAKtB;IADC,aAAoC;CAIvC;qBAlSoB,UAAU;6BACgB,aAAa"}
|
package/dist/src/batch/index.js
CHANGED
|
@@ -35,6 +35,15 @@ class Batcher {
|
|
|
35
35
|
throw new BatchCommittedError();
|
|
36
36
|
return put(this.blocks, this, key, value);
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} key The key of the value to delete.
|
|
40
|
+
* @returns {Promise<void>}
|
|
41
|
+
*/
|
|
42
|
+
async del(key) {
|
|
43
|
+
if (this.#committed)
|
|
44
|
+
throw new BatchCommittedError();
|
|
45
|
+
return del(this.blocks, this, key);
|
|
46
|
+
}
|
|
38
47
|
async commit() {
|
|
39
48
|
if (this.#committed)
|
|
40
49
|
throw new BatchCommittedError();
|
|
@@ -156,6 +165,31 @@ export const put = async (blocks, shard, key, value) => {
|
|
|
156
165
|
}
|
|
157
166
|
shard.entries = Shard.putEntry(asShardEntries(targetEntries), asShardEntry(entry));
|
|
158
167
|
};
|
|
168
|
+
/**
|
|
169
|
+
* @param {API.BlockFetcher} blocks
|
|
170
|
+
* @param {API.BatcherShard} shard
|
|
171
|
+
* @param {string} key The key of the value to delete.
|
|
172
|
+
* @returns {Promise<void>}
|
|
173
|
+
*/
|
|
174
|
+
export const del = async (blocks, shard, key) => {
|
|
175
|
+
const shards = new ShardFetcher(blocks);
|
|
176
|
+
const dest = await traverse(shards, shard, key);
|
|
177
|
+
const entryidx = dest.shard.entries.findIndex(([k]) => k === dest.key);
|
|
178
|
+
if (entryidx === -1)
|
|
179
|
+
return;
|
|
180
|
+
const entry = dest.shard.entries[entryidx];
|
|
181
|
+
if (Array.isArray(entry[1])) {
|
|
182
|
+
// cannot delete a shard-only entry (no value)
|
|
183
|
+
if (entry[1][1] == null)
|
|
184
|
+
return;
|
|
185
|
+
// remove value but keep shard link
|
|
186
|
+
dest.shard.entries[entryidx] = [entry[0], /** @type {API.ShardEntryLinkValue | API.ShardEntryShardValue} */ ([entry[1][0]])];
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
// remove the entry entirely
|
|
190
|
+
dest.shard.entries.splice(entryidx, 1);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
159
193
|
/**
|
|
160
194
|
* Traverse from the passed shard through to the correct shard for the passed
|
|
161
195
|
* key.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/crdt/batch/index.js"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/crdt/batch/index.js"],"names":[],"mappings":";AAoKO,+BAJI,GAAG,CAAC,YAAY,QAChB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,GAC5B,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAEmC;oCA/JpC,sBAAsB;qBAJrC,UAAU"}
|
|
@@ -50,6 +50,16 @@ class Batcher {
|
|
|
50
50
|
await Batch.put(this.blocks, this, key, value);
|
|
51
51
|
this.ops.push({ type: 'put', key, value });
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} key The key of the value to delete.
|
|
55
|
+
* @returns {Promise<void>}
|
|
56
|
+
*/
|
|
57
|
+
async del(key) {
|
|
58
|
+
if (this.#committed)
|
|
59
|
+
throw new BatchCommittedError();
|
|
60
|
+
await Batch.del(this.blocks, this, key);
|
|
61
|
+
this.ops.push({ type: 'del', key });
|
|
62
|
+
}
|
|
53
63
|
async commit() {
|
|
54
64
|
if (this.#committed)
|
|
55
65
|
throw new BatchCommittedError();
|
package/dist/src/crdt/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function put(blocks: API.BlockFetcher, head: API.EventLink<API.Operation>[], key: string, value: API.UnknownLink): Promise<API.Result>;
|
|
2
|
-
export function del(blocks: API.BlockFetcher, head: API.EventLink<API.Operation>[], key: string
|
|
2
|
+
export function del(blocks: API.BlockFetcher, head: API.EventLink<API.Operation>[], key: string): Promise<API.Result>;
|
|
3
3
|
export function root(blocks: API.BlockFetcher, head: API.EventLink<API.Operation>[]): Promise<{
|
|
4
4
|
root: API.ShardLink;
|
|
5
5
|
} & API.ShardDiff>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/crdt/index.js"],"names":[],"mappings":"AAmBO,4BANI,GAAG,CAAC,YAAY,QAChB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,OAC9B,MAAM,SACN,GAAG,CAAC,WAAW,GACb,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/crdt/index.js"],"names":[],"mappings":"AAmBO,4BANI,GAAG,CAAC,YAAY,QAChB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,OAC9B,MAAM,SACN,GAAG,CAAC,WAAW,GACb,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAyB/B;AAWM,4BALI,GAAG,CAAC,YAAY,QAChB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,OAC9B,MAAM,GACJ,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAO/B;AAsEM,6BAJI,GAAG,CAAC,YAAY,QAChB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,GAC5B,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC,SAAS,CAAA;CAAE,GAAG,GAAG,CAAC,SAAS,CAAC,CA0E5D;AAOM,4BAJI,GAAG,CAAC,YAAY,QAChB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,OAC9B,MAAM,6GAYhB;AAOM,gCAJI,GAAG,CAAC,YAAY,QAChB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,YAC9B,GAAG,CAAC,cAAc,kEAY5B;qBAzOoB,UAAU"}
|
package/dist/src/crdt/index.js
CHANGED
|
@@ -17,9 +17,9 @@ import * as Batch from '../batch/index.js';
|
|
|
17
17
|
* @returns {Promise<API.Result>}
|
|
18
18
|
*/
|
|
19
19
|
export const put = async (blocks, head, key, value) => {
|
|
20
|
-
const mblocks = new MemoryBlockstore();
|
|
21
|
-
blocks = new MultiBlockFetcher(mblocks, blocks);
|
|
22
20
|
if (!head.length) {
|
|
21
|
+
const mblocks = new MemoryBlockstore();
|
|
22
|
+
blocks = new MultiBlockFetcher(mblocks, blocks);
|
|
23
23
|
const shard = await ShardBlock.create();
|
|
24
24
|
mblocks.putSync(shard.cid, shard.bytes);
|
|
25
25
|
const result = await Pail.put(blocks, shard.cid, key, value);
|
|
@@ -35,65 +35,58 @@ export const put = async (blocks, head, key, value) => {
|
|
|
35
35
|
event
|
|
36
36
|
};
|
|
37
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
|
-
throw new Error(`unknown operation: ${event.data.type}`);
|
|
70
|
-
}
|
|
71
|
-
root = result.root;
|
|
72
|
-
for (const a of result.additions) {
|
|
73
|
-
mblocks.putSync(a.cid, a.bytes);
|
|
74
|
-
additions.set(a.cid.toString(), a);
|
|
75
|
-
}
|
|
76
|
-
for (const r of result.removals) {
|
|
77
|
-
removals.set(r.cid.toString(), r);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
const result = await Pail.put(blocks, root, key, value);
|
|
81
|
-
// if we didn't change the pail we're done
|
|
82
|
-
if (result.root.toString() === root.toString()) {
|
|
83
|
-
return { root, additions: [], removals: [], head };
|
|
84
|
-
}
|
|
85
|
-
for (const a of result.additions) {
|
|
38
|
+
return applyOperation(blocks, head, { type: 'put', key, value }, (b, r) => Pail.put(b, r, key, value));
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Delete the value for the given key from the bucket. If the key is not found
|
|
42
|
+
* no operation occurs.
|
|
43
|
+
*
|
|
44
|
+
* @param {API.BlockFetcher} blocks Bucket block storage.
|
|
45
|
+
* @param {API.EventLink<API.Operation>[]} head Merkle clock head.
|
|
46
|
+
* @param {string} key The key of the value to delete.
|
|
47
|
+
* @returns {Promise<API.Result>}
|
|
48
|
+
*/
|
|
49
|
+
export const del = async (blocks, head, key) => {
|
|
50
|
+
if (!head.length)
|
|
51
|
+
throw new Error('cannot delete from empty clock');
|
|
52
|
+
return applyOperation(blocks, head, { type: 'del', key }, (b, r) => Pail.del(b, r, key));
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the current root, apply an operation, create a clock event, and
|
|
56
|
+
* return the updated head.
|
|
57
|
+
*
|
|
58
|
+
* @param {API.BlockFetcher} blocks Bucket block storage.
|
|
59
|
+
* @param {API.EventLink<API.Operation>[]} head Merkle clock head.
|
|
60
|
+
* @param {API.PutOperation | API.DeleteOperation} op Operation data (without root).
|
|
61
|
+
* @param {(blocks: API.BlockFetcher, root: API.ShardLink) => Promise<{ root: API.ShardLink } & API.ShardDiff>} fn
|
|
62
|
+
* @returns {Promise<API.Result>}
|
|
63
|
+
*/
|
|
64
|
+
const applyOperation = async (blocks, head, op, fn) => {
|
|
65
|
+
const mblocks = new MemoryBlockstore();
|
|
66
|
+
blocks = new MultiBlockFetcher(mblocks, blocks);
|
|
67
|
+
const resolved = await root(blocks, head);
|
|
68
|
+
for (const a of resolved.additions) {
|
|
86
69
|
mblocks.putSync(a.cid, a.bytes);
|
|
87
|
-
additions.set(a.cid.toString(), a);
|
|
88
70
|
}
|
|
89
|
-
|
|
90
|
-
|
|
71
|
+
const result = await fn(blocks, resolved.root);
|
|
72
|
+
if (result.root.toString() === resolved.root.toString()) {
|
|
73
|
+
return { root: resolved.root, additions: [], removals: [], head };
|
|
91
74
|
}
|
|
92
75
|
/** @type {API.Operation} */
|
|
93
|
-
const data = {
|
|
76
|
+
const data = { ...op, root: result.root };
|
|
94
77
|
const event = await EventBlock.create(data, head);
|
|
95
78
|
mblocks.putSync(event.cid, event.bytes);
|
|
96
79
|
head = await Clock.advance(blocks, head, event.cid);
|
|
80
|
+
/** @type {Map<string, API.ShardBlockView>} */
|
|
81
|
+
const additions = new Map();
|
|
82
|
+
for (const a of [...resolved.additions, ...result.additions]) {
|
|
83
|
+
additions.set(a.cid.toString(), a);
|
|
84
|
+
}
|
|
85
|
+
/** @type {Map<string, API.ShardBlockView>} */
|
|
86
|
+
const removals = new Map();
|
|
87
|
+
for (const r of [...resolved.removals, ...result.removals]) {
|
|
88
|
+
removals.set(r.cid.toString(), r);
|
|
89
|
+
}
|
|
97
90
|
// filter blocks that were added _and_ removed
|
|
98
91
|
for (const k of removals.keys()) {
|
|
99
92
|
if (additions.has(k)) {
|
|
@@ -109,19 +102,6 @@ export const put = async (blocks, head, key, value) => {
|
|
|
109
102
|
event
|
|
110
103
|
};
|
|
111
104
|
};
|
|
112
|
-
/**
|
|
113
|
-
* Delete the value for the given key from the bucket. If the key is not found
|
|
114
|
-
* no operation occurs.
|
|
115
|
-
*
|
|
116
|
-
* @param {API.BlockFetcher} blocks Bucket block storage.
|
|
117
|
-
* @param {API.EventLink<API.Operation>[]} head Merkle clock head.
|
|
118
|
-
* @param {string} key The key of the value to delete.
|
|
119
|
-
* @param {object} [options]
|
|
120
|
-
* @returns {Promise<API.Result>}
|
|
121
|
-
*/
|
|
122
|
-
export const del = async (blocks, head, key, options) => {
|
|
123
|
-
throw new Error('not implemented');
|
|
124
|
-
};
|
|
125
105
|
/**
|
|
126
106
|
* Determine the effective pail root given the current merkle clock head.
|
|
127
107
|
*
|
|
@@ -165,15 +145,20 @@ export const root = async (blocks, head) => {
|
|
|
165
145
|
else if (event.data.type === 'batch') {
|
|
166
146
|
const batch = await Batch.create(blocks, root);
|
|
167
147
|
for (const op of event.data.ops) {
|
|
168
|
-
if (op.type
|
|
169
|
-
|
|
170
|
-
|
|
148
|
+
if (op.type === 'put') {
|
|
149
|
+
await batch.put(op.key, op.value);
|
|
150
|
+
}
|
|
151
|
+
else if (op.type === 'del') {
|
|
152
|
+
await batch.del(op.key);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
throw new Error(`unsupported batch operation: ${ /** @type {any} */(op).type}`);
|
|
156
|
+
}
|
|
171
157
|
}
|
|
172
158
|
result = await batch.commit();
|
|
173
159
|
}
|
|
174
160
|
else {
|
|
175
|
-
|
|
176
|
-
throw new Error(`unknown operation: ${event.data.type}`);
|
|
161
|
+
throw new Error(`unknown operation: ${ /** @type {any} */(event.data).type}`);
|
|
177
162
|
}
|
|
178
163
|
root = result.root;
|
|
179
164
|
for (const a of result.additions) {
|
|
@@ -235,7 +220,7 @@ export const entries = async function* (blocks, head, options) {
|
|
|
235
220
|
const findCommonAncestor = async (events, children) => {
|
|
236
221
|
if (!children.length)
|
|
237
222
|
return;
|
|
238
|
-
const candidates = children.map(c => [c]);
|
|
223
|
+
const candidates = children.map((c) => [c]);
|
|
239
224
|
while (true) {
|
|
240
225
|
let changed = false;
|
|
241
226
|
for (const c of candidates) {
|
|
@@ -269,14 +254,14 @@ const findAncestorCandidate = async (events, root) => {
|
|
|
269
254
|
* @param {Array<T[]>} arrays
|
|
270
255
|
*/
|
|
271
256
|
const findCommonString = (arrays) => {
|
|
272
|
-
arrays = arrays.map(a => [...a]);
|
|
257
|
+
arrays = arrays.map((a) => [...a]);
|
|
273
258
|
for (const arr of arrays) {
|
|
274
259
|
for (const item of arr) {
|
|
275
260
|
let matched = true;
|
|
276
261
|
for (const other of arrays) {
|
|
277
262
|
if (arr === other)
|
|
278
263
|
continue;
|
|
279
|
-
matched = other.some(i => String(i) === String(item));
|
|
264
|
+
matched = other.some((i) => String(i) === String(item));
|
|
280
265
|
if (!matched)
|
|
281
266
|
break;
|
|
282
267
|
}
|
|
@@ -298,7 +283,7 @@ const findSortedEvents = async (events, head, tail) => {
|
|
|
298
283
|
// get weighted events - heavier events happened first
|
|
299
284
|
/** @type {Map<string, { event: API.EventBlockView<API.Operation>, weight: number }>} */
|
|
300
285
|
const weights = new Map();
|
|
301
|
-
const all = await Promise.all(head.map(h => findEvents(events, h, tail)));
|
|
286
|
+
const all = await Promise.all(head.map((h) => findEvents(events, h, tail)));
|
|
302
287
|
for (const arr of all) {
|
|
303
288
|
for (const { event, depth } of arr) {
|
|
304
289
|
const info = weights.get(event.cid.toString());
|
|
@@ -325,7 +310,126 @@ const findSortedEvents = async (events, head, tail) => {
|
|
|
325
310
|
// sort by weight, and by CID within weight
|
|
326
311
|
return Array.from(buckets)
|
|
327
312
|
.sort((a, b) => b[0] - a[0])
|
|
328
|
-
.flatMap(([,
|
|
313
|
+
.flatMap(([, events]) => removeConcurrentDeletes(events).sort((a, b) => String(a.cid) < String(b.cid) ? -1 : 1));
|
|
314
|
+
};
|
|
315
|
+
/**
|
|
316
|
+
* Remove concurrent events that delete the same key or delete/put the same key. The conflict resolution rules are as follows:
|
|
317
|
+
* - If two or more concurrent events delete the same key, all but one of the delete events are removed (so the key is still deleted).
|
|
318
|
+
* - If one or more concurrent events delete a key while another event puts a value for the same key, the put wins (i.e. the delete events are removed).
|
|
319
|
+
* @param {API.EventBlockView<API.Operation>[]} events
|
|
320
|
+
* @returns {API.EventBlockView<API.Operation>[]}
|
|
321
|
+
*/
|
|
322
|
+
const removeConcurrentDeletes = (events) => {
|
|
323
|
+
// Simplify batch events: within a batch, ops are ordered so only the last
|
|
324
|
+
// operation per key represents the net effect. e.g. [put a, put b, del a]
|
|
325
|
+
// simplifies to [put b, del a] — the earlier put a is overridden.
|
|
326
|
+
// @ts-expect-error creating modified views with simplified ops
|
|
327
|
+
events = events.map((event) => {
|
|
328
|
+
const { data } = event.value;
|
|
329
|
+
if (data.type !== 'batch')
|
|
330
|
+
return event;
|
|
331
|
+
/** @type {Map<string, number>} */
|
|
332
|
+
const lastIndex = new Map();
|
|
333
|
+
for (let i = 0; i < data.ops.length; i++) {
|
|
334
|
+
lastIndex.set(data.ops[i].key, i);
|
|
335
|
+
}
|
|
336
|
+
const simplified = data.ops.filter((op, i) => lastIndex.get(op.key) === i);
|
|
337
|
+
if (simplified.length === data.ops.length)
|
|
338
|
+
return event;
|
|
339
|
+
return {
|
|
340
|
+
cid: event.cid,
|
|
341
|
+
bytes: event.bytes,
|
|
342
|
+
value: { ...event.value, data: { ...data, ops: simplified } }
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
/** @type {Map<string, number>} */
|
|
346
|
+
const delCounts = new Map();
|
|
347
|
+
/** @type {Set<string>} */
|
|
348
|
+
const putKeys = new Set();
|
|
349
|
+
for (const event of events) {
|
|
350
|
+
const { data } = event.value;
|
|
351
|
+
if (data.type === 'put') {
|
|
352
|
+
putKeys.add(data.key);
|
|
353
|
+
}
|
|
354
|
+
else if (data.type === 'del') {
|
|
355
|
+
delCounts.set(data.key, (delCounts.get(data.key) || 0) + 1);
|
|
356
|
+
}
|
|
357
|
+
else if (data.type === 'batch') {
|
|
358
|
+
for (const op of data.ops) {
|
|
359
|
+
if (op.type === 'put') {
|
|
360
|
+
putKeys.add(op.key);
|
|
361
|
+
}
|
|
362
|
+
else if (op.type === 'del') {
|
|
363
|
+
delCounts.set(op.key, (delCounts.get(op.key) || 0) + 1);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Keys whose delete operations should be deduplicated: put wins over delete,
|
|
369
|
+
// and multiple concurrent deletes are collapsed to one.
|
|
370
|
+
/** @type {Set<string>} */
|
|
371
|
+
const putWinsKeys = new Set();
|
|
372
|
+
/** @type {Set<string>} */
|
|
373
|
+
const dedupeDelKeys = new Set();
|
|
374
|
+
for (const [key, count] of delCounts) {
|
|
375
|
+
if (putKeys.has(key)) {
|
|
376
|
+
putWinsKeys.add(key);
|
|
377
|
+
}
|
|
378
|
+
else if (count > 1) {
|
|
379
|
+
dedupeDelKeys.add(key);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (putWinsKeys.size === 0 && dedupeDelKeys.size === 0)
|
|
383
|
+
return events;
|
|
384
|
+
// Track which deduped-delete keys have already had their first delete kept
|
|
385
|
+
/** @type {Set<string>} */
|
|
386
|
+
const keptDeletes = new Set();
|
|
387
|
+
/** @type {API.EventBlockView<API.Operation>[]} */
|
|
388
|
+
const result = [];
|
|
389
|
+
for (const event of events) {
|
|
390
|
+
const { data } = event.value;
|
|
391
|
+
if (data.type === 'del') {
|
|
392
|
+
if (putWinsKeys.has(data.key))
|
|
393
|
+
continue;
|
|
394
|
+
if (dedupeDelKeys.has(data.key)) {
|
|
395
|
+
if (keptDeletes.has(data.key))
|
|
396
|
+
continue;
|
|
397
|
+
keptDeletes.add(data.key);
|
|
398
|
+
}
|
|
399
|
+
result.push(event);
|
|
400
|
+
}
|
|
401
|
+
else if (data.type === 'batch') {
|
|
402
|
+
const filteredOps = data.ops.filter((op) => {
|
|
403
|
+
if (op.type !== 'del')
|
|
404
|
+
return true;
|
|
405
|
+
if (putWinsKeys.has(op.key))
|
|
406
|
+
return false;
|
|
407
|
+
if (dedupeDelKeys.has(op.key)) {
|
|
408
|
+
if (keptDeletes.has(op.key))
|
|
409
|
+
return false;
|
|
410
|
+
keptDeletes.add(op.key);
|
|
411
|
+
}
|
|
412
|
+
return true;
|
|
413
|
+
});
|
|
414
|
+
if (filteredOps.length === 0)
|
|
415
|
+
continue;
|
|
416
|
+
if (filteredOps.length !== data.ops.length) {
|
|
417
|
+
// @ts-expect-error creating a modified view with filtered ops
|
|
418
|
+
result.push({
|
|
419
|
+
cid: event.cid,
|
|
420
|
+
bytes: event.bytes,
|
|
421
|
+
value: { ...event.value, data: { ...data, ops: filteredOps } }
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
result.push(event);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
result.push(event);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
329
433
|
};
|
|
330
434
|
/**
|
|
331
435
|
* @param {EventFetcher<API.Operation>} events
|
|
@@ -339,6 +443,6 @@ const findEvents = async (events, start, end, depth = 0) => {
|
|
|
339
443
|
const { parents } = event.value;
|
|
340
444
|
if (parents.length === 1 && String(parents[0]) === String(end))
|
|
341
445
|
return acc;
|
|
342
|
-
const rest = await Promise.all(parents.map(p => findEvents(events, p, end, depth + 1)));
|
|
446
|
+
const rest = await Promise.all(parents.map((p) => findEvents(events, p, end, depth + 1)));
|
|
343
447
|
return acc.concat(...rest);
|
|
344
448
|
};
|
package/dist/test/batch.test.js
CHANGED
|
@@ -87,6 +87,98 @@ describe('batch', () => {
|
|
|
87
87
|
await batch.commit();
|
|
88
88
|
await expect(async () => batch.commit()).rejects.toThrowError(/batch already committed/);
|
|
89
89
|
});
|
|
90
|
+
it('batches dels', async () => {
|
|
91
|
+
const rootblk = await ShardBlock.create();
|
|
92
|
+
const blocks = new Blockstore();
|
|
93
|
+
await blocks.put(rootblk.cid, rootblk.bytes);
|
|
94
|
+
// put some keys
|
|
95
|
+
const keys = [];
|
|
96
|
+
for (let i = 0; i < 10; i++) {
|
|
97
|
+
keys.push(`test${randomString(10)}`);
|
|
98
|
+
}
|
|
99
|
+
/** @type {API.ShardLink} */
|
|
100
|
+
let current = rootblk.cid;
|
|
101
|
+
for (const key of keys) {
|
|
102
|
+
const { root, additions } = await Pail.put(blocks, current, key, await randomCID());
|
|
103
|
+
current = root;
|
|
104
|
+
for (const b of additions)
|
|
105
|
+
blocks.putSync(b.cid, b.bytes);
|
|
106
|
+
}
|
|
107
|
+
// batch-delete the first 5
|
|
108
|
+
const batch = await Batch.create(blocks, current);
|
|
109
|
+
for (const key of keys.slice(0, 5)) {
|
|
110
|
+
await batch.del(key);
|
|
111
|
+
}
|
|
112
|
+
const { root, additions, removals } = await batch.commit();
|
|
113
|
+
for (const b of removals)
|
|
114
|
+
blocks.deleteSync(b.cid);
|
|
115
|
+
for (const b of additions)
|
|
116
|
+
blocks.putSync(b.cid, b.bytes);
|
|
117
|
+
// deleted keys should be gone
|
|
118
|
+
for (const key of keys.slice(0, 5)) {
|
|
119
|
+
const value = await Pail.get(blocks, root, key);
|
|
120
|
+
assert.equal(value, undefined);
|
|
121
|
+
}
|
|
122
|
+
// remaining keys should still exist
|
|
123
|
+
for (const key of keys.slice(5)) {
|
|
124
|
+
const value = await Pail.get(blocks, root, key);
|
|
125
|
+
assert(value);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
it('mixed put and del in same batch', async () => {
|
|
129
|
+
const rootblk = await ShardBlock.create();
|
|
130
|
+
const blocks = new Blockstore();
|
|
131
|
+
await blocks.put(rootblk.cid, rootblk.bytes);
|
|
132
|
+
// put initial keys
|
|
133
|
+
/** @type {API.ShardLink} */
|
|
134
|
+
let current = rootblk.cid;
|
|
135
|
+
const value0 = await randomCID();
|
|
136
|
+
const res0 = await Pail.put(blocks, current, 'apple', value0);
|
|
137
|
+
current = res0.root;
|
|
138
|
+
for (const b of res0.additions)
|
|
139
|
+
blocks.putSync(b.cid, b.bytes);
|
|
140
|
+
const res1 = await Pail.put(blocks, current, 'banana', await randomCID());
|
|
141
|
+
current = res1.root;
|
|
142
|
+
for (const b of res1.additions)
|
|
143
|
+
blocks.putSync(b.cid, b.bytes);
|
|
144
|
+
// batch: delete apple, put cherry
|
|
145
|
+
const cherryValue = await randomCID();
|
|
146
|
+
const batch = await Batch.create(blocks, current);
|
|
147
|
+
await batch.del('apple');
|
|
148
|
+
await batch.put('cherry', cherryValue);
|
|
149
|
+
const { root, additions, removals } = await batch.commit();
|
|
150
|
+
for (const b of removals)
|
|
151
|
+
blocks.deleteSync(b.cid);
|
|
152
|
+
for (const b of additions)
|
|
153
|
+
blocks.putSync(b.cid, b.bytes);
|
|
154
|
+
assert.equal(await Pail.get(blocks, root, 'apple'), undefined);
|
|
155
|
+
assert(await Pail.get(blocks, root, 'banana'));
|
|
156
|
+
const cherry = await Pail.get(blocks, root, 'cherry');
|
|
157
|
+
assert(cherry);
|
|
158
|
+
assert.equal(cherry.toString(), cherryValue.toString());
|
|
159
|
+
});
|
|
160
|
+
it('del non-existent key is a no-op', async () => {
|
|
161
|
+
const rootblk = await ShardBlock.create();
|
|
162
|
+
const blocks = new Blockstore();
|
|
163
|
+
await blocks.put(rootblk.cid, rootblk.bytes);
|
|
164
|
+
const { root: r0, additions: a0 } = await Pail.put(blocks, rootblk.cid, 'apple', await randomCID());
|
|
165
|
+
for (const b of a0)
|
|
166
|
+
blocks.putSync(b.cid, b.bytes);
|
|
167
|
+
const batch = await Batch.create(blocks, r0);
|
|
168
|
+
await batch.del('nonexistent');
|
|
169
|
+
const { root } = await batch.commit();
|
|
170
|
+
// root should be unchanged
|
|
171
|
+
assert.equal(root.toString(), r0.toString());
|
|
172
|
+
});
|
|
173
|
+
it('error when del after commit', async () => {
|
|
174
|
+
const root = await ShardBlock.create();
|
|
175
|
+
const blocks = new Blockstore();
|
|
176
|
+
await blocks.put(root.cid, root.bytes);
|
|
177
|
+
const batch = await Batch.create(blocks, root.cid);
|
|
178
|
+
await batch.put('test', await randomCID());
|
|
179
|
+
await batch.commit();
|
|
180
|
+
await expect(async () => batch.del('test')).rejects.toThrowError(/batch already committed/);
|
|
181
|
+
});
|
|
90
182
|
it('traverses existing shards to put values', async () => {
|
|
91
183
|
const rootblk = await ShardBlock.create();
|
|
92
184
|
const blocks = new Blockstore();
|