@cero-base/core 1.3.0 → 1.5.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.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "cero p2p primitives — identity, storage, network, database, blobs, rpc, pairing.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -41,9 +41,13 @@
41
41
  "types": "./types/network/index.d.ts",
42
42
  "default": "./src/network/index.js"
43
43
  },
44
- "./network/bluetooth": {
45
- "types": "./types/network/bluetooth.d.ts",
46
- "default": "./src/network/bluetooth.js"
44
+ "./network/transports/ble": {
45
+ "types": "./types/network/transports/ble.d.ts",
46
+ "default": "./src/network/transports/ble.js"
47
+ },
48
+ "./network/transports/dht": {
49
+ "types": "./types/network/transports/dht.d.ts",
50
+ "default": "./src/network/transports/dht.js"
47
51
  },
48
52
  "./database": {
49
53
  "types": "./types/database/index.d.ts",
@@ -117,6 +121,10 @@
117
121
  "crypto": {
118
122
  "bare": "bare-crypto",
119
123
  "default": "crypto"
124
+ },
125
+ "events": {
126
+ "bare": "bare-events",
127
+ "default": "events"
120
128
  }
121
129
  },
122
130
  "scripts": {
@@ -125,28 +133,31 @@
125
133
  "build:types": "rm -rf types && tsc -p .",
126
134
  "pretest": "npm run build:test",
127
135
  "prepublishOnly": "npm run build:types",
128
- "test": "brittle-node test/*.test.js",
129
- "test:bare": "npx bare test/bare-smoke.js"
136
+ "test": "npm run test:node",
137
+ "test:bare": "brittle-bare test/bare.js test/*.test.js",
138
+ "pretest:bare": "npm run build:test",
139
+ "test:node": "brittle-node test/*.test.js"
130
140
  },
131
141
  "dependencies": {
132
142
  "@hyperswarm/secret-stream": "^6.9.1",
133
- "autobee": "^1.0.9",
143
+ "autobee": "^1.0.10",
134
144
  "b4a": "^1.8.1",
135
145
  "bare-crypto": "^1.15.3",
136
- "bare-fs": "^4.7.2",
137
- "bare-path": "^3.0.1",
146
+ "bare-fs": "^4.7.4",
147
+ "bare-path": "^3.1.1",
138
148
  "bip39-mnemonic": "^2.5.0",
139
149
  "blind-pairing": "^2.3.1",
140
- "compact-encoding": "^3.2.0",
141
- "corestore": "^7.10.1",
150
+ "blind-peering": "^2.5.0",
151
+ "compact-encoding": "^3.3.0",
152
+ "corestore": "^7.11.1",
142
153
  "framed-stream": "^1.0.1",
143
154
  "hrpc": "^4.3.0",
144
155
  "hyperblobs": "^2.12.1",
145
- "hypercore": "^11.33.1",
146
- "hypercore-blob-server": "^1.12.0",
156
+ "hypercore": "^11.34.1",
157
+ "hypercore-blob-server": "^1.15.0",
147
158
  "hypercore-crypto": "^3.7.0",
148
159
  "hypercore-id-encoding": "^1.3.0",
149
- "hypercore-storage": "^3.1.1",
160
+ "hypercore-storage": "^3.2.0",
150
161
  "hyperdb": "^6.7.0",
151
162
  "hyperdispatch": "^1.6.0",
152
163
  "hyperschema": "^1.21.0",
@@ -161,7 +172,13 @@
161
172
  },
162
173
  "devDependencies": {
163
174
  "@hyperswarm/testnet": "^3.1.4",
164
- "brittle": "^4.0.2",
175
+ "bare-abort-controller": "^1.1.2",
176
+ "bare-events": "^2.9.1",
177
+ "bare-fetch": "^3.0.3",
178
+ "bare-process": "^4.4.1",
179
+ "bare-url": "^2.2.1",
180
+ "blind-peer": "^3.12.2",
181
+ "brittle": "^4.1.0",
165
182
  "typescript": "^5.9.3"
166
183
  },
167
184
  "license": "Apache-2.0"
@@ -21,16 +21,39 @@ export async function bootstrap(db, { name, isMobile, recovering = false, timeou
21
21
  }
22
22
  const writerKey = Hypercore.key(manifest)
23
23
 
24
- if (recovering) await waitForFirstPeerAppend(db, timeout)
25
- await saveWriter(db, writerKey, { name, isMobile })
24
+ 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)
37
+ } else {
38
+ await saveWriter(db, writerKey, { name, isMobile })
39
+ }
26
40
  await swapWriter(db, keyPair, manifest)
