@scp-dev/broker 0.1.0 → 0.1.2

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 CHANGED
@@ -143,7 +143,8 @@ declare class RelayClient {
143
143
  }>;
144
144
  /**
145
145
  * at a stop: the spawned workers' unread messages, held for the hook to print, waiting up to `wait_s` while workers still run, none came,
146
- * pushes are left and no prompt moved the `prompts` mark inject handed out
146
+ * pushes are left and no prompt moved the `prompts` mark inject handed out; with `wait_s` above 0 and other workers still running, the
147
+ * broker gathers more of their results into the same push for up to ${LIMITS.pushGraceS} s, which may run past `wait_s`
147
148
  */
148
149
  stopWait(waitS?: number, prompts?: number): Promise<StopWaitResult>;
149
150
  group(params: GroupParams): Promise<unknown>;
@@ -221,6 +222,8 @@ interface Bridge {
221
222
  /** the endpoints of other accounts this account's contacts let it reach */
222
223
  contacts(): Promise<XListed[]>;
223
224
  status(): BridgeStatus;
225
+ /** reads the credential again and reconnects now, also after the hub revoked or replaced this bridge */
226
+ relink(): void;
224
227
  stop(): Promise<void>;
225
228
  }
226
229
  /** a WebSocket constructor that takes request headers, as Bun's and Node's do */
