@cero-base/core 0.5.2 → 0.7.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": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "description": "cero p2p primitives — identity, storage, network, database, blobs, rpc, pairing.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -1,5 +1,6 @@
1
1
  import Hypercore from 'hypercore'
2
2
  import { Identity } from '../identity/index.js'
3
+ import { toId } from '../lib/utils.js'
3
4
 
4
5
  /**
5
6
  * First-run device provisioning: mint a device writer keypair, persist it as a
@@ -45,7 +46,13 @@ async function saveWriter(db, writerKey, { name, isMobile }) {
45
46
  {
46
47
  master: db.identity.publicKey,
47
48
  writer: writerKey,
48
- sig: db.identity.sign(writerKey),
49
+ sig: db.identity.sign(writerKey)
50
+ }
51
+ ],
52
+ [
53
+ 'set-device',
54
+ {
55
+ id: toId(writerKey),
49
56
  name: name || null,
50
57
  isMobile: isMobile === true
51
58
  }
@@ -37,6 +37,11 @@ export function makeDispatcher(spec, ns, routes) {
37
37
  if (kind === COLLECTION) await insert(ctx.view, name, col, op)
38
38
  else await ctx.view.insert(col, op)
39
39
  }
40
+ const update = (name, col) => async (op, ctx) => {
41
+ const existing = await ctx.view.get(col, { id: op.id })
42
+ if (!existing) return
43
+ await insert(ctx.view, name, col, { ...existing, ...op })
44
+ }
40
45
  const remove = (col) => async (op, ctx) => ctx.view.delete(col, op)
41
46
  const add = (verb, fn) => dispatcher.add(`@${ns}/${verb}`, fn)
42
47
 
@@ -47,8 +52,6 @@ export function makeDispatcher(spec, ns, routes) {
47
52
  await insert(ctx.view, 'devices', `@${ns}/devices`, {
48
53
  id: toId(op.writer),
49
54
  memberId: toId(op.master),
50
- name: op.name || null,
51
- isMobile: op.isMobile === true,
52
55
  createdAt: ts,
53
56
  updatedAt: ts
54
57
  })
@@ -69,8 +72,6 @@ export function makeDispatcher(spec, ns, routes) {
69
72
  await insert(ctx.view, 'devices', `@${ns}/devices`, {
70
73
  id: toId(op.writer),
71
74
  memberId,
72
- name: op.name || null,
73
- isMobile: op.isMobile === true,
74
75
  createdAt: ts,
75
76
  updatedAt: ts
76
77
  })
@@ -120,12 +121,18 @@ export function makeDispatcher(spec, ns, routes) {
120
121
  })
121
122
  add(`set-${b.verb}`, async (op, ctx) => {
122
123
  const existing = await device(ctx.view, op.id)
123
- await insert(ctx.view, b.name, col, { ...op, memberId: existing?.memberId ?? null })
124
+ const ts = Date.now()
125
+ await insert(ctx.view, b.name, col, {
126
+ ...op,
127
+ memberId: existing?.memberId ?? op.memberId ?? null,
128
+ createdAt: existing?.createdAt ?? ts,
129
+ updatedAt: ts
130
+ })
124
131
  })
125
132
  continue
126
133
  }
127
134
  add(`add-${b.verb}`, upsert(b.name, col, COLLECTION))
128
- add(`set-${b.verb}`, upsert(b.name, col, COLLECTION))
135
+ add(`set-${b.verb}`, update(b.name, col))
129
136
  add(`del-${b.verb}`, remove(col))
130
137
  }
131
138
 
@@ -138,7 +145,7 @@ export function makeDispatcher(spec, ns, routes) {
138
145
  }
139
146
  const col = `@${ns}/${name}`
140
147
  const kind = info.kind === SINGLE ? SINGLE : COLLECTION
141
- add(`set-${name}`, upsert(name, col, kind))
148
+ add(`set-${name}`, kind === SINGLE ? upsert(name, col, kind) : update(name, col))
142
149
  if (info.kind === SINGLE) continue
143
150
  add(`add-${name}`, upsert(name, col, COLLECTION))
144
151
  add(`del-${name}`, remove(col))
@@ -243,13 +243,16 @@ export class Database extends ReadyResource {
243
243
  }
244
244
 
245
245
  /**
246
- * Upsert by merging with the existing row, preserving `createdAt`.
246
+ * Upsert by merging with the existing row, preserving `createdAt`. Pass
247
+ * `{ upsert: false }` to update-only — a missing row is left untouched
248
+ * (checked atomically in the dispatch, so it never resurrects a deleted row).
247
249
  *
248
250
  * @param {string} name
249
251
  * @param {Record<string, any>} row
252
+ * @param {{ upsert?: boolean }} [opts]
250
253
  * @returns {Promise<SingleResult | null>}
251
254
  */
