@cordfuse/crosstalk 6.0.0-alpha.6 → 6.0.0-alpha.8
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/package.json +1 -1
- package/src/actor.ts +29 -4
- package/src/dispatch.ts +18 -9
- package/src/transport.ts +13 -0
package/package.json
CHANGED
package/src/actor.ts
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { hostname as osHostname } from 'os';
|
|
3
|
+
import { hostname as osHostname, platform } from 'os';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
4
5
|
import { parseFrontmatter } from './frontmatter.js';
|
|
5
6
|
|
|
7
|
+
// Collect the names this machine might be known by. On macOS, the kernel
|
|
8
|
+
// hostname (`os.hostname()`) drifts with DHCP/VPN/Tailscale when the static
|
|
9
|
+
// HostName is unset — `scutil --get LocalHostName` is the stable Bonjour
|
|
10
|
+
// name (e.g. `Steves-MacBook-Air`), and host files commonly use the `.local`
|
|
11
|
+
// form. Trying all variants makes auto-detect deterministic across network
|
|
12
|
+
// state without forcing every Mac operator to pin `--host`.
|
|
13
|
+
function candidateHostNames(): string[] {
|
|
14
|
+
const names = new Set<string>();
|
|
15
|
+
names.add(osHostname());
|
|
16
|
+
if (platform() === 'darwin') {
|
|
17
|
+
const r = spawnSync('scutil', ['--get', 'LocalHostName'], { encoding: 'utf-8' });
|
|
18
|
+
if (r.status === 0) {
|
|
19
|
+
const local = r.stdout.trim();
|
|
20
|
+
if (local) {
|
|
21
|
+
names.add(local);
|
|
22
|
+
names.add(`${local}.local`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return [...names];
|
|
27
|
+
}
|
|
28
|
+
|
|
6
29
|
export interface HostActorTier {
|
|
7
30
|
cli: string;
|
|
8
31
|
count?: number;
|
|
@@ -36,13 +59,15 @@ export function findHostFile(transportRoot: string, override?: string): HostFile
|
|
|
36
59
|
if (!target) throw new Error(`Host file '${override}' not found in ${dir}`);
|
|
37
60
|
return parseHostFile(join(dir, target));
|
|
38
61
|
}
|
|
39
|
-
const
|
|
62
|
+
const names = candidateHostNames();
|
|
40
63
|
for (const f of files) {
|
|
41
64
|
const parsed = parseHostFile(join(dir, f));
|
|
42
|
-
|
|
65
|
+
for (const n of names) {
|
|
66
|
+
if (parsed.hostname === n || parsed.alias === n) return parsed;
|
|
67
|
+
}
|
|
43
68
|
}
|
|
44
69
|
throw new Error(
|
|
45
|
-
`No host file matches
|
|
70
|
+
`No host file matches any of [${names.join(', ')}] in ${dir}. ` +
|
|
46
71
|
`Pass --host <alias> to override.`,
|
|
47
72
|
);
|
|
48
73
|
}
|
package/src/dispatch.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
gitCommitAndPush,
|
|
30
30
|
cursorBaseline,
|
|
31
31
|
newFilesSince,
|
|
32
|
+
hostFileCommit,
|
|
32
33
|
type ChannelMessage,
|
|
33
34
|
} from './transport.js';
|
|
34
35
|
import {
|
|
@@ -361,15 +362,23 @@ async function dispatchTick(): Promise<TickResult> {
|
|
|
361
362
|
const pending: PendingDispatch[] = [];
|
|
362
363
|
|
|
363
364
|
for (const channelUuid of channels) {
|
|
364
|
-
const
|
|
365
|
-
if (
|
|
366
|
-
|
|
367
|
-
// First encounter: seed to
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
365
|
+
const persistedCursor = readCursor(transportRoot, actorName, channelUuid);
|
|
366
|
+
if (persistedCursor === head) continue;
|
|
367
|
+
|
|
368
|
+
// First encounter: seed to the commit that introduced this actor's host
|
|
369
|
+
// file. Messages sent after the host joined are delivered (store-and-
|
|
370
|
+
// forward); pre-join history is ignored. Seeding to HEAD would silently
|
|
371
|
+
// drop messages sent while the dispatcher was offline — the wrong trade.
|
|
372
|
+
// Fall through after seeding so this tick processes the post-join backlog
|
|
373
|
+
// (otherwise `--once` users hit a seed-then-dispatch two-tick gotcha).
|
|
374
|
+
let cursor: string;
|
|
375
|
+
if (persistedCursor === null) {
|
|
376
|
+
const joinCommit = hostFileCommit(transportRoot, host.alias);
|
|
377
|
+
cursor = joinCommit ?? head;
|
|
378
|
+
writeCursor(transportRoot, actorName, channelUuid, cursor);
|
|
379
|
+
if (cursor === head) continue;
|
|
380
|
+
} else {
|
|
381
|
+
cursor = persistedCursor;
|
|
373
382
|
}
|
|
374
383
|
|
|
375
384
|
const messages = listChannelMessages(transportRoot, channelUuid);
|
package/src/transport.ts
CHANGED
|
@@ -68,6 +68,19 @@ export function cursorBaseline(transportRoot: string): string | null {
|
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// Find the commit that introduced this actor's host file. Used to seed
|
|
72
|
+
// the cursor on first boot: messages sent after the host file landed are
|
|
73
|
+
// deliverable (store-and-forward); pre-join history is ignored. Falls back
|
|
74
|
+
// to HEAD if git log fails (conservative: no history replay in that case).
|
|
75
|
+
export function hostFileCommit(transportRoot: string, hostname: string): string | null {
|
|
76
|
+
const hostPath = `hosts/${hostname}.md`;
|
|
77
|
+
const r = captureGit(transportRoot, ['log', '--format=%H', '--diff-filter=A', '--', hostPath]);
|
|
78
|
+
if (r.status !== 0 || !r.stdout.trim()) return null;
|
|
79
|
+
// `git log` lists newest first; the last line is the introducing commit.
|
|
80
|
+
const lines = r.stdout.trim().split('\n').filter(Boolean);
|
|
81
|
+
return lines[lines.length - 1] ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
71
84
|
// Repo-relative paths of message files added between `sinceCommit` and
|
|
72
85
|
// HEAD. Returns null when the commit is unknown to this clone (state dir
|
|
73
86
|
// copied across transports, history rewritten) — caller falls back to a
|