teamshare-bridge 0.22.1 → 0.22.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.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * singleton-policy: the ONE decision behind "exactly one daemon runs here".
3
+ *
4
+ * WHY IT IS A SEPARATE, PURE FILE
5
+ * -------------------------------
6
+ * `daemon.lock` cannot enforce the singleton it exists to enforce. It is a
7
+ * FILE, and every way it fails is a way two daemons end up alive:
8
+ *
9
+ * - `isExpired(startedAt)` declares a lock abandoned after 24h, so a daemon
10
+ * that has been up for a day is invisible to the next `acquireDaemonLock()`;
11
+ * - the lock is cleared by a half-written `npm i -g`, a cleanup sweep, a
12
+ * restore-from-backup, or `stopAllDaemons()` clearing it while a survivor
13
+ * was still running;
14
+ * - `readLiveDaemonLock()` clears it whenever `pidAlive(lock.pid)` is false,
15
+ * and a pid we cannot signal reads as dead.
16
+ *
17
+ * Each of those leaves a LIVE daemon holding no lock. The next `start` (human
18
+ * or watchdog) sees a free machine, spawns, and now there are two. On this
19
+ * machine there were three at once — one serving, two wedged or stale — which
20
+ * is what made `npm i -g` fail with EBUSY on a mapped native DLL and left the
21
+ * install half-written.
22
+ *
23
+ * So the authority for "is a daemon already running" is PROCESS ENUMERATION
24
+ * (`listOurDaemonPids`, command-line verified), and the lock is demoted to what
25
+ * it is actually good for: naming WHICH of the live pids is the incumbent.
26
+ *
27
+ * THE RULE
28
+ * --------
29
+ * Of every verified daemon process on this machine, AT MOST ONE survives:
30
+ *
31
+ * 1. none alive -> `start` (spawn one)
32
+ * 2. alive but nothing answers /ping -> `replace` (all of them are wedged;
33
+ * stop every one, spawn one)
34
+ * 3. alive and something answers /ping -> `keep` (exactly one pid survives;
35
+ * every other one is stopped)
36
+ *
37
+ * Rule 3 is the load-bearing one: it is what a file-based lock cannot express,
38
+ * because "several processes match, one of them is real" is not a state any
39
+ * file can represent.
40
+ *
41
+ * WHICH PID SURVIVES (rule 3) — deterministic, never "whoever won the race":
42
+ * 1. the pid `daemon.lock` names, if it is still alive (the incumbent);
43
+ * 2. else the pid `runtime.json` names, if it is still alive;
44
+ * 3. else the LOWEST pid, i.e. the oldest process — on a machine that briefly
45
+ * held two, the older one has the live socket and the agent state.
46
+ *
47
+ * A `blocked` outcome covers the one case where spawning is worse than doing
48
+ * nothing: a bridge answers /ping but NO pid of ours is identifiable (it runs
49
+ * from a dev checkout, or its files were wiped under it). Starting then only
50
+ * produces a loser that exits on the port-occupant check, once per tick.
51
+ *
52
+ * No I/O here on purpose: `tests/daemon-singleton.test.mjs` drives this with
53
+ * plain arrays, and the real machine work lives in `daemon-instance.ts`.
54
+ */
55
+ /** What the caller must do to converge on one daemon. */
56
+ export type SingletonAction =
57
+ /** No daemon is alive: spawn one. */
58
+ 'start'
59
+ /** Exactly `keep` survives; every pid in `stop` must be terminated. */
60
+ | 'keep'
61
+ /** Every live daemon is wedged: terminate all of them, then spawn one. */
62
+ | 'replace'
63
+ /** A bridge answers but is not identifiable: do nothing, say why. */
64
+ | 'blocked';
65
+ export interface SingletonInput {
66
+ /** Pids VERIFIABLY running `…/index.js daemon`, ascending, no duplicates. */
67
+ live: readonly number[];
68
+ /** The pid `daemon.lock` names, or null when it names nothing/already dead. */
69
+ lockPid: number | null;
70
+ /** The pid `runtime.json` names, or null. */
71
+ runtimePid: number | null;
72
+ /** True when some daemon answers `/ping` with ok:true on a candidate port. */
73
+ anyAnswering: boolean;
74
+ }
75
+ export interface SingletonDecision {
76
+ action: SingletonAction;
77
+ /** The one pid allowed to keep running; null for `start`/`replace`/`blocked`. */
78
+ keep: number | null;
79
+ /** Verified daemon pids that must be terminated, ascending. */
80
+ stop: number[];
81
+ /** Human-readable justification for the log line. Never empty. */
82
+ reason: string;
83
+ }
84
+ export declare function decideSingleton(input: SingletonInput): SingletonDecision;
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ /**
3
+ * singleton-policy: the ONE decision behind "exactly one daemon runs here".
4
+ *
5
+ * WHY IT IS A SEPARATE, PURE FILE
6
+ * -------------------------------
7
+ * `daemon.lock` cannot enforce the singleton it exists to enforce. It is a
8
+ * FILE, and every way it fails is a way two daemons end up alive:
9
+ *
10
+ * - `isExpired(startedAt)` declares a lock abandoned after 24h, so a daemon
11
+ * that has been up for a day is invisible to the next `acquireDaemonLock()`;
12
+ * - the lock is cleared by a half-written `npm i -g`, a cleanup sweep, a
13
+ * restore-from-backup, or `stopAllDaemons()` clearing it while a survivor
14
+ * was still running;
15
+ * - `readLiveDaemonLock()` clears it whenever `pidAlive(lock.pid)` is false,
16
+ * and a pid we cannot signal reads as dead.
17
+ *
18
+ * Each of those leaves a LIVE daemon holding no lock. The next `start` (human
19
+ * or watchdog) sees a free machine, spawns, and now there are two. On this
20
+ * machine there were three at once — one serving, two wedged or stale — which
21
+ * is what made `npm i -g` fail with EBUSY on a mapped native DLL and left the
22
+ * install half-written.
23
+ *
24
+ * So the authority for "is a daemon already running" is PROCESS ENUMERATION
25
+ * (`listOurDaemonPids`, command-line verified), and the lock is demoted to what
26
+ * it is actually good for: naming WHICH of the live pids is the incumbent.
27
+ *
28
+ * THE RULE
29
+ * --------
30
+ * Of every verified daemon process on this machine, AT MOST ONE survives:
31
+ *
32
+ * 1. none alive -> `start` (spawn one)
33
+ * 2. alive but nothing answers /ping -> `replace` (all of them are wedged;
34
+ * stop every one, spawn one)
35
+ * 3. alive and something answers /ping -> `keep` (exactly one pid survives;
36
+ * every other one is stopped)
37
+ *
38
+ * Rule 3 is the load-bearing one: it is what a file-based lock cannot express,
39
+ * because "several processes match, one of them is real" is not a state any
40
+ * file can represent.
41
+ *
42
+ * WHICH PID SURVIVES (rule 3) — deterministic, never "whoever won the race":
43
+ * 1. the pid `daemon.lock` names, if it is still alive (the incumbent);
44
+ * 2. else the pid `runtime.json` names, if it is still alive;
45
+ * 3. else the LOWEST pid, i.e. the oldest process — on a machine that briefly
46
+ * held two, the older one has the live socket and the agent state.
47
+ *
48
+ * A `blocked` outcome covers the one case where spawning is worse than doing
49
+ * nothing: a bridge answers /ping but NO pid of ours is identifiable (it runs
50
+ * from a dev checkout, or its files were wiped under it). Starting then only
51
+ * produces a loser that exits on the port-occupant check, once per tick.
52
+ *
53
+ * No I/O here on purpose: `tests/daemon-singleton.test.mjs` drives this with
54
+ * plain arrays, and the real machine work lives in `daemon-instance.ts`.
55
+ */
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.decideSingleton = decideSingleton;
58
+ function listPids(pids) {
59
+ return [...new Set(pids)].filter((p) => Number.isInteger(p) && p > 0).sort((a, b) => a - b);
60
+ }
61
+ function decideSingleton(input) {
62
+ const live = listPids(input.live);
63
+ if (live.length === 0) {
64
+ if (input.anyAnswering) {
65
+ return {
66
+ action: 'blocked',
67
+ keep: null,
68
+ stop: [],
69
+ reason: 'a bridge daemon is answering /ping but no TeamShare daemon process could be ' +
70
+ 'identified - not starting a second one (run `teamshare-bridge restart` if this is wrong)',
71
+ };
72
+ }
73
+ return { action: 'start', keep: null, stop: [], reason: 'no daemon process on this machine' };
74
+ }
75
+ if (!input.anyAnswering) {
76
+ return {
77
+ action: 'replace',
78
+ keep: null,
79
+ stop: live,
80
+ reason: `${live.length} daemon process(es) alive but nothing answers /ping ` +
81
+ `(${live.join(', ')}) - replacing them with one working daemon`,
82
+ };
83
+ }
84
+ // A bridge is healthy, so exactly one process is allowed to keep running.
85
+ const claimed = [input.lockPid, input.runtimePid].filter((pid) => pid !== null && live.includes(pid));
86
+ const keep = claimed[0] ?? live[0];
87
+ const stop = live.filter((pid) => pid !== keep);
88
+ const source = keep === input.lockPid ? 'daemon.lock' : keep === input.runtimePid ? 'runtime.json' : 'oldest process';
89
+ return {
90
+ action: 'keep',
91
+ keep,
92
+ stop,
93
+ reason: stop.length > 0
94
+ ? `${live.length} daemons were running (${live.join(', ')}); keeping pid ${keep} ` +
95
+ `(${source}) and stopping ${stop.join(', ')} - only one daemon may run`
96
+ : `daemon already running (pid ${keep}, ${source})`,
97
+ };
98
+ }
99
+ //# sourceMappingURL=singleton-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"singleton-policy.js","sourceRoot":"","sources":["../../src/bridge/singleton-policy.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;;AAsCH,0CA8CC;AAlDD,SAAS,QAAQ,CAAC,IAAuB;IACvC,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAC9F,CAAC;AAED,SAAgB,eAAe,CAAC,KAAqB;IACnD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAElC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;YACvB,OAAO;gBACL,MAAM,EAAE,SAAS;gBACjB,IAAI,EAAE,IAAI;gBACV,IAAI,EAAE,EAAE;gBACR,MAAM,EACJ,8EAA8E;oBAC9E,0FAA0F;aAC7F,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,mCAAmC,EAAE,CAAC;IAChG,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;QACxB,OAAO;YACL,MAAM,EAAE,SAAS;YACjB,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,IAAI;YACV,MAAM,EACJ,GAAG,IAAI,CAAC,MAAM,sDAAsD;gBACpE,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,4CAA4C;SAClE,CAAC;IACJ,CAAC;IAED,0EAA0E;IAC1E,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC,MAAM,CACtD,CAAC,GAAG,EAAiB,EAAE,CAAC,GAAG,KAAK,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAC3D,CAAC;IACF,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC;IAChD,MAAM,MAAM,GACV,IAAI,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,gBAAgB,CAAC;IACzG,OAAO;QACL,MAAM,EAAE,MAAM;QACd,IAAI;QACJ,IAAI;QACJ,MAAM,EACJ,IAAI,CAAC,MAAM,GAAG,CAAC;YACb,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,0BAA0B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,IAAI,GAAG;gBAChF,IAAI,MAAM,kBAAkB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,4BAA4B;YACzE,CAAC,CAAC,+BAA+B,IAAI,KAAK,MAAM,GAAG;KACxD,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamshare-bridge",
3
- "version": "0.22.1",
3
+ "version": "0.22.2",
4
4
  "description": "Local bridge for TeamShare agents - CLI session runner, auto-wake WebSocket daemon, and teamshare:// protocol handler.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",