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/lib/writers.js ADDED
@@ -0,0 +1,501 @@
1
+ const b4a = require('b4a')
2
+ const encoding = require('./encoding.js')
3
+ const Triggers = require('./triggers.js')
4
+ const { WriterEncryption } = require('autobee-encryption')
5
+
6
+ const WRITER_PREFETCH = 10
7
+
8
+ const BATCH_WRITER_SANITY = 1024
9
+ const BATCH_OPTIMISTIC_SANITY = 32
10
+
11
+ const MAX_FORWARD_TIME_DRIFT = 2 * 3600 * 1000
12
+
13
+ class ActiveWriters {
14
+ constructor(auto) {
15
+ const local = auto.local
16
+
17
+ this.auto = auto
18
+ this.active = new Map()
19
+ this.triggers = new Triggers()
20
+ this.localWriter = new Writer(this, auto.system, true, local, b4a.toString(local.key, 'hex'))
21
+ this.writable = false
22
+ this.pending = []
23
+
24
+ this.localWriter.attach()
25
+
26
+ this._bootstrap()
27
+ }
28
+
29
+ [Symbol.iterator]() {
30
+ return this.active.values()
31
+ }
32
+
33
+ *external() {
34
+ for (const w of this.active.values()) {
35
+ if (w !== this.localWriter) yield w
36
+ }
37
+ }
38
+
39
+ bump() {
40
+ this.auto._bump()
41
+ }
42
+
43
+ async wakeup(key, length) {
44
+ const id = b4a.toString(key, 'hex')
45
+ let w = this.active.get(id)
46
+
47
+ if (w === this.localWriter) return
48
+ if (!w) w = await this.add(key)
49
+
50
+ // TODO: signal that we've seen length on w for gc
51
+
52
+ this.bump()
53
+ }
54
+
55
+ getLatestLocalOplog() {
56
+ return this.localWriter.getLatest()
57
+ }
58
+
59
+ appendLocal(value, timestamp, batch, links, optimistic) {
60
+ return this.localWriter.append(value, timestamp, batch, links, optimistic)
61
+ }
62
+
63
+ async rotateLocalWriter(core) {
64
+ this.clearLocal()
65
+ await this.localWriter.detachAndClose()
66
+
67
+ this.localWriter = new Writer(this, this.auto.system, true, core, b4a.toString(core.key, 'hex'))
68
+ this.localWriter.addPending()
69
+ }
70
+
71
+ clearLocal() {
72
+ return this.localWriter.clear()
73
+ }
74
+
75
+ flushLocal(head) {
76
+ return this.localWriter.flush(head)
77
+ }
78
+
79
+ async updateLocalState() {
80
+ const w = this.localWriter
81
+ const bootstrapping = w.core.length === 0 && b4a.equals(this.auto.key, w.core.key)
82
+ const info = await this.auto.system.get(this.auto.local.key)
83
+ const writable = this.writable
84
+
85
+ w.weight = info ? info.weight : 0
86
+ this.writable = info ? !info.isRemoved : bootstrapping
87
+
88
+ if (writable === this.writable) return
89
+
90
+ this.auto.emit(this.writable ? 'writable' : 'unwritable')
91
+ this.auto.emit('update')
92
+ }
93
+
94
+ has(id) {
95
+ return !!this.active.get(id)
96
+ }
97
+
98
+ async add(key) {
99
+ if (b4a.equals(key, this.auto.local.key)) await this.updateLocalState()
100
+
101
+ const id = b4a.toString(key, 'hex')
102
+ let w = this.active.get(id)
103
+ if (w) {
104
+ w.couple() // couple if we weren't ready first time
105
+ return w
106
+ }
107
+
108
+ const encryption = this.auto.encryptionKey ? new WriterEncryption(this.auto) : null
109
+
110
+ const core = this.auto.store.get({
111
+ key,
112
+ encryption
113
+ })
114
+ await core.ready()
115
+ await core.setUserData('referrer', this.auto.key)
116
+
117
+ w = new Writer(this, this.auto.system, false, core, id)
118
+ w.attach()
119
+ w.couple()
120
+
121
+ return w
122
+ }
123
+
124
+ async remove(key) {
125
+ if (b4a.equals(key, this.auto.local.key)) await this.updateLocalState()
126
+
127
+ const id = b4a.toString(key, 'hex')
128
+ const w = this.active.get(id)
129
+ if (!w) return
130
+
131
+ w.detach()
132
+ w.decouple()
133
+ if (w !== this.localWriter) await w.close()
134
+ }
135
+
136
+ _bootstrap() {
137
+ const bootstrap = this.auto.bootstrap
138
+
139
+ bootstrap.setEncryption(this.auto._getEncryptionProvider())
140
+ bootstrap.setActive(true)
141
+
142
+ const id = b4a.toString(bootstrap.key, 'hex')
143
+ let w = this.active.get(id)
144
+ if (w) {
145
+ w.couple() // couple if we weren't ready first time
146
+ return
147
+ }
148
+
149
+ w = new Writer(this, this.auto.system, false, bootstrap, id)
150
+ w.attach()
151
+ w.couple()
152
+ }
153
+ }
154
+
155
+ class Writer {
156
+ constructor(writers, system, local, core, id) {
157
+ this.writers = writers
158
+ this.system = system
159
+
160
+ this.core = core
161
+ this.id = id
162
+ this.index = 0
163
+ this.weight = 0
164
+ this.pending = null
165
+ this.waiting = null
166
+ this.download = null
167
+
168
+ this.isPending = false
169
+ this.isClosed = false
170
+ this.isAttached = false
171
+ this.isAdded = false
172
+ this.isRemoved = false
173
+ this.isCoupled = false
174
+
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)
179
+
180
+ this.couple()
181
+ }
182
+
183
+ get isIndexer() {
184
+ return this.weight === 2
185
+ }
186
+
187
+ couple() {
188
+ if (this.isCoupled) return
189
+ // if we don't have coupler yet we can't add
190
+ if (this.writers.auto._wakeup.addCore(this.core)) {
191
+ this.isCoupled = true
192
+ }
193
+ }
194
+
195
+ decouple() {
196
+ if (!this.isCoupled) return
197
+ // if we don't have coupler yet we can't remove
198
+ if (this.writers.auto._wakeup.removeCore(this.core)) {
199
+ this.isCoupled = false
200
+ }
201
+ }
202
+
203
+ hasReferrals() {
204
+ const entries = this.writers.triggers.get(this.id)
205
+ if (!entries) return false
206
+ // TODO: should implement a system where we KNOW that a future writers IS not removed
207
+ // to avoid accidental spam, for now kept simple as the algo still runs
208
+ return true
209
+ }
210
+
211
+ addPending() {
212
+ if (this.isPending) return
213
+ this.isPending = true
214
+ this.index = this.writers.pending.push(this) - 1
215
+ }
216
+
217
+ removePending() {
218
+ if (!this.isPending) return
219
+ this.isPending = false
220
+
221
+ const head = this.writers.pending.pop()
222
+ if (head !== this) {
223
+ this.writers.pending[this.index] = head
224
+ head.index = this.index
225
+ }
226
+ }
227
+
228
+ _onchange() {
229
+ if (this.waiting === null) this.bump()
230
+ }
231
+
232
+ bump() {
233
+ this.waiting = null
234
+ if (this.isAttached) this.addPending() // TODO: remove the isAttach guard
235
+ this.writers.bump()
236
+ }
237
+
238
+ attach() {
239
+ if (this.isAttached) return
240
+ this.isAttached = true
241
+ this.writers.active.set(this.id, this)
242
+ this.addPending()
243
+ }
244
+
245
+ detach() {
246
+ if (!this.isAttached) return
247
+ this.isAttached = false
248
+ this.download.destroy()
249
+ this.download = null
250
+ this.writers.active.delete(this.id)
251
+ this.removePending()
252
+ }
253
+
254
+ close() {
255
+ this.isClosed = true
256
+
257
+ if (this.waiting) {
258
+ this.writers.triggers.remove(this.waiting)
259
+ this.waiting = null
260
+ }
261
+
262
+ this.removePending()
263
+
264
+ return this.core.close()
265
+ }
266
+
267
+ detachAndClose() {
268
+ this.detach()
269
+ return this.close()
270
+ }
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
+
283
+ async getLatest() {
284
+ if (this.core.length === 0) return null
285
+ const block = await this.core.get(this.core.length - 1)
286
+ const oplog = encoding.decodeOplog(block)
287
+ return oplog
288
+ }
289
+
290
+ async update() {
291
+ const info = await this.system.get(this.core.key)
292
+
293
+ if (!info) {
294
+ this.isAdded = this.system.isGenesis()
295
+ } else {
296
+ this.isAdded = true
297
+ this.isRemoved = info.isRemoved
298
+ this.weight = info.weight
299
+ }
300
+
301
+ return info ? info.length : 0
302
+ }
303
+
304
+ async next() {
305
+ if (this.waiting !== null) return null
306
+
307
+ const batch = []
308
+ const genesis = this.system.isGenesis()
309
+
310
+ let length = await this.update()
311
+ let optimistic = false
312
+ let timestamp = 0
313
+
314
+ if (this.download) this.download.destroy()
315
+ this.download = this.core.download({ start: length, end: length + WRITER_PREFETCH })
316
+
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
+
326
+ const block = await this.core.get(length++)
327
+ const oplog = encoding.decodeOplog(block)
328
+ const node = createNode(this.core, length, this.weight, oplog)
329
+ const b = oplog.batch
330
+
331
+ // auto correct some batch invariants
332
+ node.timestamp = timestamp = Math.max(node.timestamp, timestamp)
333
+ node.optimistic = optimistic = optimistic || node.optimistic
334
+
335
+ if (!this.isAdded && !node.optimistic && !genesis) {
336
+ return null
337
+ }
338
+
339
+ // an optimistic writer cannot be genesis
340
+ if (node.optimistic && genesis) {
341
+ return null
342
+ }
343
+
344
+ if (node.optimistic && !this.isAdded && b.end >= BATCH_OPTIMISTIC_SANITY) {
345
+ return null
346
+ }
347
+
348
+ if (node.timestamp - Date.now() > MAX_FORWARD_TIME_DRIFT) {
349
+ return null
350
+ }
351
+
352
+ if (!node.optimistic && b.start === 0 && b.end > 1 && b.end < BATCH_WRITER_SANITY) {
353
+ this.core.download({ start: length, end: length + oplog.batch.end })
354
+ }
355
+
356
+ if (!(await this._isLinked(node))) {
357
+ this.removePending()
358
+ return null
359
+ }
360
+
361
+ batch.push(node)
362
+ if (b.end === 0) return batch
363
+
364
+ // TODO: mark as dead
365
+ if (batch.length >= BATCH_WRITER_SANITY) return null
366
+ }
367
+
368
+ // handle local writer
369
+ if (this.core.writable && this.pending !== null) {
370
+ for (let i = 0; i < this.pending.length; i++) {
371
+ const node = this.pending[i]
372
+
373
+ node.batch = { start: i, end: this.pending.length - 1 - i }
374
+
375
+ if (node.length <= length) continue
376
+
377
+ if (!this.writers.writable && !node.optimistic) {
378
+ this.writers.clearLocal()
379
+ throw new Error('Not writable')
380
+ }
381
+
382
+ // an optimistic writer cannot be genesis
383
+ if (node.optimistic && genesis) {
384
+ return null
385
+ }
386
+
387
+ if (node.optimistic && !this.isAdded && this.pending.length >= BATCH_OPTIMISTIC_SANITY) {
388
+ return null
389
+ }
390
+
391
+ if (node.timestamp - Date.now() > MAX_FORWARD_TIME_DRIFT) {
392
+ return null
393
+ }
394
+
395
+ if (!(await this._isLinked(node))) return null
396
+
397
+ this.removePending()
398
+
399
+ return this.pending.slice(i)
400
+ }
401
+ }
402
+
403
+ return null
404
+ }
405
+
406
+ notify(batch) {
407
+ this.writers.triggers.trigger(this.id, batch[batch.length - 1].length)
408
+ }
409
+
410
+ async _isLinked(node) {
411
+ const promises = new Array(node.links.length)
412
+
413
+ for (let i = 0; i < node.links.length; i++) {
414
+ promises[i] = this.system.has(node.links[i])
415
+ }
416
+
417
+ const has = await Promise.all(promises)
418
+
419
+ for (let i = 0; i < node.links.length; i++) {
420
+ if (has[i]) continue
421
+ const link = node.links[i]
422
+ const id = b4a.toString(link.key, 'hex')
423
+ this.waiting = this.writers.triggers.add(id, link.length, this)
424
+ this.writers.wakeup(link.key, link.length).catch(noop)
425
+ return false
426
+ }
427
+
428
+ return true
429
+ }
430
+
431
+ get appending() {
432
+ return this.pending !== null
433
+ }
434
+
435
+ append(value, timestamp, batch, links, optimistic) {
436
+ if (this.pending === null) this.pending = []
437
+
438
+ const oplog = {
439
+ version: encoding.OPLOG_VERSION,
440
+ timestamp,
441
+ links,
442
+ batch,
443
+ views: null,
444
+ optimistic,
445
+ value
446
+ }
447
+
448
+ const node = createNode(
449
+ this.core,
450
+ this.core.length + this.pending.length + 1,
451
+ this.weight,
452
+ oplog
453
+ )
454
+
455
+ this.pending.push(node)
456
+ this.addPending()
457
+
458
+ return node
459
+ }
460
+
461
+ clear() {
462
+ this.pending = null
463
+ }
464
+
465
+ flush(view) {
466
+ if (this.pending === null) return Promise.resolve()
467
+ const buffers = []
468
+ const s = this.system.bee.head()
469
+ const flushes = this.system.flushes
470
+ this.pending[this.pending.length - 1].views = { system: s, flushes, view }
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
+ }
476
+ this.pending = null
477
+ return this.core.append(buffers)
478
+ }
479
+ }
480
+
481
+ exports.ActiveWriters = ActiveWriters
482
+ exports.Writer = Writer
483
+ exports.createNode = createNode
484
+
485
+ function createNode(core, length, weight, oplog) {
486
+ return {
487
+ core,
488
+ key: core.key,
489
+ length,
490
+ weight,
491
+ version: oplog.version,
492
+ timestamp: oplog.timestamp,
493
+ links: oplog.links,
494
+ batch: oplog.batch,
495
+ views: oplog.views,
496
+ optimistic: oplog.optimistic,
497
+ value: oplog.value
498
+ }
499
+ }
500
+
501
+ function noop() {}
package/package.json CHANGED
@@ -1,12 +1,45 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "0.0.0",
4
- "description": "",
3
+ "version": "1.0.2",
4
+ "description": "Bee based autobase",
5
+ "license": "Apache-2.0",
6
+ "author": "Holepunch Inc.",
5
7
  "main": "index.js",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/holepunchto/autobee.git"
