@cero-base/cero 1.18.2 → 2.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.
Files changed (44) hide show
  1. package/README.md +38 -644
  2. package/package.json +16 -12
  3. package/src/build/index.js +21 -50
  4. package/src/build/internal.js +121 -0
  5. package/src/build/schemas.js +2 -8
  6. package/src/extensions/handle-sync.js +3 -10
  7. package/src/extensions/index.js +6 -0
  8. package/src/extensions/profile-sync.js +0 -5
  9. package/src/handle/index.js +255 -300
  10. package/src/index.js +65 -112
  11. package/src/lib/bluetooth.js +25 -56
  12. package/src/lib/constants.js +0 -15
  13. package/src/lib/operators.js +24 -74
  14. package/src/lib/peek.js +4 -8
  15. package/src/lib/refs.js +46 -0
  16. package/src/lib/spec.js +2 -3
  17. package/src/local/index.js +4 -5
  18. package/src/rpc/client.js +27 -39
  19. package/src/rpc/index.js +3 -3
  20. package/src/rpc/server.js +29 -40
  21. package/types/build/index.d.ts +19 -7
  22. package/types/build/internal.d.ts +78 -0
  23. package/types/build/schemas.d.ts +3 -3
  24. package/types/extensions/handle-sync.d.ts +4 -9
  25. package/types/extensions/index.d.ts +24 -2
  26. package/types/extensions/profile-sync.d.ts +2 -6
  27. package/types/handle/index.d.ts +218 -254
  28. package/types/index.d.ts +78 -102
  29. package/types/lib/bluetooth.d.ts +24 -46
  30. package/types/lib/constants.d.ts +5 -16
  31. package/types/lib/operators.d.ts +49 -77
  32. package/types/lib/peek.d.ts +3 -4
  33. package/types/lib/refs.d.ts +36 -0
  34. package/types/lib/spec.d.ts +5 -1
  35. package/types/local/index.d.ts +24 -23
  36. package/types/rpc/client.d.ts +107 -127
  37. package/types/rpc/index.d.ts +7 -2
  38. package/types/rpc/server.d.ts +62 -76
  39. package/src/build/builtins.js +0 -174
  40. package/src/lib/internal.js +0 -9
  41. package/src/lib/utils.js +0 -67
  42. package/types/build/builtins.d.ts +0 -100
  43. package/types/lib/internal.d.ts +0 -24
  44. package/types/lib/utils.d.ts +0 -55
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@cero-base/cero",
3
- "version": "1.18.2",
3
+ "version": "2.0.0",
4
4
  "description": "The ideal p2p API — everything is a handle, handles contain refs, refs contain rows.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "engines": {
8
- "node": ">=20"
8
+ "node": ">=22"
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
@@ -18,8 +18,7 @@
18
18
  "src",
19
19
  "types",
20
20
  "README.md",
21
- "LICENSE",
22
- "!**/CLAUDE.md"
21
+ "LICENSE"
23
22
  ],
24
23
  "publishConfig": {
25
24
  "access": "public"
@@ -82,31 +81,36 @@
82
81
  "events": {
83
82
  "bare": "bare-events",
84
83
  "default": "events"
84
+ },
85
+ "process": {
86
+ "bare": "bare-process",
87
+ "default": "process"
85
88
  }
86
89
  },
87
90
  "scripts": {
88
- "build:test": "rm -rf test/fixture/spec && node test/fixture/build.js",
91
+ "build:test": "rm -rf test/fixtures/spec && node test/fixtures/build.js",
89
92
  "build:types": "rm -rf types && tsc -p .",
90
93
  "pretest": "npm run build:test",
91
94
  "prepublishOnly": "npm run build:types",
92
95
  "test": "npm run test:bare",
93
96
  "pretest:bare": "npm run build:test",
94
- "test:bare": "ls test/*.test.js | xargs -P1 -n1 brittle-bare test/bare.js",
95
- "test:node": "ls test/*.test.js | xargs -P1 -n1 brittle-node"
97
+ "test:bare": "find test -name '*.test.js' | sort | xargs -P1 -n1 brittle-bare",
98
+ "test:node": "find test -name '*.test.js' | sort | xargs -P1 -n1 brittle-node"
96
99
  },
