@martintrojer/murmur 0.2.1 → 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 +98 -0
- package/README.md +105 -70
- package/dist/cli.js +834 -269
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +54 -2
- package/dist/index.js +19 -5
- 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,
|
|
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
|
@@ -185,8 +185,12 @@ function pidAlive(pid) {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
// src/agents.ts
|
|
188
|
+
function sessionLeaf(session) {
|
|
189
|
+
const leaf = session.split("/").filter(Boolean).at(-1);
|
|
190
|
+
return leaf ?? session;
|
|
191
|
+
}
|
|
188
192
|
function agentLabel(agent) {
|
|
189
|
-
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));
|
|
190
194
|
return terminalText(name ?? agent.window);
|
|
191
195
|
}
|
|
192
196
|
function agentLocation(agent) {
|
|
@@ -580,6 +584,7 @@ function viewSort(views) {
|
|
|
580
584
|
// src/collector.ts
|
|
581
585
|
var MAX_CONCURRENT_PEERS = 8;
|
|
582
586
|
var COLLECT_DEADLINE_MS = 4e3;
|
|
587
|
+
var COLLECT_JITTER_MS = 2e4;
|
|
583
588
|
async function mapSettled(items, limit, task, deadline) {
|
|
584
589
|
const results = new Array(items.length);
|
|
585
590
|
let cursor = 0;
|
|
@@ -601,6 +606,14 @@ async function mapSettled(items, limit, task, deadline) {
|
|
|
601
606
|
await (stop ? Promise.race([pool, stop]) : pool);
|
|
602
607
|
return results;
|
|
603
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
|
+
}
|
|
604
617
|
function isUnreachable(message) {
|
|
605
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(
|
|
606
619
|
message
|
|
@@ -615,11 +628,12 @@ function stripInvocation(message) {
|
|
|
615
628
|
function normalizeFailure(message) {
|
|
616
629
|
return stripInvocation(message).replace(/\s+/g, " ").trim();
|
|
617
630
|
}
|
|
618
|
-
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;
|
|
619
633
|
const results = [];
|
|
620
634
|
let timer;
|
|
621
635
|
try {
|
|
622
|
-
const peers = store.peers();
|
|
636
|
+
const peers = duePeers(store.peers(), now, floorMs, random);
|
|
623
637
|
const bounded = deadline ?? new Promise((resolve) => {
|
|
624
638
|
timer = setTimeout(resolve, COLLECT_DEADLINE_MS);
|
|
625
639
|
timer.unref?.();
|
|
@@ -787,9 +801,9 @@ function status(store, identity, now = Date.now()) {
|
|
|
787
801
|
}))
|
|
788
802
|
};
|
|
789
803
|
}
|
|
790
|
-
async function statusWithCollect(store, identity, now = Date.now(), channel = ssh,
|
|
804
|
+
async function statusWithCollect(store, identity, now = Date.now(), channel = ssh, options = {}) {
|
|
791
805
|
try {
|
|
792
|
-
await collect(store, channel, now,
|
|
806
|
+
await collect(store, channel, now, options);
|
|
793
807
|
} catch {
|
|
794
808
|
}
|
|
795
809
|
return status(store, identity, now);
|