@scp-dev/broker 0.2.0 → 0.2.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/dist/index.d.ts +2 -0
- package/dist/index.js +49 -8
- package/package.json +3 -3
- package/src/bridge.ts +10 -3
- package/src/server.ts +57 -5
package/dist/index.d.ts
CHANGED
|
@@ -334,6 +334,8 @@ interface BrokerOptions {
|
|
|
334
334
|
link?: LinkFactory | undefined;
|
|
335
335
|
/** how often presence that decays without a request (a hook-only endpoint) is checked for changes (default 60 s) */
|
|
336
336
|
presenceEveryMs?: number | undefined;
|
|
337
|
+
/** how often the broker checks it still owns its socket and lock, stopping once another broker or a wiped state directory took them (default 2 s) */
|
|
338
|
+
ownershipEveryMs?: number | undefined;
|
|
337
339
|
/** where a judge model's key_env is read (default the broker's environment) */
|
|
338
340
|
env?: Record<string, string | undefined> | undefined;
|
|
339
341
|
/** the judge model client for a policy's `judge.model` (default the provider's HTTP API) */
|
package/dist/index.js
CHANGED
|
@@ -706,7 +706,8 @@ function startBridge(options) {
|
|
|
706
706
|
byMachine.set(view.machine, [...byMachine.get(view.machine) ?? [], view]);
|
|
707
707
|
}
|
|
708
708
|
if (full) {
|
|
709
|
-
|
|
709
|
+
core.dropMachine(machine);
|
|
710
|
+
for (const view of core.remoteViews()) if (view.machine !== account && !byMachine.has(view.machine)) core.dropMachine(view.machine);
|
|
710
711
|
}
|
|
711
712
|
for (const [name, list] of byMachine) {
|
|
712
713
|
try {
|
|
@@ -807,7 +808,8 @@ function startBridge(options) {
|
|
|
807
808
|
if (typeof params.machine !== "string" || params.machine === machine) return;
|
|
808
809
|
if (typeof params.own === "boolean") core.setOther(params.machine, !params.own);
|
|
809
810
|
try {
|
|
810
|
-
|
|
811
|
+
if (params.released === true) core.dropMachine(params.machine);
|
|
812
|
+
else core.mirror(params.machine, params.endpoints, { full: params.full === true });
|
|
811
813
|
} catch (error) {
|
|
812
814
|
log(`relay bridge: the endpoints of ${params.machine} were not applied: ${error instanceof Error ? error.message : String(error)}`);
|
|
813
815
|
}
|
|
@@ -1224,6 +1226,7 @@ function openDatabase(path) {
|
|
|
1224
1226
|
// src/server.ts
|
|
1225
1227
|
var SWEEP_MS = 10 * 6e4;
|
|
1226
1228
|
var PRESENCE_MS = 6e4;
|
|
1229
|
+
var OWNERSHIP_MS = 2e3;
|
|
1227
1230
|
var OBSERVER_METHODS = /* @__PURE__ */ new Set(["list", "snapshot", "watch", "log", "policy", "policy_check", "cloud_status", "bridge_relink"]);
|
|
1228
1231
|
var SCOPE_METHODS = /* @__PURE__ */ new Set([
|
|
1229
1232
|
"rules",
|
|
@@ -1304,6 +1307,7 @@ async function startBroker(options) {
|
|
|
1304
1307
|
let judge;
|
|
1305
1308
|
let bridge = null;
|
|
1306
1309
|
let listener;
|
|
1310
|
+
let socketIno;
|
|
1307
1311
|
try {
|
|
1308
1312
|
privateDir(dirname2(socket));
|
|
1309
1313
|
db = openDatabase(join4(dir, "relay.db"));
|
|
@@ -1320,6 +1324,7 @@ async function startBroker(options) {
|
|
|
1320
1324
|
alive: running
|
|
1321
1325
|
});
|
|
1322
1326
|
core.migrate();
|
|
1327
|
+
core.relabelMachine();
|
|
1323
1328
|
core.resetConnections();
|
|
1324
1329
|
core.sweep();
|
|
1325
1330
|
judge = new JudgeGate({ sql, core, now: options.now ?? Date.now, env: options.env ?? process.env, client: options.judgeClient, log, wake });
|
|
@@ -1356,6 +1361,7 @@ async function startBroker(options) {
|
|
|
1356
1361
|
});
|
|
1357
1362
|
listener.on("error", (error) => log(`relay broker: the socket listener failed: ${error.message}`));
|
|
1358
1363
|
chmodSync2(socket, 384);
|
|
1364
|
+
socketIno = lstatSync2(socket).ino;
|
|
1359
1365
|
} catch (error) {
|
|
1360
1366
|
listener?.close();
|
|
1361
1367
|
db?.close();
|
|
@@ -1379,6 +1385,22 @@ async function startBroker(options) {
|
|
|
1379
1385
|
}
|
|
1380
1386
|
for (const id of known.keys()) if (!live.has(id)) known.delete(id);
|
|
1381
1387
|
}, options.presenceEveryMs ?? PRESENCE_MS);
|
|
1388
|
+
const ownership = setInterval(() => {
|
|
1389
|
+
if (closing || ownsState()) return;
|
|
1390
|
+
log(`relay broker: ${socket} or its lock was removed or taken over; stopping`);
|
|
1391
|
+
void stop();
|
|
1392
|
+
}, options.ownershipEveryMs ?? OWNERSHIP_MS);
|
|
1393
|
+
ownership.unref?.();
|
|
1394
|
+
function ownsSocket() {
|
|
1395
|
+
return lstatSync2(socket, { throwIfNoEntry: false })?.ino === socketIno;
|
|
1396
|
+
}
|
|
1397
|
+
function ownsState() {
|
|
1398
|
+
try {
|
|
1399
|
+
return ownsSocket() && readFileSync6(lock.path, "utf8") === lock.content;
|
|
1400
|
+
} catch {
|
|
1401
|
+
return false;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1382
1404
|
const remoteHooks = {
|
|
1383
1405
|
wake,
|
|
1384
1406
|
fleet: notifyFleet,
|
|
@@ -1467,6 +1489,20 @@ async function startBroker(options) {
|
|
|
1467
1489
|
function localViews() {
|
|
1468
1490
|
return core.list({ all: true }).filter((view) => core.machineOf(view.id) === null);
|
|
1469
1491
|
}
|
|
1492
|
+
function setWaker(s, endpoint) {
|
|
1493
|
+
const was = s.data.waker;
|
|
1494
|
+
if (was === endpoint) return;
|
|
1495
|
+
s.data.waker = endpoint;
|
|
1496
|
+
if (was) core.waker(was, false, { grace: s.data.transient === true });
|
|
1497
|
+
if (endpoint) core.waker(endpoint, true);
|
|
1498
|
+
for (const id of [was, endpoint]) if (id) presenceMoved(id);
|
|
1499
|
+
}
|
|
1500
|
+
function presenceMoved(id) {
|
|
1501
|
+
const view = core.whoami(id);
|
|
1502
|
+
if ((known.get(id) ?? "offline") === view.presence) return false;
|
|
1503
|
+
presenceChanged(view);
|
|
1504
|
+
return true;
|
|
1505
|
+
}
|
|
1470
1506
|
function presenceChanged(view) {
|
|
1471
1507
|
known.set(view.id, view.presence);
|
|
1472
1508
|
notifyFleet({ type: "presence", endpoint: view.address, presence: view.presence, harness: view.harness, labels: view.labels });
|
|
@@ -1618,6 +1654,7 @@ async function startBroker(options) {
|
|
|
1618
1654
|
s.data.closed = true;
|
|
1619
1655
|
clearTimeout(s.data.lineTimer);
|
|
1620
1656
|
connections.delete(s);
|
|
1657
|
+
if (!closing) setWaker(s, void 0);
|
|
1621
1658
|
if (s.data.endpoint && !closing && !s.data.transient && !s.data.lineage) {
|
|
1622
1659
|
const before = core.whoami(s.data.endpoint).presence;
|
|
1623
1660
|
const settled = core.disconnect(s.data.endpoint, { settle: !s.data.brief });
|
|
@@ -1911,7 +1948,8 @@ async function startBroker(options) {
|
|
|
1911
1948
|
case "wake": {
|
|
1912
1949
|
if (s.data.lineage && !s.data.own) throw new RelayError3("unauthorized", "only the endpoint's own session says how it wakes");
|
|
1913
1950
|
const changed = core.setWake(me, params.via, hookOf(s));
|
|
1914
|
-
|
|
1951
|
+
setWaker(s, params.via !== "none" && core.whoami(me).wake === params.via ? me : void 0);
|
|
1952
|
+
if (!presenceMoved(me) && changed) bridge?.changed(me);
|
|
1915
1953
|
return { changed };
|
|
1916
1954
|
}
|
|
1917
1955
|
case "rename": {
|
|
@@ -2214,6 +2252,7 @@ async function startBroker(options) {
|
|
|
2214
2252
|
closing = true;
|
|
2215
2253
|
clearInterval(sweeper);
|
|
2216
2254
|
clearInterval(presenceTimer);
|
|
2255
|
+
clearInterval(ownership);
|
|
2217
2256
|
await bridge?.stop();
|
|
2218
2257
|
if (idleTimer) clearTimeout(idleTimer);
|
|
2219
2258
|
for (const set of [...waiters.values()]) for (const fn of [...set]) fn();
|
|
@@ -2221,11 +2260,13 @@ async function startBroker(options) {
|
|
|
2221
2260
|
while (pending > 0 && Date.now() < deadline) await sleep2(5);
|
|
2222
2261
|
await Promise.race([judge.settled(), sleep2(Math.max(0, deadline - Date.now()))]);
|
|
2223
2262
|
for (const s of connections) s.end();
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2263
|
+
if (ownsSocket()) {
|
|
2264
|
+
listener.close();
|
|
2265
|
+
try {
|
|
2266
|
+
unlinkSync2(socket);
|
|
2267
|
+
} catch {
|
|
2268
|
+
}
|
|
2269
|
+
} else listener.unref();
|
|
2229
2270
|
if (socket !== join4(dir, "relay.sock")) {
|
|
2230
2271
|
try {
|
|
2231
2272
|
rmdirSync(dirname2(socket));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scp-dev/broker",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Swarm Communication Protocol local broker: a unix socket JSON-RPC daemon over the relay core, its client and the bridge to an account hub.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://scp.dev",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"typecheck": "tsc -p ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@scp-dev/core": "0.1.
|
|
41
|
-
"@scp-dev/hub": "0.5.
|
|
40
|
+
"@scp-dev/core": "0.1.6",
|
|
41
|
+
"@scp-dev/hub": "0.5.1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"tsup": "^8.5.1"
|
package/src/bridge.ts
CHANGED
|
@@ -300,14 +300,20 @@ export function startBridge(options: BridgeOptions): Bridge {
|
|
|
300
300
|
for (const fn of waitingForLink.splice(0)) fn()
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
/**
|
|
303
|
+
/**
|
|
304
|
+
* the other machines' endpoints, grouped by machine; with `full`, a known machine the hub no longer lists was released and its rows go,
|
|
305
|
+
* as do rows mirrored under this machine's own name from before it was renamed back to it
|
|
306
|
+
*/
|
|
304
307
|
function applyRemote(views: RemoteView[], full: boolean) {
|
|
305
308
|
const byMachine = new Map<string, RemoteView[]>()
|
|
306
309
|
for (const view of views) {
|
|
307
310
|
if (view.machine === machine) continue
|
|
308
311
|
byMachine.set(view.machine, [...(byMachine.get(view.machine) ?? []), view])
|
|
309
312
|
}
|
|
310
|
-
if (full)
|
|
313
|
+
if (full) {
|
|
314
|
+
core.dropMachine(machine)
|
|
315
|
+
for (const view of core.remoteViews()) if (view.machine !== account && !byMachine.has(view.machine)) core.dropMachine(view.machine)
|
|
316
|
+
}
|
|
311
317
|
for (const [name, list] of byMachine) {
|
|
312
318
|
try {
|
|
313
319
|
core.mirror(name, list, { full })
|
|
@@ -423,7 +429,8 @@ export function startBridge(options: BridgeOptions): Bridge {
|
|
|
423
429
|
if (typeof params.machine !== "string" || params.machine === machine) return
|
|
424
430
|
if (typeof params.own === "boolean") core.setOther(params.machine, !params.own)
|
|
425
431
|
try {
|
|
426
|
-
|
|
432
|
+
if (params.released === true) core.dropMachine(params.machine)
|
|
433
|
+
else core.mirror(params.machine, params.endpoints, { full: params.full === true })
|
|
427
434
|
} catch (error) {
|
|
428
435
|
log(`relay bridge: the endpoints of ${params.machine} were not applied: ${error instanceof Error ? error.message : String(error)}`)
|
|
429
436
|
}
|
package/src/server.ts
CHANGED
|
@@ -64,6 +64,8 @@ export interface BrokerOptions {
|
|
|
64
64
|
link?: LinkFactory | undefined
|
|
65
65
|
/** how often presence that decays without a request (a hook-only endpoint) is checked for changes (default 60 s) */
|
|
66
66
|
presenceEveryMs?: number | undefined
|
|
67
|
+
/** how often the broker checks it still owns its socket and lock, stopping once another broker or a wiped state directory took them (default 2 s) */
|
|
68
|
+
ownershipEveryMs?: number | undefined
|
|
67
69
|
/** where a judge model's key_env is read (default the broker's environment) */
|
|
68
70
|
env?: Record<string, string | undefined> | undefined
|
|
69
71
|
/** the judge model client for a policy's `judge.model` (default the provider's HTTP API) */
|
|
@@ -114,6 +116,8 @@ interface Connection {
|
|
|
114
116
|
process?: string
|
|
115
117
|
/** a one-shot CLI command's connection: its comings and goings are not fleet presence events */
|
|
116
118
|
brief?: boolean
|
|
119
|
+
/** the endpoint whose wake this connection set: it counts as its waker until it closes or sets none */
|
|
120
|
+
waker?: string | undefined
|
|
117
121
|
cwd?: string | undefined
|
|
118
122
|
watching: boolean
|
|
119
123
|
closed: boolean
|
|
@@ -126,6 +130,7 @@ interface Connection {
|
|
|
126
130
|
|
|
127
131
|
const SWEEP_MS = 10 * 60_000
|
|
128
132
|
const PRESENCE_MS = 60_000
|
|
133
|
+
const OWNERSHIP_MS = 2_000
|
|
129
134
|
const OBSERVER_METHODS = new Set(["list", "snapshot", "watch", "log", "policy", "policy_check", "cloud_status", "bridge_relink"])
|
|
130
135
|
const SCOPE_METHODS = new Set([
|
|
131
136
|
"rules",
|
|
@@ -219,6 +224,7 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
|
|
|
219
224
|
let judge!: JudgeGate
|
|
220
225
|
let bridge: Bridge | null = null
|
|
221
226
|
let listener!: Server
|
|
227
|
+
let socketIno: number | bigint | undefined
|
|
222
228
|
try {
|
|
223
229
|
privateDir(dirname(socket))
|
|
224
230
|
db = openDatabase(join(dir, "relay.db"))
|
|
@@ -235,6 +241,7 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
|
|
|
235
241
|
alive: running,
|
|
236
242
|
})
|
|
237
243
|
core.migrate()
|
|
244
|
+
core.relabelMachine()
|
|
238
245
|
core.resetConnections()
|
|
239
246
|
core.sweep()
|
|
240
247
|
judge = new JudgeGate({ sql, core, now: options.now ?? Date.now, env: options.env ?? process.env, client: options.judgeClient, log, wake })
|
|
@@ -271,6 +278,7 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
|
|
|
271
278
|
})
|
|
272
279
|
listener.on("error", (error) => log(`relay broker: the socket listener failed: ${error.message}`))
|
|
273
280
|
chmodSync(socket, 0o600)
|
|
281
|
+
socketIno = lstatSync(socket).ino
|
|
274
282
|
} catch (error) {
|
|
275
283
|
listener?.close()
|
|
276
284
|
db?.close()
|
|
@@ -294,6 +302,26 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
|
|
|
294
302
|
}
|
|
295
303
|
for (const id of known.keys()) if (!live.has(id)) known.delete(id)
|
|
296
304
|
}, options.presenceEveryMs ?? PRESENCE_MS)
|
|
305
|
+
const ownership = setInterval(() => {
|
|
306
|
+
if (closing || ownsState()) return
|
|
307
|
+
log(`relay broker: ${socket} or its lock was removed or taken over; stopping`)
|
|
308
|
+
void stop()
|
|
309
|
+
}, options.ownershipEveryMs ?? OWNERSHIP_MS)
|
|
310
|
+
ownership.unref?.()
|
|
311
|
+
|
|
312
|
+
/** whether the socket path is still this broker's listener */
|
|
313
|
+
function ownsSocket(): boolean {
|
|
314
|
+
return lstatSync(socket, { throwIfNoEntry: false })?.ino === socketIno
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** whether the socket path and the lock are still this broker's */
|
|
318
|
+
function ownsState(): boolean {
|
|
319
|
+
try {
|
|
320
|
+
return ownsSocket() && readFileSync(lock.path, "utf8") === lock.content
|
|
321
|
+
} catch {
|
|
322
|
+
return false
|
|
323
|
+
}
|
|
324
|
+
}
|
|
297
325
|
const remoteHooks: BridgeHooks = {
|
|
298
326
|
wake,
|
|
299
327
|
fleet: notifyFleet,
|
|
@@ -399,6 +427,24 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
|
|
|
399
427
|
return core.list({ all: true }).filter((view) => core.machineOf(view.id) === null)
|
|
400
428
|
}
|
|
401
429
|
|
|
430
|
+
/** counts `s` as the waker of `endpoint`, or of none, and reports the presence that moves */
|
|
431
|
+
function setWaker(s: Conn<Connection>, endpoint: string | undefined) {
|
|
432
|
+
const was = s.data.waker
|
|
433
|
+
if (was === endpoint) return
|
|
434
|
+
s.data.waker = endpoint
|
|
435
|
+
if (was) core.waker(was, false, { grace: s.data.transient === true })
|
|
436
|
+
if (endpoint) core.waker(endpoint, true)
|
|
437
|
+
for (const id of [was, endpoint]) if (id) presenceMoved(id)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** reports an endpoint's presence when it is not the one last reported; whether it was not */
|
|
441
|
+
function presenceMoved(id: string): boolean {
|
|
442
|
+
const view = core.whoami(id)
|
|
443
|
+
if ((known.get(id) ?? "offline") === view.presence) return false
|
|
444
|
+
presenceChanged(view)
|
|
445
|
+
return true
|
|
446
|
+
}
|
|
447
|
+
|
|
402
448
|
/** an endpoint's presence changed outside a connection's own hello or close: watchers and the other machines hear it */
|
|
403
449
|
function presenceChanged(view: EndpointView) {
|
|
404
450
|
known.set(view.id, view.presence)
|
|
@@ -583,6 +629,7 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
|
|
|
583
629
|
s.data.closed = true
|
|
584
630
|
clearTimeout(s.data.lineTimer)
|
|
585
631
|
connections.delete(s)
|
|
632
|
+
if (!closing) setWaker(s, undefined)
|
|
586
633
|
if (s.data.endpoint && !closing && !s.data.transient && !s.data.lineage) {
|
|
587
634
|
const before = core.whoami(s.data.endpoint).presence
|
|
588
635
|
const settled = core.disconnect(s.data.endpoint, { settle: !s.data.brief })
|
|
@@ -891,7 +938,8 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
|
|
|
891
938
|
case "wake": {
|
|
892
939
|
if (s.data.lineage && !s.data.own) throw new RelayError("unauthorized", "only the endpoint's own session says how it wakes")
|
|
893
940
|
const changed = core.setWake(me, params.via, hookOf(s))
|
|
894
|
-
|
|
941
|
+
setWaker(s, params.via !== "none" && core.whoami(me).wake === params.via ? me : undefined)
|
|
942
|
+
if (!presenceMoved(me) && changed) bridge?.changed(me)
|
|
895
943
|
return { changed }
|
|
896
944
|
}
|
|
897
945
|
case "rename": {
|
|
@@ -1210,6 +1258,7 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
|
|
|
1210
1258
|
closing = true
|
|
1211
1259
|
clearInterval(sweeper)
|
|
1212
1260
|
clearInterval(presenceTimer)
|
|
1261
|
+
clearInterval(ownership)
|
|
1213
1262
|
await bridge?.stop()
|
|
1214
1263
|
if (idleTimer) clearTimeout(idleTimer)
|
|
1215
1264
|
for (const set of [...waiters.values()]) for (const fn of [...set]) fn()
|
|
@@ -1217,10 +1266,13 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
|
|
|
1217
1266
|
while (pending > 0 && Date.now() < deadline) await sleep(5)
|
|
1218
1267
|
await Promise.race([judge.settled(), sleep(Math.max(0, deadline - Date.now()))])
|
|
1219
1268
|
for (const s of connections) s.end()
|
|
1220
|
-
listener
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1269
|
+
// closing a listener unlinks its path, which another broker may own by now
|
|
1270
|
+
if (ownsSocket()) {
|
|
1271
|
+
listener.close()
|
|
1272
|
+
try {
|
|
1273
|
+
unlinkSync(socket)
|
|
1274
|
+
} catch {}
|
|
1275
|
+
} else listener.unref()
|
|
1224
1276
|
if (socket !== join(dir, "relay.sock")) {
|
|
1225
1277
|
try {
|
|
1226
1278
|
rmdirSync(dirname(socket))
|