97
100
  "dependencies": {
98
- "@cero-base/core": "^1.18.2",
101
+ "@cero-base/core": "^2.0.0",
99
102
  "b4a": "^1.8.1",
100
103
  "bare-abort-controller": "^1.1.2",
101
104
  "bare-crypto": "^1.15.3",
102
105
  "bare-fs": "^4.8.1",
103
106
  "bare-path": "^3.1.2",
104
107
  "ble-swarm": "^2.3.0",
105
- "compact-encoding": "^3.3.2",
106
- "corestore": "^7.12.2",
108
+ "compact-encoding": "^3.4.0",
109
+ "corestore": "^7.12.3",
107
110
  "hrpc": "^4.3.1",
108
- "hypercore": "^11.35.2",
111
+ "hypercore": "^11.35.4",
109
112
  "hypercore-crypto": "^3.7.0",
113
+ "hypercore-id-encoding": "^1.3.0",
110
114
  "hypercore-storage": "^3.2.1",
111
115
  "hyperdb": "^6.9.0",
112
116
  "hyperdispatch": "^1.6.0",
@@ -124,7 +128,7 @@
124
128
  "bare-process": "^4.5.1",
125
129
  "bare-url": "^2.5.4",
126
130
  "brittle": "^4.1.1",
127
- "typescript": "^5.9.3"
131
+ "typescript": "^7.0.2"
128
132
  },
129
133
  "license": "Apache-2.0"
130
134
  }
@@ -9,18 +9,8 @@ import HRPCBuilder from 'hrpc'
9
9
  import { CeroError } from '@cero-base/core/errors'
10
10
 
11
11
  import { NS } from '../lib/constants.js'
12
- import { internal } from '../lib/internal.js'
13
- import {
14
- refs,
15
- builtinRefs,
16
- builtinTypes,
17
- builtinCollections,
18
- builtinDispatches,
19
- rotateDispatch,
20
- rpcTypes,
21
- rpcCommands,
22
- getHyperdbType
23
- } from './builtins.js'
12
+ import { registry } from '../extensions/index.js'
13
+ import * as internal from './internal.js'
24
14
 
25
15
  /**
26
16
  * @typedef {import('@cero-base/core/schema').Schema} Schema
@@ -34,29 +24,22 @@ import {
34
24
  /**
35
25
  * Compile a cero schema into wire-level artifacts and write them to disk.
36
26
  *
37
- * Emits a `main/` tree (schema + hyperdb + dispatch + rpc), a `local/` tree
38
- * for per-device data, one `handles/<name>/` tree per child handle type, and
39
- * an `index.js` that re-exports a ready-to-use `spec` object.
40
- *
41
27
  * @param {string} specDir Output directory.
42
28
  * @param {SchemaInput} schema Either a `schema(...)` wrapper or its raw defs object.
43
29
  * @param {BuildOpts} [opts]
44
30
  * @returns {Promise<void>}
45
31
  */