@@ -400,6 +403,8 @@ interface BrokerOptions {
400
403
  machine?: string | undefined;
401
404
  /** the link to the hub; without it the broker never opens a network connection */
402
405
  bridge?: BridgeConfig | undefined;
406
+ /** reads the bridge configuration again for bridge_relink; one that appeared or names another hub or machine restarts the broker */
407
+ bridgeConfig?: (() => Promise<BridgeConfig | undefined>) | undefined;
403
408
  /** how often presence that decays without a request (a hook-only endpoint) is checked for changes (default 60 s) */
404
409
  presenceEveryMs?: number | undefined;
405
410
  /** where a judge model's key_env is read (default the broker's environment) */
package/dist/index.js CHANGED
@@ -408,7 +408,8 @@ var RelayClient = class _RelayClient {
408
408
  }
409
409
  /**
410
410
  * at a stop: the spawned workers' unread messages, held for the hook to print, waiting up to `wait_s` while workers still run, none came,
411
- * pushes are left and no prompt moved the `prompts` mark inject handed out
411
+ * pushes are left and no prompt moved the `prompts` mark inject handed out; with `wait_s` above 0 and other workers still running, the
412
+ * broker gathers more of their results into the same push for up to ${LIMITS.pushGraceS} s, which may run past `wait_s`
412
413
  */
413
414
  stopWait(waitS = 0, prompts) {
414
415
  return this.request("stop_wait", { wait_s: waitS, ...prompts !== void 0 ? { prompts } : {} });
@@ -562,6 +563,7 @@ function startBridge(options) {
562
563
  let connected = false;
563
564
  let ready = false;
564
565
  let stopped = false;
566
+ let shutDown = false;
565
567
  let lastError = options.token ? null : options.tokenError ?? "no hub credential; the broker runs local-only";
566
568
  let since = (/* @__PURE__ */ new Date()).toISOString();
567
569
  let attempt = 0;
@@ -602,7 +604,7 @@ function startBridge(options) {
602
604
  });
603
605
  }
604
606
  function connect() {
605
- if (stopped || !token) return;
607
+ if (stopped || !token || ws) return;
606
608
  let socket;
607
609
  try {
608
610
  socket = new Socket(relaySocketUrl(options.url), { headers: { Authorization: `Bearer ${token}` } });
@@ -1008,8 +1010,19 @@ function startBridge(options) {
1008
1010
  function status() {
1009
1011
  return { enabled: true, machine, url: options.url, connected, last_error: lastError, since };
1010
1012
  }
1013
+ function relink() {
1014
+ if (shutDown) return;
1015
+ stopped = false;
1016
+ attempt = 0;
1017
+ clearTimeout(retryTimer);
1018
+ if (ws) return;
1019
+ void reconnect().then(() => {
1020
+ if (!token && !ws) setState(false, "no hub credential; log in again");
1021
+ });
1022
+ }
1011
1023
  async function stop() {
1012
1024
  stopped = true;
1025
+ shutDown = true;
1013
1026
  clearTimeout(retryTimer);
1014
1027
  for (const fn of waitingForLink.splice(0)) fn();
1015
1028
  const socket = ws;
@@ -1021,7 +1034,7 @@ function startBridge(options) {
1021
1034
  if (socket && socket.readyState <= 1) socket.close(1e3, "the broker stopped");
1022
1035
  }
1023
1036
  connect();
1024
- return { forward, changed, resync, activity, contacts, status, stop };
1037
+ return { forward, changed, resync, activity, contacts, status, relink, stop };
1025
1038
  }
1026
1039
 
1027
1040
  // src/server.ts
@@ -1576,7 +1589,7 @@ function openDatabase(path) {
1576
1589
  // src/server.ts
1577
1590
  var SWEEP_MS = 10 * 6e4;
1578
1591
  var PRESENCE_MS = 6e4;
1579
- var OBSERVER_METHODS = /* @__PURE__ */ new Set(["list", "snapshot", "watch", "log", "policy", "policy_check", "cloud_status"]);
1592
+ var OBSERVER_METHODS = /* @__PURE__ */ new Set(["list", "snapshot", "watch", "log", "policy", "policy_check", "cloud_status", "bridge_relink"]);
1580
1593
  var SCOPE_METHODS = /* @__PURE__ */ new Set([
1581
1594
  "rules",
1582
1595
  "rules_set",
@@ -1598,6 +1611,13 @@ var ASK_RECHECK_MS = 1e3;
1598
1611
  var STOP_RECHECK_MS = 1e3;
1599
1612
  var TRANSIENT_METHODS = /* @__PURE__ */ new Set(["event", "status", "whoami", "inject", "injected", "peek_turn", "stop_wait", "prompted"]);
1600
1613
  var GRANT_UNBOUND = `a spawn-env grant binds only a new harness process: run the harness with it, or name the child (${CLI_NAME} spawn-env --name / --as)`;
1614
+ function workerResults(result) {
1615
+ return result.messages.length - (result.others_shown ?? 0) + result.more;
1616
+ }
1617
+ function othersRunning(result) {
1618
+ const senders = new Set(result.messages.slice(0, result.messages.length - (result.others_shown ?? 0)).map((m) => m.from.address));
1619
+ return result.workers.filter((address) => !senders.has(address)).length + result.starting;
1620
+ }
1601
1621
  function running(pid) {
1602
1622
  try {
1603
1623
  process.kill(pid, 0);
@@ -1726,6 +1746,15 @@ async function startBroker(options) {
1726
1746
  }
1727
1747
  });
1728
1748
  }
1749
+ async function relink() {
1750
+ const config = options.bridgeConfig ? await options.bridgeConfig() : options.bridge;
1751
+ if (!config || bridge && config.url === options.bridge?.url && config.machine === options.bridge?.machine) {
1752
+ bridge?.relink();
1753
+ return bridge?.status() ?? { enabled: false };
1754
+ }
1755
+ setImmediate(() => void stop());
1756
+ return { enabled: false, restarting: true };
1757
+ }
1729
1758
  function activity() {
1730
1759
  if (idleTimer) clearTimeout(idleTimer);
1731
1760
  idleTimer = void 0;
@@ -1886,6 +1915,21 @@ async function startBroker(options) {
1886
1915
  if (s.data.closed) done();
1887
1916
  });
1888
1917
  }
1918
+ async function gather(me, result, peek, s) {
1919
+ const gathering = (r) => workerResults(r) > 0 && !r.more && !r.prompted && othersRunning(r) > 0;
1920
+ const cap = Date.now() + LIMITS2.pushGraceS * 1e3;
1921
+ let quiet = Date.now() + LIMITS2.pushQuietS * 1e3;
1922
+ let results = workerResults(result);
1923
+ while (gathering(result) && !s.data.closed && Date.now() < Math.min(quiet, cap)) {
1924
+ await waitFor(me, Math.min(quiet, cap) - Date.now(), s);
1925
+ if (closing) throw new RelayError4("unauthorized", "the relay broker is stopping");
1926
+ result = core.stopWait(me, peek);
1927
+ if (workerResults(result) > results) {
1928
+ results = workerResults(result);
1929
+ quiet = Date.now() + LIMITS2.pushQuietS * 1e3;
1930
+ }
1931
+ }
1932
+ }
1889
1933
  function onData(s, chunk) {
1890
1934
  const { lines, tooLong } = s.data.reader.push(chunk);
1891
1935
  for (const line of lines) s.data.queue.push(line);
@@ -2206,22 +2250,28 @@ async function startBroker(options) {
2206
2250
  ...hookOf(s)
2207
2251
  });
2208
2252
  case "stop_wait": {
2209
- const deadline = Date.now() + waitSeconds(params.wait_s);
2253
+ const waitMs = waitSeconds(params.wait_s);
2254
+ const deadline = Date.now() + waitMs;
2210
2255
  const options2 = { ...hookOf(s), ...typeof params.prompts === "number" ? { prompts: params.prompts } : {} };
2211
- let result = core.stopWait(me, options2);
2256
+ const peek = { ...options2, peek: true };
2257
+ let result = core.stopWait(me, peek);
2212
2258
  const idle = (r) => !r.messages.length && !r.more && r.workers.length + r.starting > 0 && r.pushes_left > 0 && !r.prompted;
2213
2259
  while (idle(result) && !s.data.closed && Date.now() < deadline) {
2214
2260
  await waitFor(me, Math.min(STOP_RECHECK_MS, deadline - Date.now()), s);
2215
2261
  if (closing) throw new RelayError4("unauthorized", "the relay broker is stopping");
2216
- result = core.stopWait(me, options2);
2262
+ result = core.stopWait(me, peek);
2217
2263
  }
2218
- return result;
2264
+ if (waitMs > 0) await gather(me, result, peek, s);
2265
+ return core.stopWait(me, options2);
2266
+ }
2267
+ case "prompted": {
2268
+ const prompted = core.prompted(me, hookOf(s));
2269
+ if (prompted) wake(me);
2270
+ return { prompted };
2219
2271
  }
2220
- case "prompted":
2221
- return { prompted: core.prompted(me, hookOf(s)) };
2222
2272
  case "injected":
2223
2273
  return {
2224
- injected: core.injected(params.seqs, params.hold, { endpoint: me, read: params.read }),
2274
+ injected: core.injected(params.seqs, params.hold, { endpoint: me, read: params.read, deferred: params.deferred === true }),
2225
2275
  ...params.stop === true ? { held: core.stopHeld(me, hookOf(s)) } : {},
2226
2276
  ...params.push === true ? { pushed: core.pushed(me, params.read, hookOf(s)) } : {},
2227
2277
  ...params.nudged !== void 0 ? { nudged: core.nudged(me, params.nudged, hookOf(s)) } : {},
@@ -2231,6 +2281,8 @@ async function startBroker(options) {
2231
2281
  };
2232
2282
  case "message":
2233
2283
  return { message: core.delivered(me, params.id) ?? (params.member === true ? core.threadMessage(me, params.id) : null) };
2284
+ case "receipts":
2285
+ return core.receipts(me, params.id);
2234
2286
  }
2235
2287
  throw new RelayError4("method_not_found", `no method ${method}`);
2236
2288
  }
@@ -2304,6 +2356,8 @@ async function startBroker(options) {
2304
2356
  }
2305
2357
  case "cloud_status":
2306
2358
  return bridge?.status() ?? { enabled: false };
2359
+ case "bridge_relink":
2360
+ return relink();
2307
2361
  }
2308
2362
  throw new RelayError4("method_not_found", `no method ${method}`);
2309
2363
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scp-dev/broker",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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.0",
41
- "@scp-dev/hub": "0.1.0"
40
+ "@scp-dev/core": "0.1.1",
41
+ "@scp-dev/hub": "0.1.1"
42
42
  },
43
43
  "devDependencies": {
44
44
  "tsup": "^8.5.1"
package/src/bridge.ts CHANGED
@@ -78,6 +78,8 @@ export interface Bridge {
78
78
  /** the endpoints of other accounts this account's contacts let it reach */
79
79
  contacts(): Promise<XListed[]>
80
80
  status(): BridgeStatus
81
+ /** reads the credential again and reconnects now, also after the hub revoked or replaced this bridge */
82
+ relink(): void
81
83
  stop(): Promise<void>
82
84
  }
83
85
 
@@ -132,7 +134,9 @@ export function startBridge(options: BridgeOptions): Bridge {
132
134
  let connected = false
133
135
  /** hello went through on the current socket: deltas may go out */
134
136
  let ready = false
137
+ /** the hub turned this bridge away, or the broker stopped it: no more retries until relink */
135
138
  let stopped = false
139
+ let shutDown = false
136
140
  let lastError: string | null = options.token ? null : (options.tokenError ?? "no hub credential; the broker runs local-only")
137
141
  let since = new Date().toISOString()
138
142
  let attempt = 0
@@ -177,7 +181,7 @@ export function startBridge(options: BridgeOptions): Bridge {
177
181
  }
178
182
 
179
183
  function connect() {
180
- if (stopped || !token) return
184
+ if (stopped || !token || ws) return
181
185
  let socket: WebSocket
182
186
  try {
183
187
  socket = new Socket(relaySocketUrl(options.url), { headers: { Authorization: `Bearer ${token}` } })
@@ -624,8 +628,20 @@ export function startBridge(options: BridgeOptions): Bridge {
624
628
  return { enabled: true, machine, url: options.url, connected, last_error: lastError, since }
625
629
  }
626
630
 
631
+ function relink() {
632
+ if (shutDown) return
633
+ stopped = false
634
+ attempt = 0
635
+ clearTimeout(retryTimer)
636
+ if (ws) return
637
+ void reconnect().then(() => {
638
+ if (!token && !ws) setState(false, "no hub credential; log in again")
639
+ })
640
+ }
641
+
627
642
  async function stop() {
628
643
  stopped = true
644
+ shutDown = true
629
645
  clearTimeout(retryTimer)
630
646
  for (const fn of waitingForLink.splice(0)) fn()
631
647
  const socket = ws
@@ -638,5 +654,5 @@ export function startBridge(options: BridgeOptions): Bridge {
638
654
  }
639
655
 
640
656
  connect()
641
- return { forward, changed, resync, activity, contacts, status, stop }
657
+ return { forward, changed, resync, activity, contacts, status, relink, stop }
642
658
  }
package/src/client.ts CHANGED
@@ -196,7 +196,8 @@ export class RelayClient {
196
196
 
197
197
  /**
198
198
  * at a stop: the spawned workers' unread messages, held for the hook to print, waiting up to `wait_s` while workers still run, none came,
199
- * pushes are left and no prompt moved the `prompts` mark inject handed out
199
+ * pushes are left and no prompt moved the `prompts` mark inject handed out; with `wait_s` above 0 and other workers still running, the
200
+ * broker gathers more of their results into the same push for up to ${LIMITS.pushGraceS} s, which may run past `wait_s`
200
201
  */
201
202
  stopWait(waitS = 0, prompts?: number): Promise<StopWaitResult> {
202
203
  return this.request<StopWaitResult>("stop_wait", { wait_s: waitS, ...(prompts !== undefined ? { prompts } : {}) })
package/src/server.ts CHANGED
@@ -57,6 +57,8 @@ export interface BrokerOptions {
57
57
  machine?: string | undefined
58
58
  /** the link to the hub; without it the broker never opens a network connection */
59
59
  bridge?: BridgeConfig | undefined
60
+ /** reads the bridge configuration again for bridge_relink; one that appeared or names another hub or machine restarts the broker */
61
+ bridgeConfig?: (() => Promise<BridgeConfig | undefined>) | undefined
60
62
  /** how often presence that decays without a request (a hook-only endpoint) is checked for changes (default 60 s) */
61
63
  presenceEveryMs?: number | undefined
62
64
  /** where a judge model's key_env is read (default the broker's environment) */
@@ -110,7 +112,7 @@ interface Connection {
110
112
 
111
113
  const SWEEP_MS = 10 * 60_000
112
114
  const PRESENCE_MS = 60_000
113
- const OBSERVER_METHODS = new Set(["list", "snapshot", "watch", "log", "policy", "policy_check", "cloud_status"])
115
+ const OBSERVER_METHODS = new Set(["list", "snapshot", "watch", "log", "policy", "policy_check", "cloud_status", "bridge_relink"])
114
116
  const SCOPE_METHODS = new Set([
115
117
  "rules",
116
118
  "rules_set",
@@ -135,6 +137,17 @@ const STOP_RECHECK_MS = 1000
135
137
  const TRANSIENT_METHODS = new Set(["event", "status", "whoami", "inject", "injected", "peek_turn", "stop_wait", "prompted"])
136
138
  const GRANT_UNBOUND = `a spawn-env grant binds only a new harness process: run the harness with it, or name the child (${CLI_NAME} spawn-env --name / --as)`
137
139
 
140
+ /** the workers' messages and handbacks a stop's push would show, and those of theirs left unread past it */
141
+ function workerResults(result: StopWaitResult): number {
142
+ return result.messages.length - (result.others_shown ?? 0) + result.more
143
+ }
144
+
145
+ /** the workers still running that sent none of the messages a stop's push would show, and those not joined yet */
146
+ function othersRunning(result: StopWaitResult): number {
147
+ const senders = new Set(result.messages.slice(0, result.messages.length - (result.others_shown ?? 0)).map((m) => m.from.address))
148
+ return result.workers.filter((address) => !senders.has(address)).length + result.starting
149
+ }
150
+
138
151
  type Request = { jsonrpc?: string; id?: string | number | null; method?: unknown; params?: unknown }
139
152
 
140
153
  /** whether a process runs (one of another user's counts too) */
@@ -270,6 +283,17 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
270
283
  })
271
284
  }
272
285
 
286
+ /** links the bridge again with the credential on disk; a configuration this broker cannot take without a restart stops it */
287
+ async function relink(): Promise<object> {
288
+ const config = options.bridgeConfig ? await options.bridgeConfig() : options.bridge
289
+ if (!config || (bridge && config.url === options.bridge?.url && config.machine === options.bridge?.machine)) {
290
+ bridge?.relink()
291
+ return bridge?.status() ?? { enabled: false }
292
+ }
293
+ setImmediate(() => void stop())
294
+ return { enabled: false, restarting: true }
295
+ }
296
+
273
297
  function activity() {
274
298
  if (idleTimer) clearTimeout(idleTimer)
275
299
  idleTimer = undefined
@@ -461,6 +485,32 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
461
485
  })
462
486
  }
463
487
 
488
+ /**
489
+ * a stop's push with worker results to show while other workers still run: waits on, peeking, for more of theirs to show with them until
490
+ * none came for ${LIMITS.pushQuietS} s, ${LIMITS.pushGraceS} s after it began (which may run past the stop's wait), the push budget is
491
+ * full (`more`), no other worker runs ({@link othersRunning}), a prompt came or the connection closed
492
+ */
493
+ async function gather(
494
+ me: string,
495
+ result: StopWaitResult,
496
+ peek: ReturnType<typeof hookOf> & { prompts?: number; peek: boolean },
497
+ s: Conn<Connection>,
498
+ ): Promise<void> {
499
+ const gathering = (r: StopWaitResult) => workerResults(r) > 0 && !r.more && !r.prompted && othersRunning(r) > 0
500
+ const cap = Date.now() + LIMITS.pushGraceS * 1000
501
+ let quiet = Date.now() + LIMITS.pushQuietS * 1000
502
+ let results = workerResults(result)
503
+ while (gathering(result) && !s.data.closed && Date.now() < Math.min(quiet, cap)) {
504
+ await waitFor(me, Math.min(quiet, cap) - Date.now(), s)
505
+ if (closing) throw new RelayError("unauthorized", "the relay broker is stopping")
506
+ result = core.stopWait(me, peek)
507
+ if (workerResults(result) > results) {
508
+ results = workerResults(result)
509
+ quiet = Date.now() + LIMITS.pushQuietS * 1000
510
+ }
511
+ }
512
+ }
513
+
464
514
  function onData(s: Conn<Connection>, chunk: Uint8Array) {
465
515
  const { lines, tooLong } = s.data.reader.push(chunk)
466
516
  for (const line of lines) s.data.queue.push(line)
@@ -796,22 +846,28 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
796
846
  ...hookOf(s),
797
847
  })
798
848
  case "stop_wait": {
799
- const deadline = Date.now() + waitSeconds(params.wait_s)
849
+ const waitMs = waitSeconds(params.wait_s)
850
+ const deadline = Date.now() + waitMs
800
851
  const options = { ...hookOf(s), ...(typeof params.prompts === "number" ? { prompts: params.prompts } : {}) }
801
- let result = core.stopWait(me, options)
852
+ const peek = { ...options, peek: true }
853
+ let result = core.stopWait(me, peek)
802
854
  const idle = (r: StopWaitResult) => !r.messages.length && !r.more && r.workers.length + r.starting > 0 && r.pushes_left > 0 && !r.prompted
803
855
  while (idle(result) && !s.data.closed && Date.now() < deadline) {
804
856
  await waitFor(me, Math.min(STOP_RECHECK_MS, deadline - Date.now()), s)
805
857
  if (closing) throw new RelayError("unauthorized", "the relay broker is stopping")
806
- result = core.stopWait(me, options)
858
+ result = core.stopWait(me, peek)
807
859
  }
808
- return result
860
+ if (waitMs > 0) await gather(me, result, peek, s)
861
+ return core.stopWait(me, options)
862
+ }
863
+ case "prompted": {
864
+ const prompted = core.prompted(me, hookOf(s))
865
+ if (prompted) wake(me)
866
+ return { prompted }
809
867
  }
810
- case "prompted":
811
- return { prompted: core.prompted(me, hookOf(s)) }
812
868
  case "injected":
813
869
  return {
814
- injected: core.injected(params.seqs, params.hold, { endpoint: me, read: params.read }),
870
+ injected: core.injected(params.seqs, params.hold, { endpoint: me, read: params.read, deferred: params.deferred === true }),
815
871
  ...(params.stop === true ? { held: core.stopHeld(me, hookOf(s)) } : {}),
816
872
  ...(params.push === true ? { pushed: core.pushed(me, params.read, hookOf(s)) } : {}),
817
873
  ...(params.nudged !== undefined ? { nudged: core.nudged(me, params.nudged, hookOf(s)) } : {}),
@@ -821,6 +877,8 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
821
877
  }
822
878
  case "message":
823
879
  return { message: core.delivered(me, params.id) ?? (params.member === true ? core.threadMessage(me, params.id) : null) }
880
+ case "receipts":
881
+ return core.receipts(me, params.id)
824
882
  }
825
883
  throw new RelayError("method_not_found", `no method ${method}`)
826
884
  }
@@ -903,6 +961,8 @@ export async function startBroker(options: BrokerOptions): Promise<Broker> {
903
961
  }
904
962
  case "cloud_status":
905
963
  return bridge?.status() ?? { enabled: false }
964
+ case "bridge_relink":
965
+ return relink()
906
966
  }
907
967
  throw new RelayError("method_not_found", `no method ${method}`)
908
968
  }