@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.
@@ -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":"AAiEO,4BANI,GAAG,CAAC,YAAY,SAChB,GAAG,CAAC,YAAY,OAChB,MAAM,SACN,GAAG,CAAC,WAAW,GACb,OAAO,CAAC,IAAI,CAAC,CAsGzB;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;qBAhQoB,UAAU;6BACgB,aAAa"}
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"}
@@ -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":";AA0JO,+BAJI,GAAG,CAAC,YAAY,QAChB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,GAC5B,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAEmC;oCArJpC,sBAAsB;qBAJrC,UAAU"}
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();
@@ -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, options?: object): Promise<API.Result>;
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,CAoG/B;AAYM,4BANI,GAAG,CAAC,YAAY,QAChB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,OAC9B,MAAM,YACN,MAAM,GACJ,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAI/B;AAYM,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,CAsE5D;AAOM,4BAJI,GAAG,CAAC,YAAY,QAChB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,OAC9B,MAAM,6GAShB;AAOM,gCAJI,GAAG,CAAC,YAAY,QAChB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,YAC9B,GAAG,CAAC,cAAc,kEAS5B;qBA9OoB,UAAU"}
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"}
@@ -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
- /** @type {EventFetcher<API.Operation>} */
39
- const events = new EventFetcher(blocks);
40
- const ancestor = await findCommonAncestor(events, head);
41
- if (!ancestor)
42
- throw new Error('failed to find common ancestor event');
43
- const aevent = await events.get(ancestor);
44
- let { root } = aevent.value.data;
45
- const sorted = await findSortedEvents(events, head, ancestor);
46
- /** @type {Map<string, API.ShardBlockView>} */
47
- const additions = new Map();
48
- /** @type {Map<string, API.ShardBlockView>} */
49
- const removals = new Map();
50
- for (const { value: event } of sorted) {
51
- let result;
52
- if (event.data.type === 'put') {
53
- result = await Pail.put(blocks, root, event.data.key, event.data.value);
54
- }
55
- else if (event.data.type === 'del') {
56
- result = await Pail.del(blocks, root, event.data.key);
57
- }
58
- else if (event.data.type === 'batch') {
59
- const batch = await Batch.create(blocks, root);
60
- for (const op of event.data.ops) {
61
- if (op.type !== 'put')
62
- throw new Error(`unsupported batch operation: ${op.type}`);
63
- await batch.put(op.key, op.value);
64
- }
65
- result = await batch.commit();
66
- }
67
- else {
68
- // @ts-expect-error type does not exist on never
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
- for (const r of result.removals) {
90
- removals.set(r.cid.toString(), r);
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 = { type: 'put', root: result.root, key, value };
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 !== 'put')
169
- throw new Error(`unsupported batch operation: ${op.type}`);
170
- await batch.put(op.key, op.value);
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
- // @ts-expect-error type does not exist on never
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(([, es]) => es.sort((a, b) => String(a.cid) < String(b.cid) ? -1 : 1));
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
  };
@@ -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();