@cordfuse/crosstalk 6.0.0-alpha.1 → 6.0.0-alpha.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/bin/crosstalk.js +1 -1
- package/package.json +1 -1
- package/src/dispatch.ts +28 -14
- package/src/send.ts +1 -1
- package/src/state.ts +30 -0
- package/src/stop.ts +37 -0
package/bin/crosstalk.js
CHANGED
|
@@ -14,7 +14,7 @@ import { fileURLToPath } from 'url';
|
|
|
14
14
|
import { createRequire } from 'module';
|
|
15
15
|
|
|
16
16
|
const SUBCOMMANDS = [
|
|
17
|
-
'dispatch', 'send', 'replies', 'wake', 'status', 'init', 'dlq', 'channel',
|
|
17
|
+
'dispatch', 'stop', 'send', 'replies', 'wake', 'status', 'init', 'dlq', 'channel',
|
|
18
18
|
'chat', 'open', 'attach', 'upgrade',
|
|
19
19
|
];
|
|
20
20
|
const STANDALONE_SUBCOMMANDS = new Set(['init']);
|
package/package.json
CHANGED
package/src/dispatch.ts
CHANGED
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
readCursor,
|
|
37
37
|
writeCursor,
|
|
38
38
|
writeHeartbeat,
|
|
39
|
+
writePidfile,
|
|
40
|
+
removePidfile,
|
|
39
41
|
logError,
|
|
40
42
|
} from './state.js';
|
|
41
43
|
import { recipients, reList, decideWake, splitForConcurrency } from './activation.js';
|
|
@@ -360,26 +362,32 @@ async function dispatchTick(): Promise<TickResult> {
|
|
|
360
362
|
const cursor = readCursor(transportRoot, actorName, channelUuid);
|
|
361
363
|
if (cursor === head) continue;
|
|
362
364
|
|
|
365
|
+
// First encounter: seed to HEAD so only future messages are dispatched.
|
|
366
|
+
// Without this, a null cursor falls through to `post = messages` and
|
|
367
|
+
// replays the full channel history on every fresh-state boot.
|
|
368
|
+
if (cursor === null) {
|
|
369
|
+
writeCursor(transportRoot, actorName, channelUuid, head);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
363
373
|
const messages = listChannelMessages(transportRoot, channelUuid);
|
|
364
374
|
const senderByRelPath = new Map(messages.map((m) => [m.relPath, messageSender(m)]));
|
|
365
375
|
const senderOf = (relPath: string) => senderByRelPath.get(relPath);
|
|
366
376
|
|
|
367
|
-
let
|
|
368
|
-
if (
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if (added === null) {
|
|
375
|
-
logError(transportRoot, 'other', `cursor commit ${cursor.slice(0, 12)} unknown to this clone — full channel re-scan`);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
if (added !== null) {
|
|
379
|
-
const prefix = `data/channels/${channelUuid}/`;
|
|
380
|
-
post = messages.filter((m) => added.has(prefix + m.relPath));
|
|
377
|
+
let added = addedSince.get(cursor);
|
|
378
|
+
if (added === undefined) {
|
|
379
|
+
const files = newFilesSince(transportRoot, cursor);
|
|
380
|
+
added = files === null ? null : new Set(files);
|
|
381
|
+
addedSince.set(cursor, added);
|
|
382
|
+
if (added === null) {
|
|
383
|
+
logError(transportRoot, 'other', `cursor commit ${cursor.slice(0, 12)} unknown to this clone — full channel re-scan`);
|
|
381
384
|
}
|
|
382
385
|
}
|
|
386
|
+
let post = messages;
|
|
387
|
+
if (added !== null) {
|
|
388
|
+
const prefix = `data/channels/${channelUuid}/`;
|
|
389
|
+
post = messages.filter((m) => added.has(prefix + m.relPath));
|
|
390
|
+
}
|
|
383
391
|
if (post.length === 0) {
|
|
384
392
|
writeCursor(transportRoot, actorName, channelUuid, head);
|
|
385
393
|
continue;
|
|
@@ -466,6 +474,12 @@ async function waitForWakeOrTimeout(ms: number): Promise<void> {
|
|
|
466
474
|
}
|
|
467
475
|
|
|
468
476
|
async function main(): Promise<void> {
|
|
477
|
+
writePidfile(transportRoot);
|
|
478
|
+
const cleanup = () => removePidfile(transportRoot);
|
|
479
|
+
process.on('exit', cleanup);
|
|
480
|
+
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|
|
481
|
+
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
482
|
+
|
|
469
483
|
log('dispatch_start', { transport: transportRoot, version: RUNTIME_VERSION, state_dir: stateDir(transportRoot) });
|
|
470
484
|
if (onceMode) {
|
|
471
485
|
await dispatchTick();
|
package/src/send.ts
CHANGED
|
@@ -77,7 +77,7 @@ async function main(): Promise<void> {
|
|
|
77
77
|
console.error(`but git ${pushResult.committed ? 'push' : 'commit'} FAILED:`);
|
|
78
78
|
console.error(` ${pushResult.error.slice(0, 300)}`);
|
|
79
79
|
console.error('\nYour message is in the local clone but not on origin. Recover with:');
|
|
80
|
-
console.error(' git
|
|
80
|
+
console.error(' git fetch origin && git rebase origin/main && git push');
|
|
81
81
|
process.exit(3);
|
|
82
82
|
}
|
|
83
83
|
|
package/src/state.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// dlq/<id>.md — failed-dispatch entries
|
|
9
9
|
// errors.log — infra failures, JSONL, append-only
|
|
10
10
|
// heartbeat — last tick timestamp + pid + version
|
|
11
|
+
// dispatcher.pid — PID of the running dispatcher process
|
|
11
12
|
// wake.signal — touched to wake the dispatch loop
|
|
12
13
|
//
|
|
13
14
|
// Location: $CROSSTALK_STATE_DIR if set (exact dir — container-friendly),
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
mkdirSync,
|
|
20
21
|
readFileSync,
|
|
21
22
|
writeFileSync,
|
|
23
|
+
unlinkSync,
|
|
22
24
|
appendFileSync,
|
|
23
25
|
} from 'fs';
|
|
24
26
|
import { join, dirname } from 'path';
|
|
@@ -95,6 +97,34 @@ export function writeCursor(
|
|
|
95
97
|
writeFileSync(p, commit + '\n');
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
// ── pidfile ──
|
|
101
|
+
|
|
102
|
+
export function pidfilePath(transportRoot: string): string {
|
|
103
|
+
return join(stateDir(transportRoot), 'dispatcher.pid');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function writePidfile(transportRoot: string): void {
|
|
107
|
+
try {
|
|
108
|
+
writeFileSync(pidfilePath(transportRoot), `${process.pid}\n`);
|
|
109
|
+
} catch { /* best-effort */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function removePidfile(transportRoot: string): void {
|
|
113
|
+
try {
|
|
114
|
+
unlinkSync(pidfilePath(transportRoot));
|
|
115
|
+
} catch { /* already gone */ }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function readPidfile(transportRoot: string): number | null {
|
|
119
|
+
try {
|
|
120
|
+
const raw = readFileSync(pidfilePath(transportRoot), 'utf-8').trim();
|
|
121
|
+
const n = parseInt(raw, 10);
|
|
122
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
98
128
|
// ── heartbeat + wake ──
|
|
99
129
|
|
|
100
130
|
export function writeHeartbeat(transportRoot: string, version: string): void {
|
package/src/stop.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// crosstalk stop — send SIGTERM to the running dispatcher and wait for it to exit.
|
|
2
|
+
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import { readPidfile, removePidfile } from './state.js';
|
|
6
|
+
|
|
7
|
+
const transportRoot = resolve(process.cwd());
|
|
8
|
+
|
|
9
|
+
function processRunning(pid: number): boolean {
|
|
10
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const pid = readPidfile(transportRoot);
|
|
14
|
+
if (pid === null) {
|
|
15
|
+
console.error('crosstalk stop: no dispatcher.pid found — is dispatch running?');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!processRunning(pid)) {
|
|
20
|
+
console.error(`crosstalk stop: pid ${pid} is not running — removing stale pidfile`);
|
|
21
|
+
removePidfile(transportRoot);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
process.kill(pid, 'SIGTERM');
|
|
26
|
+
|
|
27
|
+
const deadline = Date.now() + 5_000;
|
|
28
|
+
while (Date.now() < deadline) {
|
|
29
|
+
if (!processRunning(pid)) {
|
|
30
|
+
console.log(`crosstalk stop: dispatcher (pid ${pid}) stopped`);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
spawnSync('sleep', ['0.1']);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.error(`crosstalk stop: pid ${pid} did not exit within 5s — try: kill -9 ${pid}`);
|
|
37
|
+
process.exit(1);
|