libp2p-mesh 2026.6.19 → 2026.6.21
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 +227 -5
- package/dist/src/agent-tools.d.ts +34 -14
- package/dist/src/agent-tools.js +99 -14
- package/dist/src/instance-peer-store.js +6 -2
- package/dist/src/instance-router.js +137 -21
- package/dist/src/labels-wizard.d.ts +21 -0
- package/dist/src/labels-wizard.js +168 -0
- package/dist/src/peer-label-store.d.ts +11 -0
- package/dist/src/peer-label-store.js +172 -0
- package/dist/src/plugin.js +13 -4
- package/dist/src/profile-cli.d.ts +9 -0
- package/dist/src/profile-cli.js +33 -0
- package/dist/src/prompt-config.js +35 -9
- package/dist/src/types.d.ts +44 -7
- package/dist/src/user-md-agent-attributes.d.ts +24 -0
- package/dist/src/user-md-agent-attributes.js +180 -0
- package/package.json +1 -1
- package/src/agent-tools.ts +144 -21
- package/src/instance-peer-store.ts +7 -2
- package/src/instance-router.ts +194 -24
- package/src/labels-wizard.ts +221 -0
- package/src/peer-label-store.ts +224 -0
- package/src/plugin.ts +14 -5
- package/src/profile-cli.ts +48 -0
- package/src/prompt-config.ts +35 -9
- package/src/types.ts +50 -5
- package/src/user-md-agent-attributes.ts +243 -0
package/src/agent-tools.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
DeliveryTargetResult,
|
|
3
|
+
InstancePeerRecord,
|
|
3
4
|
InstanceRouter,
|
|
5
|
+
LocalPeerLabelAttribute,
|
|
4
6
|
MeshNetwork,
|
|
7
|
+
PeerLabelStore,
|
|
5
8
|
UserAttributeMatch,
|
|
9
|
+
UserAttributeMatchScope,
|
|
6
10
|
UserAttributeMessageDeliveryResult,
|
|
7
11
|
UserAttributeMessageTarget,
|
|
8
12
|
UserPublicAttribute,
|
|
@@ -28,7 +32,7 @@ function formatDeliveryResults(
|
|
|
28
32
|
return [heading, ...lines].join("\n");
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
function attributeLabel(attribute: UserPublicAttribute): string {
|
|
35
|
+
function attributeLabel(attribute: UserPublicAttribute | LocalPeerLabelAttribute): string {
|
|
32
36
|
if (attribute.kind === "tag") {
|
|
33
37
|
return `tag:${attribute.value}`;
|
|
34
38
|
}
|
|
@@ -42,7 +46,7 @@ function instanceTargetLabel(target: Pick<UserAttributeMessageTarget, "instanceI
|
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
function formatUserAttributeTargets(targets: UserAttributeMessageTarget[]) {
|
|
45
|
-
return targets.map((target) => `${instanceTargetLabel(target)} [${attributeLabel(target.matchedAttribute)}]`);
|
|
49
|
+
return targets.map((target) => `${instanceTargetLabel(target)} [${target.matchSource}:${attributeLabel(target.matchedAttribute)}]`);
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
function formatUserAttributeResults(results: UserAttributeMessageDeliveryResult[]) {
|
|
@@ -58,6 +62,82 @@ function formatUserAttributeResults(results: UserAttributeMessageDeliveryResult[
|
|
|
58
62
|
});
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
type ListInstanceRow = InstancePeerRecord & {
|
|
66
|
+
connected: boolean;
|
|
67
|
+
localLabels: LocalPeerLabelAttribute[];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type BuildP2PToolsOptions = {
|
|
71
|
+
peerLabelStore?: Pick<PeerLabelStore, "listLabels">;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function formatPublicAttribute(attribute: UserPublicAttribute): string[] {
|
|
75
|
+
if (attribute.kind === "tag") {
|
|
76
|
+
return [
|
|
77
|
+
" - kind: tag",
|
|
78
|
+
` value: ${attribute.value}`,
|
|
79
|
+
` label: ${attribute.label}`,
|
|
80
|
+
` source: ${attribute.source}`,
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
" - kind: structured",
|
|
86
|
+
` key: ${attribute.key}`,
|
|
87
|
+
` value: ${attribute.value}`,
|
|
88
|
+
` label: ${attribute.label}`,
|
|
89
|
+
` source: ${attribute.source}`,
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatLocalLabel(label: LocalPeerLabelAttribute): string[] {
|
|
94
|
+
return [
|
|
95
|
+
" - kind: structured",
|
|
96
|
+
` key: ${label.key}`,
|
|
97
|
+
` value: ${label.value}`,
|
|
98
|
+
` label: ${label.label}`,
|
|
99
|
+
` source: ${label.source}`,
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatInstanceList(rows: ListInstanceRow[]): string {
|
|
104
|
+
if (rows.length === 0) {
|
|
105
|
+
return "No OpenClaw instances discovered yet.";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const lines = [`Discovered OpenClaw instances: ${rows.length}`];
|
|
109
|
+
|
|
110
|
+
rows.forEach((entry, index) => {
|
|
111
|
+
lines.push(
|
|
112
|
+
"",
|
|
113
|
+
`${index + 1}. ${entry.instanceId}`,
|
|
114
|
+
` peerId: ${entry.peerId}`,
|
|
115
|
+
` instanceName: ${entry.instanceName ?? "(none)"}`,
|
|
116
|
+
` connected: ${entry.connected}`,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if ((entry.userPublicAttributes ?? []).length === 0) {
|
|
120
|
+
lines.push(" userPublicAttributes: none");
|
|
121
|
+
} else {
|
|
122
|
+
lines.push(" userPublicAttributes:");
|
|
123
|
+
for (const attribute of entry.userPublicAttributes ?? []) {
|
|
124
|
+
lines.push(...formatPublicAttribute(attribute));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (entry.localLabels.length === 0) {
|
|
129
|
+
lines.push(" localLabels: none");
|
|
130
|
+
} else {
|
|
131
|
+
lines.push(" localLabels:");
|
|
132
|
+
for (const label of entry.localLabels) {
|
|
133
|
+
lines.push(...formatLocalLabel(label));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return lines.join("\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
61
141
|
type SendUserAttributeToolParams = {
|
|
62
142
|
selector?: unknown;
|
|
63
143
|
match?: {
|
|
@@ -67,8 +147,34 @@ type SendUserAttributeToolParams = {
|
|
|
67
147
|
};
|
|
68
148
|
message?: unknown;
|
|
69
149
|
dryRun?: unknown;
|
|
150
|
+
scope?: unknown;
|
|
70
151
|
};
|
|
71
152
|
|
|
153
|
+
function normalizeUserAttributeScope(scope: unknown): UserAttributeMatchScope | undefined {
|
|
154
|
+
if (scope === undefined) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (scope === "public" || scope === "local" || scope === "all") {
|
|
159
|
+
return scope;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function validateUserAttributeScope(scope: unknown): string | undefined {
|
|
166
|
+
if (
|
|
167
|
+
scope === undefined ||
|
|
168
|
+
scope === "public" ||
|
|
169
|
+
scope === "local" ||
|
|
170
|
+
scope === "all"
|
|
171
|
+
) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return 'scope must be "public", "local", or "all".';
|
|
176
|
+
}
|
|
177
|
+
|
|
72
178
|
function normalizeUserAttributeSelector(selector: unknown): UserAttributeMatch | string {
|
|
73
179
|
const value = typeof selector === "string" ? selector.trim() : "";
|
|
74
180
|
if (!value) {
|
|
@@ -124,7 +230,11 @@ function normalizeUserAttributeMatch(params: SendUserAttributeToolParams): UserA
|
|
|
124
230
|
return 'match.kind must be "tag" or "structured".';
|
|
125
231
|
}
|
|
126
232
|
|
|
127
|
-
export function buildP2PTools(
|
|
233
|
+
export function buildP2PTools(
|
|
234
|
+
mesh: MeshNetwork,
|
|
235
|
+
router?: InstanceRouter,
|
|
236
|
+
options: BuildP2PToolsOptions = {},
|
|
237
|
+
) {
|
|
128
238
|
return [
|
|
129
239
|
{
|
|
130
240
|
name: "p2p_send_message",
|
|
@@ -334,19 +444,17 @@ export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
|
|
|
334
444
|
try {
|
|
335
445
|
const instances = await router.listInstances();
|
|
336
446
|
const connected = new Set(mesh.getConnectedPeers());
|
|
337
|
-
const rows =
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
)
|
|
349
|
-
.join("\n");
|
|
447
|
+
const rows = await Promise.all(
|
|
448
|
+
instances.map(async (entry): Promise<ListInstanceRow> => ({
|
|
449
|
+
...entry,
|
|
450
|
+
userPublicAttributes: entry.userPublicAttributes ?? [],
|
|
451
|
+
connected: connected.has(entry.peerId),
|
|
452
|
+
localLabels: options.peerLabelStore
|
|
453
|
+
? await options.peerLabelStore.listLabels(entry.instanceId)
|
|
454
|
+
: [],
|
|
455
|
+
})),
|
|
456
|
+
);
|
|
457
|
+
const text = formatInstanceList(rows);
|
|
350
458
|
return {
|
|
351
459
|
content: [{ type: "text" as const, text }],
|
|
352
460
|
details: { instances: rows, count: rows.length },
|
|
@@ -486,7 +594,7 @@ export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
|
|
|
486
594
|
name: "p2p_send_user_attribute_message",
|
|
487
595
|
label: "P2P Send User Attribute Message",
|
|
488
596
|
description:
|
|
489
|
-
'Send a user message to discovered OpenClaw instances matching
|
|
597
|
+
'Send a user message to discovered OpenClaw instances matching an attribute selector. scope controls the source: "public" for announced user attributes, "local" for local peer labels, or "all" for both; defaults to public. Use selectors like "group=实验室", "project=小龙虾", "tag:P2P", or "#P2P". First run a dry run with dryRun=true to preview targets; if targets match, call again immediately with dryRun=false and the same selector, scope, and message.',
|
|
490
598
|
parameters: {
|
|
491
599
|
type: "object" as const,
|
|
492
600
|
properties: {
|
|
@@ -539,8 +647,15 @@ export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
|
|
|
539
647
|
type: "boolean" as const,
|
|
540
648
|
description: "Preview matching instances without sending. Run this before group sending.",
|
|
541
649
|
},
|
|
650
|
+
scope: {
|
|
651
|
+
type: "string" as const,
|
|
652
|
+
enum: ["public", "local", "all"],
|
|
653
|
+
description:
|
|
654
|
+
"Attribute source to match: public announced attributes, local peer labels, or both. Defaults to public.",
|
|
655
|
+
},
|
|
542
656
|
},
|
|
543
|
-
required: ["
|
|
657
|
+
required: ["message"],
|
|
658
|
+
anyOf: [{ required: ["selector"] }, { required: ["match"] }],
|
|
544
659
|
},
|
|
545
660
|
async execute(_toolCallId: string, params: SendUserAttributeToolParams) {
|
|
546
661
|
if (!router) {
|
|
@@ -552,9 +667,16 @@ export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
|
|
|
552
667
|
}
|
|
553
668
|
|
|
554
669
|
const match = normalizeUserAttributeMatch(params);
|
|
670
|
+
const scopeError = validateUserAttributeScope(params.scope);
|
|
671
|
+
const scope = normalizeUserAttributeScope(params.scope);
|
|
555
672
|
const message = typeof params.message === "string" ? params.message.trim() : "";
|
|
556
|
-
if (typeof match === "string" || !message) {
|
|
557
|
-
const error =
|
|
673
|
+
if (typeof match === "string" || scopeError || !message) {
|
|
674
|
+
const error =
|
|
675
|
+
typeof match === "string"
|
|
676
|
+
? match
|
|
677
|
+
: scopeError
|
|
678
|
+
? scopeError
|
|
679
|
+
: "message is required.";
|
|
558
680
|
return {
|
|
559
681
|
content: [{ type: "text" as const, text: error }],
|
|
560
682
|
details: { error },
|
|
@@ -563,7 +685,8 @@ export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
|
|
|
563
685
|
}
|
|
564
686
|
|
|
565
687
|
const dryRun = params.dryRun === true;
|
|
566
|
-
const
|
|
688
|
+
const options = scope ? { dryRun, scope } : { dryRun };
|
|
689
|
+
const result = await router.sendUserAttributeMessage(match, message, options);
|
|
567
690
|
if (result.error) {
|
|
568
691
|
return {
|
|
569
692
|
content: [{ type: "text" as const, text: result.error }],
|
|
@@ -66,13 +66,15 @@ function sameRecord(
|
|
|
66
66
|
|
|
67
67
|
const recordAttributes = normalizeUserPublicAttributes(record.userPublicAttributes);
|
|
68
68
|
const payloadAttributes = normalizeUserPublicAttributes(payload.userPublicAttributes);
|
|
69
|
+
const payloadIncludesAttributes = "userPublicAttributes" in payload;
|
|
69
70
|
|
|
70
71
|
return (
|
|
71
72
|
record.peerId === payload.peerId &&
|
|
72
73
|
record.instanceName === payload.instanceName &&
|
|
73
74
|
record.pubkey === payload.pubkey &&
|
|
74
75
|
sameStringArray(record.multiaddrs, payload.multiaddrs) &&
|
|
75
|
-
|
|
76
|
+
(!payloadIncludesAttributes ||
|
|
77
|
+
sameUserPublicAttributes(recordAttributes, payloadAttributes)) &&
|
|
76
78
|
record.lastAnnouncedAt === payload.announcedAt
|
|
77
79
|
);
|
|
78
80
|
}
|
|
@@ -187,7 +189,10 @@ export function createInstancePeerStore(options?: {
|
|
|
187
189
|
return runMutation(async () => {
|
|
188
190
|
const table = await load();
|
|
189
191
|
const existing = table.instances[payload.instanceId];
|
|
190
|
-
const userPublicAttributes =
|
|
192
|
+
const userPublicAttributes =
|
|
193
|
+
"userPublicAttributes" in payload
|
|
194
|
+
? normalizeUserPublicAttributes(payload.userPublicAttributes)
|
|
195
|
+
: normalizeUserPublicAttributes(existing?.userPublicAttributes);
|
|
191
196
|
const changed = !sameRecord(existing, payload);
|
|
192
197
|
const record: InstancePeerRecord = {
|
|
193
198
|
instanceId: payload.instanceId,
|
package/src/instance-router.ts
CHANGED
|
@@ -6,14 +6,23 @@ import type {
|
|
|
6
6
|
InstanceAnnouncePayload,
|
|
7
7
|
InstanceRouter,
|
|
8
8
|
InstanceRouterOptions,
|
|
9
|
+
LocalPeerLabelAttribute,
|
|
9
10
|
MeshConfig,
|
|
10
11
|
P2PMessage,
|
|
11
12
|
UserAttributeMatch,
|
|
13
|
+
UserAttributeMessageOptions,
|
|
14
|
+
UserAttributeMatchScope,
|
|
15
|
+
UserAttributeMatchSource,
|
|
12
16
|
UserAttributeMessageTarget,
|
|
13
17
|
UserPublicAttribute,
|
|
14
18
|
UserMessagePayload,
|
|
15
19
|
} from "./types.js";
|
|
16
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
matchesUserAttribute,
|
|
22
|
+
mergeUserPublicAttributes,
|
|
23
|
+
normalizeAttributeKey,
|
|
24
|
+
normalizeAttributeValue,
|
|
25
|
+
} from "./user-attributes.js";
|
|
17
26
|
|
|
18
27
|
export type RouterLogger = {
|
|
19
28
|
info?: (message: string) => void;
|
|
@@ -77,6 +86,40 @@ function describeAttributeMatch(match: UserAttributeMatch): string {
|
|
|
77
86
|
return `structured attribute ${match.key}=${match.value}`;
|
|
78
87
|
}
|
|
79
88
|
|
|
89
|
+
function effectiveAttributeMatchScope(
|
|
90
|
+
scope: UserAttributeMatchScope | undefined,
|
|
91
|
+
): UserAttributeMatchScope {
|
|
92
|
+
return scope ?? "public";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function matchesLocalPeerLabel(
|
|
96
|
+
attribute: LocalPeerLabelAttribute,
|
|
97
|
+
match: UserAttributeMatch,
|
|
98
|
+
): boolean {
|
|
99
|
+
if (match.kind !== "structured") {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
normalizeAttributeKey(attribute.key) === normalizeAttributeKey(match.key) &&
|
|
105
|
+
normalizeAttributeValue(attribute.value) === normalizeAttributeValue(match.value)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildUserAttributeTarget(
|
|
110
|
+
record: { instanceId: string; instanceName?: string; peerId: string },
|
|
111
|
+
matchedAttribute: UserPublicAttribute | LocalPeerLabelAttribute,
|
|
112
|
+
matchSource: UserAttributeMatchSource,
|
|
113
|
+
): UserAttributeMessageTarget {
|
|
114
|
+
return {
|
|
115
|
+
instanceId: record.instanceId,
|
|
116
|
+
...(record.instanceName ? { instanceName: record.instanceName } : {}),
|
|
117
|
+
peerId: record.peerId,
|
|
118
|
+
matchedAttribute,
|
|
119
|
+
matchSource,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
80
123
|
type EffectiveInboundTarget = {
|
|
81
124
|
id?: string;
|
|
82
125
|
channel: string;
|
|
@@ -174,6 +217,8 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
174
217
|
const pendingAcks = new Map<string, PendingAck>();
|
|
175
218
|
const deliveryCache = new Map<string, DeliveryCacheEntry>();
|
|
176
219
|
const unsubs: Array<() => void> = [];
|
|
220
|
+
const pendingAttributePeers = new Set<string>();
|
|
221
|
+
let attributeRefreshPromise: Promise<void> | undefined;
|
|
177
222
|
|
|
178
223
|
function localInstanceId(): string {
|
|
179
224
|
const identity = mesh.getInstanceIdentity();
|
|
@@ -183,26 +228,35 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
183
228
|
return identity.id;
|
|
184
229
|
}
|
|
185
230
|
|
|
186
|
-
async function loadUserPublicAttributes(
|
|
187
|
-
|
|
188
|
-
|
|
231
|
+
async function loadUserPublicAttributes(loadOptions?: {
|
|
232
|
+
refreshUserMd?: boolean;
|
|
233
|
+
}): Promise<UserPublicAttribute[]> {
|
|
234
|
+
const userMdPromise = loadOptions?.refreshUserMd && options.userAttributeSource?.refreshTags
|
|
235
|
+
? options.userAttributeSource.refreshTags()
|
|
236
|
+
: options.userAttributeSource?.loadTags() ?? Promise.resolve([]);
|
|
237
|
+
|
|
238
|
+
const profilePromise =
|
|
239
|
+
options.userProfileStore?.listAttributes().catch((error) => {
|
|
189
240
|
logger?.warn?.(
|
|
190
|
-
`[libp2p-mesh] Failed to load
|
|
241
|
+
`[libp2p-mesh] Failed to load profile public attributes: ${summarizeError(error)}`,
|
|
191
242
|
);
|
|
192
243
|
return [];
|
|
193
|
-
}) ?? Promise.resolve([])
|
|
194
|
-
|
|
244
|
+
}) ?? Promise.resolve([]);
|
|
245
|
+
|
|
246
|
+
const [userMdTags, profileAttributes] = await Promise.all([
|
|
247
|
+
userMdPromise.catch((error) => {
|
|
195
248
|
logger?.warn?.(
|
|
196
|
-
`[libp2p-mesh] Failed to load
|
|
249
|
+
`[libp2p-mesh] Failed to load USER.md public attributes: ${summarizeError(error)}`,
|
|
197
250
|
);
|
|
198
251
|
return [];
|
|
199
|
-
})
|
|
252
|
+
}),
|
|
253
|
+
profilePromise,
|
|
200
254
|
]);
|
|
201
255
|
|
|
202
256
|
return mergeUserPublicAttributes(userMdTags, profileAttributes);
|
|
203
257
|
}
|
|
204
258
|
|
|
205
|
-
|
|
259
|
+
function buildBaseAnnouncePayload(): InstanceAnnouncePayload {
|
|
206
260
|
const identity = mesh.getInstanceIdentity();
|
|
207
261
|
if (!identity) {
|
|
208
262
|
throw new Error("Local instance identity is not initialized");
|
|
@@ -214,17 +268,102 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
214
268
|
instanceName: identity.name,
|
|
215
269
|
multiaddrs: mesh.getMultiaddrs(),
|
|
216
270
|
pubkey: identity.pubkey,
|
|
217
|
-
userPublicAttributes: await loadUserPublicAttributes(),
|
|
218
271
|
announcedAt: Date.now(),
|
|
219
272
|
};
|
|
220
273
|
}
|
|
221
274
|
|
|
275
|
+
async function buildAttributeAnnouncePayload(): Promise<InstanceAnnouncePayload> {
|
|
276
|
+
return {
|
|
277
|
+
...buildBaseAnnouncePayload(),
|
|
278
|
+
userPublicAttributes: await loadUserPublicAttributes({ refreshUserMd: true }),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function attributeAnnouncePeers(extraPeerId?: string): string[] {
|
|
283
|
+
const peers = new Set<string>();
|
|
284
|
+
for (const peerId of mesh.getConnectedPeers()) {
|
|
285
|
+
if (isNonEmptyString(peerId) && peerId !== mesh.getLocalPeerId()) {
|
|
286
|
+
peers.add(peerId);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const peerId of announcedPeers) {
|
|
290
|
+
if (isNonEmptyString(peerId) && peerId !== mesh.getLocalPeerId()) {
|
|
291
|
+
peers.add(peerId);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (extraPeerId && extraPeerId !== mesh.getLocalPeerId()) {
|
|
295
|
+
peers.add(extraPeerId);
|
|
296
|
+
}
|
|
297
|
+
return [...peers];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function sendAttributeAnnounceToPeers(peers: string[]): Promise<void> {
|
|
301
|
+
if (peers.length === 0) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const payload = await buildAttributeAnnouncePayload();
|
|
306
|
+
for (const peerId of peers) {
|
|
307
|
+
try {
|
|
308
|
+
await mesh.sendStructuredMessage(peerId, {
|
|
309
|
+
id: crypto.randomUUID(),
|
|
310
|
+
type: "instance-announce",
|
|
311
|
+
to: peerId,
|
|
312
|
+
payload: JSON.stringify(payload),
|
|
313
|
+
});
|
|
314
|
+
logAnnounce("Sent", peerId, payload);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
logger?.warn?.(
|
|
317
|
+
`[libp2p-mesh] Failed to send attribute announce to ${peerId}: ${summarizeError(error)}`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function queueAttributeRefresh(extraPeerId?: string): void {
|
|
324
|
+
const peers =
|
|
325
|
+
extraPeerId === undefined
|
|
326
|
+
? attributeAnnouncePeers()
|
|
327
|
+
: isNonEmptyString(extraPeerId) && extraPeerId !== mesh.getLocalPeerId()
|
|
328
|
+
? [extraPeerId]
|
|
329
|
+
: [];
|
|
330
|
+
for (const peerId of peers) {
|
|
331
|
+
pendingAttributePeers.add(peerId);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (pendingAttributePeers.size === 0 && !attributeRefreshPromise) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
drainAttributeRefresh();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function drainAttributeRefresh(): Promise<void> {
|
|
342
|
+
if (!attributeRefreshPromise) {
|
|
343
|
+
attributeRefreshPromise = (async () => {
|
|
344
|
+
while (pendingAttributePeers.size > 0) {
|
|
345
|
+
const peers = [...pendingAttributePeers];
|
|
346
|
+
pendingAttributePeers.clear();
|
|
347
|
+
await sendAttributeAnnounceToPeers(peers).catch((error) => {
|
|
348
|
+
logger?.warn?.(
|
|
349
|
+
`[libp2p-mesh] Failed to refresh public attributes: ${summarizeError(error)}`,
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
})().finally(() => {
|
|
354
|
+
attributeRefreshPromise = undefined;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return attributeRefreshPromise;
|
|
359
|
+
}
|
|
360
|
+
|
|
222
361
|
async function announceToPeer(peerId: string): Promise<void> {
|
|
223
362
|
if (!isNonEmptyString(peerId) || peerId === mesh.getLocalPeerId()) {
|
|
224
363
|
return;
|
|
225
364
|
}
|
|
226
365
|
|
|
227
|
-
const payload =
|
|
366
|
+
const payload = buildBaseAnnouncePayload();
|
|
228
367
|
await mesh.sendStructuredMessage(peerId, {
|
|
229
368
|
id: crypto.randomUUID(),
|
|
230
369
|
type: "instance-announce",
|
|
@@ -233,6 +372,15 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
233
372
|
});
|
|
234
373
|
announcedPeers.add(peerId);
|
|
235
374
|
logAnnounce("Sent", peerId, payload);
|
|
375
|
+
queueAttributeRefresh(peerId);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function refreshPublicAttributes(): Promise<void> {
|
|
379
|
+
for (const peerId of attributeAnnouncePeers()) {
|
|
380
|
+
pendingAttributePeers.add(peerId);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
await drainAttributeRefresh();
|
|
236
384
|
}
|
|
237
385
|
|
|
238
386
|
function announceSummary(
|
|
@@ -638,24 +786,38 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
638
786
|
|
|
639
787
|
async function resolveUserAttributeTargets(
|
|
640
788
|
match: UserAttributeMatch,
|
|
789
|
+
scope: UserAttributeMatchScope,
|
|
641
790
|
): Promise<UserAttributeMessageTarget[]> {
|
|
642
791
|
const records = await store.list();
|
|
643
792
|
const targets: UserAttributeMessageTarget[] = [];
|
|
644
793
|
|
|
645
794
|
for (const record of records) {
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
795
|
+
const publicAttribute =
|
|
796
|
+
scope === "local"
|
|
797
|
+
? undefined
|
|
798
|
+
: record.userPublicAttributes?.find((attribute) =>
|
|
799
|
+
matchesUserAttribute(attribute, match),
|
|
800
|
+
);
|
|
801
|
+
const localAttribute =
|
|
802
|
+
scope === "public"
|
|
803
|
+
? undefined
|
|
804
|
+
: (await options.peerLabelStore?.listLabels(record.instanceId))?.find((attribute) =>
|
|
805
|
+
matchesLocalPeerLabel(attribute, match),
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
if (publicAttribute && localAttribute && scope === "all") {
|
|
809
|
+
targets.push(buildUserAttributeTarget(record, publicAttribute, "all"));
|
|
650
810
|
continue;
|
|
651
811
|
}
|
|
652
812
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
813
|
+
if (publicAttribute) {
|
|
814
|
+
targets.push(buildUserAttributeTarget(record, publicAttribute, "public"));
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (localAttribute) {
|
|
819
|
+
targets.push(buildUserAttributeTarget(record, localAttribute, "local"));
|
|
820
|
+
}
|
|
659
821
|
}
|
|
660
822
|
|
|
661
823
|
return targets;
|
|
@@ -664,11 +826,16 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
664
826
|
async function sendUserAttributeMessage(
|
|
665
827
|
match: UserAttributeMatch,
|
|
666
828
|
message: string,
|
|
667
|
-
sendOptions:
|
|
829
|
+
sendOptions: UserAttributeMessageOptions = {},
|
|
668
830
|
) {
|
|
669
|
-
const
|
|
831
|
+
const scope = effectiveAttributeMatchScope(sendOptions.scope);
|
|
832
|
+
const targets = await resolveUserAttributeTargets(
|
|
833
|
+
match,
|
|
834
|
+
scope,
|
|
835
|
+
);
|
|
670
836
|
if (targets.length === 0) {
|
|
671
837
|
return {
|
|
838
|
+
scope,
|
|
672
839
|
matched: 0,
|
|
673
840
|
sent: 0,
|
|
674
841
|
delivered: 0,
|
|
@@ -679,6 +846,7 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
679
846
|
|
|
680
847
|
if (sendOptions.dryRun === true) {
|
|
681
848
|
return {
|
|
849
|
+
scope,
|
|
682
850
|
matched: targets.length,
|
|
683
851
|
sent: 0,
|
|
684
852
|
delivered: 0,
|
|
@@ -712,6 +880,7 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
712
880
|
}
|
|
713
881
|
|
|
714
882
|
return {
|
|
883
|
+
scope,
|
|
715
884
|
matched: targets.length,
|
|
716
885
|
sent: results.filter((result) => result.sent).length,
|
|
717
886
|
delivered: results.filter((result) => result.delivered).length,
|
|
@@ -727,6 +896,7 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
727
896
|
stop,
|
|
728
897
|
handleMessage,
|
|
729
898
|
announceToPeer,
|
|
899
|
+
refreshPublicAttributes,
|
|
730
900
|
listInstances,
|
|
731
901
|
resolveInstance,
|
|
732
902
|
sendInstanceMessage,
|