27
41
 
28
42
  return { id: writerKey, writer: keyPair }
29
43
  }
30
44
 
31
45
  async function waitForFirstPeerAppend(db, timeout = 30000) {
32
- if (db.bee.local.length > 0 || !db.network?.swarm) return
33
- await new Promise((resolve, reject) => {
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) => {
34
57
  let done = false
35
58
  const finish = (err) => {
36
59
  if (done) return
@@ -43,7 +66,7 @@ async function waitForFirstPeerAppend(db, timeout = 30000) {
43
66
  }
44
67
  // timeout + close handling so a peer that never replicates can't hang the
45
68
  // recovery forever, and the append listener is always removed.
46
- const onAppend = () => db.bee.local.length > 0 && finish()
69
+ const onAppend = () => db.bee.local.length >= length && finish()
47
70
  const onClose = () => finish(CeroError.CLOSED('Database'))
48
71
  const timer = setTimeout(
49
72
  () => finish(CeroError.TIMED_OUT('recovery — no peer append')),
@@ -52,17 +75,46 @@ async function waitForFirstPeerAppend(db, timeout = 30000) {
52
75
  db.bee.local.on('append', onAppend)
53
76
  db.on('close', onClose)
54
77
  })
55
- await db.bee.update()
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) {
91
+ const deadline = Date.now() + timeout
92
+ while (Date.now() < deadline) {
93
+ const { data } = await db.get('devices', id)
94
+ if (data) return true
95
+ await db.bee.update()
96
+ await new Promise((resolve) => setTimeout(resolve, 25))
97
+ }
98
+ return false
99
+ }
100
+
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()
56
106
  }
57
107
 
58
108
  async function saveWriter(db, writerKey, { name, isMobile }) {
109
+ const ts = Date.now()
59
110
  await db.write([
60
111
  [
61
112
  'add-writer',
62
113
  {
63
114
  master: db.identity.publicKey,
64
115
  writer: writerKey,
65
- sig: db.identity.sign(addWriterPayload(db.key, writerKey, db.writerKey))
116
+ sig: db.identity.sign(addWriterPayload(db.key, writerKey, db.writerKey)),
117
+ ts
66
118
  }
67
119
  ],
68
120
  [
@@ -70,7 +122,9 @@ async function saveWriter(db, writerKey, { name, isMobile }) {
70
122
  {
71
123
  id: toId(writerKey),
72
124
  name: name || null,
73
- isMobile: isMobile === true
125
+ isMobile: isMobile === true,
126
+ createdAt: ts,
127
+ updatedAt: ts
74
128
  }
75
129
  ]
76
130
  ])
@@ -0,0 +1,120 @@
1
+ import { Readable } from 'streamx'
2
+ import b4a from 'b4a'
3
+
4
+ // the pinned hyperbee2 snapshot inside a hyperdb snapshot — internals reach
5
+ // (pear-sdk does the same); kept in one helper so an upstream rename is a
6
+ // one-line fix
7
+ const beeOf = (snap) => snap.engineSnapshot.snapshot
8
+
9
+ /**
10
+ * Pull-driven delta stream over a collection: batches of `{ prev, next }`
11
+ * pairs diffed between per-stream snapshot cursors. Update ticks only mark
12
+ * the stream dirty — the diff runs when the reader demands, so backpressure
13
+ * folds bursts into one bigger batch and nothing is ever dropped. The first
14
+ * batch, and any batch after the view is swapped or fast-forwarded to a new
15
+ * core, carries the full matching state as inserts with `reset: true`.
16
+ *
17
+ * @param {import('./index.js').Database} db
18
+ * @param {string} name Ref name (scopes the update ticks).
19
+ * @param {string} col Collection path (`@ns/name`).
20
+ * @param {(row: any) => boolean} matches
21
+ * @returns {import('streamx').Readable}
22
+ */
23
+ export function makeChanges(db, name, col, matches) {
24
+ let cursor = null
25
+ let cursorView = null
26
+ let dirty = true
27
+ let busy = false
28
+ let off = null
29
+
30
+ const stream = new Readable({
31
+ open(cb) {
32
+ off = db.onUpdate(mark, name)
33
+ pump()
34
+ cb(null)
35
+ },
36
+ read(cb) {
37
+ pump()
38
+ cb(null)
39
+ },
40
+ predestroy() {
41
+ if (off) off()
42
+ },
43
+ destroy(cb) {
44
+ release().then(() => cb(null), cb)
45
+ }
46
+ })
47
+
48
+ const mark = () => {
49
+ dirty = true
50
+ pump()
51
+ }
52
+
53
+ const pump = () => {
54
+ if (busy || !dirty || stream.destroyed) return
55
+ busy = true
56
+ run()
57
+ .catch((err) => stream.destroy(err))
58
+ .finally(() => {
59
+ busy = false
60
+ })
61
+ }
62
+
63
+ const run = async () => {
64
+ while (dirty && !stream.destroyed) {
65
+ dirty = false
66
+ const batch = await diff()
67
+ if (batch && stream.push(batch) === false) return
68
+ }
69
+ }
70
+
71
+ const diff = async () => {
72
+ const snap = db.view.snapshot()
73
+ try {
74
+ const fresh =
75
+ !cursor ||
76
+ cursorView !== db.view ||
77
+ !b4a.equals(beeOf(cursor).core.key, beeOf(snap).core.key)
78
+ const batch = fresh ? await initial(snap) : await delta(snap)
79
+ if (cursor) await cursor.close()
80
+ cursor = snap
81
+ cursorView = db.view
82
+ return batch
83
+ } catch (err) {
84
+ await snap.close().catch(() => {})
85
+ throw err
86
+ }
87
+ }
88
+
89
+ const initial = async (snap) => {
90
+ const changes = []
91
+ for (const next of await snap.find(col, {}).toArray()) {
92
+ if (matches(next)) changes.push({ prev: null, next })
93
+ }
94
+ return { changes, reset: true }
95
+ }
96
+
97
+ const delta = async (snap) => {
98
+ const collection = db.view.definition.resolveCollection(col)
99
+ const range = collection.encodeKeyRange({})
100
+ const changes = []
101
+ for await (const { left, right } of beeOf(cursor).createDiffStream(beeOf(snap), range)) {
102
+ const prev = left ? collection.reconstruct(db.view.versions, left.key, left.value) : null
103
+ const next = right ? collection.reconstruct(db.view.versions, right.key, right.value) : null
104
+ const from = prev && matches(prev) ? prev : null
105
+ const to = next && matches(next) ? next : null
106
+ if (!from && !to) continue
107
+ changes.push({ prev: from, next: to })
108
+ }
109
+ return changes.length ? { changes, reset: false } : null
110
+ }
111
+
112
+ const release = async () => {
113
+ if (!cursor) return
114
+ const held = cursor
115
+ cursor = null
116
+ await held.close().catch(() => {})
117
+ }
118
+
119
+ return stream
120
+ }
@@ -105,7 +105,7 @@ export function makeDispatcher(spec, ns, routes, onerror = safetyCatch, getDbKey
105
105
  if (!Identity.verify(op.master, addWriterPayload(ctx.dbKey, op.writer, ctx.key), op.sig)) return
106
106
  if (!(await isGenesis(ctx.view)) && !can(await getRole(ctx.view, op.master), INVITE)) return
107
107
  await ctx.host.addWriter(op.writer, { isIndexer: op.isIndexer !== false })
108
- const ts = Date.now()
108
+ const ts = op.ts || 0
109
109
  await insert(ctx.view, 'devices', `@${ns}/devices`, {
110
110
  id: toId(op.writer),
111
111
  memberId: toId(op.master),
@@ -131,7 +131,7 @@ export function makeDispatcher(spec, ns, routes, onerror = safetyCatch, getDbKey
131
131
  const memberId = toId(op.identity)
132
132
  if (!can(await getRole(ctx.view, op.identity), WRITE)) return
133
133
  await ctx.host.addWriter(op.writer, { isIndexer: true })
134
- const ts = Date.now()
134
+ const ts = op.ts || 0
135
135
  await insert(ctx.view, 'devices', `@${ns}/devices`, {
136
136
  id: toId(op.writer),
137
137
  memberId,
@@ -149,13 +149,13 @@ export function makeDispatcher(spec, ns, routes, onerror = safetyCatch, getDbKey
149
149
  await insert(ctx.view, 'members', `@${ns}/members`, op)
150
150
  const deviceId = toId(op.key)
151
151
  const existingDevice = await getDevice(ctx.view, deviceId)
152
- const ts = Date.now()
152
+ const ts = op.updatedAt || 0
153
153
  await insert(ctx.view, 'devices', `@${ns}/devices`, {
154
154
  id: deviceId,
155
155
  memberId: op.id,
156
156
  name: existingDevice?.name ?? null,
157
157
  isMobile: existingDevice?.isMobile ?? false,
158
- createdAt: existingDevice?.createdAt ?? ts,
158
+ createdAt: existingDevice?.createdAt || op.createdAt || ts,
159
159
  updatedAt: ts
160
160
  })
161
161
  })
@@ -203,11 +203,11 @@ export function makeDispatcher(spec, ns, routes, onerror = safetyCatch, getDbKey
203
203
  })
204
204
  add(`set-${b.verb}`, async (op, ctx) => {
205
205
  const existing = await getDevice(ctx.view, op.id)
206
- const ts = Date.now()
206
+ const ts = op.updatedAt || 0
207
207
  await insert(ctx.view, b.name, col, {
208
208
  ...op,
209
209
  memberId: existing?.memberId ?? op.memberId ?? null,
210
- createdAt: existing?.createdAt ?? ts,
210
+ createdAt: existing?.createdAt || op.createdAt || ts,
211
211
  updatedAt: ts
212
212
  })
213
213
  })
@@ -0,0 +1,32 @@
1
+ import b4a from 'b4a'
2
+ import c from 'compact-encoding'
3
+
4
+ // 0xff is the c.uint prefix for a 64-bit route id — unreachable for real
5
+ // hyperdispatch routes, so it safely marks a version-enveloped op. Peers too
6
+ // old to know the envelope throw on it and skip deterministically.
7
+ const SENTINEL = 0xff
8
+
9
+ /**
10
+ * Prefix an encoded op with the app's contract version.
11
+ *
12
+ * @param {number} version
13
+ * @param {Uint8Array} body
14
+ * @returns {Uint8Array}
15
+ */
16
+ export function wrap(version, body) {
17
+ return b4a.concat([b4a.from([SENTINEL]), c.encode(c.uint, version), body])
18
+ }
19
+
20
+ /**
21
+ * Split an op into contract version and payload. Ops written before the
22
+ * envelope existed carry no sentinel and read as version 0.
23
+ *
24
+ * @param {Uint8Array} buf
25
+ * @returns {{ version: number, body: Uint8Array }}
26
+ */
27
+ export function unwrap(buf) {
28
+ if (buf.byteLength === 0 || buf[0] !== SENTINEL) return { version: 0, body: buf }
29
+ const state = { buffer: buf, start: 1, end: buf.byteLength }
30
+ const version = c.uint.decode(state)
31
+ return { version, body: buf.subarray(state.start) }
32
+ }
@@ -1,5 +1,6 @@
1
1
  import Autobee from 'autobee'
2
2
  import HyperDB from 'hyperdb'
3
+ import c from 'compact-encoding'
3
4
  import Hypercore from 'hypercore'
4
5
  import ReadyResource from 'ready-resource'
5
6
  import safetyCatch from 'safety-catch'
@@ -7,8 +8,10 @@ import b4a from 'b4a'
7
8
 
8
9
  import { NAMESPACE, SINGLE, COLLECTION, ACTION, ACTIVE, PASSIVE } from '../lib/constants.js'
9
10
  import { genId, subscribe, addWriterPayload, claimWriterPayload } from '../lib/utils.js'
11
+ import { wrap, unwrap } from './envelope.js'
10
12
  import { CeroError } from '../lib/errors.js'
11
13
  import { bootstrap } from './bootstrap.js'
14
+ import { makeChanges } from './changes.js'
12
15
  import { makeDispatcher } from './dispatch.js'
13
16
 
14
17
  /**
@@ -57,6 +60,8 @@ export class Database extends ReadyResource {
57
60
  this.meta = opts.spec.meta || { ns: NAMESPACE, refs: {} }
58
61
  this.ns = this.meta.ns || NAMESPACE
59
62
  this.refs = this.meta.refs || {}
63
+ this.version = this.meta.version || 1
64
+ this.behind = null
60
65
  this.routes = opts.routes || {}
61
66
  this.namespace = opts.namespace || NAMESPACE
62
67
  this.encryptionKey = opts.encryptionKey || opts.identity.encryptionKey || null
@@ -124,9 +129,21 @@ export class Database extends ReadyResource {
124
129
  wakeup: wakeup || undefined,
125
130
  open: (b) => HyperDB.bee2(b, this.spec.database, { autoUpdate: true }),
126
131
  apply: async (nodes, view, host) => {
127
- const result = await (this.applyOverride || this.dispatcher.apply)(nodes, view, host)
128
- if (this.updaters.size) this._touch(nodes)
129
- if (this.onApplyHooks.size) this._observe(nodes)
132
+ // envelope handling lives here, once: unwrap every node and skip ops
133
+ // from a newer app version (deterministic — they stay in the log), so
134
+ // the dispatcher AND custom apply overrides see plain op bytes
135
+ const ready = []
136
+ for (const node of nodes) {
137
+ const { version, body } = unwrap(node.value)
138
+ if (version > this.version) {
139
+ this._onFuture(version)
140
+ continue
141
+ }
142
+ ready.push(version === 0 ? node : { ...node, value: body })
143
+ }
144
+ const result = await (this.applyOverride || this.dispatcher.apply)(ready, view, host)
145
+ if (this.updaters.size) this._touch(ready)
146
+ if (this.onApplyHooks.size) this._observe(ready)
130
147
  return result
131
148
  },
132
149
  update: async (db) => {
@@ -149,6 +166,21 @@ export class Database extends ReadyResource {
149
166
  this.bee = bee
150
167
  this.key = bee.key
151
168
 
169
+ const behind = await bee.local.getUserData('cero/behind')
170
+ this.behind = behind ? c.decode(c.uint, behind) : null
171
+
172
+ // ops we once skipped are now within our contract — wipe the boot record
173
+ // so the next open replays the whole log through the current handlers
174
+ if (this.behind !== null && this.behind <= this.version) {
175
+ await bee.local.setUserData('autobee/head', null)
176
+ await bee.local.setUserData('cero/behind', null)
177
+ await bee.close()
178
+ this.behind = null
179
+ await this.openBee()
180
+ this.emit('rebuild')
181
+ return
182
+ }
183
+
152
184
  bee.on('writable', () => this.emit('writable'))
153
185
  // without a listener autobee escalates apply/view errors to a process crash
154
186
  bee.on('error', this._onerror)
@@ -503,7 +535,49 @@ export class Database extends ReadyResource {
503
535
  const encoded = ops.map(([op, payload]) =>
504
536
  this.spec.dispatch.encode(`@${this.ns}/${op}`, payload)
505
537
  )
506
- await this.bee.append(encoded.length === 1 ? encoded[0] : encoded)
538
+ await this._dryRun(encoded)
539
+ const wrapped = encoded.map((value) => wrap(this.version, value))
540
+ await this.bee.append(wrapped.length === 1 ? wrapped[0] : wrapped)
541
+ }
542
+
543
+ /**
544
+ * Record that the log contains ops from a newer app version than this
545
+ * peer understands. Fires `'behind'` once per version so apps can prompt
546
+ * an upgrade; the marker survives restarts via local core userData.
547
+ *
548
+ * @param {number} version
549
+ */
550
+ _onFuture(version) {
551
+ if (this.behind !== null && version <= this.behind) return
552
+ this.behind = version
553
+ this.bee.local.setUserData('cero/behind', c.encode(c.uint, version)).catch(this._onerror)
554
+ this.emit('behind', version)
555
+ }
556
+
557
+ /**
558
+ * Validate ops against the committed view before appending: each op runs in
559
+ * a throwaway transaction with host effects stubbed, so a throwing handler
560
+ * rejects the write and nothing enters the permanent log. Permission gates
561
+ * that `return` are not rejections — apply stays the authority at
562
+ * linearization time.
563
+ *
564
+ * @param {Uint8Array[]} encoded
565
+ */
566
+ async _dryRun(encoded) {
567
+ const tx = this.view.transaction()
568
+ const host = { addWriter: async () => {}, removeWriter: async () => {} }
569
+ try {
570
+ for (const value of encoded) {
571
+ await this.dispatcher.dispatcher.dispatch(value, {
572
+ view: tx,
573
+ host,
574
+ key: this.writerKey,
575
+ dbKey: this.key
576
+ })
577
+ }
578
+ } finally {
579
+ await tx.close()
580
+ }
507
581
  }
508
582
 
509
583
  /**
@@ -634,6 +708,31 @@ export class Database extends ReadyResource {
634
708
  })
635
709
  }
636
710
 
711
+ /**
712
+ * Delta subscription: batches of `{ prev, next }` row pairs instead of
713
+ * full snapshots. The first batch (and any batch after a view swap) carries
714
+ * the current matching rows as inserts with `reset: true` — replaying every
715
+ * batch into a Map keyed by row id always reconstructs current state.
716
+ * `limit`/`reverse` are not applied; deltas are unwindowed by design.
717
+ *
718
+ * @param {string} name
719
+ * @param {Query} [query]
720
+ * @returns {import('streamx').Readable}
721
+ */
722
+ changes(name, query = {}) {
723
+ this.guard()
724
+ const col = `@${this.ns}/${name}`
725
+ if (!this.view.definition.resolveCollection(col)) {
726
+ throw CeroError.UNKNOWN('collection', name)
727
+ }
728
+ const filter = { ...query }
729
+ delete filter.limit
730
+ delete filter.reverse
731
+ delete filter.total
732
+ const matches = (row) => paginate([row], filter).length > 0
733
+ return makeChanges(this, name, col, matches)
734
+ }
735
+
637
736
  /**
638
737
  * First-run bootstrap: create the device writer, save it, and swap into it.
639
738
  *
@@ -661,9 +760,10 @@ export class Database extends ReadyResource {
661
760
  const encoded = this.spec.dispatch.encode(`@${this.ns}/claim-writer`, {
662
761
  identity: this.identity.publicKey,
663
762
  writer: writerKey,
664
- sig
763
+ sig,
764
+ ts: Date.now()
665
765
  })
666
- await this.bee.append(encoded, { optimistic: true })
766
+ await this.bee.append(wrap(this.version, encoded), { optimistic: true })
667
767
  await this.bee.update()
668
768
  if (!this.bee.writable) await this.whenWritable()
669
769
  }
@@ -826,7 +926,9 @@ export class Database extends ReadyResource {
826
926
  signers: [{ publicKey }]
827
927
  })
828
928
  const sig = this.identity.sign(addWriterPayload(this.key, writerKey, this.writerKey))
829
- await this.write([[verb, { master: this.identity.publicKey, writer: writerKey, sig }]])
929
+ await this.write([
930
+ [verb, { master: this.identity.publicKey, writer: writerKey, sig, ts: Date.now() }]
931
+ ])
830
932
  await this.runAfter(hook, ctx)
831
933
  }
832
934
  }
@@ -1,12 +1,12 @@
1
1
  // This file is autogenerated by the hyperschema compiler
2
- // Schema Version: 1
2
+ // Schema Version: 2
3
3
  /* eslint-disable camelcase */
4
4
  /* eslint-disable quotes */
5
5
  /* eslint-disable space-before-function-paren */
6
6
 
7
7
  import { c } from 'hyperschema/runtime'
8
8
 
9
- const VERSION = 1
9
+ const VERSION = 2
10
10
 
11
11
  // eslint-disable-next-line no-unused-vars
12
12
  let version = VERSION
@@ -274,6 +274,37 @@ const encoding6 = {
274
274
  }
275
275
  }
276
276
 
277
+ // @cero/changes.prev
278
+ const encoding7_0 = encoding3_0
279
+ // @cero/changes.next
280
+ const encoding7_1 = encoding3_0
281
+
282
+ // @cero/changes
283
+ const encoding7 = {
284
+ preencode(state, m) {
285
+ state.end++ // max flag is 2 so always one byte
286
+
287
+ if (version >= 2 && m.prev) encoding7_0.preencode(state, m.prev)
288
+ if (version >= 2 && m.next) encoding7_1.preencode(state, m.next)
289
+ },
290
+ encode(state, m) {
291
+ const flags = ((version >= 2 && m.prev) ? 1 : 0) | ((version >= 2 && m.next) ? 2 : 0)
292
+
293
+ c.uint.encode(state, flags)
294
+
295
+ if (version >= 2 && m.prev) encoding7_0.encode(state, m.prev)
296
+ if (version >= 2 && m.next) encoding7_1.encode(state, m.next)
297
+ },
298
+ decode(state) {
299
+ const flags = c.uint.decode(state)
300
+
301
+ return {
302
+ prev: (version >= 2 && (flags & 1) !== 0) ? encoding7_0.decode(state) : null,
303
+ next: (version >= 2 && (flags & 2) !== 0) ? encoding7_1.decode(state) : null
304
+ }
305
+ }
306
+ }
307
+
277
308
  function setVersion(v) {
278
309
  version = v
279
310
  }
@@ -311,6 +342,8 @@ function getEncoding(name) {
311
342
  return encoding5
312
343
  case '@cero/blob-id':
313
344
  return encoding6
345
+ case '@cero/changes':
346
+ return encoding7
314
347
  default:
315
348
  throw new Error('Encoder not found ' + name)
316
349
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": 1,
2
+ "version": 2,
3
3
  "schema": [
4
4
  {
5
5
  "name": "invite-body",
@@ -273,6 +273,28 @@
273
273
  "version": 1
274
274
  }
275
275
  ]
276
+ },
277
+ {
278
+ "name": "changes",
279
+ "namespace": "cero",
280
+ "compact": false,
281
+ "flagsPosition": 0,
282
+ "fields": [
283
+ {
284
+ "name": "prev",
285
+ "required": false,
286
+ "array": true,
287
+ "type": "buffer",
288
+ "version": 2
289
+ },
290
+ {
291
+ "name": "next",
292
+ "required": false,
293
+ "array": true,
294
+ "type": "buffer",
295
+ "version": 2
296
+ }
297
+ ]
276
298
  }
277
299
  ]
278
300
  }