@cero-base/core 1.10.1 → 1.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cero-base/core",
3
- "version": "1.10.1",
3
+ "version": "1.11.0",
4
4
  "description": "cero p2p primitives — identity, storage, network, database, blobs, rpc, pairing.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -144,7 +144,7 @@
144
144
  },
145
145
  "dependencies": {
146
146
  "@hyperswarm/secret-stream": "^6.9.1",
147
- "autobee": "2.0.0-rc.6",
147
+ "autobee": "2.0.0-rc.14",
148
148
  "autobee-encryption": "0.1.3",
149
149
  "b4a": "^1.8.1",
150
150
  "bare-crypto": "^1.15.3",
@@ -1,13 +1,16 @@
1
- import b4a from 'b4a'
2
1
  import Hypercore from 'hypercore'
3
2
  import { Identity } from '../identity/index.js'
4
3
  import { CeroError } from '../lib/errors.js'
5
4
  import { toId, addWriterPayload } from '../lib/utils.js'
5
+ import { wrap } from './envelope.js'
6
6
 
7
7
  /**
8
8
  * First-run device provisioning: mint a device writer keypair, persist it as a
9
9
  * writer, then swap the autobee's local core over to it. With `recovering`,
10
- * waits for the first peer-replicated append before swapping.
10
+ * the swap happens FIRST and admission rides an optimistic append the
11
+ * backfilled genesis core is never appended to (autobee's contract: a writable
12
+ * core has exactly one author, ever; resuming a replicated core is unsupported
13
+ * and drops appends silently).
11
14
  *
12
15
  * @param {import('./index.js').Database} db
13
16
  * @param {{ name?: string | null, isMobile?: boolean, recovering?: boolean }} [opts]
@@ -22,87 +25,64 @@ export async function bootstrap(db, { name, isMobile, recovering = false, timeou
22
25
  const writerKey = Hypercore.key(manifest)
23
26
 
24
27
  if (recovering) {
25
- await waitForFirstPeerAppend(db, timeout)
26
- // reopen so the bee's writer state is built over the replicated genesis
27
- // core a writer opened before replication stamps ops at stale lengths
28
- // and autobee silently drops them
29
- await reopenBee(db)
30
- }
31
- if (recovering) {
32
- // gate the swap on the admission actually applying: write() can resolve
33
- // with the ops stalled in the drain (update() bumps heal that) or dropped
34
- // outright when they were stamped behind a mid-replication core — each
35
- // retry stamps past the collision, and the ops are idempotent
36
- await admitWriter(db, writerKey, { name, isMobile }, timeout)
28
+ // swap before anything else: the genesis core then replicates as a REMOTE
29
+ // writer, so its backfilled ops apply through the normal download path
30
+ await swapWriter(db, keyPair, manifest)
31
+ await waitForBackfill(db, timeout)
32
+ await claimWriter(db, writerKey, timeout)
33
+ await saveDevice(db, writerKey, { name, isMobile })
37
34
  } else {
38
35
  await saveWriter(db, writerKey, { name, isMobile })
36
+ await swapWriter(db, keyPair, manifest)
39
37
  }
40
- await swapWriter(db, keyPair, manifest)
41
38
 
42
39
  return { id: writerKey, writer: keyPair }
43
40
  }
44
41
 
45
- async function waitForFirstPeerAppend(db, timeout = 30000) {
46
- if (!db.network?.swarm) return
47
- if (db.bee.local.length === 0) await waitForLength(db, 1, timeout)
48
- await db.bee.update()
49
- // the first block may not be the whole core — writes stamped below the
50
- // length the system already attributes to this writer are silently dropped
51
- const info = await db.bee.system.get(db.bee.local.key).catch(() => null)
52
- if (info && info.length > db.bee.local.length) await waitForLength(db, info.length, timeout)
53
- }
54
-
55
- function waitForLength(db, length, timeout) {
56
- return new Promise((resolve, reject) => {
57
- let done = false
58
- const finish = (err) => {
59
- if (done) return
60
- done = true
61
- clearTimeout(timer)
62
- db.bee.local.off('append', onAppend)
63
- db.off('close', onClose)
64
- if (err) reject(err)
65
- else resolve()
66
- }
67
- // timeout + close handling so a peer that never replicates can't hang the
68
- // recovery forever, and the append listener is always removed.
69
- const onAppend = () => db.bee.local.length >= length && finish()
70
- const onClose = () => finish(CeroError.CLOSED('Database'))
71
- const timer = setTimeout(
72
- () => finish(CeroError.TIMED_OUT('recovery — no peer append')),
73
- timeout
74
- )
75
- db.bee.local.on('append', onAppend)
76
- db.on('close', onClose)
77
- })
78
- }
79
-
80
- async function admitWriter(db, writerKey, opts, timeout) {
81
- const deadline = Date.now() + timeout
82
- const id = toId(writerKey)
83
- while (true) {
84
- await saveWriter(db, writerKey, opts)
85
- if (await deviceApplied(db, id, Math.min(3000, deadline - Date.now()))) return
86
- if (Date.now() >= deadline) throw CeroError.TIMED_OUT('recovery — writer not admitted')
87
- }
88
- }
89
-
90
- async function deviceApplied(db, id, timeout) {
42
+ // recovery has state to recover by definition — wait until replication has
43
+ // delivered and applied it (the genesis device row is the first thing every
44
+ // db writes), so a peer that never replicates fails loud instead of hanging
45
+ async function waitForBackfill(db, timeout) {
91
46
  const deadline = Date.now() + timeout
92
47
  while (Date.now() < deadline) {
93
- const { data } = await db.get('devices', id)
94
- if (data) return true
48
+ if (db.closing || db.closed) throw CeroError.CLOSED('Database')
49
+ const { data } = await db.get('devices')
50
+ if (data.length > 0) return
95
51
  await db.bee.update()
96
- await new Promise((resolve) => setTimeout(resolve, 25))
52
+ await new Promise((resolve) => setTimeout(resolve, 50))
97
53
  }
98
- return false
54
+ throw CeroError.TIMED_OUT('recovery — no peer replicated')
99
55
  }
100
56
 
101
- async function reopenBee(db) {
102
- if (db.network) db.network.detach(db.bee)
103
- await db.bee.close()
104
- db.bee = null
105
- await db.openBee()
57
+ // self-admission from the fresh core: the same master-signed add-writer op a
58
+ // live device would write, appended optimistically so an unadmitted writer
59
+ // can carry it — apply verifies the attestation and admits the core
60
+ async function claimWriter(db, writerKey, timeout) {
61
+ const encoded = db.spec.dispatch.encode(`@${db.ns}/add-writer`, {
62
+ master: db.identity.publicKey,
63
+ writer: writerKey,
64
+ sig: db.identity.sign(addWriterPayload(db.key, writerKey, db.writerKey)),
65
+ ts: Date.now()
66
+ })
67
+ await db.bee.append(wrap(db.version, encoded), { optimistic: true })
68
+ await db.bee.update()
69
+ if (!db.bee.writable) await db.whenWritable({ timeout })
70
+ }
71
+
72
+ async function saveDevice(db, writerKey, { name, isMobile }) {
73
+ const ts = Date.now()
74
+ await db.write([
75
+ [
76
+ 'set-device',
77
+ {
78
+ id: toId(writerKey),
79
+ name: name || null,
80
+ isMobile: isMobile === true,
81
+ createdAt: ts,
82
+ updatedAt: ts
83
+ }
84
+ ]
85
+ ])
106
86
  }
107
87
 
108
88
  async function saveWriter(db, writerKey, { name, isMobile }) {
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * First-run device provisioning: mint a device writer keypair, persist it as a
3
3
  * writer, then swap the autobee's local core over to it. With `recovering`,
4
- * waits for the first peer-replicated append before swapping.
4
+ * the swap happens FIRST and admission rides an optimistic append the
5
+ * backfilled genesis core is never appended to (autobee's contract: a writable
6
+ * core has exactly one author, ever; resuming a replicated core is unsupported
7
+ * and drops appends silently).
5
8
  *
6
9
  * @param {import('./index.js').Database} db
7
10
  * @param {{ name?: string | null, isMobile?: boolean, recovering?: boolean }} [opts]