autobee 1.0.0 → 1.0.3

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/index.js CHANGED
@@ -18,6 +18,7 @@ const { ActiveWriters } = require('./lib/writers.js')
18
18
  const UpdateChanges = require('./lib/updates.js')
19
19
 
20
20
  const EMPTY_HEAD = { length: 0, key: null }
21
+ const INTERRUPT = new Error('Apply interrupted')
21
22
 
22
23
  module.exports = class Autobee extends ReadyResource {
23
24
  constructor(store, key = null, handlers = {}) {
@@ -81,6 +82,10 @@ module.exports = class Autobee extends ReadyResource {
81
82
  this._updateLocalCore = null
82
83
  this._host = new ApplyCalls(this)
83
84
 
85
+ this.interrupted = null
86
+ this._interrupting = false
87
+ this._onErrorBound = this._onError.bind(this)
88
+
84
89
  this._wakeup = new AutobeeWakeup(this, handlers)
85
90
  this.wakeupCapability = null
86
91
 
@@ -126,6 +131,22 @@ module.exports = class Autobee extends ReadyResource {
126
131
  this._wakeup.setCapability(this.wakeupCapability.key, this.wakeupCapability.discoveryKey)
127
132
  }
128
133
 
134
+ getExternalWriters() {
135
+ const keys = []
136
+ for (const w of this.writers.active.values()) {
137
+ if (w === this.writers.localWriter) continue
138
+ keys.push(w.core.key)
139
+ }
140
+ return keys
141
+ }
142
+
143
+ async getWriterViews(key) {
144
+ const id = b4a.toString(key, 'hex')
145
+ const w = this.writers.active.get(id)
146
+ if (!w) return []
147
+ return w.views()
148
+ }
149
+
129
150
  views() {
130
151
  const sys = this.system.bee.context.local
131
152
  const view = this._workingBee.context.local
@@ -138,7 +159,8 @@ module.exports = class Autobee extends ReadyResource {
138
159
  }
139
160
 
140
161
  async _close() {
141
- await this.interrupt()
162
+ this._interrupting = true
163
+ if (this._draining) await this._draining
142
164
 
143
165
  if (this._handlers.close) await this._handlers.close(this.view)
144
166
 
@@ -254,8 +276,7 @@ module.exports = class Autobee extends ReadyResource {
254
276
  this.bumping++
255
277
 
256
278
  if (!this._draining) {
257
- this._draining = this._drain()
258
- this._draining.catch(safetyCatch)
279
+ this._draining = this._drain().catch(this._onErrorBound)
259
280
  }
260
281
 
261
282
  return this._draining
@@ -265,11 +286,46 @@ module.exports = class Autobee extends ReadyResource {
265
286
  return this._bump()
266
287
  }
267
288
 
268
- updated() {
289
+ async updated() {
290
+ if (this.opened === false) await this.ready()
269
291
  if (this._draining) return this._draining
270
292
  return Promise.resolve()
271
293
  }
272
294
 
295
+ interrupt(reason) {
296
+ asserts.assert(!!this._host.applying, 'Interrupt is only allowed in apply')
297
+ this._interrupting = true
298
+ if (reason) this.interrupted = reason
299
+ throw INTERRUPT
300
+ }
301
+
302
+ getLastError() {
303
+ return this._lastError
304
+ }
305
+
306
+ _onError(err) {
307
+ if (this.closing) return
308
+
309
+ this._lastError = err
310
+
311
+ if (err === INTERRUPT) {
312
+ this.emit('interrupt', this.interrupted)
313
+ this.emit('update')
314
+ return
315
+ }
316
+
317
+ this.close().catch(safetyCatch)
318
+
319
+ // if no one is listening we should crash! we cannot rely on the EE here
320
+ // as this is wrapped in a promise so instead of nextTick throw it
321
+ if (ReadyResource.listenerCount(this, 'error') === 0) {
322
+ crashSoon(err)
323
+ return
324
+ }
325
+
326
+ this.emit('error', err)
327
+ }
328
+
273
329
  async _drain() {
274
330
  if (this._updateLocalCore !== null) {
275
331
  await this._rotateLocalWriter(this._updateLocalCore)
@@ -376,11 +432,6 @@ module.exports = class Autobee extends ReadyResource {
376
432
  this.emit('rotate-local-writer')
377
433
  }
378
434
 
379
- interrupt() {
380
- this._interrupting = true
381
- if (this._draining) return this._draining
382
- }
383
-
384
435
  async createAnchor() {
385
436
  const node = this._host.applying[this._host.applying.length - 1]
386
437
 
@@ -424,7 +475,7 @@ module.exports = class Autobee extends ReadyResource {
424
475
  await core.append(block, { writable: true, maxLength: 1 })
425
476
  }
426
477
 
427
- await this.system.addWriter(core.key)
478
+ await this.system.addWriter(core.key, { weight: 1 })
428
479
 
429
480
  const anchor = { key: core.key, length: core.length }
430
481
 
@@ -475,8 +526,6 @@ module.exports = class Autobee extends ReadyResource {
475
526
  this._workingBee.move(t.view)
476
527
  }
477
528
 
478
- asserts.assert(batch === t.tip[0], 'Batch must be first part of tip')
479
-
480
529
  let failed = true
481
530
 
482
531
  try {
@@ -594,9 +643,7 @@ module.exports = class Autobee extends ReadyResource {
594
643
  const value = values[i]
595
644
  const buffer = typeof value === 'string' ? b4a.from(value) : value
596
645
  const lnk = i === 0 ? links : []
597
- const b = { start: i, end: values.length - 1 - i }
598
-
599
- const node = this.writers.appendLocal(buffer, t, b, lnk, optimistic)
646
+ const node = this.writers.appendLocal(buffer, t, null, lnk, optimistic)
600
647
  batch.push(node)
601
648
  }
602
649
 
@@ -607,6 +654,10 @@ module.exports = class Autobee extends ReadyResource {
607
654
  // analyze is worth the trade off adding the view here also (technically not needed)
608
655
  await this.writers.flushLocal(this._workingBee.head())
609
656
  }
657
+
658
+ async replay() {
659
+ return topo.replay(this)
660
+ }
610
661
  }
611
662
 
612
663
  function isObject(o) {
@@ -634,3 +685,10 @@ function createAnchorCore(store, prologue, manifestData) {
634
685
 
635
686
  return core
636
687
  }
688
+
689
+ function crashSoon(err) {
690
+ queueMicrotask(() => {
691
+ throw err
692
+ })
693
+ throw err
694
+ }
@@ -22,13 +22,17 @@ class ApplyCalls {
22
22
  return this.auto.name
23
23
  }
24
24
 
25
+ get clock() {
26
+ return this.auto.system.flushes
27
+ }
28
+
25
29
  get genesis() {
26
30
  return this.auto.system.isGenesis()
27
31
  }
28
32
 
29
- addWriter(key, { isIndexer = true } = {}) {
33
+ addWriter(key, { isIndexer = true, weight = isIndexer ? 2 : 1 } = {}) {
30
34
  if (typeof key === 'string') key = ID.decode(key)
31
- return this.auto.system.addWriter(key, { isIndexer })
35
+ return this.auto.system.addWriter(key, { weight })
32
36
  }
33
37
 
34
38
  ackWriter(key) {
@@ -46,8 +50,7 @@ class ApplyCalls {
46
50
  }
47
51
 
48
52
  interrupt(reason) {
49
- throw new Error('TODO')
50
- // this.auto._interrupt(reason)
53
+ this.auto.interrupt(reason)
51
54
  }
52
55
 
53
56
  removeable(key) {
package/lib/encoding.js CHANGED
@@ -3,7 +3,7 @@ const c = require('compact-encoding')
3
3
  const crypto = require('hypercore-crypto')
4
4
  const { AutobeeEncryption } = require('autobee-encryption')
5
5
 
6
- const { getEncoding } = require('../spec/hyperschema')
6
+ const { getEncoding } = require('../encoding/spec/autobee')
7
7
  const { LEGACY_OPLOG_VERSION, OPLOG_VERSION } = require('./constants')
8
8
 
9
9
  const Oplog = getEncoding('@autobee/oplog')
package/lib/system.js CHANGED
@@ -28,12 +28,12 @@ module.exports = class Systembee {
28
28
  this.encrypted = opts.encrypted === true
29
29
  }
30
30
 
31
- async addWriter(key, { length = 0, isIndexer = false } = {}) {
31
+ async addWriter(key, { length = 0, weight = 1 } = {}) {
32
32
  if (length === 0) {
33
33
  const info = await this.get(key, { unflushed: false })
34
34
  length = info ? info.length : 0
35
35
  }
36
- this.update(key, length, isIndexer, false, true, false)
36
+ this.update(key, length, weight, false, true, false)
37
37
  }
38
38
 
39
39
  async ackWriter(key, { length = 0 } = {}) {
@@ -41,7 +41,7 @@ module.exports = class Systembee {
41
41
  const info = await this.get(key, { unflushed: false })
42
42
  length = info ? info.length : 0
43
43
  }
44
- this.update(key, length, false, false, false, true)
44
+ this.update(key, length, 0, false, false, true)
45
45
  }
46
46
 
47
47
  async removeWriter(key, { length = 0 } = {}) {
@@ -49,7 +49,7 @@ module.exports = class Systembee {
49
49
  const info = await this.get(key, { unflushed: true })
50
50
  length = info ? info.length : 0
51
51
  }
52
- this.update(key, length, false, true, false, false)
52
+ this.update(key, length, 0, true, false, false)
53
53
  }
54
54
 
55
55
  isGenesis() {
@@ -133,7 +133,7 @@ module.exports = class Systembee {
133
133
  this.heads.push({ key: node.key, length: node.length })
134
134
  if (node.timestamp > this.timestamp) this.timestamp = node.timestamp // TODO: support smoothing
135
135
 
136
- this.update(node.key, node.length, false, false, false, false)
136
+ this.update(node.key, node.length, 0, false, false, false)
137
137
  }
138
138
 
139
139
  async canApply(key, optimistic) {
@@ -151,6 +151,7 @@ module.exports = class Systembee {
151
151
 
152
152
  const upd = this.updates.get(b4a.toString(key, 'hex'))
153
153
  if (!upd) return info
154
+ if (!info) return upd
154
155
 
155
156
  if (upd.isAdded) info.isAdded = true
156
157
  if (upd.isRemoved) info.isRemoved = true
@@ -183,7 +184,7 @@ module.exports = class Systembee {
183
184
  return true
184
185
  }
185
186
 
186
- update(key, length, isIndexer, isRemoved, isAdded, isAcked) {
187
+ update(key, length, weight, isRemoved, isAdded, isAcked) {
187
188
  const id = b4a.toString(key, 'hex')
188
189
 
189
190
  let upd = this.updates.get(id)
@@ -195,17 +196,27 @@ module.exports = class Systembee {
195
196
  }
196
197
 
197
198
  if (!upd) {
198
- upd = { key, length, isIndexer, isRemoved, isAdded, isAcked, isOplog: false }
199
+ upd = {
200
+ version: 4,
201
+ key,
202
+ length,
203
+ weight,
204
+ clock: 0,
205
+ isRemoved,
206
+ isAdded,
207
+ isAcked,
208
+ isOplog: false
209
+ }
199
210
  this.updates.set(id, upd)
200
211
  return
201
212
  }
202
213
 
203
214
  if (isAdded) {
204
- upd.isIndexer = isIndexer
215
+ upd.weight = weight
205
216
  upd.isRemoved = false
206
217
  }
207
218
  if (isRemoved) {
208
- upd.isIndexer = false
219
+ upd.weight = 0
209
220
  upd.isRemoved = true
210
221
  }
211
222
  if (isAcked) {
@@ -226,7 +237,7 @@ module.exports = class Systembee {
226
237
 
227
238
  if (v) {
228
239
  if (!changed) {
229
- upd.isIndexer = v.isIndexer
240
+ upd.weight = v.weight
230
241
  upd.isRemoved = v.isRemoved
231
242
  }
232
243
 
@@ -234,6 +245,8 @@ module.exports = class Systembee {
234
245
  }
235
246
  }
236
247
 
248
+ upd.clock = this.flushes
249
+
237
250
  // TODO: we can optimise this a bit, if the above node hasnt changed, do not set this
238
251
  // less writes, but future thing
239
252
  upd.isOplog = b4a.equals(upd.key, oplog)
package/lib/topo.js CHANGED
@@ -5,11 +5,43 @@ const asserts = require('./asserts.js')
5
5
 
6
6
  const EMPTY_HEAD = { length: 0, key: null }
7
7
 
8
+ exports.replay = replay
8
9
  exports.sort = sort
9
10
 
10
11
  exports.isLinking = isLinking
11
12
  exports.isLinkingAll = isLinkingAll
12
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
+
13
45
  async function sort(auto, batch) {
14
46
  const node = batch[0]
15
47
  const ch = auto.system.bee.createChangesStream()
@@ -17,52 +49,98 @@ async function sort(auto, batch) {
17
49
 
18
50
  for await (const data of ch) {
19
51
  const { ref, info } = getOplog(data)
20
- const link = { key: ref.oplog.key, length: ref.oplog.length }
52
+ const link = {
53
+ key: ref.oplog.key,
54
+ length: ref.oplog.length,
55
+ weight: ref.oplog.weight,
56
+ timestamp: info.timestamp
57
+ }
21
58
 
22
- if (isLinking(node, link) || node.timestamp > info.timestamp) break
59
+ if (!isOrderedBefore(node, link)) break
23
60
  reorder.push(ref)
24
61
  }
25
62
 
26
63
  const inflated = await inflate(auto, reorder.reverse())
27
64
 
28
- for (let i = 0; i < inflated.length; i++) {
29
- const b = inflated[i]
30
- const op = b.batch[0] // just use first op, they are all the same writer
31
- const c = cmp(op, node)
65
+ const { undo, shared } = addSorted(inflated, batch)
66
+
67
+ const tip = inflated.slice(shared).map((b) => b.batch)
32
68
 
33
- if (c === -1) continue
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
+ }
34
83
 
35
- const tip = new Array(inflated.length - i + 1)
36
- tip[0] = batch
37
- for (let j = i; j < inflated.length; j++) {
38
- tip[j - i + 1] = inflated[j].batch
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
39
101
  }
40
102
 
41
- return {
42
- undo: b.undo,
43
- view: null,
44
- tip
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
45
109
  }
46
- }
47
110
 
48
- return {
49
- undo: null,
50
- view: null,
51
- tip: [batch]
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
+ }
52
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 }
53
131
  }
54
132
 
55
133
  function cmp(a, b) {
56
- const c = b4a.compare(a.key, b.key)
57
-
58
- if (c === 0) {
59
- return a.length - b.length
60
- }
134
+ const w = a.weight - b.weight
135
+ if (w) return w > 0 ? -1 : 1
61
136
 
62
137
  const t = a.timestamp - b.timestamp
138
+ if (t) return t < 0 ? -1 : 1
63
139
 
64
- if (t === 0) return c
65
- return t < 0 ? -1 : 1
140
+ const c = b4a.compare(a.key, b.key)
141
+ if (c) return c
142
+
143
+ return a.length < b.length ? -1 : 1
66
144
  }
67
145
 
68
146
  async function inflate(auto, reorder) {
@@ -74,7 +152,7 @@ async function inflate(auto, reorder) {
74
152
 
75
153
  const local = auto.writers.localWriter
76
154
  if (local && local.id === id) {
77
- promises.push(getLocalBatch(ref.undo, local, ref.oplog.length))
155
+ promises.push(getLocalBatch(ref.undo, local, ref.oplog.length, ref.oplog.weight))
78
156
  continue
79
157
  }
80
158
 
@@ -84,7 +162,7 @@ async function inflate(auto, reorder) {
84
162
  sessions.set(id, core)
85
163
  }
86
164
 
87
- promises.push(getOplogBatch(ref.undo, core, ref.oplog.length))
165
+ promises.push(getOplogBatch(ref.undo, core, ref.oplog.length, ref.oplog.weight))
88
166
  }
89
167
 
90
168
  try {
@@ -94,11 +172,11 @@ async function inflate(auto, reorder) {
94
172
  }
95
173
  }
96
174
 
97
- async function getOplogBatch(undo, core, length) {
175
+ async function getOplogBatch(undo, core, length, weight) {
98
176
  const seq = length - 1
99
177
  const block = await core.get(seq)
100
178
  const oplog = encoding.decodeOplog(block)
101
- const head = createNode(core, length, oplog)
179
+ const head = createNode(core, length, weight, oplog)
102
180
 
103
181
  const batch = []
104
182
  const result = { undo, batch }
@@ -115,7 +193,7 @@ async function getOplogBatch(undo, core, length) {
115
193
  const blocks = await Promise.all(remaining)
116
194
  for (let i = 0; i < blocks.length; i++) {
117
195
  const oplog = encoding.decodeOplog(blocks[i])
118
- batch.push(createNode(core, i + start + 1, oplog))
196
+ batch.push(createNode(core, i + start + 1, weight, oplog))
119
197
  }
120
198
  }
121
199
 
@@ -123,7 +201,7 @@ async function getOplogBatch(undo, core, length) {
123
201
  return result
124
202
  }
125
203
 
126
- async function getLocalBatch(undo, writer, length) {
204
+ function getLocalBatch(undo, writer, length, weight) {
127
205
  if (length > writer.core.length) {
128
206
  if (!writer.pending) throw new Error('No local nodes')
129
207
 
@@ -142,16 +220,13 @@ async function getLocalBatch(undo, writer, length) {
142
220
  return { undo, batch }
143
221
  }
144
222
 
145
- return getOplogBatch(undo, writer.core, length)
223
+ return getOplogBatch(undo, writer.core, length, weight)
146
224
  }
147
225
 
148
226
  function getOplog(data) {
149
227
  const result = { ref: null, info: null }
150
- for (let i = 0; i < data.batch.length; i++) {
151
- const keys = data.batch[i].keys
152
-
153
- for (let j = 0; j < keys.length; j++) {
154
- const k = keys[j]
228
+ for (const { keys } of data.batch) {
229
+ for (const k of keys) {
155
230
  const prefix = k.key[0]
156
231
 
157
232
  if (prefix === 0) {