@web3-storage/pail 0.4.0

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.
Files changed (55) hide show
  1. package/LICENSE.md +232 -0
  2. package/README.md +84 -0
  3. package/cli.js +259 -0
  4. package/dist/src/api.d.ts +33 -0
  5. package/dist/src/api.d.ts.map +1 -0
  6. package/dist/src/batch/api.d.ts +33 -0
  7. package/dist/src/batch/api.d.ts.map +1 -0
  8. package/dist/src/batch/index.d.ts +74 -0
  9. package/dist/src/batch/index.d.ts.map +1 -0
  10. package/dist/src/batch/shard.d.ts +3 -0
  11. package/dist/src/batch/shard.d.ts.map +1 -0
  12. package/dist/src/block.d.ts +35 -0
  13. package/dist/src/block.d.ts.map +1 -0
  14. package/dist/src/clock/api.d.ts +10 -0
  15. package/dist/src/clock/api.d.ts.map +1 -0
  16. package/dist/src/clock/index.d.ts +48 -0
  17. package/dist/src/clock/index.d.ts.map +1 -0
  18. package/dist/src/crdt/api.d.ts +26 -0
  19. package/dist/src/crdt/api.d.ts.map +1 -0
  20. package/dist/src/crdt/batch/api.d.ts +11 -0
  21. package/dist/src/crdt/batch/api.d.ts.map +1 -0
  22. package/dist/src/crdt/batch/index.d.ts +5 -0
  23. package/dist/src/crdt/batch/index.d.ts.map +1 -0
  24. package/dist/src/crdt/index.d.ts +11 -0
  25. package/dist/src/crdt/index.d.ts.map +1 -0
  26. package/dist/src/diff.d.ts +13 -0
  27. package/dist/src/diff.d.ts.map +1 -0
  28. package/dist/src/index.d.ts +12 -0
  29. package/dist/src/index.d.ts.map +1 -0
  30. package/dist/src/merge.d.ts +5 -0
  31. package/dist/src/merge.d.ts.map +1 -0
  32. package/dist/src/shard.d.ts +51 -0
  33. package/dist/src/shard.d.ts.map +1 -0
  34. package/dist/tsconfig.tsbuildinfo +1 -0
  35. package/package.json +174 -0
  36. package/src/api.js +1 -0
  37. package/src/api.ts +47 -0
  38. package/src/batch/api.js +1 -0
  39. package/src/batch/api.ts +61 -0
  40. package/src/batch/index.js +245 -0
  41. package/src/batch/shard.js +14 -0
  42. package/src/block.js +75 -0
  43. package/src/clock/api.js +1 -0
  44. package/src/clock/api.ts +12 -0
  45. package/src/clock/index.js +179 -0
  46. package/src/crdt/api.js +1 -0
  47. package/src/crdt/api.ts +33 -0
  48. package/src/crdt/batch/api.js +1 -0
  49. package/src/crdt/batch/api.ts +31 -0
  50. package/src/crdt/batch/index.js +156 -0
  51. package/src/crdt/index.js +355 -0
  52. package/src/diff.js +151 -0
  53. package/src/index.js +285 -0
  54. package/src/merge.js +43 -0
  55. package/src/shard.js +248 -0