252
- async set(name, row) {
255
+ async set(name, row, { upsert = true } = {}) {
253
256
  this.guard()
254
257
  const ref = this.ref(name)
255
258
  const col = this.col(ref)
@@ -260,6 +263,7 @@ export class Database extends ReadyResource {
260
263
  : row?.id
261
264
  ? ((await this.view.get(col, { id: row.id })) ?? null)
262
265
  : null
266
+ if (!upsert && !existing) return null
263
267
  const stored = {
264
268
  ...existing,
265
269
  ...row,
@@ -271,7 +275,11 @@ export class Database extends ReadyResource {
271
275
  const ctx = { op: 'set', name, row: stored }
272
276
  if ((await this.runBefore('set', ctx)) === false) return null
273
277
 
274
- await this.write([[`set-${ref.verb}`, ctx.row]])
278
+ // member/device have bespoke set handlers — only generic collections upsert via add-
279
+ const special = ref.verb === 'member' || ref.verb === 'device'
280
+ const verb =
281
+ ref.kind === COLLECTION && upsert && !special ? `add-${ref.verb}` : `set-${ref.verb}`
282
+ await this.write([[verb, ctx.row]])
275
283
 
276
284
  const result = { data: ctx.row }
277
285
  await this.runAfter('set', { ...ctx, result })
@@ -422,12 +430,12 @@ export class Database extends ReadyResource {
422
430
 
423
431
  /**
424
432
  * Claim writership on an existing room by signing our writer key with the
425
- * member identity and appending optimistically.
433
+ * member identity and appending optimistically. Admission only — the device
434
+ * is named separately (bootstrap → set-device).
426
435
  *
427
- * @param {{ name?: string | null, isMobile?: boolean }} [opts]
428
436
  * @returns {Promise<void>}
429
437
  */
430
- async claim({ name = null, isMobile = false } = {}) {
438
+ async claim() {
431
439
  this.guard()
432
440
  if (this.bee.writable) return
433
441
 
@@ -436,9 +444,7 @@ export class Database extends ReadyResource {
436
444
  const encoded = this.spec.dispatch.encode(`@${this.ns}/claim-writer`, {
437
445
  identity: this.identity.publicKey,
438
446
  writer: writerKey,
439
- sig,
440
- name,
441
- isMobile
447
+ sig
442
448
  })
443
449
  await this.bee.append(encoded, { optimistic: true })
444
450
  await this.bee.update()
@@ -549,12 +555,7 @@ export class Database extends ReadyResource {
549
555
  signers: [{ publicKey }]
550
556
  })
551
557
  const sig = this.identity.sign(writerKey)
552
- await this.write([
553
- [
554
- verb,
555
- { master: this.identity.publicKey, writer: writerKey, sig, name: null, isMobile: false }
556
- ]
557
- ])
558
+ await this.write([[verb, { master: this.identity.publicKey, writer: writerKey, sig }]])
558
559
  await this.runAfter(hook, ctx)
559
560
  }
560
561
  }
package/src/lib/schema.js CHANGED
@@ -2,7 +2,7 @@ import { CeroError } from './errors.js'
2
2
  import { SINGLE, COLLECTION, ACTION } from './constants.js'
3
3
 
4
4
  /**
5
- * @typedef {{ prim: string }} Prim
5
+ * @typedef {{ prim: string, required?: boolean }} Prim
6
6
  * @typedef {{ kind: 'single' | 'collection' | 'action', fields: Record<string, Prim> }} TypeDef
7
7
  * @typedef {{ [name: string]: TypeDef | Record<string, TypeDef> } & { local?: Record<string, TypeDef> }} SchemaDefs
8
8
  * @typedef {{ defs: SchemaDefs }} Schema
@@ -25,6 +25,14 @@ export const t = {
25
25
  fixed32: prim('fixed32'),
26
26
  fixed64: prim('fixed64'),
27
27
 
28
+ /**
29
+ * Mark a field as required. Fields are optional by default.
30
+ *
31
+ * @param {Prim} marker
32
+ * @returns {Prim}
33
+ */
34
+ required: (marker) => ({ ...marker, required: true }),
35
+
28
36
  /**
29
37
  * Single-row table (one record, set/get by name).
30
38
  *
@@ -53,6 +61,17 @@ export const t = {
53
61
  */
54
62
  action(fields) {
55
63
  return { kind: ACTION, fields }
64
+ },
65
+
66
+ /**
67
+ * Add fields to a builtin type (members, devices, …). Merged into the
68
+ * builtin's base fields at build time; redeclaring a base field throws.
69
+ *
70
+ * @param {Record<string, Prim>} fields
71
+ * @returns {{ kind: 'extend', fields: Record<string, Prim> }}
72
+ */
73
+ extend(fields) {
74
+ return { kind: 'extend', fields }
56
75
  }
57
76
  }
58
77
 
@@ -126,13 +126,18 @@ export class Database extends ReadyResource {
126
126
  */
127
127
  put(name: string, row: Record<string, any>): Promise<SingleResult | null>;
128
128
  /**
129
- * Upsert by merging with the existing row, preserving `createdAt`.
129
+ * Upsert by merging with the existing row, preserving `createdAt`. Pass
130
+ * `{ upsert: false }` to update-only — a missing row is left untouched
131
+ * (checked atomically in the dispatch, so it never resurrects a deleted row).
130
132
  *
131
133
  * @param {string} name
132
134
  * @param {Record<string, any>} row
135
+ * @param {{ upsert?: boolean }} [opts]
133
136
  * @returns {Promise<SingleResult | null>}
134
137
  */
135
- set(name: string, row: Record<string, any>): Promise<SingleResult | null>;
138
+ set(name: string, row: Record<string, any>, { upsert }?: {
139
+ upsert?: boolean;
140
+ }): Promise<SingleResult | null>;
136
141
  /**
137
142
  * Delete by id (collection) or wipe the whole single-row table.
138
143
  *
@@ -210,15 +215,12 @@ export class Database extends ReadyResource {
210
215
  }>;
211
216
  /**
212
217
  * Claim writership on an existing room by signing our writer key with the
213
- * member identity and appending optimistically.
218
+ * member identity and appending optimistically. Admission only — the device
219
+ * is named separately (bootstrap → set-device).
214
220
  *
215
- * @param {{ name?: string | null, isMobile?: boolean }} [opts]
216
221
  * @returns {Promise<void>}
217
222
  */
218
- claim({ name, isMobile }?: {
219
- name?: string | null;
220
- isMobile?: boolean;
221
- }): Promise<void>;
223
+ claim(): Promise<void>;
222
224
  /**
223
225
  * Resolve once the bee becomes writable, or reject after `timeout` ms.
224
226
  *
@@ -14,6 +14,7 @@ export namespace t {
14
14
  let json: Prim;
15
15
  let fixed32: Prim;
16
16
  let fixed64: Prim;
17
+ function required(marker: Prim): Prim;
17
18
  /**
18
19
  * Single-row table (one record, set/get by name).
19
20
  *
@@ -35,9 +36,21 @@ export namespace t {
35
36
  * @returns {TypeDef}
36
37
  */
37
38
  function action(fields: Record<string, Prim>): TypeDef;
39
+ /**
40
+ * Add fields to a builtin type (members, devices, …). Merged into the
41
+ * builtin's base fields at build time; redeclaring a base field throws.
42
+ *
43
+ * @param {Record<string, Prim>} fields
44
+ * @returns {{ kind: 'extend', fields: Record<string, Prim> }}
45
+ */
46
+ function extend(fields: Record<string, Prim>): {
47
+ kind: "extend";
48
+ fields: Record<string, Prim>;
49
+ };
38
50
  }
39
51
  export type Prim = {
40
52
  prim: string;
53
+ required?: boolean;
41
54
  };
42
55
  export type TypeDef = {
43
56
  kind: "single" | "collection" | "action";