@tienne/gestalt 0.108.0 → 0.110.0
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/CLAUDE.md +1 -0
- package/dist/package.json +1 -1
- package/dist/src/cli/commands/peer.d.ts +49 -0
- package/dist/src/cli/commands/peer.d.ts.map +1 -0
- package/dist/src/cli/commands/peer.js +302 -0
- package/dist/src/cli/commands/peer.js.map +1 -0
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +81 -2
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/peer/client.d.ts +9 -0
- package/dist/src/peer/client.d.ts.map +1 -0
- package/dist/src/peer/client.js +51 -0
- package/dist/src/peer/client.js.map +1 -0
- package/dist/src/peer/daemon.d.ts +83 -0
- package/dist/src/peer/daemon.d.ts.map +1 -0
- package/dist/src/peer/daemon.js +516 -0
- package/dist/src/peer/daemon.js.map +1 -0
- package/dist/src/peer/errors.d.ts +2 -0
- package/dist/src/peer/errors.d.ts.map +1 -0
- package/dist/src/peer/errors.js +4 -0
- package/dist/src/peer/errors.js.map +1 -0
- package/dist/src/peer/feed.d.ts +103 -0
- package/dist/src/peer/feed.d.ts.map +1 -0
- package/dist/src/peer/feed.js +167 -0
- package/dist/src/peer/feed.js.map +1 -0
- package/dist/src/peer/index.d.ts +24 -0
- package/dist/src/peer/index.d.ts.map +1 -0
- package/dist/src/peer/index.js +14 -0
- package/dist/src/peer/index.js.map +1 -0
- package/dist/src/peer/keys.d.ts +26 -0
- package/dist/src/peer/keys.d.ts.map +1 -0
- package/dist/src/peer/keys.js +95 -0
- package/dist/src/peer/keys.js.map +1 -0
- package/dist/src/peer/locate.d.ts +44 -0
- package/dist/src/peer/locate.d.ts.map +1 -0
- package/dist/src/peer/locate.js +116 -0
- package/dist/src/peer/locate.js.map +1 -0
- package/dist/src/peer/lock.d.ts +32 -0
- package/dist/src/peer/lock.d.ts.map +1 -0
- package/dist/src/peer/lock.js +147 -0
- package/dist/src/peer/lock.js.map +1 -0
- package/dist/src/peer/mirror.d.ts +21 -0
- package/dist/src/peer/mirror.d.ts.map +1 -0
- package/dist/src/peer/mirror.js +68 -0
- package/dist/src/peer/mirror.js.map +1 -0
- package/dist/src/peer/network.d.ts +148 -0
- package/dist/src/peer/network.d.ts.map +1 -0
- package/dist/src/peer/network.js +809 -0
- package/dist/src/peer/network.js.map +1 -0
- package/dist/src/peer/paths.d.ts +23 -0
- package/dist/src/peer/paths.d.ts.map +1 -0
- package/dist/src/peer/paths.js +50 -0
- package/dist/src/peer/paths.js.map +1 -0
- package/dist/src/peer/protocol.d.ts +112 -0
- package/dist/src/peer/protocol.d.ts.map +1 -0
- package/dist/src/peer/protocol.js +8 -0
- package/dist/src/peer/protocol.js.map +1 -0
- package/dist/src/peer/redact.d.ts +30 -0
- package/dist/src/peer/redact.d.ts.map +1 -0
- package/dist/src/peer/redact.js +214 -0
- package/dist/src/peer/redact.js.map +1 -0
- package/dist/src/peer/relay.d.ts +70 -0
- package/dist/src/peer/relay.d.ts.map +1 -0
- package/dist/src/peer/relay.js +410 -0
- package/dist/src/peer/relay.js.map +1 -0
- package/dist/src/peer/roster.d.ts +89 -0
- package/dist/src/peer/roster.d.ts.map +1 -0
- package/dist/src/peer/roster.js +160 -0
- package/dist/src/peer/roster.js.map +1 -0
- package/dist/src/peer/seal.d.ts +37 -0
- package/dist/src/peer/seal.d.ts.map +1 -0
- package/dist/src/peer/seal.js +115 -0
- package/dist/src/peer/seal.js.map +1 -0
- package/dist/src/peer/share.d.ts +41 -0
- package/dist/src/peer/share.d.ts.map +1 -0
- package/dist/src/peer/share.js +70 -0
- package/dist/src/peer/share.js.map +1 -0
- package/dist/src/peer/state.d.ts +68 -0
- package/dist/src/peer/state.d.ts.map +1 -0
- package/dist/src/peer/state.js +161 -0
- package/dist/src/peer/state.js.map +1 -0
- package/dist/src/peer/tail.d.ts +55 -0
- package/dist/src/peer/tail.d.ts.map +1 -0
- package/dist/src/peer/tail.js +148 -0
- package/dist/src/peer/tail.js.map +1 -0
- package/dist/src/peer/transform.d.ts +79 -0
- package/dist/src/peer/transform.d.ts.map +1 -0
- package/dist/src/peer/transform.js +237 -0
- package/dist/src/peer/transform.js.map +1 -0
- package/dist/src/peer/transport.d.ts +89 -0
- package/dist/src/peer/transport.d.ts.map +1 -0
- package/dist/src/peer/transport.js +345 -0
- package/dist/src/peer/transport.js.map +1 -0
- package/dist/src/peer/viewer-html.d.ts +10 -0
- package/dist/src/peer/viewer-html.d.ts.map +1 -0
- package/dist/src/peer/viewer-html.js +165 -0
- package/dist/src/peer/viewer-html.js.map +1 -0
- package/dist/src/peer/viewer.d.ts +58 -0
- package/dist/src/peer/viewer.d.ts.map +1 -0
- package/dist/src/peer/viewer.js +249 -0
- package/dist/src/peer/viewer.js.map +1 -0
- package/dist/src/peer/wire.d.ts +90 -0
- package/dist/src/peer/wire.d.ts.map +1 -0
- package/dist/src/peer/wire.js +10 -0
- package/dist/src/peer/wire.js.map +1 -0
- package/dist/src/peer/ws.d.ts +47 -0
- package/dist/src/peer/ws.d.ts.map +1 -0
- package/dist/src/peer/ws.js +308 -0
- package/dist/src/peer/ws.js.map +1 -0
- package/package.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -1
- package/plugin/mcp.json +1 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { isAbsolute, join } from 'node:path';
|
|
4
|
+
/** 설정 디렉토리. Orca 프로필처럼 `CLAUDE_CONFIG_DIR`로 옮겨 둔 경우가 있다 */
|
|
5
|
+
export function claudeConfigDir(env = process.env) {
|
|
6
|
+
const raw = env['CLAUDE_CONFIG_DIR'];
|
|
7
|
+
if (raw && isAbsolute(raw))
|
|
8
|
+
return raw;
|
|
9
|
+
return join(homedir(), '.claude');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* cwd를 `projects/` 아래 디렉토리 이름으로 바꾼다. 실제 이름을 보면 영숫자와 `-`
|
|
13
|
+
* 말고는 전부 `-`가 된다(`/Users/a/.x` → `-Users-a--x`, 한글도 `-`).
|
|
14
|
+
*/
|
|
15
|
+
export function cwdSlug(cwd) {
|
|
16
|
+
return cwd.replace(/[^A-Za-z0-9-]/g, '-');
|
|
17
|
+
}
|
|
18
|
+
/** sessionId가 경로 조각으로 쓰이므로 `..`이나 `/`가 들어오면 안 된다 */
|
|
19
|
+
const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
20
|
+
export function isSessionId(value) {
|
|
21
|
+
return SESSION_ID.test(value);
|
|
22
|
+
}
|
|
23
|
+
/** `sessions/<pid>.json` 하나를 읽는다. 형식이 다르면 null */
|
|
24
|
+
export function readSessionFile(path) {
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
33
|
+
return null;
|
|
34
|
+
const rec = parsed;
|
|
35
|
+
const sessionId = rec['sessionId'];
|
|
36
|
+
const cwd = rec['cwd'];
|
|
37
|
+
if (typeof sessionId !== 'string' || typeof cwd !== 'string')
|
|
38
|
+
return null;
|
|
39
|
+
const pid = typeof rec['pid'] === 'number' ? rec['pid'] : undefined;
|
|
40
|
+
return { sessionId, cwd, pid };
|
|
41
|
+
}
|
|
42
|
+
/** 부모 pid로 세션을 찾는다. MCP 서버처럼 세션의 자식이 쓴다 */
|
|
43
|
+
export function findSessionByPid(pid, configDir = claudeConfigDir()) {
|
|
44
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
45
|
+
return null;
|
|
46
|
+
return readSessionFile(join(configDir, 'sessions', `${pid}.json`));
|
|
47
|
+
}
|
|
48
|
+
function findSessionById(sessionId, configDir) {
|
|
49
|
+
const dir = join(configDir, 'sessions');
|
|
50
|
+
let names;
|
|
51
|
+
try {
|
|
52
|
+
names = readdirSync(dir);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
for (const name of names) {
|
|
58
|
+
if (!/^\d+\.json$/.test(name))
|
|
59
|
+
continue;
|
|
60
|
+
const rec = readSessionFile(join(dir, name));
|
|
61
|
+
if (rec?.sessionId === sessionId)
|
|
62
|
+
return rec;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function isFile(path) {
|
|
67
|
+
try {
|
|
68
|
+
return statSync(path).isFile();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* sessionId로 대화 기록 경로를 찾는다.
|
|
76
|
+
*
|
|
77
|
+
* 1. `sessions/*.json`에서 그 세션의 cwd를 찾아 슬러그 경로를 본다
|
|
78
|
+
* 2. 없으면(세션이 끝났거나 슬러그 규칙이 다르면) `projects/*` 아래를 훑는다
|
|
79
|
+
*/
|
|
80
|
+
export function locateTranscript(sessionId, configDir = claudeConfigDir()) {
|
|
81
|
+
if (!isSessionId(sessionId)) {
|
|
82
|
+
return { ok: false, reason: 'sessionId 형식이 아니에요', configDir };
|
|
83
|
+
}
|
|
84
|
+
if (!existsSync(configDir)) {
|
|
85
|
+
return { ok: false, reason: '설정 디렉토리가 없어요', configDir, sessionId };
|
|
86
|
+
}
|
|
87
|
+
const session = findSessionById(sessionId, configDir);
|
|
88
|
+
if (session) {
|
|
89
|
+
const bySlug = join(configDir, 'projects', cwdSlug(session.cwd), `${sessionId}.jsonl`);
|
|
90
|
+
if (isFile(bySlug))
|
|
91
|
+
return { ok: true, path: bySlug, sessionId, configDir, cwd: session.cwd };
|
|
92
|
+
}
|
|
93
|
+
const projects = join(configDir, 'projects');
|
|
94
|
+
let dirs;
|
|
95
|
+
try {
|
|
96
|
+
dirs = readdirSync(projects);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return { ok: false, reason: 'projects 디렉토리를 못 읽었어요', configDir, sessionId };
|
|
100
|
+
}
|
|
101
|
+
for (const dir of dirs) {
|
|
102
|
+
const candidate = join(projects, dir, `${sessionId}.jsonl`);
|
|
103
|
+
if (isFile(candidate)) {
|
|
104
|
+
return { ok: true, path: candidate, sessionId, configDir, cwd: session?.cwd };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
reason: session
|
|
110
|
+
? '세션은 있는데 대화 기록 파일을 못 찾았어요'
|
|
111
|
+
: '그 sessionId의 세션도 대화 기록도 못 찾았어요',
|
|
112
|
+
configDir,
|
|
113
|
+
sessionId,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=locate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locate.js","sourceRoot":"","sources":["../../../src/peer/locate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface DaemonLock {
|
|
2
|
+
path: string;
|
|
3
|
+
dev: number;
|
|
4
|
+
ino: number;
|
|
5
|
+
/** 적어 둔 내용. 지운 파일의 inode를 새 파일이 다시 받을 수 있어서 내용까지 맞춰 본다 */
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 잠금 파일이 막 생긴 뒤 소켓이 아직 안 열렸어도 살아 있는 데몬으로 치는 시간. 이보다
|
|
10
|
+
* 오래됐는데 소켓이 답하지 않으면 pid가 다른 프로세스에 다시 쓰인 걸로 보고 치운다.
|
|
11
|
+
*/
|
|
12
|
+
export declare const LOCK_GRACE_MS = 10000;
|
|
13
|
+
export declare function lockPath(root: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* 데몬 잠금. 이걸 쥔 데몬만 소켓 자리를 확인하고 치우고 만든다.
|
|
16
|
+
*
|
|
17
|
+
* 남은 잠금 파일을 치우는 규칙:
|
|
18
|
+
* 1. 적힌 pid가 없는 프로세스면 남은 것이다.
|
|
19
|
+
* 2. pid가 살아 있어도 잠금이 `LOCK_GRACE_MS`보다 오래됐고 소켓이 답하지 않으면 남은 것이다
|
|
20
|
+
* (데몬이 죽고 그 pid를 다른 프로세스가 받은 경우).
|
|
21
|
+
* 3. 남은 잠금은 지우지 않고 내 이름으로 옮긴 뒤 옮긴 파일이 내가 읽은 그 파일인지 본다.
|
|
22
|
+
* 그 사이 다른 데몬이 남은 잠금을 치우고 새로 잡았으면 옮긴 게 그 데몬의 잠금이라 같은
|
|
23
|
+
* 이름으로 되돌리고 물러난다. 옮기기는 한쪽만 성공하므로 둘이 같은 남은 잠금을 동시에
|
|
24
|
+
* 치워도 둘 다 잠금을 쥐었다고 믿지 않는다.
|
|
25
|
+
*
|
|
26
|
+
* 되돌리는 사이에 세 번째 데몬이 빈 자리에 잠금을 만들면 그 데몬과 원래 데몬이 함께 쥔다.
|
|
27
|
+
* 남은 잠금 하나를 데몬 셋이 같은 순간에 치울 때만 생기는 틈이다.
|
|
28
|
+
*/
|
|
29
|
+
export declare function acquireDaemonLock(root: string, sock: string): Promise<DaemonLock>;
|
|
30
|
+
/** 내 잠금일 때만 지운다. 이미 다른 데몬이 그 자리를 잡았으면 안 건드린다 */
|
|
31
|
+
export declare function releaseDaemonLock(lock: DaemonLock): void;
|
|
32
|
+
//# sourceMappingURL=lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../../../src/peer/lock.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,QAAS,CAAC;AAEpC,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAqBvF;AAED,gDAAgD;AAChD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CASxD"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { closeSync, linkSync, lstatSync, openSync, readFileSync, renameSync, unlinkSync, writeSync, } from 'node:fs';
|
|
3
|
+
import { createConnection } from 'node:net';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
/**
|
|
6
|
+
* 잠금 파일이 막 생긴 뒤 소켓이 아직 안 열렸어도 살아 있는 데몬으로 치는 시간. 이보다
|
|
7
|
+
* 오래됐는데 소켓이 답하지 않으면 pid가 다른 프로세스에 다시 쓰인 걸로 보고 치운다.
|
|
8
|
+
*/
|
|
9
|
+
export const LOCK_GRACE_MS = 10_000;
|
|
10
|
+
export function lockPath(root) {
|
|
11
|
+
return join(root, 'peer.lock');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 데몬 잠금. 이걸 쥔 데몬만 소켓 자리를 확인하고 치우고 만든다.
|
|
15
|
+
*
|
|
16
|
+
* 남은 잠금 파일을 치우는 규칙:
|
|
17
|
+
* 1. 적힌 pid가 없는 프로세스면 남은 것이다.
|
|
18
|
+
* 2. pid가 살아 있어도 잠금이 `LOCK_GRACE_MS`보다 오래됐고 소켓이 답하지 않으면 남은 것이다
|
|
19
|
+
* (데몬이 죽고 그 pid를 다른 프로세스가 받은 경우).
|
|
20
|
+
* 3. 남은 잠금은 지우지 않고 내 이름으로 옮긴 뒤 옮긴 파일이 내가 읽은 그 파일인지 본다.
|
|
21
|
+
* 그 사이 다른 데몬이 남은 잠금을 치우고 새로 잡았으면 옮긴 게 그 데몬의 잠금이라 같은
|
|
22
|
+
* 이름으로 되돌리고 물러난다. 옮기기는 한쪽만 성공하므로 둘이 같은 남은 잠금을 동시에
|
|
23
|
+
* 치워도 둘 다 잠금을 쥐었다고 믿지 않는다.
|
|
24
|
+
*
|
|
25
|
+
* 되돌리는 사이에 세 번째 데몬이 빈 자리에 잠금을 만들면 그 데몬과 원래 데몬이 함께 쥔다.
|
|
26
|
+
* 남은 잠금 하나를 데몬 셋이 같은 순간에 치울 때만 생기는 틈이다.
|
|
27
|
+
*/
|
|
28
|
+
export async function acquireDaemonLock(root, sock) {
|
|
29
|
+
const path = lockPath(root);
|
|
30
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
31
|
+
let fd;
|
|
32
|
+
try {
|
|
33
|
+
fd = openSync(path, 'wx', 0o600);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (err.code !== 'EEXIST')
|
|
37
|
+
throw err;
|
|
38
|
+
await clearStaleLock(path, sock);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const content = `${process.pid}\n${Date.now()}\n${randomBytes(8).toString('hex')}\n`;
|
|
42
|
+
try {
|
|
43
|
+
writeSync(fd, content);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
closeSync(fd);
|
|
47
|
+
}
|
|
48
|
+
const st = lstatSync(path);
|
|
49
|
+
return { path, dev: st.dev, ino: st.ino, content };
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`데몬 잠금을 못 잡았어요: ${path}`);
|
|
52
|
+
}
|
|
53
|
+
/** 내 잠금일 때만 지운다. 이미 다른 데몬이 그 자리를 잡았으면 안 건드린다 */
|
|
54
|
+
export function releaseDaemonLock(lock) {
|
|
55
|
+
try {
|
|
56
|
+
const st = lstatSync(lock.path);
|
|
57
|
+
if (st.dev !== lock.dev || st.ino !== lock.ino)
|
|
58
|
+
return;
|
|
59
|
+
if (readFileSync(lock.path, 'utf8') !== lock.content)
|
|
60
|
+
return;
|
|
61
|
+
unlinkSync(lock.path);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// 이미 없으면 그만이다
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function clearStaleLock(path, sock) {
|
|
68
|
+
let st;
|
|
69
|
+
let text;
|
|
70
|
+
try {
|
|
71
|
+
st = lstatSync(path);
|
|
72
|
+
text = readFileSync(path, 'utf8');
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err.code === 'ENOENT')
|
|
76
|
+
return;
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
if (!st.isFile())
|
|
80
|
+
throw new Error(`잠금 파일 자리에 다른 파일이 있어요: ${path}`);
|
|
81
|
+
const [pidLine, atLine] = text.split('\n');
|
|
82
|
+
const pid = Number(pidLine);
|
|
83
|
+
const at = Number(atLine);
|
|
84
|
+
const validPid = pidLine !== '' && Number.isSafeInteger(pid) && pid > 0;
|
|
85
|
+
// 다른 데몬이 막 만들고 pid를 아직 안 적은 파일일 수 있다. 오래된 것만 남은 것으로 친다
|
|
86
|
+
if (!validPid && Date.now() - st.mtimeMs < LOCK_GRACE_MS) {
|
|
87
|
+
throw new Error(`다른 데몬이 같이 뜨는 중이에요: ${sock}`);
|
|
88
|
+
}
|
|
89
|
+
if (validPid && processAlive(pid)) {
|
|
90
|
+
const young = Number.isFinite(at) && Date.now() - at < LOCK_GRACE_MS;
|
|
91
|
+
if (young || (await socketAnswers(sock))) {
|
|
92
|
+
throw new Error(`데몬이 이미 떠 있어요 (pid ${pid}): ${sock}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const aside = `${path}.${process.pid}.stale`;
|
|
96
|
+
try {
|
|
97
|
+
renameSync(path, aside);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
// 다른 데몬이 먼저 옮겼다. 처음부터 다시 본다
|
|
101
|
+
if (err.code === 'ENOENT')
|
|
102
|
+
return;
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
const moved = lstatSync(aside);
|
|
106
|
+
// inode는 지운 파일 것을 새 파일이 다시 받을 수 있다. 내용까지 같아야 내가 읽은 그 파일이다
|
|
107
|
+
if (moved.dev !== st.dev || moved.ino !== st.ino || readFileSync(aside, 'utf8') !== text) {
|
|
108
|
+
try {
|
|
109
|
+
linkSync(aside, path);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// 그새 또 다른 데몬이 잡았다. 그쪽 잠금이 남는다
|
|
113
|
+
}
|
|
114
|
+
unlinkSync(aside);
|
|
115
|
+
throw new Error(`다른 데몬이 같이 뜨는 중이에요: ${sock}`);
|
|
116
|
+
}
|
|
117
|
+
unlinkSync(aside);
|
|
118
|
+
}
|
|
119
|
+
function processAlive(pid) {
|
|
120
|
+
try {
|
|
121
|
+
process.kill(pid, 0);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
// 남의 프로세스면 EPERM이다. 살아 있다
|
|
126
|
+
return err.code === 'EPERM';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function socketAnswers(sock) {
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
const c = createConnection(sock);
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
c.destroy();
|
|
134
|
+
resolve(false);
|
|
135
|
+
}, 1_000);
|
|
136
|
+
c.once('connect', () => {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
c.destroy();
|
|
139
|
+
resolve(true);
|
|
140
|
+
});
|
|
141
|
+
c.once('error', () => {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
resolve(false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../../src/peer/lock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ShareFeed } from './feed.js';
|
|
2
|
+
import type { ShareFrame } from './feed.js';
|
|
3
|
+
/**
|
|
4
|
+
* 보는 사람 데몬이 원격 공유를 받아 로컬 뷰어로 옮기는 자리. 뷰어 서버는 내 공유와
|
|
5
|
+
* 똑같이 다룬다.
|
|
6
|
+
*
|
|
7
|
+
* 이벤트 번호는 주인 쪽 번호를 그대로 쓴다. 다시 붙어서 이어 받을 때 주인 쪽이 그 사이를
|
|
8
|
+
* 버렸으면 번호가 건너뛴다. 그때는 들고 있던 걸 비우고 건너뛴 개수를 생략으로 알린다.
|
|
9
|
+
*/
|
|
10
|
+
export declare class MirrorFeed extends ShareFeed {
|
|
11
|
+
readonly ownerKey: string;
|
|
12
|
+
readonly ownerPerson: string;
|
|
13
|
+
readonly shareId: string;
|
|
14
|
+
private unknown;
|
|
15
|
+
constructor(id: string, ownerKey: string, ownerPerson: string, shareId: string, maxBacklogBytes?: number);
|
|
16
|
+
get unknownLines(): number;
|
|
17
|
+
/** 주인 쪽 프레임을 반영한다. 주인 쪽 trimmed는 번호 건너뜀으로 따로 잡으므로 무시한다 */
|
|
18
|
+
apply(frames: ShareFrame[]): void;
|
|
19
|
+
markOffline(): void;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=mirror.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mirror.d.ts","sourceRoot":"","sources":["../../../src/peer/mirror.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,WAAW,CAAC;AAEzD;;;;;;GAMG;AACH,qBAAa,UAAW,SAAQ,SAAS;IAKrC,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM;IAN1B,OAAO,CAAC,OAAO,CAAK;gBAGlB,EAAE,EAAE,MAAM,EACD,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACxB,eAAe,CAAC,EAAE,MAAM;IAK1B,IAAa,YAAY,IAAI,MAAM,CAElC;IAED,0DAA0D;IAC1D,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI;IAkCjC,WAAW,IAAI,IAAI;CAGpB"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ShareFeed } from './feed.js';
|
|
2
|
+
/**
|
|
3
|
+
* 보는 사람 데몬이 원격 공유를 받아 로컬 뷰어로 옮기는 자리. 뷰어 서버는 내 공유와
|
|
4
|
+
* 똑같이 다룬다.
|
|
5
|
+
*
|
|
6
|
+
* 이벤트 번호는 주인 쪽 번호를 그대로 쓴다. 다시 붙어서 이어 받을 때 주인 쪽이 그 사이를
|
|
7
|
+
* 버렸으면 번호가 건너뛴다. 그때는 들고 있던 걸 비우고 건너뛴 개수를 생략으로 알린다.
|
|
8
|
+
*/
|
|
9
|
+
export class MirrorFeed extends ShareFeed {
|
|
10
|
+
ownerKey;
|
|
11
|
+
ownerPerson;
|
|
12
|
+
shareId;
|
|
13
|
+
unknown = 0;
|
|
14
|
+
constructor(id, ownerKey, ownerPerson, shareId, maxBacklogBytes) {
|
|
15
|
+
super(id, maxBacklogBytes);
|
|
16
|
+
this.ownerKey = ownerKey;
|
|
17
|
+
this.ownerPerson = ownerPerson;
|
|
18
|
+
this.shareId = shareId;
|
|
19
|
+
}
|
|
20
|
+
get unknownLines() {
|
|
21
|
+
return this.unknown;
|
|
22
|
+
}
|
|
23
|
+
/** 주인 쪽 프레임을 반영한다. 주인 쪽 trimmed는 번호 건너뜀으로 따로 잡으므로 무시한다 */
|
|
24
|
+
apply(frames) {
|
|
25
|
+
if (this.isClosed)
|
|
26
|
+
return;
|
|
27
|
+
for (const frame of frames) {
|
|
28
|
+
switch (frame.type) {
|
|
29
|
+
case 'event': {
|
|
30
|
+
if (!Number.isSafeInteger(frame.seq) || typeof frame.json !== 'string')
|
|
31
|
+
continue;
|
|
32
|
+
if (frame.seq <= this.eventCount)
|
|
33
|
+
continue;
|
|
34
|
+
if (frame.seq > this.eventCount + 1) {
|
|
35
|
+
const gap = frame.seq - 1 - this.eventCount;
|
|
36
|
+
this.skipTo(frame.seq - 1);
|
|
37
|
+
this.emit({ type: 'trimmed', dropped: gap });
|
|
38
|
+
}
|
|
39
|
+
this.pushEvent(frame.json);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case 'stats':
|
|
43
|
+
if (Number.isSafeInteger(frame.unknownLines) && frame.unknownLines !== this.unknown) {
|
|
44
|
+
this.unknown = frame.unknownLines;
|
|
45
|
+
this.emit({ type: 'stats', unknownLines: this.unknown });
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
case 'state':
|
|
49
|
+
if (isShareStatus(frame.status))
|
|
50
|
+
this.setStatus(frame.status);
|
|
51
|
+
break;
|
|
52
|
+
case 'closed':
|
|
53
|
+
this.close(typeof frame.reason === 'string' ? frame.reason : 'closed');
|
|
54
|
+
return;
|
|
55
|
+
case 'trimmed':
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
this.trim();
|
|
60
|
+
}
|
|
61
|
+
markOffline() {
|
|
62
|
+
this.setStatus('offline');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function isShareStatus(v) {
|
|
66
|
+
return v === 'live' || v === 'missing' || v === 'truncated' || v === 'offline';
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=mirror.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mirror.js","sourceRoot":"","sources":["../../../src/peer/mirror.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC;;;;;;GAMG;AACH,MAAM,OAAO,UAAW,SAAQ,SAAS;IAK5B;IACA;IACA;IANH,OAAO,GAAG,CAAC,CAAC;IAEpB,YACE,EAAU,EACD,QAAgB,EAChB,WAAmB,EACnB,OAAe,EACxB,eAAwB;QAExB,KAAK,CAAC,EAAE,EAAE,eAAe,CAAC,CAAC;QALlB,aAAQ,GAAR,QAAQ,CAAQ;QAChB,gBAAW,GAAX,WAAW,CAAQ;QACnB,YAAO,GAAP,OAAO,CAAQ;IAI1B,CAAC;IAED,IAAa,YAAY;QACvB,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,0DAA0D;IAC1D,KAAK,CAAC,MAAoB;QACxB,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC1B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;gBACnB,KAAK,OAAO,CAAC,CAAC,CAAC;oBACb,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;wBAAE,SAAS;oBACjF,IAAI,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,UAAU;wBAAE,SAAS;oBAC3C,IAAI,KAAK,CAAC,GAAG,GAAG,IAAI,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;wBACpC,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC;wBAC5C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;wBAC3B,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;oBAC/C,CAAC;oBACD,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;oBAC3B,MAAM;gBACR,CAAC;gBACD,KAAK,OAAO;oBACV,IAAI,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,YAAY,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;wBACpF,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,YAAY,CAAC;wBAClC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;oBAC3D,CAAC;oBACD,MAAM;gBACR,KAAK,OAAO;oBACV,IAAI,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC;wBAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;oBAC9D,MAAM;gBACR,KAAK,QAAQ;oBACX,IAAI,CAAC,KAAK,CAAC,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;oBACvE,OAAO;gBACT,KAAK,SAAS;oBACZ,MAAM;YACV,CAAC;QACH,CAAC;QACD,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAED,WAAW;QACT,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAC5B,CAAC;CACF;AAED,SAAS,aAAa,CAAC,CAAU;IAC/B,OAAO,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,WAAW,IAAI,CAAC,KAAK,SAAS,CAAC;AACjF,CAAC"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ShareFeed } from './feed.js';
|
|
2
|
+
import type { MachineKey } from './keys.js';
|
|
3
|
+
import { MirrorFeed } from './mirror.js';
|
|
4
|
+
import type { PeerState } from './state.js';
|
|
5
|
+
import type { RelayTransportOptions, TransportState } from './transport.js';
|
|
6
|
+
/** 주인 데몬이 내 공유를 넘겨줄 때 쓰는 것 */
|
|
7
|
+
export interface OwnedShare {
|
|
8
|
+
stream: ShareFeed;
|
|
9
|
+
shareKey: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PeerNetworkOptions {
|
|
12
|
+
state: PeerState;
|
|
13
|
+
key: MachineKey;
|
|
14
|
+
lookupShare(id: string): OwnedShare | undefined;
|
|
15
|
+
/** 테스트가 다시 붙는 간격을 줄일 때 쓴다 */
|
|
16
|
+
transport?: Partial<Pick<RelayTransportOptions, 'minBackoffMs' | 'maxBackoffMs' | 'ackTimeoutMs'>>;
|
|
17
|
+
/** 원격 구독 하나의 릴레이 소켓에 밀린 양이 이걸 넘으면 그 구독을 끊는다 */
|
|
18
|
+
maxBufferedBytes?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface TeamStatus {
|
|
21
|
+
team: string;
|
|
22
|
+
person: string;
|
|
23
|
+
relay: string;
|
|
24
|
+
epoch: number;
|
|
25
|
+
role: 'admin' | 'member' | null;
|
|
26
|
+
connection: TransportState;
|
|
27
|
+
remoteViewers: number;
|
|
28
|
+
watches: number;
|
|
29
|
+
}
|
|
30
|
+
export interface MemberInfo {
|
|
31
|
+
person: string;
|
|
32
|
+
machineKey: string;
|
|
33
|
+
role: 'admin' | 'member';
|
|
34
|
+
addedAt: number;
|
|
35
|
+
online: boolean;
|
|
36
|
+
me: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface WatchInfo {
|
|
39
|
+
id: string;
|
|
40
|
+
share: string;
|
|
41
|
+
owner: string;
|
|
42
|
+
feed: MirrorFeed;
|
|
43
|
+
token: string;
|
|
44
|
+
}
|
|
45
|
+
/** 초대 유효 시간. 설계 원문의 24시간 */
|
|
46
|
+
export declare const INVITE_TTL_MS: number;
|
|
47
|
+
/** 원격 구독 하나가 릴레이 소켓에 밀어 둘 수 있는 양 */
|
|
48
|
+
export declare const MAX_REMOTE_BUFFERED_BYTES: number;
|
|
49
|
+
export declare class PeerNetworkError extends Error {
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 팀, 명단, 릴레이 통로, 원격 공유를 묶는다. 데몬이 하나 들고 있다.
|
|
53
|
+
*
|
|
54
|
+
* 주인 쪽: 보는 사람이 링크의 공유 키로 증명을 보내면 명단을 확인하고 구독마다 새 공유
|
|
55
|
+
* 세션 키를 정해 그 키로 봉인한 프레임을 흘린다. 세션 키는 링크의 공유 키와 주인이 고른
|
|
56
|
+
* salt로 만든다. salt는 보는 사람 X25519 키에 봉인해 보내므로 링크만 가진 사람도,
|
|
57
|
+
* 릴레이도 못 연다.
|
|
58
|
+
*
|
|
59
|
+
* 보는 쪽: 받은 프레임을 MirrorFeed에 옮겨 로컬 뷰어로 보여준다.
|
|
60
|
+
*/
|
|
61
|
+
export declare class PeerNetwork {
|
|
62
|
+
private readonly options;
|
|
63
|
+
private transport;
|
|
64
|
+
private team;
|
|
65
|
+
private roster;
|
|
66
|
+
private subs;
|
|
67
|
+
private watches;
|
|
68
|
+
private online;
|
|
69
|
+
private announced;
|
|
70
|
+
/** 최근 받은 watch 요청의 `기기|nonce`와 받은 시각. 릴레이가 옛 요청을 다시 보내면 거절한다 */
|
|
71
|
+
private seenWatch;
|
|
72
|
+
private readonly maxBuffered;
|
|
73
|
+
constructor(options: PeerNetworkOptions);
|
|
74
|
+
get key(): MachineKey;
|
|
75
|
+
/** 팀이 있으면 릴레이에 붙기 시작한다. 첫 접속을 기다리지 않는다 */
|
|
76
|
+
start(): void;
|
|
77
|
+
stop(): Promise<void>;
|
|
78
|
+
status(): TeamStatus | null;
|
|
79
|
+
members(): MemberInfo[];
|
|
80
|
+
/** 팀을 만들고 첫 관리자가 된다 */
|
|
81
|
+
init(person: string, relay: string): TeamStatus;
|
|
82
|
+
/** 1회용 초대 코드를 만든다. 관리자만 */
|
|
83
|
+
invite(person: string, now?: number): {
|
|
84
|
+
code: string;
|
|
85
|
+
expiresAt: number;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* 초대를 받는다. 릴레이에 join 모드로 붙어 초대한 관리자에게 봉인한 요청을 넘기고 관리자가
|
|
89
|
+
* 나를 넣은 명단에 서명해 돌릴 때까지 기다린다.
|
|
90
|
+
*/
|
|
91
|
+
join(code: string, timeoutMs?: number): Promise<TeamStatus>;
|
|
92
|
+
/** 사람 이름으로 그 사람의 기기를 모두 뺀다. 관리자만 */
|
|
93
|
+
remove(person: string): {
|
|
94
|
+
removed: number;
|
|
95
|
+
epoch: number;
|
|
96
|
+
};
|
|
97
|
+
/** 내 공유의 링크. 팀이 없으면 undefined */
|
|
98
|
+
linkFor(shareId: string, shareKey: string): string | undefined;
|
|
99
|
+
/** 지금 연 공유 id 전부를 릴레이에 알린다 */
|
|
100
|
+
announce(shareIds: string[]): void;
|
|
101
|
+
lookupWatch(id: string): {
|
|
102
|
+
stream: ShareFeed;
|
|
103
|
+
token: string;
|
|
104
|
+
} | undefined;
|
|
105
|
+
listWatches(): {
|
|
106
|
+
id: string;
|
|
107
|
+
share: string;
|
|
108
|
+
owner: string;
|
|
109
|
+
status: string;
|
|
110
|
+
events: number;
|
|
111
|
+
}[];
|
|
112
|
+
/** 링크로 원격 공유를 보기 시작한다. 주인 데몬이 받아 줄 때까지 기다린다 */
|
|
113
|
+
watch(link: string): Promise<WatchInfo>;
|
|
114
|
+
unwatch(id: string): boolean;
|
|
115
|
+
private connect;
|
|
116
|
+
private onWelcome;
|
|
117
|
+
private onTransportState;
|
|
118
|
+
private onPresence;
|
|
119
|
+
private onRemoteRoster;
|
|
120
|
+
/**
|
|
121
|
+
* 명단을 받는다. 서명이 안 맞거나 판이 낮으면 버리고 감사 로그에 남긴다. 받으면 명단에서
|
|
122
|
+
* 빠진 기기의 원격 구독을 바로 끊는다.
|
|
123
|
+
*/
|
|
124
|
+
private applyRoster;
|
|
125
|
+
private requireAdmin;
|
|
126
|
+
private onReceive;
|
|
127
|
+
private onJoinRequest;
|
|
128
|
+
private handleJoin;
|
|
129
|
+
private onWatchRequest;
|
|
130
|
+
/**
|
|
131
|
+
* 처음 보는 nonce면 기억하고 true. 공유는 데몬이 다시 뜨면 닫히므로 메모리에만 둔다.
|
|
132
|
+
* 10분 지난 것과 상한을 넘는 오래된 것부터 잊는다.
|
|
133
|
+
*/
|
|
134
|
+
private rememberWatchNonce;
|
|
135
|
+
/** 뷰어 서버의 pump와 같다. 릴레이 소켓이 비는 만큼씩 앞부분을 보내고 다 따라잡으면 바로 붙는다 */
|
|
136
|
+
private pump;
|
|
137
|
+
private onLocalFrame;
|
|
138
|
+
private post;
|
|
139
|
+
private endSub;
|
|
140
|
+
private requestWatch;
|
|
141
|
+
private rewatch;
|
|
142
|
+
private onWatchReply;
|
|
143
|
+
private onWatchEnd;
|
|
144
|
+
private onStream;
|
|
145
|
+
private closeWatch;
|
|
146
|
+
private sendBox;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=network.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../../src/peer/network.ts"],"names":[],"mappings":"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"}
|