46
- export { getHyperdbType } from './builtins.js'
47
-
48
32
  export async function build(specDir, schema, { ns = NS, extensions = true } = {}) {
49
33
  const raw = /** @type {SchemaDefs & { local?: SchemaDefs }} */ (schema?.defs || schema)
50
34
  if (!raw || typeof raw !== 'object') throw CeroError.REQUIRED('schema')
51
35
 
52
- // Pull out `t.extend(...)` entries (keyed by builtin ref name) builtin type
53
- // name, merged across scopes. App schema first, then each `cero.use()` extension.
36
+ // t.extend entries by internal type: app schema first, then each extension
54
37
  const extend = {}
55
38
  const defs = {}
56
39
  const collect = (entries, fromExt = false) => {
57
40
  for (const [k, v] of Object.entries(entries)) {
58
41
  if (v && v.kind === 'extend') {
59
- const type = refs.main[k]?.type
42
+ const type = internal.defs.main[k]?.type
60
43
  if (!type) throw CeroError.INVALID(`'${k}' is not an extendable builtin`)
61
44
  // app schema wins on field conflicts, extensions only add new fields
62
45
  extend[type] = fromExt ? { ...v.fields, ...extend[type] } : { ...extend[type], ...v.fields }
@@ -66,7 +49,7 @@ export async function build(specDir, schema, { ns = NS, extensions = true } = {}
66
49
  }
67
50
  }
68
51
  collect(raw)
69
- for (const ext of internal.extensions) {
52
+ for (const ext of registry) {
70
53
  if (ext.bundled && extensions === false) continue
71
54
  if (ext.schema) collect(ext.schema, true)
72
55
  }
@@ -93,7 +76,7 @@ export async function build(specDir, schema, { ns = NS, extensions = true } = {}
93
76
  )
94
77
  }
95
78
 
96
- // Hang a `type: <name>` marker on each handle ref in main so the runtime knows which type to open.
79
+ // the type marker tells the runtime which handle type to open
97
80
  for (const [name] of Object.entries(handles)) {
98
81
  if (meta.refs[name]) meta.refs[name] = { kind: 'handle', type: name, schema: `@${ns}/handle` }
99
82
  }
@@ -138,7 +121,7 @@ function compile(root, ns, scope = 'main') {
138
121
  scope
139
122
  }
140
123
 
141
- Object.assign(ctx.meta.refs, builtinRefs(ns, scope))
124
+ Object.assign(ctx.meta.refs, internal.refs(ns, scope))
142
125
 
143
126
  for (const [name, node] of Object.entries(root)) {
144
127
  if (node.kind === 'handle') {
@@ -166,13 +149,13 @@ function fileFieldNames(fields) {
166
149
  function register(name, node, ctx) {
167
150
  const fqn = `@${ctx.ns}/${name}`
168
151
  if (node.kind === 'action') {
169
- ctx.types.push({ name, compact: false, fields: fieldsFor(node.fields) })
152
+ ctx.types.push({ name, compact: false, fields: internal.fields(node.fields) })
170
153
  ctx.dispatches.push({ name, requestType: fqn })
171
154
  ctx.meta.refs[name] = { kind: 'action', path: [name], schema: fqn }
172
155
  return
173
156
  }
174
157
  if (node.kind === 'single') {
175
- ctx.types.push({ name, compact: false, fields: fieldsFor(node.fields) })
158
+ ctx.types.push({ name, compact: false, fields: internal.fields(node.fields) })
176
159
  ctx.collections.push({ name, schema: fqn, key: [] })
177
160
  ctx.dispatches.push({ name: `set-${name}`, requestType: fqn })
178
161
  ctx.dispatches.push({ name: `del-${name}`, requestType: `@${ctx.ns}/del-by-id` })
@@ -192,7 +175,7 @@ function register(name, node, ctx) {
192
175
  { name: 'index', type: 'uint', required: false },
193
176
  { name: 'createdAt', type: 'int', required: false },
194
177
  { name: 'updatedAt', type: 'int', required: false },
195
- ...fieldsFor(node.fields)
178
+ ...internal.fields(node.fields)
196
179
  ]
197
180
  })
198
181
  ctx.collections.push({ name, schema: fqn, key: ['id'] })
@@ -215,10 +198,7 @@ function register(name, node, ctx) {
215
198
  }
216
199
  ctx.meta.refs[name].indexes = node.indexes
217
200
  }
218
- // implicit secondary index on the auto-increment `index` lets reads push
219
- // reverse/limit down to hyperdb instead of scanning the whole collection.
220
- // main scope only: local (Storage) rows never get `index` stamped, and an
221
- // undefined index key crashes the encoder on insert.
201
+ // implicit index on the auto-increment `index`, so reverse and limit push down; main scope only
222
202
  if (ctx.scope === 'main' && !node.indexes?.index) {
223
203
  ctx.indexes.push({ name: `${name}-index`, collection: fqn, key: ['index'] })
224
204
  refEntry.orderIndex = true
@@ -226,17 +206,7 @@ function register(name, node, ctx) {
226
206
  }
227
207
  }
228
208
 
229
- function fieldsFor(fields) {
230
- return Object.entries(fields).map(([name, marker]) => ({
231
- name,
232
- type: getHyperdbType(marker.prim),
233
- required: marker.required === true
234
- }))
235
- }
236
-
237
- // The contract version is the highest of the three auto-bumped artifact
238
- // versions — any schema/db/dispatch change advances it, so a peer can tell
239
- // whether an op predates or postdates its own build.
209
+ // the contract version is the highest of the schema, db and dispatch versions
240
210
  async function contractVersion(mainDir) {
241
211
  const read = async (rel) => {
242
212
  const j = JSON.parse(await fs.readFile(join(mainDir, rel), 'utf-8'))
@@ -258,29 +228,30 @@ function emitMain(dir, ns, { types, collections, dispatches, indexes = [] }, { r
258
228
 
259
229
  const s = Hyperschema.from(schemaDir)
260
230
  const sns = s.namespace(ns)
261
- for (const desc of builtinTypes('main', extend)) sns.register(desc)
231
+ for (const desc of internal.types('main', extend)) sns.register(desc)
262
232
  for (const desc of types) sns.register(desc)
263
- if (rpc) for (const desc of rpcTypes()) sns.register(desc)
233
+ if (rpc) for (const desc of internal.types('rpc')) sns.register(desc)
264
234
  Hyperschema.toDisk(s, schemaDir, { esm: true })
265
235
 
266
236
  const db = HyperdbBuilder.from(schemaDir, dbDir)
267
237
  const dns = db.namespace(ns)
268
- for (const desc of builtinCollections(ns, 'main')) dns.collections.register(desc)
238
+ for (const desc of internal.collections(ns, 'main')) dns.collections.register(desc)
269
239
  for (const desc of collections) dns.collections.register(desc)
270
240
  for (const desc of indexes) dns.indexes.register(desc)
271
241
  HyperdbBuilder.toDisk(db, dbDir, { esm: true })
272
242
 
273
243
  const d = Hyperdispatch.from(schemaDir, dispatchDir)
274
244
  const xns = d.namespace(ns)
275
- for (const desc of builtinDispatches(ns)) xns.register(desc)
245
+ for (const desc of internal.dispatches(ns)) xns.register(desc)
276
246
  for (const desc of dispatches) xns.register(desc)
277
- xns.register(rotateDispatch(ns))
247
+ // after the app's dispatches: hyperdispatch numbers routes positionally and persists them
248
+ xns.register({ name: 'rotate-key', requestType: `@${ns}/epoch` })
278
249
  Hyperdispatch.toDisk(d, dispatchDir, { esm: true })
279
250
 
280
251
  if (rpc) {
281
252
  const r = HRPCBuilder.from(schemaDir, rpcDir)
282
253
  const rns = r.namespace(ns)
283
- for (const desc of rpcCommands(ns)) rns.register(desc)
254
+ for (const desc of internal.commands(ns)) rns.register(desc)
284
255
  HRPCBuilder.toDisk(r, rpcDir, { esm: true })
285
256
  }
286
257
  }
@@ -291,13 +262,13 @@ function emitLocal(dir, ns, { types, collections, indexes = [] }) {
291
262
 
292
263
  const s = Hyperschema.from(schemaDir)
293
264
  const sns = s.namespace(ns)
294
- for (const desc of builtinTypes('local')) sns.register(desc)
265
+ for (const desc of internal.types('local')) sns.register(desc)
295
266
  for (const desc of types) sns.register(desc)
296
267
  Hyperschema.toDisk(s, schemaDir, { esm: true })
297
268
 
298
269
  const db = HyperdbBuilder.from(schemaDir, dbDir)
299
270
  const dns = db.namespace(ns)
300
- for (const desc of builtinCollections(ns, 'local')) dns.collections.register(desc)
271
+ for (const desc of internal.collections(ns, 'local')) dns.collections.register(desc)
301
272
  for (const desc of collections) dns.collections.register(desc)
302
273
  for (const desc of indexes) dns.indexes.register(desc)
303
274
  HyperdbBuilder.toDisk(db, dbDir, { esm: true })
@@ -0,0 +1,121 @@
1
+ import { CeroError } from '@cero-base/core/errors'
2
+ import { COUNTERS, EPOCHS } from '../lib/constants.js'
3
+ import * as schemas from './schemas.js'
4
+
5
+ // internal refs by scope; kind defaults to collection
6
+ export const defs = {
7
+ main: {
8
+ members: { type: 'member' },
9
+ devices: { type: 'device' },
10
+ invites: { type: 'invite' },
11
+ handles: { type: 'handle' },
12
+ files: { type: 'file' }
13
+ },
14
+ local: {
15
+ master: { type: 'master', kind: 'single' },
16
+ keypair: { type: 'keypair', kind: 'single' },
17
+ 'handle-keypairs': { type: 'handle-keypair' },
18
+ environment: { type: 'environment', kind: 'single' }
19
+ }
20
+ }
21
+
22
+ // a DSL primitive is its hyperdb column type, except bytes (a buffer) and file (an id string)
23
+ const COLUMN = { bytes: 'buffer', file: 'string' }
24
+
25
+ export function fields(map) {
26
+ return Object.entries(map).map(([name, m]) => ({
27
+ name,
28
+ type: COLUMN[m.prim] || m.prim,
29
+ required: m.required === true
30
+ }))
31
+ }
32
+
33
+ // a base field cannot be redeclared
34
+ function merge(type, base, extra) {
35
+ if (!extra) return base
36
+ const dup = Object.keys(extra).find((k) => k in base)
37
+ if (dup) throw CeroError.INVALID(`'${dup}' is a base field of '${type}' and cannot be redeclared`)
38
+ return { ...base, ...extra }
39
+ }
40
+
41
+ export function types(scope, extend = {}) {
42
+ return Object.entries(schemas[scope]).map(([name, base]) => ({
43
+ name,
44
+ compact: false,
45
+ fields: fields(merge(name, base, extend[name]))
46
+ }))
47
+ }
48
+
49
+ export function refs(ns, scope) {
50
+ return Object.fromEntries(
51
+ Object.entries(defs[scope]).map(([name, def]) => [
52
+ name,
53
+ {
54
+ kind: def.kind || 'collection',
55
+ path: [name],
56
+ internal: true,
57
+ ...(scope === 'main' && { verb: def.type }),
58
+ schema: `@${ns}/${def.type}`
59
+ }
60
+ ])
61
+ )
62
+ }
63
+
64
+ export function collections(ns, scope) {
65
+ const out = Object.entries(defs[scope]).map(([name, def]) => ({
66
+ name,
67
+ schema: `@${ns}/${def.type}`,
68
+ key: def.kind === 'single' ? [] : ['id']
69
+ }))
70
+ if (scope === 'main') {
71
+ out.push({ name: COUNTERS, schema: `@${ns}/counter`, key: ['name'] })
72
+ out.push({ name: EPOCHS, schema: `@${ns}/epoch`, key: ['epoch'] })
73
+ }
74
+ return out
75
+ }
76
+
77
+ export function dispatches(ns) {
78
+ return [
79
+ { name: 'add-writer', requestType: `@${ns}/writer` },
80
+ { name: 'del-writer', requestType: `@${ns}/writer` },
81
+ { name: 'claim-writer', requestType: `@${ns}/claim` },
82
+ ...Object.values(defs.main).flatMap(({ type }) => [
83
+ { name: `add-${type}`, requestType: `@${ns}/${type}` },
84
+ { name: `set-${type}`, requestType: `@${ns}/${type}` },
85
+ { name: `del-${type}`, requestType: `@${ns}/del-by-id` }
86
+ ])
87
+ ]
88
+ }
89
+
90
+ // hrpc commands: name, request type, response type, streaming response
91
+ const COMMANDS = [
92
+ ['init', 'req-empty', 'res-identity'],
93
+ ['restore', 'req-restore', 'res-identity'],
94
+ ['seed', 'req-empty', 'res-seed'],
95
+ ['add-row', 'req-row', 'res-data'],
96
+ ['add-file', 'req-add-file', 'res-data'],
97
+ ['add-handle', 'req-row', 'res-handle'],
98
+ ['set', 'req-row', 'res-data'],
99
+ ['get', 'req-query', 'res-rows'],
100
+ ['get-one', 'req-id', 'res-data'],
101
+ ['del', 'req-id', 'res-ok'],
102
+ ['count', 'req-query', 'res-count'],
103
+ ['watch', 'req-query', 'res-rows', true],
104
+ ['call', 'req-call', 'res-data'],
105
+ ['invite', 'req-invite', 'res-invite'],
106
+ ['revoke', 'req-revoke', 'res-ok'],
107
+ ['join', 'req-join', 'res-handle'],
108
+ ['open-handle', 'req-open', 'res-handle'],
109
+ ['close-handle', 'req-handle', 'res-ok'],
110
+ ['leave', 'req-handle', 'res-ok'],
111
+ ['changes', 'req-query', 'res-changes', true],
112
+ ['rotate', 'req-handle', 'res-epoch']
113
+ ]
114
+
115
+ export function commands(ns) {
116
+ return COMMANDS.map(([name, req, res, stream]) => ({
117
+ name,
118
+ request: { name: `@${ns}/${req}` },
119
+ response: { name: `@${ns}/${res}`, ...(stream && { stream }) }
120
+ }))
121
+ }
@@ -1,6 +1,4 @@
1
- // Builtin type schemas grouped by scope, in the schema DSL kept separate from
2
- // the build wiring (builtins.js) so apps can extend them with `t.extend`.
3
- // Field order is the wire order; do not reorder.
1
+ // field order is the wire format: append only, never reorder
4
2
  import { t } from '../lib/spec.js'
5
3
 
6
4
  const { string, bytes, int, uint, bool, required } = t
@@ -15,11 +13,7 @@ export const main = {
15
13
  sig: required(bytes),
16
14
  isIndexer: bool,
17
15
  ts: int,
18
- // Who this writer belongs to. The paired add-member decides the rank in
19
- // the same transaction, and a refused grant discards this admission with
20
- // it — so `role` is unread since 77e5ed0 and stays only for older peers.
21
- // Absent on a device you add for yourself (recovery), where the device
22
- // binds to the master identity.
16
+ // the paired add-member decides the rank in the same transaction; `role` here is unread
23
17
  memberId: string,
24
18
  role: string
25
19
  },
@@ -2,13 +2,8 @@ import { t } from '../lib/spec.js'
2
2
  import { get, set, watch } from '../lib/operators.js'
3
3
 
4
4
  /**
5
- * Mirror a child handle's `profile` (name + avatar) onto its row in the parent's
6
- * `handles` list — so a handle list shows names + avatars without opening each
7
- * one. Adds the synced `fields` to the `handle` builtin.
8
- *
9
- * On create it seeds the handle's `profile.name` from the open `{ name }`, then
10
- * reflects the (app-owned) `profile` onto the row. Requires your handle types to
11
- * declare a `profile` single — handles without one are left untouched.
5
+ * Mirror a child handle's `profile` (name + avatar) onto its row in the parent's `handles`
6
+ * list — so a handle list shows names + avatars without opening each one.
12
7
  *
13
8
  * @param {{ fields?: Record<string, any> }} [opts]
14
9
  */
@@ -19,9 +14,7 @@ export function handleSync({ fields = { avatar: t.string } } = {}) {
19
14
  schema: { handles: t.extend(fields) },
20
15
  setup(me) {
21
16
  const keys = ['name', ...Object.keys(fields)]
22
- // skip the write when the row already mirrors the profile every open
23
- // re-emits the watch snapshot, and an unconditional set is a new op in
24
- // the replicated log forever
17
+ // an unconditional set on every open is a new op in the log forever
25
18
  const reflect = async (child, data) => {
26
19
  child.name = data.name
27
20
  const { data: row } = await get(me.handles, child.id)
@@ -1,2 +1,8 @@
1
+ import { profileSync } from './profile-sync.js'
2
+ import { handleSync } from './handle-sync.js'
3
+
1
4
  export * from './profile-sync.js'
2
5
  export * from './handle-sync.js'
6
+
7
+ // process-wide: build() folds in each schema, cero() runs each setup
8
+ export const registry = [profileSync(), handleSync()]
@@ -3,10 +3,6 @@ import { get, set, after } from '../lib/operators.js'
3
3
 
4
4
  /**
5
5
  * Mirror your `profile` onto your `member` row in every handle you're in.
6
- * Declares a `profile` single (`name`, `avatar`, plus any extra `fields`) and
7
- * mirrors them onto the `member` builtin, then publishes when you open/join a
8
- * handle and whenever your profile changes. An app may declare its own richer
9
- * `profile` instead — the app schema wins.
10
6
  *
11
7
  * @param {{ fields?: Record<string, any> }} [opts]
12
8
  */
@@ -23,7 +19,6 @@ export function profileSync({ fields = { avatar: t.string } } = {}) {
23
19
  const publish = (child, profile) =>
24
20
  set(child.members, { id: me.identity.id, ...profile }, { upsert: false }).catch(me._onerror)
25
21
 
26
- // skip the broadcast when the member row already mirrors the profile —
27
22
  // an unconditional set on every open is a room-wide op forever
28
23
  const onHandle = async (child) => {
29
24
  const { data: profile } = await get(me.profile)