autobee 0.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +255 -0
- package/index.js +694 -0
- package/lib/apply-calls.js +65 -0
- package/lib/asserts.js +25 -0
- package/lib/boot.js +117 -0
- package/lib/constants.js +9 -0
- package/lib/encoding.js +150 -0
- package/lib/system.js +327 -0
- package/lib/topo.js +271 -0
- package/lib/triggers.js +69 -0
- package/lib/updates.js +31 -0
- package/lib/writers.js +501 -0
- package/package.json +39 -6
package/lib/topo.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
const b4a = require('b4a')
|
|
2
|
+
const encoding = require('./encoding.js')
|
|
3
|
+
const { createNode } = require('./writers.js')
|
|
4
|
+
const asserts = require('./asserts.js')
|
|
5
|
+
|
|
6
|
+
const EMPTY_HEAD = { length: 0, key: null }
|
|
7
|
+
|
|
8
|
+
exports.replay = replay
|
|
9
|
+
exports.sort = sort
|
|
10
|
+
|
|
11
|
+
exports.isLinking = isLinking
|
|
12
|
+
exports.isLinkingAll = isLinkingAll
|
|
13
|
+
|
|
14
|
+
class Clock {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.map = new Map()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
add(link) {
|
|
20
|
+
const hex = b4a.toString(link.key, 'hex')
|
|
21
|
+
const p = this.map.get(hex)
|
|
22
|
+
if (p < link.length) this.map.set(hex, link.length)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
has(link) {
|
|
26
|
+
const hex = b4a.toString(link.key, 'hex')
|
|
27
|
+
const p = this.map.get(hex)
|
|
28
|
+
return p >= link.length
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function replay(auto) {
|
|
33
|
+
const ch = auto.system.bee.createChangesStream()
|
|
34
|
+
const replay = []
|
|
35
|
+
|
|
36
|
+
for await (const data of ch) {
|
|
37
|
+
const { ref, info } = getOplog(data)
|
|
38
|
+
replay.push(ref)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const inflated = await inflate(auto, replay.reverse())
|
|
42
|
+
return inflated.flatMap((b) => b.batch)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function sort(auto, batch) {
|
|
46
|
+
const node = batch[0]
|
|
47
|
+
const ch = auto.system.bee.createChangesStream()
|
|
48
|
+
const reorder = []
|
|
49
|
+
|
|
50
|
+
for await (const data of ch) {
|
|
51
|
+
const { ref, info } = getOplog(data)
|
|
52
|
+
const link = {
|
|
53
|
+
key: ref.oplog.key,
|
|
54
|
+
length: ref.oplog.length,
|
|
55
|
+
weight: ref.oplog.weight,
|
|
56
|
+
timestamp: info.timestamp
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!isOrderedBefore(node, link)) break
|
|
60
|
+
reorder.push(ref)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const inflated = await inflate(auto, reorder.reverse())
|
|
64
|
+
|
|
65
|
+
const { undo, shared } = addSorted(inflated, batch)
|
|
66
|
+
|
|
67
|
+
const tip = inflated.slice(shared).map((b) => b.batch)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
undo,
|
|
71
|
+
view: null,
|
|
72
|
+
tip
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isOrderedBefore(node, oplog) {
|
|
77
|
+
const { version, weight, timestamp } = oplog
|
|
78
|
+
if (version === 0) return false
|
|
79
|
+
if (node.weight < weight) return false
|
|
80
|
+
if (isLinking(node, oplog)) return false
|
|
81
|
+
return node.timestamp <= timestamp
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function addSorted(list, batch) {
|
|
85
|
+
const node = batch[0]
|
|
86
|
+
const clock = new Clock()
|
|
87
|
+
for (const link of node.links) {
|
|
88
|
+
clock.add(link)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const tip = []
|
|
92
|
+
const linked = []
|
|
93
|
+
|
|
94
|
+
let undo = null
|
|
95
|
+
let shared = list.length
|
|
96
|
+
|
|
97
|
+
while (list.length) {
|
|
98
|
+
// reached a stable sorting point
|
|
99
|
+
if (cmp(node, list[list.length - 1].batch[0]) > 0) {
|
|
100
|
+
break
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const b = list.pop()
|
|
104
|
+
const target = b.batch[0]
|
|
105
|
+
|
|
106
|
+
if (list.length < shared) {
|
|
107
|
+
undo = b.undo
|
|
108
|
+
shared = list.length
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// move past this node
|
|
112
|
+
if (!clock.has(target)) {
|
|
113
|
+
tip.push(b)
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// remove node
|
|
118
|
+
linked.push(b)
|
|
119
|
+
|
|
120
|
+
// update the clock to catch linked nodes
|
|
121
|
+
for (const link of target.links) {
|
|
122
|
+
clock.add(link)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
while (linked.length) addSorted(list, linked.pop())
|
|
127
|
+
list.push({ undo: null, batch })
|
|
128
|
+
while (tip.length) list.push(tip.pop())
|
|
129
|
+
|
|
130
|
+
return { undo, shared }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function cmp(a, b) {
|
|
134
|
+
const w = a.weight - b.weight
|
|
135
|
+
if (w) return w > 0 ? -1 : 1
|
|
136
|
+
|
|
137
|
+
const t = a.timestamp - b.timestamp
|
|
138
|
+
if (t) return t < 0 ? -1 : 1
|
|
139
|
+
|
|
140
|
+
const c = b4a.compare(a.key, b.key)
|
|
141
|
+
if (c) return c
|
|
142
|
+
|
|
143
|
+
return a.length < b.length ? -1 : 1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function inflate(auto, reorder) {
|
|
147
|
+
const promises = []
|
|
148
|
+
const sessions = new Map()
|
|
149
|
+
|
|
150
|
+
for (const ref of reorder) {
|
|
151
|
+
const id = b4a.toString(ref.oplog.key, 'hex')
|
|
152
|
+
|
|
153
|
+
const local = auto.writers.localWriter
|
|
154
|
+
if (local && local.id === id) {
|
|
155
|
+
promises.push(getLocalBatch(ref.undo, local, ref.oplog.length, ref.oplog.weight))
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let core = sessions.get(id)
|
|
160
|
+
if (!core) {
|
|
161
|
+
core = auto.openCore(ref.oplog.key)
|
|
162
|
+
sessions.set(id, core)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
promises.push(getOplogBatch(ref.undo, core, ref.oplog.length, ref.oplog.weight))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
return await Promise.all(promises)
|
|
170
|
+
} finally {
|
|
171
|
+
for (const core of sessions.values()) await core.close()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function getOplogBatch(undo, core, length, weight) {
|
|
176
|
+
const seq = length - 1
|
|
177
|
+
const block = await core.get(seq)
|
|
178
|
+
const oplog = encoding.decodeOplog(block)
|
|
179
|
+
const head = createNode(core, length, weight, oplog)
|
|
180
|
+
|
|
181
|
+
const batch = []
|
|
182
|
+
const result = { undo, batch }
|
|
183
|
+
|
|
184
|
+
const remaining = []
|
|
185
|
+
const start = seq - head.batch.start
|
|
186
|
+
const end = seq + head.batch.end // skip head, we have it
|
|
187
|
+
|
|
188
|
+
for (let i = start; i < end; i++) {
|
|
189
|
+
remaining.push(core.get(i))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (remaining.length) {
|
|
193
|
+
const blocks = await Promise.all(remaining)
|
|
194
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
195
|
+
const oplog = encoding.decodeOplog(blocks[i])
|
|
196
|
+
batch.push(createNode(core, i + start + 1, weight, oplog))
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
batch.push(head)
|
|
201
|
+
return result
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function getLocalBatch(undo, writer, length, weight) {
|
|
205
|
+
if (length > writer.core.length) {
|
|
206
|
+
if (!writer.pending) throw new Error('No local nodes')
|
|
207
|
+
|
|
208
|
+
let i = 0
|
|
209
|
+
while (i < writer.pending.length) {
|
|
210
|
+
if (writer.pending[i].length === length) break
|
|
211
|
+
i++
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const head = writer.pending[i]
|
|
215
|
+
|
|
216
|
+
const start = i - head.batch.start
|
|
217
|
+
const end = i + head.batch.end + 1 // include head
|
|
218
|
+
|
|
219
|
+
const batch = writer.pending.slice(start, end)
|
|
220
|
+
return { undo, batch }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return getOplogBatch(undo, writer.core, length, weight)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getOplog(data) {
|
|
227
|
+
const result = { ref: null, info: null }
|
|
228
|
+
for (const { keys } of data.batch) {
|
|
229
|
+
for (const k of keys) {
|
|
230
|
+
const prefix = k.key[0]
|
|
231
|
+
|
|
232
|
+
if (prefix === 0) {
|
|
233
|
+
result.info = encoding.decodeSystemInfo(k.value)
|
|
234
|
+
if (result.ref) return result
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (prefix === 1) {
|
|
238
|
+
// expect inlined for now
|
|
239
|
+
const value = encoding.decodeSystemWriter(k.key, k.value)
|
|
240
|
+
if (!value.isOplog) continue
|
|
241
|
+
|
|
242
|
+
result.ref = { undo: data.tail || EMPTY_HEAD, oplog: value }
|
|
243
|
+
if (result.info) return result
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
asserts.bail('Bad system node')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isLinking(node, link) {
|
|
252
|
+
if (node.length >= link.length && b4a.equals(node.key, link.key)) {
|
|
253
|
+
return true
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const l of node.links) {
|
|
257
|
+
if (l.length >= link.length && b4a.equals(l.key, link.key)) {
|
|
258
|
+
return true
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function isLinkingAll(node, heads) {
|
|
266
|
+
for (const h of heads) {
|
|
267
|
+
if (!isLinking(node, h)) return false
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return true
|
|
271
|
+
}
|
package/lib/triggers.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module.exports = class Triggers {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.waiting = new Map()
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
add(id, length, writer) {
|
|
7
|
+
let w = this.waiting.get(id)
|
|
8
|
+
|
|
9
|
+
if (!w) {
|
|
10
|
+
w = []
|
|
11
|
+
this.waiting.set(id, w)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < w.length; i++) {
|
|
15
|
+
const entry = w[i]
|
|
16
|
+
if (entry.writer !== writer) continue
|
|
17
|
+
entry.length = length
|
|
18
|
+
return entry
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const entry = { index: w.length, id, writer, length }
|
|
22
|
+
w.push(entry)
|
|
23
|
+
return entry
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
remove(entry) {
|
|
27
|
+
if (!entry.writer) return
|
|
28
|
+
|
|
29
|
+
const w = this.waiting.get(entry.id)
|
|
30
|
+
if (w) this._remove(w, entry)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
has(id) {
|
|
34
|
+
return this.waiting.get(id) !== undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get(id) {
|
|
38
|
+
return this.waiting.get(id) || null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
trigger(id, length) {
|
|
42
|
+
const w = this.waiting.get(id)
|
|
43
|
+
if (!w) return
|
|
44
|
+
|
|
45
|
+
for (let i = w.length - 1; i >= 0; i--) {
|
|
46
|
+
const entry = w[i]
|
|
47
|
+
if (entry.length > length) continue
|
|
48
|
+
|
|
49
|
+
const writer = entry.writer
|
|
50
|
+
this._remove(w, entry)
|
|
51
|
+
writer.bump()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_remove(w, entry) {
|
|
56
|
+
const head = w.pop()
|
|
57
|
+
|
|
58
|
+
if (head !== entry) {
|
|
59
|
+
w[entry.index] = head
|
|
60
|
+
head.index = entry.index
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
entry.writer = null
|
|
64
|
+
|
|
65
|
+
if (w.length === 0) {
|
|
66
|
+
this.waiting.delete(entry.id)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
package/lib/updates.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module.exports = class UpdateChanges {
|
|
2
|
+
constructor(auto) {
|
|
3
|
+
this.auto = auto
|
|
4
|
+
this.byName = new Map()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
get(key) {
|
|
8
|
+
return this.byName.get(key)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
track() {
|
|
12
|
+
this.tracking = {
|
|
13
|
+
view: this.auto._workingBee,
|
|
14
|
+
system: this.auto.system.bee
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const k in this.tracking) {
|
|
18
|
+
this.byName.set(k, { from: this.tracking[k].head(), to: null })
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
finalise() {
|
|
23
|
+
for (const k in this.tracking) {
|
|
24
|
+
const from = this.byName.get(k).from
|
|
25
|
+
const to = this.tracking[k].head()
|
|
26
|
+
this.byName.set(k, { to, from })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return this.byName
|
|
30
|
+
}
|
|
31
|
+
}
|