@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.
- package/LICENSE.md +232 -0
- package/README.md +84 -0
- package/cli.js +259 -0
- package/dist/src/api.d.ts +33 -0
- package/dist/src/api.d.ts.map +1 -0
- package/dist/src/batch/api.d.ts +33 -0
- package/dist/src/batch/api.d.ts.map +1 -0
- package/dist/src/batch/index.d.ts +74 -0
- package/dist/src/batch/index.d.ts.map +1 -0
- package/dist/src/batch/shard.d.ts +3 -0
- package/dist/src/batch/shard.d.ts.map +1 -0
- package/dist/src/block.d.ts +35 -0
- package/dist/src/block.d.ts.map +1 -0
- package/dist/src/clock/api.d.ts +10 -0
- package/dist/src/clock/api.d.ts.map +1 -0
- package/dist/src/clock/index.d.ts +48 -0
- package/dist/src/clock/index.d.ts.map +1 -0
- package/dist/src/crdt/api.d.ts +26 -0
- package/dist/src/crdt/api.d.ts.map +1 -0
- package/dist/src/crdt/batch/api.d.ts +11 -0
- package/dist/src/crdt/batch/api.d.ts.map +1 -0
- package/dist/src/crdt/batch/index.d.ts +5 -0
- package/dist/src/crdt/batch/index.d.ts.map +1 -0
- package/dist/src/crdt/index.d.ts +11 -0
- package/dist/src/crdt/index.d.ts.map +1 -0
- package/dist/src/diff.d.ts +13 -0
- package/dist/src/diff.d.ts.map +1 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/merge.d.ts +5 -0
- package/dist/src/merge.d.ts.map +1 -0
- package/dist/src/shard.d.ts +51 -0
- package/dist/src/shard.d.ts.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +174 -0
- package/src/api.js +1 -0
- package/src/api.ts +47 -0
- package/src/batch/api.js +1 -0
- package/src/batch/api.ts +61 -0
- package/src/batch/index.js +245 -0
- package/src/batch/shard.js +14 -0
- package/src/block.js +75 -0
- package/src/clock/api.js +1 -0
- package/src/clock/api.ts +12 -0
- package/src/clock/index.js +179 -0
- package/src/crdt/api.js +1 -0
- package/src/crdt/api.ts +33 -0
- package/src/crdt/batch/api.js +1 -0
- package/src/crdt/batch/api.ts +31 -0
- package/src/crdt/batch/index.js +156 -0
- package/src/crdt/index.js +355 -0
- package/src/diff.js +151 -0
- package/src/index.js +285 -0
- package/src/merge.js +43 -0
- 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>
|
package/src/batch/api.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/src/batch/api.ts
ADDED
|
@@ -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
|
+
}
|
package/src/clock/api.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/src/clock/api.ts
ADDED
|
@@ -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)}`
|
package/src/crdt/api.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/src/crdt/api.ts
ADDED
|
@@ -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 {}
|