@cero-base/cero 1.7.1 → 1.8.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/README.md CHANGED
@@ -521,14 +521,14 @@ The same `spec/index.js` is imported on both sides. `@cero-base/cero/build` emit
521
521
 
522
522
  Everything the local API offers, plus lifecycle methods on returned handles:
523
523
 
524
- | Operator / method | Works over RPC |
525
- | -------------------------------------------------------------------------------------------------------------------- | -------------- |
526
- | `cero.put` / `set` / `get` / `del` / `count` / `watch` / `call` | ✓ |
527
- | `cero.open(ref, …)` — create, join, load by id | ✓ |
528
- | `room.invite({ role, expiresIn, multiUse })` — mint an invite (`multiUse: true` keeps it alive after the first join) | ✓ |
529
- | `room.revoke(invite)` — invalidate an outstanding invite; `true` if it existed | ✓ |
530
- | `room.close()` — release the handle on the server | ✓ |
531
- | `room.leave()` — remove yourself from the room and from your handles list | ✓ |
524
+ | Operator / method | Works over RPC |
525
+ | -------------------------------------------------------------------------------------------------------------- | -------------- |
526
+ | `cero.put` / `set` / `get` / `del` / `count` / `watch` / `call` | ✓ |
527
+ | `cero.open(ref, …)` — create, join, load by id | ✓ |
528
+ | `room.invite({ role, expiresIn, reuse })` — mint an invite (`reuse: true` keeps it alive after the first join) | ✓ |
529
+ | `room.revoke(invite)` — invalidate an outstanding invite; `true` if it existed | ✓ |
530
+ | `room.close()` — release the handle on the server | ✓ |
531
+ | `room.leave()` — remove yourself from the room and from your handles list | ✓ |
532
532
 
533
533
  `cero.watch(ref)` returns a Readable on both sides; snapshots flow as a server-streamed RPC.
534
534
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cero-base/cero",
3
- "version": "1.7.1",
3
+ "version": "1.8.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,
@@ -89,27 +89,27 @@
89
89
  "build:types": "rm -rf types && tsc -p .",
90
90
  "pretest": "npm run build:test",
91
91
  "prepublishOnly": "npm run build:types",
92
- "test": "npm run test:node",
92
+ "test": "npm run test:bare",
93
93
  "pretest:bare": "npm run build:test",
94
94
  "test:bare": "ls test/*.test.js | xargs -P1 -n1 brittle-bare test/bare.js",
95
95
  "test:node": "ls test/*.test.js | xargs -P1 -n1 brittle-node"
96
96
  },