11
+ },
12
+ "files": [
13
+ "index.js",
14
+ "lib/*.js",
15
+ "spec"
16
+ ],
6
17
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
18
+ "format": "prettier . --write",
19
+ "lint": "lunte && prettier . --check",
20
+ "test": "node test/all.js",
21
+ "test:bare": "bare test/all.js",
22
+ "test:encrypted": "bare test/all.js --encrypt-all",
23
+ "test:generate": "brittle -r test/all.js test/*.js"
24
+ },
25
+ "dependencies": {
26
+ "autobee-encryption": "^0.0.5",
27
+ "autobee-wakeup": "^1.0.5",
28
+ "b4a": "^1.8.0",
29
+ "compact-encoding": "^2.18.0",
30
+ "hyperbee2": "^2.8.0",
31
+ "hypercore": "^11.28.1",
32
+ "hypercore-id-encoding": "^1.3.0",
33
+ "hyperschema": "^1.20.1",
34
+ "ready-resource": "^1.2.0",
35
+ "safety-catch": "^1.0.3"
8
36
  },
9
- "keywords": [],
10
- "author": "",
11
- "license": "ISC"
37
+ "devDependencies": {
38
+ "brittle": "^3.19.1",
39
+ "corestore": "^7.7.0",
40
+ "hypercore-crypto": "^3.6.1",
41
+ "lunte": "^1.6.0",
42
+ "prettier": "^3.8.1",
43
+ "prettier-config-holepunch": "^2.0.0"
44
+ }
12
45
  }