@martintrojer/murmur 0.2.0 → 0.2.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/ARCHITECTURE.md +129 -81
- package/CHANGELOG.md +120 -0
- package/README.md +105 -70
- package/dist/cli.js +840 -271
- package/dist/cli.js.map +1 -1
- package/dist/extension/murmur-pi.js +7 -3
- package/dist/extension/murmur-pi.js.map +1 -1
- package/dist/extension/store.js.map +1 -1
- package/dist/index.d.ts +54 -2
- package/dist/index.js +27 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -412,6 +412,21 @@ declare function pidAlive(pid: number): boolean;
|
|
|
412
412
|
* owns the pane, so this reads the same for a local and a remote pane -- a
|
|
413
413
|
* reader cannot resolve a remote window id against its own tmux.
|
|
414
414
|
*
|
|
415
|
+
* The session name is shortened to its last segment, and that is load-bearing
|
|
416
|
+
* rather than cosmetic. It is reached far more often than it looks: `agent_name`
|
|
417
|
+
* is set only by mu, `window_name` is null whenever tmux is auto-renaming (its
|
|
418
|
+
* default), and `pi_session` is null for the whole life of an unnamed session --
|
|
419
|
+
* pi's own auto-namer runs at CLOSE, so a live agent, which is exactly the one
|
|
420
|
+
* you are looking at, has no session name yet. So the common row for a
|
|
421
|
+
* hand-started pi fell through to here and printed `hacking/murmur`: a path,
|
|
422
|
+
* where a name belongs.
|
|
423
|
+
*
|
|
424
|
+
* The full session name is not lost. The picker's stream column shows it, and
|
|
425
|
+
* shows it precisely BECAUSE of this shortening: that column is blanked when it
|
|
426
|
+
* would repeat the name, so `hacking/murmur` in both cells collapsed to one
|
|
427
|
+
* path and a blank. Shortened, the row reads `murmur` + `hacking/murmur` -- the
|
|
428
|
+
* same width, carrying strictly more.
|
|
429
|
+
*
|
|
415
430
|
* Falls back to the window id only when a node recorded no names at all, which
|
|
416
431
|
* means a non-tmux harness.
|
|
417
432
|
*/
|
|
@@ -457,6 +472,36 @@ declare const ssh: Channel;
|
|
|
457
472
|
declare function hasWarmSocket(target: string): boolean;
|
|
458
473
|
|
|
459
474
|
declare const MAX_CONCURRENT_PEERS = 8;
|
|
475
|
+
/**
|
|
476
|
+
* The optional half of a collect, as a bag rather than four positional
|
|
477
|
+
* arguments.
|
|
478
|
+
*
|
|
479
|
+
* `collect(store, ssh, now, undefined, mux)` was already the call site before
|
|
480
|
+
* the floor was added, and a fifth positional -- a bare number, next to another
|
|
481
|
+
* bare number -- is how `now` and `floorMs` get silently swapped.
|
|
482
|
+
*/
|
|
483
|
+
type CollectOptions = {
|
|
484
|
+
/** Bounds the whole run. Injectable so tests need not wait out real time. */
|
|
485
|
+
deadline?: Promise<void>;
|
|
486
|
+
/** The mux reconciliation asks which panes are alive. */
|
|
487
|
+
mux?: Mux;
|
|
488
|
+
/**
|
|
489
|
+
* Skip peers attempted within this many ms. Zero -- the default -- fetches
|
|
490
|
+
* every peer, which is what a deliberate `murmur collect` and the picker both
|
|
491
|
+
* want. Only the status bar, which repaints on a timer, passes a floor.
|
|
492
|
+
*
|
|
493
|
+
* Opt IN rather than opt out: a surface that forgets this argument keeps the
|
|
494
|
+
* old always-fetch behaviour, which is merely wasteful. The opposite default
|
|
495
|
+
* would mean a new surface silently serves stale data.
|
|
496
|
+
*/
|
|
497
|
+
floorMs?: number;
|
|
498
|
+
/**
|
|
499
|
+
* Source of the floor's jitter, in [0, 1). Injected for the same reason `now`
|
|
500
|
+
* and `isAlive` are: a test pins both edges of the window by returning 0 and
|
|
501
|
+
* a value approaching 1, rather than sampling and hoping.
|
|
502
|
+
*/
|
|
503
|
+
random?: () => number;
|
|
504
|
+
};
|
|
460
505
|
type CollectResult = {
|
|
461
506
|
peer: string;
|
|
462
507
|
ok: boolean;
|
|
@@ -486,7 +531,7 @@ type CollectResult = {
|
|
|
486
531
|
* One round trip per peer, and never a second: the document is complete, so what
|
|
487
532
|
* arrives either replaces the cache entirely or does not touch it.
|
|
488
533
|
*/
|
|
489
|
-
declare function collect(store: Store, channel: Channel, now?: number,
|
|
534
|
+
declare function collect(store: Store, channel: Channel, now?: number, options?: CollectOptions): Promise<CollectResult[]>;
|
|
490
535
|
|
|
491
536
|
declare function glance(store: Store, agent: PaneView, lines?: number): string | null;
|
|
492
537
|
|
|
@@ -554,8 +599,15 @@ declare function status(store: Store, identity: NodeIdentity, now?: number): Sta
|
|
|
554
599
|
* display-popup, so one sleeping laptop would otherwise write ssh diagnostics
|
|
555
600
|
* to stderr several times a minute, forever. `murmur collect`, which a human
|
|
556
601
|
* runs deliberately, is the only place that prints.
|
|
602
|
+
*
|
|
603
|
+
* `floorMs` is how a caller says whether it is a TIMER or a PERSON. The status
|
|
604
|
+
* bar repaints on `status-interval` and passes COLLECT_FLOOR_MS, so fetch rate
|
|
605
|
+
* stops being tied to redraw rate. The picker passes nothing: pressing the key
|
|
606
|
+
* is a person asking now, and its `^r` reload comes back through here too --
|
|
607
|
+
* a refresh key that skipped the fetch would be a key that silently does
|
|
608
|
+
* nothing.
|
|
557
609
|
*/
|
|
558
|
-
declare function statusWithCollect(store: Store, identity: NodeIdentity, now?: number, channel?: Channel): Promise<Status>;
|
|
610
|
+
declare function statusWithCollect(store: Store, identity: NodeIdentity, now?: number, channel?: Channel, options?: CollectOptions): Promise<Status>;
|
|
559
611
|
|
|
560
612
|
declare const MURMUR_VERSION: string;
|
|
561
613
|
|
package/dist/index.js
CHANGED
|
@@ -62,6 +62,10 @@ function runTmux(args) {
|
|
|
62
62
|
return null;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
function chosenWindowName(name, autoRename) {
|
|
66
|
+
if (autoRename === "1") return null;
|
|
67
|
+
return name || null;
|
|
68
|
+
}
|
|
65
69
|
function exactSession(session) {
|
|
66
70
|
return `=${session}`;
|
|
67
71
|
}
|
|
@@ -81,16 +85,16 @@ var tmux = {
|
|
|
81
85
|
"-t",
|
|
82
86
|
pane,
|
|
83
87
|
"-p",
|
|
84
|
-
"#{session_id} #{window_id} #{session_name} #{window_name}"
|
|
88
|
+
"#{session_id} #{window_id} #{session_name} #{window_name} #{?automatic-rename,1,0}"
|
|
85
89
|
]);
|
|
86
|
-
const [session, window, sessionName, windowName] = fields?.split(" ") ?? [];
|
|
90
|
+
const [session, window, sessionName, windowName, autoRename] = fields?.split(" ") ?? [];
|
|
87
91
|
if (!session || !window) return null;
|
|
88
92
|
return {
|
|
89
93
|
session: asSessionId(session),
|
|
90
94
|
window: asWindowId(window),
|
|
91
95
|
pane,
|
|
92
96
|
session_name: sessionName || null,
|
|
93
|
-
window_name: windowName
|
|
97
|
+
window_name: chosenWindowName(windowName, autoRename)
|
|
94
98
|
};
|
|
95
99
|
},
|
|
96
100
|
// Which of this host's PANES still exist. The only liveness question tmux is
|
|
@@ -181,8 +185,12 @@ function pidAlive(pid) {
|
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
// src/agents.ts
|
|
188
|
+
function sessionLeaf(session) {
|
|
189
|
+
const leaf = session.split("/").filter(Boolean).at(-1);
|
|
190
|
+
return leaf ?? session;
|
|
191
|
+
}
|
|
184
192
|
function agentLabel(agent) {
|
|
185
|
-
const name = agent.agent_name ?? agent.pi_session ?? agent.window_name ?? agent.session_name;
|
|
193
|
+
const name = agent.agent_name ?? agent.pi_session ?? agent.window_name ?? (agent.session_name === null ? null : sessionLeaf(agent.session_name));
|
|
186
194
|
return terminalText(name ?? agent.window);
|
|
187
195
|
}
|
|
188
196
|
function agentLocation(agent) {
|
|
@@ -576,6 +584,7 @@ function viewSort(views) {
|
|
|
576
584
|
// src/collector.ts
|
|
577
585
|
var MAX_CONCURRENT_PEERS = 8;
|
|
578
586
|
var COLLECT_DEADLINE_MS = 4e3;
|
|
587
|
+
var COLLECT_JITTER_MS = 2e4;
|
|
579
588
|
async function mapSettled(items, limit, task, deadline) {
|
|
580
589
|
const results = new Array(items.length);
|
|
581
590
|
let cursor = 0;
|
|
@@ -597,6 +606,14 @@ async function mapSettled(items, limit, task, deadline) {
|
|
|
597
606
|
await (stop ? Promise.race([pool, stop]) : pool);
|
|
598
607
|
return results;
|
|
599
608
|
}
|
|
609
|
+
function duePeers(peers, now, floorMs, random) {
|
|
610
|
+
if (floorMs <= 0) return [...peers];
|
|
611
|
+
return peers.filter((peer) => {
|
|
612
|
+
if (peer.last_attempt_at === null) return true;
|
|
613
|
+
const jitter = (random() - 0.5) * COLLECT_JITTER_MS;
|
|
614
|
+
return now - peer.last_attempt_at >= floorMs + jitter;
|
|
615
|
+
});
|
|
616
|
+
}
|
|
600
617
|
function isUnreachable(message) {
|
|
601
618
|
return /Host is down|No route to host|Connection refused|Connection timed out|Connection closed|Operation timed out|Network is unreachable|Name or service not known|Could not resolve hostname|timed out after/i.test(
|
|
602
619
|
message
|
|
@@ -611,11 +628,12 @@ function stripInvocation(message) {
|
|
|
611
628
|
function normalizeFailure(message) {
|
|
612
629
|
return stripInvocation(message).replace(/\s+/g, " ").trim();
|
|
613
630
|
}
|
|
614
|
-
async function collect(store, channel, now = Date.now(),
|
|
631
|
+
async function collect(store, channel, now = Date.now(), options = {}) {
|
|
632
|
+
const { deadline, mux = tmux, floorMs = 0, random = Math.random } = options;
|
|
615
633
|
const results = [];
|
|
616
634
|
let timer;
|
|
617
635
|
try {
|
|
618
|
-
const peers = store.peers();
|
|
636
|
+
const peers = duePeers(store.peers(), now, floorMs, random);
|
|
619
637
|
const bounded = deadline ?? new Promise((resolve) => {
|
|
620
638
|
timer = setTimeout(resolve, COLLECT_DEADLINE_MS);
|
|
621
639
|
timer.unref?.();
|
|
@@ -658,7 +676,7 @@ async function collect(store, channel, now = Date.now(), deadline) {
|
|
|
658
676
|
clearTimeout(timer);
|
|
659
677
|
}
|
|
660
678
|
try {
|
|
661
|
-
store.reconcileLocal({ panes:
|
|
679
|
+
store.reconcileLocal({ panes: mux.livePanes(), now });
|
|
662
680
|
} catch {
|
|
663
681
|
}
|
|
664
682
|
return results;
|
|
@@ -783,9 +801,9 @@ function status(store, identity, now = Date.now()) {
|
|
|
783
801
|
}))
|
|
784
802
|
};
|
|
785
803
|
}
|
|
786
|
-
async function statusWithCollect(store, identity, now = Date.now(), channel = ssh) {
|
|
804
|
+
async function statusWithCollect(store, identity, now = Date.now(), channel = ssh, options = {}) {
|
|
787
805
|
try {
|
|
788
|
-
await collect(store, channel, now);
|
|
806
|
+
await collect(store, channel, now, options);
|
|
789
807
|
} catch {
|
|
790
808
|
}
|
|
791
809
|
return status(store, identity, now);
|