97
97
  "dependencies": {
98
- "@cero-base/core": "^1.7.1",
98
+ "@cero-base/core": "^1.8.0",
99
99
  "b4a": "^1.8.1",
100
100
  "bare-abort-controller": "^1.1.2",
101
101
  "bare-crypto": "^1.15.3",
102
102
  "bare-fs": "^4.7.4",
103
103
  "bare-path": "^3.1.1",
104
104
  "compact-encoding": "^3.3.0",
105
- "corestore": "^7.11.1",
105
+ "corestore": "^7.12.0",
106
106
  "hrpc": "^4.3.0",
107
- "hypercore": "^11.34.1",
107
+ "hypercore": "^11.35.1",
108
108
  "hypercore-crypto": "^3.7.0",
109
109
  "hypercore-storage": "^3.2.0",
110
- "hyperdb": "^6.7.0",
110
+ "hyperdb": "^6.8.0",
111
111
  "hyperdispatch": "^1.6.0",
112
- "hyperschema": "^1.21.0",
112
+ "hyperschema": "^1.22.0",
113
113
  "ready-resource": "^1.2.0",
114
114
  "safety-catch": "^1.0.3",
115
115
  "streamx": "^2.28.0",
@@ -138,7 +138,7 @@ export const rpc = {
138
138
  handle: required(string),
139
139
  role: string,
140
140
  expiresIn: uint,
141
- multiUse: bool
141
+ reuse: bool
142
142
  },
143
143
  'req-revoke': {
144
144
  handle: required(string),
@@ -122,6 +122,7 @@ export class Handle extends ReadyResource {
122
122
  this._onerror = this._opts.onerror || safetyCatch
123
123
  this.children = parent ? null : new Set()
124
124
  this._loading = parent ? null : new Map()
125
+ this._joining = parent ? null : new Map()
125
126
  this._coreKeys = parent ? null : new Map()
126
127
  this._fileServer = null
127
128
  this._blobs = null
@@ -630,7 +631,33 @@ export class Handle extends ReadyResource {
630
631
  * @param {JoinChildOpts} [opts]
631
632
  * @returns {Promise<Handle>}
632
633
  */
633
- async _join(invite, type, { routes, timeout } = {}) {
634
+ async _join(invite, type, opts = {}) {
635
+ // Coalesce concurrent joins of the same room (a retry racing a pending
636
+ // join, a double tap): the pairing layer allows one candidate per invite,
637
+ // so the second attempt must attach to the in-flight join, not start
638
+ // another handshake.
639
+ const target = Pairing.inviteTopic(invite)
640
+ const key = target && `${type}/${b4a.toString(target, 'hex')}`
641
+ if (!key) return this._pair(invite, type, opts, target)
642
+ const pending = this._joining.get(key)
643
+ if (pending) return pending
644
+ const joining = this._pair(invite, type, opts, target)
645
+ this._joining.set(key, joining)
646
+ try {
647
+ return await joining
648
+ } finally {
649
+ this._joining.delete(key)
650
+ }
651
+ }
652
+
653
+ /**
654
+ * @param {string} invite
655
+ * @param {string} type
656
+ * @param {JoinChildOpts} [opts]
657
+ * @param {Uint8Array | null} [target]
658
+ * @returns {Promise<Handle>}
659
+ */
660
+ async _pair(invite, type, { routes, timeout } = {}, target = null) {
634
661
  const deadline = timeout || TIMEOUT
635
662
 
636
663
  // Idempotent: if the invite targets a handle we already have AND we are
@@ -639,7 +666,6 @@ export class Handle extends ReadyResource {
639
666
  // removed), the stored writer is revoked and the old session can never
640
667
  // become writable again — holding a fresh invite is exactly the
641
668
  // re-admission path, so fall through to a real pairing instead.
642
- const target = Pairing.inviteTopic(invite)
643
669
  if (target) {
644
670
  const { data: joined } = await this.store.get('handles')
645
671
  const existing = joined.find(
@@ -812,7 +838,9 @@ export class Handle extends ReadyResource {
812
838
  if (!spec) throw CeroError.REQUIRED('spec')
813
839
 
814
840
  const writer = Identity.randomKeyPair()
815
- const pair = new Pairing({ network: net, identity: id })
841
+ // join-only: no member listener, so concurrent joins never collide on the
842
+ // identity-derived default topic ('Active member already exist')
843
+ const pair = new Pairing({ network: net, identity: id, host: false })
816
844
  await pair.ready()
817
845
 
818
846
  let key, encryptionKey, additional
package/src/rpc/client.js CHANGED
@@ -385,12 +385,12 @@ const operators = {
385
385
  * @param {{ role?: string }} [opts]
386
386
  * @returns {Promise<string>}
387
387
  */
388
- async invite({ role, expiresIn, multiUse } = {}) {
388
+ async invite({ role, expiresIn, reuse } = {}) {
389
389
  const { invite } = await this.rpc.invite({
390
390
  handle: this.id,
391
391
  role: role || '',
392
392
  expiresIn: expiresIn || 0,
393
- multiUse: multiUse === true
393
+ reuse: reuse === true
394
394
  })
395
395
  return invite
396
396
  },
package/src/rpc/server.js CHANGED
@@ -267,12 +267,12 @@ export class Server extends RPCServer {
267
267
 
268
268
  /** Register invite/revoke/join RPC handlers. */
269
269
  _wirePairing() {
270
- this.rpc.onInvite(async ({ handle, role, expiresIn, multiUse }) => {
270
+ this.rpc.onInvite(async ({ handle, role, expiresIn, reuse }) => {
271
271
  const h = this._resolve(handle)
272
272
  const invite = await h.invite({
273
273
  role: role || undefined,
274
274
  expiresIn: expiresIn || undefined,
275
- multiUse: multiUse === true
275
+ reuse: reuse === true
276
276
  })
277
277
  return { invite }
278
278
  })
@@ -129,7 +129,7 @@ export const rpc: {
129
129
  handle: import("@cero-base/core").Prim;
130
130
  role: import("@cero-base/core").Prim;
131
131
  expiresIn: import("@cero-base/core").Prim;
132
- multiUse: import("@cero-base/core").Prim;
132
+ reuse: import("@cero-base/core").Prim;
133
133
  };
134
134
  'req-revoke': {
135
135
  handle: import("@cero-base/core").Prim;
@@ -91,6 +91,7 @@ export class Handle extends ReadyResource {
91
91
  _onerror: any;
92
92
  children: Set<any>;
93
93
  _loading: Map<any, any>;
94
+ _joining: Map<any, any>;
94
95
  _coreKeys: Map<any, any>;
95
96
  _fileServer: FileServer;
96
97
  _blobs: Blobs;
@@ -277,7 +278,15 @@ export class Handle extends ReadyResource {
277
278
  * @param {JoinChildOpts} [opts]
278
279
  * @returns {Promise<Handle>}
279
280
  */
280
- _join(invite: string, type: string, { routes, timeout }?: JoinChildOpts): Promise<Handle>;
281
+ _join(invite: string, type: string, opts?: JoinChildOpts): Promise<Handle>;
282
+ /**
283
+ * @param {string} invite
284
+ * @param {string} type
285
+ * @param {JoinChildOpts} [opts]
286
+ * @param {Uint8Array | null} [target]
287
+ * @returns {Promise<Handle>}
288
+ */
289
+ _pair(invite: string, type: string, { routes, timeout }?: JoinChildOpts, target?: Uint8Array | null): Promise<Handle>;
281
290
  /**
282
291
  * Get an open child by id, or re-open it. Concurrent calls for the same id
283
292
  * share one in-flight load, so the child is built — and `handle` emitted —