mslxdff 0.1.22 → 0.1.24
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 +72 -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,76 @@ function markJoined(entry) {
|
|
|
331
331
|
saveGroupsJoined([...list, entry]);
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
// -leavegroup | -leave-groups: leave every joined group as a member.
|
|
335
|
+
// Leaders cannot "leave" — they must disband with -delgroup <name>.
|
|
336
|
+
const leaveAll = args.includes("-leavegroup") || args.includes("--leavegroup") || args.includes("-leave-groups");
|
|
337
|
+
if (leaveAll) {
|
|
338
|
+
const peers = createPeersService({});
|
|
339
|
+
const joined = loadGroupsJoined();
|
|
340
|
+
if (!joined.length) {
|
|
341
|
+
console.log("not joined to any group");
|
|
342
|
+
process.exit(0);
|
|
343
|
+
}
|
|
344
|
+
const myToken = (await loadToken()).token;
|
|
345
|
+
const leaders = [];
|
|
346
|
+
for (const g of joined) {
|
|
347
|
+
if (g.leaderUrl) {
|
|
348
|
+
// member: deregister from the leader so we stop showing up in -group list
|
|
349
|
+
const peersRemoved = peers.removeByGroup(g.name);
|
|
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
|
+
leaders.push(g.name);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const left = joined.filter((g) => g.leaderUrl).map((g) => g.name);
|
|
369
|
+
saveGroupsJoined(loadGroupsJoined().filter((g) => !left.includes(g.name)));
|
|
370
|
+
console.log(`left ${left.length} group(s)`);
|
|
371
|
+
if (leaders.length) {
|
|
372
|
+
console.log(`\nskipped ${leaders.length} group(s) where this node is the leader:`);
|
|
373
|
+
for (const n of leaders) console.log(` ${n} — leaders can't leave; disband it with: mslxdff -delgroup ${n}`);
|
|
374
|
+
}
|
|
375
|
+
process.exit(0);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// -delgroup <name>: a leader disbands one of its groups (local groups state +
|
|
379
|
+
// any member records). Fails if this node is not the leader of that group.
|
|
380
|
+
const delGroupName = argValue("-delgroup", "--delgroup");
|
|
381
|
+
if (delGroupName) {
|
|
382
|
+
const groups = createGroupsService({});
|
|
383
|
+
const peers = createPeersService({});
|
|
384
|
+
const local = groups.list()[delGroupName];
|
|
385
|
+
if (!local) {
|
|
386
|
+
const joined0 = loadGroupsJoined().find((g) => g.name === delGroupName);
|
|
387
|
+
if (joined0?.leaderUrl) {
|
|
388
|
+
console.log(`"${delGroupName}" is led by ${joined0.leaderUrl} — you are a member, use -leavegroup to leave it`);
|
|
389
|
+
} else if (joined0) {
|
|
390
|
+
console.log(`"${delGroupName}" exists in local state but has no group definition — nothing to delete`);
|
|
391
|
+
} else {
|
|
392
|
+
console.log(`group "${delGroupName}" not found on this node`);
|
|
393
|
+
}
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
const disbanded = groups.delete(delGroupName);
|
|
397
|
+
const members = Object.values(disbanded?.members || {});
|
|
398
|
+
peers.removeByGroup(delGroupName);
|
|
399
|
+
saveGroupsJoined(loadGroupsJoined().filter((g) => g.name !== delGroupName));
|
|
400
|
+
console.log(`group "${delGroupName}" disbanded (${members.length} member${members.length === 1 ? "" : "s"} removed)`);
|
|
401
|
+
process.exit(0);
|
|
402
|
+
}
|
|
403
|
+
|
|
334
404
|
function errMsg(err) { return String(err?.message || err); }
|
|
335
405
|
|
|
336
406
|
// Probe a member's health endpoint concurrently (v1/health preferred,
|
|
@@ -645,6 +715,8 @@ Usage:
|
|
|
645
715
|
mslxdff -group sync pull the freshest member list for all joined groups
|
|
646
716
|
mslxdff -group leave <name> leave a group (removes its members from this node)
|
|
647
717
|
mslxdff -group list list groups on this node
|
|
718
|
+
mslxdff -leavegroup leave every joined group as a member (leaders: use -delgroup)
|
|
719
|
+
mslxdff -delgroup <name> disband a group this node leads (deletes it and its members)
|
|
648
720
|
mslxdff -resetban [ip] clear join-failure bans (all, or one ip)
|
|
649
721
|
mslxdff -help show this help
|
|
650
722
|
|
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",
|