@kubun/plugin-p2p 0.16.0 → 0.16.1
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/lib/context/group.js +8 -0
- package/lib/context/join.js +27 -0
- package/lib/context/peer.js +2 -2
- package/lib/context/sync.js +1 -1
- package/lib/hub/hub-like.js +7 -6
- package/lib/schema.js +2 -1
- package/package.json +3 -3
package/lib/context/group.js
CHANGED
|
@@ -536,6 +536,14 @@ export function createGroupContext(ctx, deps) {
|
|
|
536
536
|
createCatalog: async (p)=>{
|
|
537
537
|
const graphStore = await getGraphStore();
|
|
538
538
|
const id = p.catalogID ?? deps.runtime.getRandomID();
|
|
539
|
+
// Idempotent when the caller pins the id: a crashed flow (e.g. a device
|
|
540
|
+
// join) recreates its own catalog deterministically on retry. `catalog:set`
|
|
541
|
+
// is replace-by-construction, so a re-create against an existing pinned id
|
|
542
|
+
// returns the stored catalog instead of failing a primary-key insert.
|
|
543
|
+
if (p.catalogID != null) {
|
|
544
|
+
const existing = await graphStore.getCatalog(id);
|
|
545
|
+
if (existing != null) return toCatalogData(existing);
|
|
546
|
+
}
|
|
539
547
|
const hlcStr = HLC.serialize(deps.hlc.now());
|
|
540
548
|
const filterCriteria = JSON.parse(p.filterCriteria);
|
|
541
549
|
// Sign a self-contained `catalog:set` token at creation so the catalog can
|
package/lib/context/join.js
CHANGED
|
@@ -40,6 +40,33 @@ export function createJoinContext(ctx, deps) {
|
|
|
40
40
|
const p2pStore = await getP2PStore(deps.stores);
|
|
41
41
|
const stored = await p2pStore.getPendingJoinRequest(ctx.viewerDID);
|
|
42
42
|
if (stored == null) {
|
|
43
|
+
// Idempotent by construction: completing a join is not atomic with the
|
|
44
|
+
// caller's own progress record (the offline exchange routinely spans two
|
|
45
|
+
// processes), so a caller that crashes after a successful completion
|
|
46
|
+
// retries with the same payload. A successful completion consumes the
|
|
47
|
+
// pending request at its end, so "no pending request" AND already an
|
|
48
|
+
// active member of the invited group means this exact join already
|
|
49
|
+
// landed — return it instead of failing. A fresh rejoin/re-admit always
|
|
50
|
+
// carries a new pending request (a new prepareJoinRequest), so it never
|
|
51
|
+
// takes this path; a genuinely bogus payload either fails to decode or
|
|
52
|
+
// leaves the device a non-member, so it still errors below.
|
|
53
|
+
let replayed;
|
|
54
|
+
try {
|
|
55
|
+
replayed = decodeInvitePayload(invitePayloadStr);
|
|
56
|
+
} catch {
|
|
57
|
+
replayed = undefined;
|
|
58
|
+
}
|
|
59
|
+
if (replayed != null) {
|
|
60
|
+
const existing = await p2pStore.getGroup(replayed.groupID);
|
|
61
|
+
if (existing?.status === 'joined') {
|
|
62
|
+
return await finalizeJoinedGroup({
|
|
63
|
+
stores: deps.stores,
|
|
64
|
+
emitter: deps.emitter,
|
|
65
|
+
invite: replayed,
|
|
66
|
+
logger: deps.logger
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
43
70
|
// Naming the way out matters: "no pending join request" reads to an
|
|
44
71
|
// operator as "the payload you were handed is bad", and they go asking
|
|
45
72
|
// for a new invite against key material that was never the problem.
|
package/lib/context/peer.js
CHANGED
|
@@ -80,8 +80,8 @@ import { toISO } from './types.js';
|
|
|
80
80
|
// The version and variant nibbles are overwritten rather than derived, so the
|
|
81
81
|
// string is a well-formed UUID that carries 122 bits of the digest.
|
|
82
82
|
const bytes = Uint8Array.from(digest);
|
|
83
|
-
bytes[6] = bytes[6] & 0x0f | 0x40;
|
|
84
|
-
bytes[8] = bytes[8] & 0x3f | 0x80;
|
|
83
|
+
bytes[6] = (bytes[6] ?? 0) & 0x0f | 0x40;
|
|
84
|
+
bytes[8] = (bytes[8] ?? 0) & 0x3f | 0x80;
|
|
85
85
|
const hex = Array.from(bytes, (b)=>b.toString(16).padStart(2, '0')).join('');
|
|
86
86
|
return [
|
|
87
87
|
hex.slice(0, 8),
|
package/lib/context/sync.js
CHANGED
|
@@ -132,7 +132,7 @@ export function createSyncContext(_ctx, deps) {
|
|
|
132
132
|
const counts = await Promise.all(uniqueOwners.map((owner)=>store.listProvisioningExpectations({
|
|
133
133
|
ownerDID: owner
|
|
134
134
|
})));
|
|
135
|
-
const withExpectation = uniqueOwners.filter((_owner, index)=>counts[index]
|
|
135
|
+
const withExpectation = uniqueOwners.filter((_owner, index)=>(counts[index]?.length ?? 0) > 0);
|
|
136
136
|
if (withExpectation.length === 0) {
|
|
137
137
|
return [];
|
|
138
138
|
}
|
package/lib/hub/hub-like.js
CHANGED
|
@@ -386,16 +386,18 @@ function rethrowHubError(error) {
|
|
|
386
386
|
this.#logger?.debug('hub-like open reached no hub', {
|
|
387
387
|
topics: topics.length
|
|
388
388
|
});
|
|
389
|
-
throw first
|
|
389
|
+
throw first?.reason ?? new Error('hub did not answer any subscribe');
|
|
390
390
|
}
|
|
391
391
|
this.#proven = true;
|
|
392
392
|
const failed = [];
|
|
393
|
-
for(
|
|
394
|
-
const result = results[i];
|
|
393
|
+
for (const [i, result] of results.entries()){
|
|
395
394
|
if (result.status !== 'rejected') {
|
|
396
395
|
continue;
|
|
397
396
|
}
|
|
398
397
|
const topicID = topics[i];
|
|
398
|
+
if (topicID == null) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
399
401
|
const error = asHubError(result.reason);
|
|
400
402
|
this.#logger?.warn('hub-like re-subscribe failed', {
|
|
401
403
|
topicID,
|
|
@@ -444,10 +446,9 @@ function rethrowHubError(error) {
|
|
|
444
446
|
}
|
|
445
447
|
})));
|
|
446
448
|
const stillFailing = [];
|
|
447
|
-
for(
|
|
448
|
-
const result = results[i];
|
|
449
|
+
for (const [i, result] of results.entries()){
|
|
449
450
|
const topicID = pending[i];
|
|
450
|
-
if (result.status !== 'rejected') {
|
|
451
|
+
if (topicID == null || result.status !== 'rejected') {
|
|
451
452
|
continue;
|
|
452
453
|
}
|
|
453
454
|
if (isPermanentSubscribeFailure(asHubError(result.reason))) {
|
package/lib/schema.js
CHANGED
|
@@ -726,7 +726,7 @@ extend type Mutation {
|
|
|
726
726
|
createGroup(input: CreateGroupInput!): CreateGroupPayload!
|
|
727
727
|
requestCreateCircle(input: RequestCreateCircleInput!): RequestCreateCirclePayload!
|
|
728
728
|
requestAddCircleMember(input: RequestAddCircleMemberInput!): RequestAddCircleMemberPayload!
|
|
729
|
-
createCatalog(name: String!, description: String, filterCriteria: String!): PeerCatalog!
|
|
729
|
+
createCatalog(catalogID: ID, name: String!, description: String, filterCriteria: String!): PeerCatalog!
|
|
730
730
|
requestInviteToGroup(input: RequestInviteToGroupInput!): RequestInviteToGroupPayload!
|
|
731
731
|
joinGroup(invitePayload: String!, joinRequestPayload: String!): JoinGroupPayload!
|
|
732
732
|
prepareJoinRequest: PrepareJoinRequestPayload!
|
|
@@ -1101,6 +1101,7 @@ export function createP2PSchemaExtension(emitter, syncManager, logger = getKubun
|
|
|
1101
1101
|
},
|
|
1102
1102
|
createCatalog: async (_source, args, context)=>{
|
|
1103
1103
|
return await requireP2P(context).group.createCatalog({
|
|
1104
|
+
catalogID: args.catalogID,
|
|
1104
1105
|
name: args.name,
|
|
1105
1106
|
description: args.description,
|
|
1106
1107
|
filterCriteria: args.filterCriteria
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/plugin-p2p",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"license": "see LICENSE.md",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
"scripts": {
|
|
92
92
|
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|
|
93
93
|
"build:clean": "del lib",
|
|
94
|
-
"build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
95
|
-
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
94
|
+
"build:js": "del 'lib/**/*.js' && swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
95
|
+
"build:types": "del 'lib/**/*.d.ts' 'lib/**/*.d.ts.map' && tsc --emitDeclarationOnly --skipLibCheck",
|
|
96
96
|
"test": "pnpm run test:types && pnpm run test:unit",
|
|
97
97
|
"test:types": "tsc --noEmit -p tsconfig.test.json",
|
|
98
98
|
"test:unit": "vitest run"
|