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