package/src/api.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { Link, UnknownLink, BlockView, Block, Version } from 'multiformats'
2
+ import { sha256 } from 'multiformats/hashes/sha2'
3
+ import * as dagCBOR from '@ipld/dag-cbor'
4
+
5
+ export { Link, UnknownLink, BlockView, Block, Version }
6
+
7
+ export type ShardEntryValueValue = UnknownLink
8
+
9
+ export type ShardEntryLinkValue = [ShardLink]
10
+
11
+ export type ShardEntryLinkAndValueValue = [ShardLink, UnknownLink]
12
+
13
+ export type ShardValueEntry = [key: string, value: ShardEntryValueValue]
14
+
15
+ export type ShardLinkEntry = [key: string, value: ShardEntryLinkValue | ShardEntryLinkAndValueValue]
16
+
17
+ /** Single key/value entry within a shard. */
18
+ export type ShardEntry = [key: string, value: ShardEntryValueValue | ShardEntryLinkValue | ShardEntryLinkAndValueValue]
19
+
20
+ export interface Shard extends ShardConfig {
21
+ entries: ShardEntry[]
22
+ }
23
+
24
+ export type ShardLink = Link<Shard, typeof dagCBOR.code, typeof sha256.code, 1>
25
+
26
+ export interface ShardBlockView extends BlockView<Shard, typeof dagCBOR.code, typeof sha256.code, 1> {
27
+ prefix: string
28
+ }
29
+
30
+ export interface ShardDiff {
31
+ additions: ShardBlockView[]
32
+ removals: ShardBlockView[]
33
+ }
34
+
35
+ export interface BlockFetcher {
36
+ get<T = unknown, C extends number = number, A extends number = number, V extends Version = 1> (link: Link<T, C, A, V>):
37
+ Promise<Block<T, C, A, V> | undefined>
38
+ }
39
+
40
+ export interface ShardConfig {
41
+ /** Max encoded shard size in bytes - default 512 KiB. */
42
+ maxSize: number
43
+ /** Max key length (in UTF-8 encoded characters) - default 64. */
44
+ maxKeyLength: number
45
+ }
46
+
47
+ export type ShardOptions = Partial<ShardConfig>
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,61 @@
1
+ import {
2
+ UnknownLink,
3
+ ShardLink,
4
+ ShardDiff,
5
+ ShardEntry,
6
+ ShardEntryValueValue,
7
+ ShardEntryLinkValue,
8
+ ShardEntryLinkAndValueValue,
9
+ ShardConfig,
10
+ ShardOptions,
11
+ ShardBlockView,
12
+ BlockFetcher
13
+ } from '../api.js'
14
+
15
+ export {
16
+ UnknownLink,
17
+ ShardLink,
18
+ ShardDiff,
19
+ ShardEntry,
20
+ ShardEntryValueValue,
21
+ ShardEntryLinkValue,
22
+ ShardEntryLinkAndValueValue,
23
+ ShardConfig,
24
+ ShardOptions,
25
+ ShardBlockView,
26
+ BlockFetcher
27
+ }
28
+
29
+ export interface BatcherShard extends ShardConfig {
30
+ base?: ShardBlockView
31
+ prefix: string
32
+ entries: BatcherShardEntry[]
33
+ }
34
+
35
+ export interface BatcherShardInit extends ShardOptions {
36
+ base?: ShardBlockView
37
+ prefix?: string
38
+ entries?: BatcherShardEntry[]
39
+ }
40
+
41
+ export type BatcherShardEntry = [
42
+ key: string,
43
+ value: ShardEntryValueValue | ShardEntryLinkValue | ShardEntryLinkAndValueValue | ShardEntryShardValue | ShardEntryShardAndValueValue
44
+ ]
45
+
46
+ export type ShardEntryShardValue = [BatcherShard]
47
+
48
+ export type ShardEntryShardAndValueValue = [BatcherShard, UnknownLink]
49
+
50
+ export interface Batcher {
51
+ /**
52
+ * Put a value (a CID) for the given key. If the key exists it's value is
53
+ * overwritten.
54
+ */
55
+ put (key: string, value: UnknownLink): Promise<void>
56
+ /**
57
+ * Encode all altered shards in the batch and return the new root CID and
58
+ * difference blocks.
59
+ */
60
+ commit (): Promise<{ root: ShardLink } & ShardDiff>
61
+ }
@@ -0,0 +1,245 @@
1
+ // eslint-disable-next-line no-unused-vars
2
+ import * as API from './api.js'
3
+ import { ShardFetcher } from '../shard.js'
4
+ import * as Shard from '../shard.js'
5
+ import * as BatcherShard from './shard.js'
6
+
7
+ /** @implements {API.Batcher} */
8
+ class Batcher {
9
+ #committed = false
10
+
11
+ /**
12
+ * @param {object} init
13
+ * @param {API.BlockFetcher} init.blocks Block storage.
14
+ * @param {API.BatcherShardEntry[]} init.entries The entries in this shard.
15
+ * @param {string} init.prefix Key prefix.
16
+ * @param {number} init.maxSize
17
+ * @param {number} init.maxKeyLength
18
+ * @param {API.ShardBlockView} init.base Original shard this batcher is based on.
19
+ */
20
+ constructor ({ blocks, entries, prefix, maxSize, maxKeyLength, base }) {
21
+ this.blocks = blocks
22
+ this.prefix = prefix
23
+ this.entries = entries
24
+ this.base = base
25
+ this.maxSize = maxSize
26
+ this.maxKeyLength = maxKeyLength
27
+ }
28
+
29
+ /**
30
+ * @param {string} key The key of the value to put.
31
+ * @param {API.UnknownLink} value The value to put.
32
+ * @returns {Promise<void>}
33
+ */
34
+ async put (key, value) {
35
+ if (this.#committed) throw new BatchCommittedError()
36
+ return put(this.blocks, this, key, value)
37
+ }
38
+
39
+ async commit () {
40
+ if (this.#committed) throw new BatchCommittedError()
41
+ this.#committed = true
42
+ return commit(this)
43
+ }
44
+
45
+ /**
46
+ * @param {object} init
47
+ * @param {API.BlockFetcher} init.blocks Block storage.
48
+ * @param {API.ShardLink} init.link CID of the shard block.
49
+ * @param {string} init.prefix
50
+ */
51
+ static async create ({ blocks, link, prefix }) {
52
+ const shards = new ShardFetcher(blocks)
53
+ const base = await shards.get(link)
54
+ return new Batcher({ blocks, entries: base.value.entries, prefix, base, ...Shard.configure(base.value) })
55
+ }
56
+ }
57
+
58
+ /**
59
+ * @param {API.BlockFetcher} blocks
60
+ * @param {API.BatcherShard} shard
61
+ * @param {string} key The key of the value to put.
62
+ * @param {API.UnknownLink} value The value to put.
63
+ * @returns {Promise<void>}
64
+ */
65
+ export const put = async (blocks, shard, key, value) => {
66
+ const shards = new ShardFetcher(blocks)
67
+ const dest = await traverse(shards, key, shard)
68
+ if (dest.shard !== shard) {
69
+ shard = dest.shard
70
+ key = dest.key
71
+ }
72
+
73
+ /** @type {API.BatcherShardEntry} */
74
+ let entry = [key, value]
75
+ /** @type {API.BatcherShard|undefined} */
76
+ let batcher
77
+
78
+ // if the key in this shard is longer than allowed, then we need to make some
79
+ // intermediate shards.
80
+ if (key.length > shard.maxKeyLength) {
81
+ const pfxskeys = Array.from(Array(Math.ceil(key.length / shard.maxKeyLength)), (_, i) => {
82
+ const start = i * shard.maxKeyLength
83
+ return {
84
+ prefix: shard.prefix + key.slice(0, start),
85
+ key: key.slice(start, start + shard.maxKeyLength)
86
+ }
87
+ })
88
+
89
+ entry = [pfxskeys[pfxskeys.length - 1].key, value]
90
+ batcher = BatcherShard.create({
91
+ entries: [entry],
92
+ prefix: pfxskeys[pfxskeys.length - 1].prefix,
93
+ ...Shard.configure(shard)
94
+ })
95
+
96
+ for (let i = pfxskeys.length - 2; i > 0; i--) {
97
+ entry = [pfxskeys[i].key, [batcher]]
98
+ batcher = BatcherShard.create({
99
+ entries: [entry],
100
+ prefix: pfxskeys[i].prefix,
101
+ ...Shard.configure(shard)
102
+ })
103
+ }
104
+
105
+ entry = [pfxskeys[0].key, [batcher]]
106
+ }
107
+
108
+ shard.entries = Shard.putEntry(asShardEntries(shard.entries), asShardEntry(entry))
109
+
110
+ // TODO: adjust size automatically
111
+ const size = Shard.encodedLength(Shard.withEntries(asShardEntries(shard.entries), shard))
112
+ if (size > shard.maxSize) {
113
+ const common = Shard.findCommonPrefix(
114
+ asShardEntries(shard.entries),
115
+ entry[0]
116
+ )
117
+ if (!common) throw new Error('shard limit reached')
118
+ const { prefix } = common
119
+ /** @type {API.BatcherShardEntry[]} */
120
+ const matches = common.matches
121
+
122
+ const entries = matches
123
+ .filter(m => m[0] !== prefix)
124
+ .map(m => {
125
+ m = [...m]
126
+ m[0] = m[0].slice(prefix.length)
127
+ return m
128
+ })
129
+
130
+ const batcher = BatcherShard.create({
131
+ entries,
132
+ prefix: shard.prefix + prefix,
133
+ ...Shard.configure(shard)
134
+ })
135
+
136
+ /** @type {API.ShardEntryShardValue | API.ShardEntryShardAndValueValue} */
137
+ let value
138
+ const pfxmatch = matches.find(m => m[0] === prefix)
139
+ if (pfxmatch) {
140
+ if (Array.isArray(pfxmatch[1])) {
141
+ // should not happen! all entries with this prefix should have been
142
+ // placed within this shard already.
143
+ throw new Error(`expected "${prefix}" to be a shard value but found a shard link`)
144
+ }
145
+ value = [batcher, pfxmatch[1]]
146
+ } else {
147
+ value = [batcher]
148
+ }
149
+
150
+ shard.entries = Shard.putEntry(
151
+ asShardEntries(shard.entries.filter(e => matches.every(m => e[0] !== m[0]))),
152
+ asShardEntry([prefix, value])
153
+ )
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Traverse from the passed shard through to the correct shard for the passed
159
+ * key.
160
+ *
161
+ * @param {ShardFetcher} shards
162
+ * @param {string} key
163
+ * @param {API.BatcherShard} shard
164
+ * @returns {Promise<{ shard: API.BatcherShard, key: string }>}
165
+ */
166
+ export const traverse = async (shards, key, shard) => {
167
+ for (const e of shard.entries) {
168
+ const [k, v] = e
169
+ if (key <= k) break
170
+ if (key.startsWith(k) && Array.isArray(v)) {
171
+ if (Shard.isShardLink(v[0])) {
172
+ const blk = await shards.get(v[0], shard.prefix + k)
173
+ v[0] = BatcherShard.create({ base: blk, prefix: blk.prefix, ...blk.value })
174
+ }
175
+ return traverse(shards, key.slice(k.length), v[0])
176
+ }
177
+ }
178
+ return { shard, key }
179
+ }
180
+
181
+ /**
182
+ * Encode all altered shards in the batch and return the new root CID and
183
+ * difference blocks.
184
+ *
185
+ * @param {API.BatcherShard} shard
186
+ */
187
+ export const commit = async shard => {
188
+ /** @type {API.ShardBlockView[]} */
189
+ const additions = []
190
+ /** @type {API.ShardBlockView[]} */
191
+ const removals = []
192
+
193
+ /** @type {API.ShardEntry[]} */
194
+ const entries = []
195
+ for (const entry of shard.entries) {
196
+ if (Array.isArray(entry[1]) && !Shard.isShardLink(entry[1][0])) {
197
+ const result = await commit(entry[1][0])
198
+ entries.push([
199
+ entry[0],
200
+ entry[1][1] == null ? [result.root] : [result.root, entry[1][1]]
201
+ ])
202
+ additions.push(...result.additions)
203
+ removals.push(...result.removals)
204
+ } else {
205
+ entries.push(asShardEntry(entry))
206
+ }
207
+ }
208
+
209
+ const block = await Shard.encodeBlock(Shard.withEntries(entries, shard), shard.prefix)
210
+ additions.push(block)
211
+
212
+ if (shard.base && shard.base.cid.toString() === block.cid.toString()) {
213
+ return { root: block.cid, additions: [], removals: [] }
214
+ }
215
+
216
+ if (shard.base) removals.push(shard.base)
217
+
218
+ return { root: block.cid, additions, removals }
219
+ }
220
+
221
+ /** @param {API.BatcherShardEntry[]} entries */
222
+ const asShardEntries = entries => /** @type {API.ShardEntry[]} */ (entries)
223
+
224
+ /** @param {API.BatcherShardEntry} entry */
225
+ const asShardEntry = entry => /** @type {API.ShardEntry} */ (entry)
226
+
227
+ /**
228
+ * @param {API.BlockFetcher} blocks Bucket block storage.
229
+ * @param {API.ShardLink} root CID of the root shard block.
230
+ * @returns {Promise<API.Batcher>}
231
+ */
232
+ export const create = (blocks, root) => Batcher.create({ blocks, link: root, prefix: '' })
233
+
234
+ export class BatchCommittedError extends Error {
235
+ /**
236
+ * @param {string} [message]
237
+ * @param {ErrorOptions} [options]
238
+ */
239
+ constructor (message, options) {
240
+ super(message ?? 'batch already committed', options)
241
+ this.code = BatchCommittedError.code
242
+ }
243
+
244
+ static code = 'ERR_BATCH_COMMITTED'
245
+ }
@@ -0,0 +1,14 @@
1
+ // eslint-disable-next-line no-unused-vars
2
+ import * as API from './api.js'
3
+ import { configure } from '../shard.js'
4
+
5
+ /**
6
+ * @param {API.BatcherShardInit} [init]
7
+ * @returns {API.BatcherShard}
8
+ */
9
+ export const create = init => ({
10
+ base: init?.base,
11
+ prefix: init?.prefix ?? '',
12
+ entries: init?.entries ?? [],
13
+ ...configure(init)
14
+ })
package/src/block.js ADDED
@@ -0,0 +1,75 @@
1
+ // eslint-disable-next-line no-unused-vars
2
+ import * as API from './api.js'
3
+ import { parse } from 'multiformats/link'
4
+
5
+ /** @implements {API.BlockFetcher} */
6
+ export class MemoryBlockstore {
7
+ /** @type {Map<string, Uint8Array>} */
8
+ #blocks = new Map()
9
+
10
+ /**
11
+ * @param {Array<import('multiformats').Block>} [blocks]
12
+ */
13
+ constructor (blocks) {
14
+ if (blocks) {
15
+ this.#blocks = new Map(blocks.map(b => [b.cid.toString(), b.bytes]))
16
+ }
17
+ }
18
+
19
+ /** @type {API.BlockFetcher['get']} */
20
+ async get (cid) {
21
+ const bytes = this.#blocks.get(cid.toString())
22
+ if (!bytes) return
23
+ return { cid, bytes }
24
+ }
25
+
26
+ /**
27
+ * @param {API.UnknownLink} cid
28
+ * @param {Uint8Array} bytes
29
+ */
30
+ async put (cid, bytes) {
31
+ this.#blocks.set(cid.toString(), bytes)
32
+ }
33
+
34
+ /**
35
+ * @param {API.UnknownLink} cid
36
+ * @param {Uint8Array} bytes
37
+ */
38
+ putSync (cid, bytes) {
39
+ this.#blocks.set(cid.toString(), bytes)
40
+ }
41
+
42
+ /** @param {API.UnknownLink} cid */
43
+ async delete (cid) {
44
+ this.#blocks.delete(cid.toString())
45
+ }
46
+
47
+ /** @param {API.UnknownLink} cid */
48
+ deleteSync (cid) {
49
+ this.#blocks.delete(cid.toString())
50
+ }
51
+
52
+ * entries () {
53
+ for (const [str, bytes] of this.#blocks) {
54
+ yield { cid: parse(str), bytes }
55
+ }
56
+ }
57
+ }
58
+
59
+ export class MultiBlockFetcher {
60
+ /** @type {API.BlockFetcher[]} */
61
+ #fetchers
62
+
63
+ /** @param {API.BlockFetcher[]} fetchers */
64
+ constructor (...fetchers) {
65
+ this.#fetchers = fetchers
66
+ }
67
+
68
+ /** @type {API.BlockFetcher['get']} */
69
+ async get (link) {
70
+ for (const f of this.#fetchers) {
71
+ const v = await f.get(link)
72
+ if (v) return v
73
+ }
74
+ }
75
+ }
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,12 @@
1
+ import { Link, BlockView } from 'multiformats'
2
+
3
+ export { BlockFetcher } from '../api.js'
4
+
5
+ export type EventLink<T> = Link<EventView<T>>
6
+
7
+ export interface EventView<T> {
8
+ parents: EventLink<T>[]
9
+ data: T
10
+ }
11
+
12
+ export interface EventBlockView<T> extends BlockView<EventView<T>> {}
@@ -0,0 +1,179 @@
1
+ import { Block, encode, decode } from 'multiformats/block'
2
+ import { sha256 } from 'multiformats/hashes/sha2'
3
+ import * as cbor from '@ipld/dag-cbor'
4
+ // eslint-disable-next-line no-unused-vars
5
+ import * as API from './api.js'
6
+
7
+ /**
8
+ * Advance the clock by adding an event.
9
+ *
10
+ * @template T
11
+ * @param {API.BlockFetcher} blocks Block storage.
12
+ * @param {API.EventLink<T>[]} head The head of the clock.
13
+ * @param {API.EventLink<T>} event The event to add.
14
+ */
15
+ export const advance = async (blocks, head, event) => {
16
+ const events = new EventFetcher(blocks)
17
+ const headmap = new Map(head.map(cid => [cid.toString(), cid]))
18
+ if (headmap.has(event.toString())) return head
19
+
20
+ // does event contain the clock?
21
+ let changed = false
22
+ for (const cid of head) {
23
+ if (await contains(events, event, cid)) {
24
+ headmap.delete(cid.toString())
25
+ headmap.set(event.toString(), event)
26
+ changed = true
27
+ }
28
+ }
29
+ if (changed) {
30
+ return [...headmap.values()]
31
+ }
32
+
33
+ // does clock contain the event?
34
+ for (const p of head) {
35
+ if (await contains(events, p, event)) {
36
+ return head
37
+ }
38
+ }
39
+
40
+ return head.concat(event)
41
+ }
42
+
43
+ /**
44
+ * @template T
45
+ * @extends {Block<API.EventView<T>, typeof cbor.code, typeof sha256.code, 1>}
46
+ * @implements {API.EventBlockView<T>}
47
+ */
48
+ export class EventBlock extends Block {
49
+ /**
50
+ * @param {object} config
51
+ * @param {API.EventLink<T>} config.cid
52
+ * @param {Event} config.value
53
+ * @param {Uint8Array} config.bytes
54
+ * @param {string} config.prefix
55
+ */
56
+ constructor ({ cid, value, bytes, prefix }) {
57
+ // @ts-expect-error
58
+ super({ cid, value, bytes })
59
+ this.prefix = prefix
60
+ }
61
+
62
+ /**
63
+ * @template T
64
+ * @param {T} data
65
+ * @param {API.EventLink<T>[]} [parents]
66
+ */
67
+ static create (data, parents) {
68
+ return encodeEventBlock({ data, parents: parents ?? [] })
69
+ }
70
+ }
71
+
72
+ /** @template T */
73
+ export class EventFetcher {
74
+ /** @param {API.BlockFetcher} blocks */
75
+ constructor (blocks) {
76
+ /** @private */
77
+ this._blocks = blocks
78
+ }
79
+
80
+ /**
81
+ * @param {API.EventLink<T>} link
82
+ * @returns {Promise<API.EventBlockView<T>>}
83
+ */
84
+ async get (link) {
85
+ const block = await this._blocks.get(link)
86
+ if (!block) throw new Error(`missing block: ${link}`)
87
+ return decodeEventBlock(block.bytes)
88
+ }
89
+ }
90
+
91
+ /**
92
+ * @template T
93
+ * @param {API.EventView<T>} value
94
+ * @returns {Promise<API.EventBlockView<T>>}
95
+ */
96
+ export const encodeEventBlock = async (value) => {
97
+ // TODO: sort parents
98
+ const { cid, bytes } = await encode({ value, codec: cbor, hasher: sha256 })
99
+ // @ts-expect-error
100
+ return new Block({ cid, value, bytes })
101
+ }
102
+
103
+ /**
104
+ * @template T
105
+ * @param {Uint8Array} bytes
106
+ * @returns {Promise<API.EventBlockView<T>>}
107
+ */
108
+ export const decodeEventBlock = async (bytes) => {
109
+ const { cid, value } = await decode({ bytes, codec: cbor, hasher: sha256 })
110
+ // @ts-expect-error
111
+ return new Block({ cid, value, bytes })
112
+ }
113
+
114
+ /**
115
+ * Returns true if event "a" contains event "b". Breadth first search.
116
+ * @template T
117
+ * @param {EventFetcher<T>} events
118
+ * @param {API.EventLink<T>} a
119
+ * @param {API.EventLink<T>} b
120
+ */
121
+ const contains = async (events, a, b) => {
122
+ if (a.toString() === b.toString()) return true
123
+ const [{ value: aevent }, { value: bevent }] = await Promise.all([events.get(a), events.get(b)])
124
+ const links = [...aevent.parents]
125
+ while (links.length) {
126
+ const link = links.shift()
127
+ if (!link) break
128
+ if (link.toString() === b.toString()) return true
129
+ // if any of b's parents are this link, then b cannot exist in any of the
130
+ // tree below, since that would create a cycle.
131
+ if (bevent.parents.some(p => link.toString() === p.toString())) continue
132
+ const { value: event } = await events.get(link)
133
+ links.push(...event.parents)
134
+ }
135
+ return false
136
+ }
137
+
138
+ /**
139
+ * @template T
140
+ * @param {API.BlockFetcher} blocks Block storage.
141
+ * @param {API.EventLink<T>[]} head
142
+ * @param {object} [options]
143
+ * @param {(b: API.EventBlockView<T>) => string} [options.renderNodeLabel]
144
+ */
145
+ export const vis = async function * (blocks, head, options = {}) {
146
+ const renderNodeLabel = options.renderNodeLabel ?? (b => shortLink(b.cid))
147
+ const events = new EventFetcher(blocks)
148
+ yield 'digraph clock {'
149
+ yield ' node [shape=point fontname="Courier"]; head;'
150
+ const hevents = await Promise.all(head.map(link => events.get(link)))
151
+ /** @type {import('multiformats').Link<API.EventView<any>>[]} */
152
+ const links = []
153
+ const nodes = new Set()
154
+ for (const e of hevents) {
155
+ nodes.add(e.cid.toString())
156
+ yield ` node [shape=oval fontname="Courier"]; ${e.cid} [label="${renderNodeLabel(e)}"];`
157
+ yield ` head -> ${e.cid};`
158
+ for (const p of e.value.parents) {
159
+ yield ` ${e.cid} -> ${p};`
160
+ }
161
+ links.push(...e.value.parents)
162
+ }
163
+ while (links.length) {
164
+ const link = links.shift()
165
+ if (!link) break
166
+ if (nodes.has(link.toString())) continue
167
+ nodes.add(link.toString())
168
+ const block = await events.get(link)
169
+ yield ` node [shape=oval]; ${link} [label="${renderNodeLabel(block)}" fontname="Courier"];`
170
+ for (const p of block.value.parents) {
171
+ yield ` ${link} -> ${p};`
172
+ }
173
+ links.push(...block.value.parents)
174
+ }
175
+ yield '}'
176
+ }
177
+
178
+ /** @param {import('multiformats').UnknownLink} l */
179
+ const shortLink = l => `${String(l).slice(0, 4)}..${String(l).slice(-4)}`
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,33 @@
1
+ import { ShardDiff, ShardLink, UnknownLink } from '../api.js'
2
+ import { EventLink, EventBlockView } from '../clock/api.js'
3
+
4
+ export { BlockFetcher, UnknownLink, ShardBlockView, ShardDiff, ShardLink } from '../api.js'
5
+ export { EventBlockView, EventLink } from '../clock/api.js'
6
+
7
+ export interface Result extends ShardDiff {
8
+ root: ShardLink
9
+ head: EventLink<Operation>[]
10
+ event?: EventBlockView<Operation>
11
+ }
12
+
13
+ export type Operation = (
14
+ | PutOperation
15
+ | DeleteOperation
16
+ | BatchOperation
17
+ ) & { root: ShardLink }
18
+
19
+ export interface PutOperation {
20
+ type: 'put',
21
+ key: string
22
+ value: UnknownLink
23
+ }
24
+
25
+ export interface DeleteOperation {
26
+ type: 'del',
27
+ key: string
28
+ }
29
+
30
+ export interface BatchOperation {
31
+ type: 'batch',
32
+ ops: Array<PutOperation|DeleteOperation>
33
+ }
@@ -0,0 +1 @@
1
+ export {}