mslxdff 0.1.22 → 0.1.23
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/bin/mslxdff.js +44 -0
- package/package.json +1 -1
- package/src/groups.js +31 -0
- package/src/routes.js +32 -0
package/bin/mslxdff.js
CHANGED
|
@@ -331,6 +331,49 @@ function markJoined(entry) {
|
|
|
331
331
|
saveGroupsJoined([...list, entry]);
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
// -leavegroup | -leave-groups: leave every joined group. Members deregister
|
|
335
|
+
// from their leader; a leader disbands the group (deletes it entirely).
|
|
336
|
+
const leaveAll = args.includes("-leavegroup") || args.includes("--leavegroup") || args.includes("-leave-groups");
|
|
337
|
+
if (leaveAll) {
|
|
338
|
+
const groups = createGroupsService({});
|
|
339
|
+
const peers = createPeersService({});
|
|
340
|
+
const joined = loadGroupsJoined();
|
|
341
|
+
if (!joined.length) {
|
|
342
|
+
console.log("not joined to any group");
|
|
343
|
+
process.exit(0);
|
|
344
|
+
}
|
|
345
|
+
const myToken = (await loadToken()).token;
|
|
346
|
+
for (const g of joined) {
|
|
347
|
+
const peersRemoved = peers.removeByGroup(g.name);
|
|
348
|
+
if (g.leaderUrl) {
|
|
349
|
+
// member: deregister from the leader so we stop showing up in -group list
|
|
350
|
+
try {
|
|
351
|
+
const res = await fetch(`${g.leaderUrl}/v1/groups/leave`, {
|
|
352
|
+
method: "POST",
|
|
353
|
+
headers: {
|
|
354
|
+
"Content-Type": "application/json",
|
|
355
|
+
"Authorization": `Bearer ${myToken}`,
|
|
356
|
+
},
|
|
357
|
+
body: JSON.stringify({ name: g.name }),
|
|
358
|
+
});
|
|
359
|
+
if (res.ok) console.log(`${g.name}: left (deregistered from ${g.leaderUrl})`);
|
|
360
|
+
else console.log(`${g.name}: left locally (leader said: ${await res.text().catch(() => `HTTP ${res.status}`)})`);
|
|
361
|
+
} catch (err) {
|
|
362
|
+
console.log(`${g.name}: left locally (leader unreachable: ${errMsg(err)})`);
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
// leader: disband the whole group
|
|
366
|
+
const disbanded = groups.delete(g.name);
|
|
367
|
+
const members = Object.values(disbanded?.members || {});
|
|
368
|
+
console.log(`${g.name}: disbanded (${members.length} member${members.length === 1 ? "" : "s"} removed)`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const left = joined.map((g) => g.name);
|
|
372
|
+
saveGroupsJoined(loadGroupsJoined().filter((g) => !left.includes(g.name)));
|
|
373
|
+
console.log(`left ${left.length} group(s)`);
|
|
374
|
+
process.exit(0);
|
|
375
|
+
}
|
|
376
|
+
|
|
334
377
|
function errMsg(err) { return String(err?.message || err); }
|
|
335
378
|
|
|
336
379
|
// Probe a member's health endpoint concurrently (v1/health preferred,
|
|
@@ -645,6 +688,7 @@ Usage:
|
|
|
645
688
|
mslxdff -group sync pull the freshest member list for all joined groups
|
|
646
689
|
mslxdff -group leave <name> leave a group (removes its members from this node)
|
|
647
690
|
mslxdff -group list list groups on this node
|
|
691
|
+
mslxdff -leavegroup leave every group (members deregister, a leader disbands)
|
|
648
692
|
mslxdff -resetban [ip] clear join-failure bans (all, or one ip)
|
|
649
693
|
mslxdff -help show this help
|
|
650
694
|
|
package/package.json
CHANGED
package/src/groups.js
CHANGED
|
@@ -91,6 +91,35 @@ export function listGroups({ file } = {}) {
|
|
|
91
91
|
return loadGroups(file ? { file } : {});
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// Leader side: remove a member by exact url (a member leaving the group).
|
|
95
|
+
export function removeGroupMember(name, { url, file } = {}) {
|
|
96
|
+
const groups = loadGroups(file ? { file } : {});
|
|
97
|
+
const group = groups[name];
|
|
98
|
+
if (!group) return null;
|
|
99
|
+
if (!url) throw new Error("member url is required");
|
|
100
|
+
for (const [id, m] of Object.entries(group.members || {})) {
|
|
101
|
+
if (m.url === String(url).replace(/\/+$/, "")) {
|
|
102
|
+
delete group.members[id];
|
|
103
|
+
saveGroups(groups, file ? { file } : {});
|
|
104
|
+
return { removed: { id, url: m.url } };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Leader side: disband a group entirely — deletes the group and all its members.
|
|
111
|
+
// Used by -leavegroup on the leader node. Also returns the member list so the
|
|
112
|
+
// caller can notify members before (or after) the group is gone.
|
|
113
|
+
export function deleteGroup(name, { file } = {}) {
|
|
114
|
+
const groups = loadGroups(file ? { file } : {});
|
|
115
|
+
const group = groups[name];
|
|
116
|
+
if (!group) return null;
|
|
117
|
+
const members = group.members || {};
|
|
118
|
+
delete groups[name];
|
|
119
|
+
saveGroups(groups, file ? { file } : {});
|
|
120
|
+
return { members };
|
|
121
|
+
}
|
|
122
|
+
|
|
94
123
|
export function createGroupsService({ file } = {}) {
|
|
95
124
|
const opts = (o = {}) => (file ? { ...o, file } : o);
|
|
96
125
|
return {
|
|
@@ -99,6 +128,8 @@ export function createGroupsService({ file } = {}) {
|
|
|
99
128
|
upsertMember: (name, o = {}) => upsertMember(name, opts(o)),
|
|
100
129
|
listMembers: (name, o = {}) => listGroupMembers(name, opts(o)),
|
|
101
130
|
membersForToken: (name, token) => membersForToken(name, token, opts()),
|
|
131
|
+
removeMember: (name, o = {}) => removeGroupMember(name, opts(o)),
|
|
132
|
+
delete: (name, o = {}) => deleteGroup(name, opts(o)),
|
|
102
133
|
list: () => listGroups(opts()),
|
|
103
134
|
};
|
|
104
135
|
}
|
package/src/routes.js
CHANGED
|
@@ -454,6 +454,38 @@ const ROUTES = [
|
|
|
454
454
|
}
|
|
455
455
|
},
|
|
456
456
|
},
|
|
457
|
+
{
|
|
458
|
+
method: "POST",
|
|
459
|
+
path: "/v1/groups/leave",
|
|
460
|
+
requiresAuth: false,
|
|
461
|
+
handler: async ({ req, res, groups }) => {
|
|
462
|
+
if (!groups) return json(res, 501, { error: "Groups service not configured" });
|
|
463
|
+
let body;
|
|
464
|
+
try {
|
|
465
|
+
body = await readBody(req);
|
|
466
|
+
} catch {
|
|
467
|
+
return json(res, 400, { error: "Invalid JSON body" });
|
|
468
|
+
}
|
|
469
|
+
if (!body?.name) return json(res, 400, { error: "group name is required" });
|
|
470
|
+
const auth = /^Bearer (.+)$/.exec(req.headers["authorization"] || "");
|
|
471
|
+
if (!auth) return json(res, 401, { error: "bearer token required" });
|
|
472
|
+
const group = groups.list()[body.name];
|
|
473
|
+
if (!group) return json(res, 404, { error: `group "${body.name}" not found` });
|
|
474
|
+
const hit = groups.membersForToken(body.name, auth[1]);
|
|
475
|
+
if (!hit) return json(res, 403, { error: "invalid member token" });
|
|
476
|
+
try {
|
|
477
|
+
const removed = groups.removeMember(body.name, { url: hit.member.url });
|
|
478
|
+
return json(res, 200, {
|
|
479
|
+
object: "group",
|
|
480
|
+
name: body.name,
|
|
481
|
+
removed: removed?.removed ?? null,
|
|
482
|
+
members: groups.list()[body.name]?.members ?? {},
|
|
483
|
+
});
|
|
484
|
+
} catch (err) {
|
|
485
|
+
return json(res, 400, { error: errMsg(err) });
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
},
|
|
457
489
|
{
|
|
458
490
|
method: "GET",
|
|
459
491
|
path: "/v1/models",
|