autobee 1.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/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) {
package/lib/writers.js CHANGED
@@ -3,6 +3,8 @@ const encoding = require('./encoding.js')
3
3
  const Triggers = require('./triggers.js')
4
4
  const { WriterEncryption } = require('autobee-encryption')
5
5
 
6
+ const WRITER_PREFETCH = 10
7
+
6
8
  const BATCH_WRITER_SANITY = 1024
7
9
  const BATCH_OPTIMISTIC_SANITY = 32
8
10
 
@@ -80,7 +82,7 @@ class ActiveWriters {
80
82
  const info = await this.auto.system.get(this.auto.local.key)
81
83
  const writable = this.writable
82
84
 
83
- w.isIndexer = info ? info.isIndexer : false
85
+ w.weight = info ? info.weight : 0
84
86
  this.writable = info ? !info.isRemoved : bootstrapping
85
87
 
86
88
  if (writable === this.writable) return
@@ -158,23 +160,30 @@ class Writer {
158
160
  this.core = core
159
161
  this.id = id
160
162
  this.index = 0
163
+ this.weight = 0
161
164
  this.pending = null
162
165
  this.waiting = null
166
+ this.download = null
163
167
 
164
168
  this.isPending = false
165
169
  this.isClosed = false
166
170
  this.isAttached = false
167
- this.isIndexer = false
168
171
  this.isAdded = false
169
172
  this.isRemoved = false
170
173
  this.isCoupled = false
171
174
 
172
- this._onappendBound = this._onappend.bind(this)
173
- if (!local) this.core.on('append', this._onappendBound)
175
+ this._onchangeBound = this._onchange.bind(this)
176
+
177
+ if (!local) this.core.on('append', this._onchangeBound)
178
+ if (!local) this.core.on('download', this._onchangeBound)
174
179
 
175
180
  this.couple()
176
181
  }
177
182
 
183
+ get isIndexer() {
184
+ return this.weight === 2
185
+ }
186
+
178
187
  couple() {
179
188
  if (this.isCoupled) return
180
189
  // if we don't have coupler yet we can't add
@@ -216,7 +225,7 @@ class Writer {
216
225
  }
217
226
  }
218
227
 
219
- _onappend() {
228
+ _onchange() {
220
229
  if (this.waiting === null) this.bump()
221
230
  }
222
231
 
@@ -236,6 +245,8 @@ class Writer {
236
245
  detach() {
237
246
  if (!this.isAttached) return
238
247
  this.isAttached = false
248
+ this.download.destroy()
249
+ this.download = null
239
250
  this.writers.active.delete(this.id)
240
251
  this.removePending()
241
252
  }
@@ -258,6 +269,17 @@ class Writer {
258
269
  return this.close()
259
270
  }
260
271
 
272
+ async views() {
273
+ const latest = await this.getLatest()
274
+ if (!latest || !latest.views) return []
275
+
276
+ // signedLength for autobase compat
277
+ return [
278
+ { ...latest.views.system, signedLength: latest.views.system.length },
279
+ { ...latest.views.view, signedLength: latest.views.view.length }
280
+ ]
281
+ }
282
+
261
283
  async getLatest() {
262
284
  if (this.core.length === 0) return null
263
285
  const block = await this.core.get(this.core.length - 1)
@@ -273,7 +295,7 @@ class Writer {
273
295
  } else {
274
296
  this.isAdded = true
275
297
  this.isRemoved = info.isRemoved
276
- this.isIndexer = info.isIndexer
298
+ this.weight = info.weight
277
299
  }
278
300
 
279
301
  return info ? info.length : 0
@@ -289,10 +311,21 @@ class Writer {
289
311
  let optimistic = false
290
312
  let timestamp = 0
291
313
 
314
+ if (this.download) this.download.destroy()
315
+ this.download = this.core.download({ start: length, end: length + WRITER_PREFETCH })
316
+
292
317
  while (length < this.core.length) {
318
+ // @todo aim to prefetch when writer added, with read-ahead
319
+ // download the block if it's not available locally
320
+ // writer will be added back as pending automatically
321
+ if (!(await this.core.has(length))) {
322
+ this.removePending()
323
+ return null
324
+ }
325
+
293
326
  const block = await this.core.get(length++)
294
327
  const oplog = encoding.decodeOplog(block)
295
- const node = createNode(this.core, length, oplog)
328
+ const node = createNode(this.core, length, this.weight, oplog)
296
329
  const b = oplog.batch
297
330
 
298
331
  // auto correct some batch invariants
@@ -336,6 +369,9 @@ class Writer {
336
369
  if (this.core.writable && this.pending !== null) {
337
370
  for (let i = 0; i < this.pending.length; i++) {
338
371
  const node = this.pending[i]
372
+
373
+ node.batch = { start: i, end: this.pending.length - 1 - i }
374
+
339
375
  if (node.length <= length) continue
340
376
 
341
377
  if (!this.writers.writable && !node.optimistic) {
@@ -409,7 +445,12 @@ class Writer {
409
445
  value
410
446
  }
411
447
 
412
- const node = createNode(this.core, this.core.length + this.pending.length + 1, oplog)
448
+ const node = createNode(
449
+ this.core,
450
+ this.core.length + this.pending.length + 1,
451
+ this.weight,
452
+ oplog
453
+ )
413
454
 
414
455
  this.pending.push(node)
415
456
  this.addPending()
@@ -427,7 +468,11 @@ class Writer {
427
468
  const s = this.system.bee.head()
428
469
  const flushes = this.system.flushes
429
470
  this.pending[this.pending.length - 1].views = { system: s, flushes, view }
430
- for (const node of this.pending) buffers.push(encoding.encodeOplog(node))
471
+ for (let i = 0; i < this.pending.length; i++) {
472
+ const node = this.pending[i]
473
+ node.batch = { start: i, end: this.pending.length - 1 - i }
474
+ buffers.push(encoding.encodeOplog(node))
475
+ }
431
476
  this.pending = null
432
477
  return this.core.append(buffers)
433
478
  }
@@ -437,11 +482,12 @@ exports.ActiveWriters = ActiveWriters
437
482
  exports.Writer = Writer
438
483
  exports.createNode = createNode
439
484
 
440
- function createNode(core, length, oplog) {
485
+ function createNode(core, length, weight, oplog) {
441
486
  return {
442
487
  core,
443
488
  key: core.key,
444
489
  length,
490
+ weight,
445
491
  version: oplog.version,
446
492
  timestamp: oplog.timestamp,
447
493
  links: oplog.links,
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",
7
7
  "main": "index.js",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/holepunchto/autobee.git"
11
+ },
8
12
  "files": [
9
13
  "index.js",
10
14
  "lib/*.js",
@@ -13,7 +17,7 @@
13
17
  "scripts": {
14
18
  "format": "prettier . --write",
15
19
  "lint": "lunte && prettier . --check",
16
- "test": "prettier . --check && lunte && node test/all.js",
20
+ "test": "node test/all.js",
17
21
  "test:bare": "bare test/all.js",
18
22
  "test:encrypted": "bare test/all.js --encrypt-all",
19
23
  "test:generate": "brittle -r test/all.js test/*.js"