@tienne/gestalt 0.109.0 → 0.111.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/dist/package.json +1 -1
- package/dist/src/cli/commands/peer.d.ts +30 -0
- package/dist/src/cli/commands/peer.d.ts.map +1 -1
- package/dist/src/cli/commands/peer.js +159 -4
- package/dist/src/cli/commands/peer.js.map +1 -1
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +48 -2
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/digest/render-html.d.ts +5 -0
- package/dist/src/digest/render-html.d.ts.map +1 -1
- package/dist/src/digest/render-html.js +31 -10
- package/dist/src/digest/render-html.js.map +1 -1
- package/dist/src/digest/render-lens.d.ts +1 -1
- package/dist/src/digest/render-lens.d.ts.map +1 -1
- package/dist/src/digest/render-lens.js +17 -2
- package/dist/src/digest/render-lens.js.map +1 -1
- package/dist/src/digest/server.d.ts.map +1 -1
- package/dist/src/digest/server.js +7 -4
- package/dist/src/digest/server.js.map +1 -1
- package/dist/src/digest/theme-diary.d.ts +10 -0
- package/dist/src/digest/theme-diary.d.ts.map +1 -0
- package/dist/src/digest/theme-diary.js +111 -0
- package/dist/src/digest/theme-diary.js.map +1 -0
- package/dist/src/digest/viewer-js.d.ts +6 -0
- package/dist/src/digest/viewer-js.d.ts.map +1 -1
- package/dist/src/digest/viewer-js.js +78 -4
- package/dist/src/digest/viewer-js.js.map +1 -1
- package/dist/src/peer/daemon.d.ts +17 -5
- package/dist/src/peer/daemon.d.ts.map +1 -1
- package/dist/src/peer/daemon.js +124 -15
- package/dist/src/peer/daemon.js.map +1 -1
- 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 +10 -0
- package/dist/src/peer/index.d.ts.map +1 -1
- package/dist/src/peer/index.js +5 -0
- package/dist/src/peer/index.js.map +1 -1
- 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/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/protocol.d.ts +52 -1
- package/dist/src/peer/protocol.d.ts.map +1 -1
- package/dist/src/peer/protocol.js +1 -0
- package/dist/src/peer/protocol.js.map +1 -1
- 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 +4 -86
- package/dist/src/peer/share.d.ts.map +1 -1
- package/dist/src/peer/share.js +6 -140
- package/dist/src/peer/share.js.map +1 -1
- package/dist/src/peer/state.d.ts +35 -4
- package/dist/src/peer/state.d.ts.map +1 -1
- package/dist/src/peer/state.js +65 -3
- package/dist/src/peer/state.js.map +1 -1
- 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.map +1 -1
- package/dist/src/peer/viewer-html.js +10 -2
- package/dist/src/peer/viewer-html.js.map +1 -1
- package/dist/src/peer/viewer.d.ts +2 -2
- package/dist/src/peer/viewer.d.ts.map +1 -1
- package/dist/src/peer/viewer.js.map +1 -1
- 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,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 뷰어 하나가 받는 것. seq는 SSE id로 써서 다시 붙었을 때 이어 받게 한다. 이벤트는
|
|
3
|
+
* 쌓을 때 한 번 직렬화한 글자를 그대로 넘긴다. 뷰어마다 다시 직렬화하지 않는다.
|
|
4
|
+
*/
|
|
5
|
+
export type ShareFrame = {
|
|
6
|
+
type: 'event';
|
|
7
|
+
seq: number;
|
|
8
|
+
json: string;
|
|
9
|
+
}
|
|
10
|
+
/** 붙은 뷰어가 받을 앞부분 중 메모리 상한에 밀려 버려진 이벤트 수 */
|
|
11
|
+
| {
|
|
12
|
+
type: 'trimmed';
|
|
13
|
+
dropped: number;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'stats';
|
|
16
|
+
unknownLines: number;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'state';
|
|
19
|
+
status: ShareStatus;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 공유 종료. 앞부분을 따라잡는 중에 종료되면 아직 못 보낸 이벤트는 사라지므로 그 개수를
|
|
23
|
+
* missed에 싣는다. 붙어 있던 뷰어는 다 받은 뒤라 안 싣는다
|
|
24
|
+
*/
|
|
25
|
+
| {
|
|
26
|
+
type: 'closed';
|
|
27
|
+
reason: string;
|
|
28
|
+
missed?: number;
|
|
29
|
+
};
|
|
30
|
+
/** offline은 원격 공유에서 주인 데몬이 릴레이에서 빠졌을 때다 */
|
|
31
|
+
export type ShareStatus = 'live' | 'missing' | 'truncated' | 'offline';
|
|
32
|
+
export type ShareListener = (frame: ShareFrame) => void;
|
|
33
|
+
/**
|
|
34
|
+
* 새 뷰어에게 다시 보낼 앞부분의 상한. 넘으면 오래된 것부터 버린다. 공유는 최대 7일
|
|
35
|
+
* 열려 있고 도구 출력이 크면 events가 끝없이 는다.
|
|
36
|
+
*/
|
|
37
|
+
export declare const DEFAULT_MAX_BACKLOG_BYTES: number;
|
|
38
|
+
export interface BacklogSlice {
|
|
39
|
+
/** `afterSeq` 뒤로 상한에 밀려 버려진 이벤트 수 */
|
|
40
|
+
dropped: number;
|
|
41
|
+
items: {
|
|
42
|
+
seq: number;
|
|
43
|
+
json: string;
|
|
44
|
+
}[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 바꾼 이벤트를 직렬화한 글자로 상한까지 들고 붙은 뷰어에 흘린다. 뷰어 서버는 이것만 안다.
|
|
48
|
+
* 내 대화 기록을 읽는 ShareStream과 원격 공유를 받아 옮기는 MirrorFeed가 이걸 물려받는다.
|
|
49
|
+
*
|
|
50
|
+
* seq는 공유가 연 뒤로 나온 이벤트의 번호다. 앞부분을 버려도 번호는 안 당긴다.
|
|
51
|
+
* 배열 위치로 매기면 버린 만큼 번호가 밀려 Last-Event-ID로 이어 받을 때 겹치거나 빠진다.
|
|
52
|
+
*/
|
|
53
|
+
export declare class ShareFeed {
|
|
54
|
+
readonly id: string;
|
|
55
|
+
/**
|
|
56
|
+
* `events[head]`부터가 살아 있다. 앞에서 하나씩 shift하면 큰 배열에서 매번 다 옮긴다.
|
|
57
|
+
* head 앞 칸은 버릴 때 비워 둔다. 압축은 head가 1024를 넘어야 돌아서 칸에 참조를
|
|
58
|
+
* 남기면 버린 이벤트를 GC가 못 가져간다.
|
|
59
|
+
*/
|
|
60
|
+
private events;
|
|
61
|
+
private head;
|
|
62
|
+
/** `events[0]` 앞에서 잘라 낸 이벤트 수. `events[i]`의 seq는 `base + i + 1`이다 */
|
|
63
|
+
private base;
|
|
64
|
+
private backlogBytes;
|
|
65
|
+
private readonly maxBacklogBytes;
|
|
66
|
+
private listeners;
|
|
67
|
+
protected status: ShareStatus;
|
|
68
|
+
private closedReason;
|
|
69
|
+
constructor(id: string, maxBacklogBytes?: number);
|
|
70
|
+
get currentStatus(): ShareStatus;
|
|
71
|
+
get unknownLines(): number;
|
|
72
|
+
get eventCount(): number;
|
|
73
|
+
/** 상한에 밀려 버린 이벤트 수 */
|
|
74
|
+
get droppedEvents(): number;
|
|
75
|
+
get viewerCount(): number;
|
|
76
|
+
get isClosed(): boolean;
|
|
77
|
+
get closeReason(): string | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* 쌓인 것 중 `afterSeq` 뒤를 `maxBytes`쯤까지 꺼낸다. 이벤트 하나가 상한보다 커도 하나는
|
|
80
|
+
* 꺼낸다. 뷰어가 소켓이 비는 만큼씩 앞부분을 보내려고 쓴다. 한 번에 다 넘기면 안 읽는
|
|
81
|
+
* 뷰어마다 앞부분 전체가 소켓 버퍼에 쌓인다.
|
|
82
|
+
*/
|
|
83
|
+
backlogAfter(afterSeq: number, maxBytes: number): BacklogSlice;
|
|
84
|
+
/**
|
|
85
|
+
* 뷰어를 붙인다. 지금 상태와 `afterSeq` 뒤 앞부분을 한 번에 보내고 이후는 생기는 대로.
|
|
86
|
+
* 떼어내는 함수를 돌려준다. 앞부분이 큰 뷰어는 backlogAfter로 먼저 따라잡은 뒤 이벤트
|
|
87
|
+
* 루프로 돌아가기 전에 바로 붙어야 사이에 poll이 끼어 이벤트가 빠지지 않는다.
|
|
88
|
+
*/
|
|
89
|
+
subscribe(listener: ShareListener, afterSeq?: number): () => void;
|
|
90
|
+
close(reason: string): void;
|
|
91
|
+
/** 이벤트 하나를 쌓고 흘린다. 상한 정리는 부른 쪽이 trim으로 한 번에 한다 */
|
|
92
|
+
protected pushEvent(json: string): void;
|
|
93
|
+
/**
|
|
94
|
+
* 들고 있던 걸 다 버리고 다음 이벤트 번호를 `seq + 1`부터 잇는다. 원격 공유에서 주인
|
|
95
|
+
* 쪽이 버린 구간을 건너뛸 때 쓴다. 버린 개수는 droppedEvents에 들어간다.
|
|
96
|
+
*/
|
|
97
|
+
protected skipTo(seq: number): void;
|
|
98
|
+
/** 가장 최근 이벤트 하나는 상한을 넘어도 남긴다. 다 버리면 새 뷰어가 지금 상태를 못 본다 */
|
|
99
|
+
protected trim(): void;
|
|
100
|
+
protected setStatus(next: ShareStatus): void;
|
|
101
|
+
protected emit(frame: ShareFrame): void;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=feed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feed.d.ts","sourceRoot":"","sources":["../../../src/peer/feed.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAC9C,2CAA2C;GACzC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE;AACxC;;;GAGG;GACD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,2CAA2C;AAC3C,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;AAEvE,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AAExD;;;GAGG;AACH,eAAO,MAAM,yBAAyB,QAAmB,CAAC;AAO1D,MAAM,WAAW,YAAY;IAC3B,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACxC;AAED;;;;;;GAMG;AACH,qBAAa,SAAS;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,IAAI,CAAK;IACjB,qEAAqE;IACrE,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,SAAS,CAA4B;IAC7C,SAAS,CAAC,MAAM,EAAE,WAAW,CAAU;IACvC,OAAO,CAAC,YAAY,CAAqB;gBAE7B,EAAE,EAAE,MAAM,EAAE,eAAe,SAA4B;IAKnE,IAAI,aAAa,IAAI,WAAW,CAE/B;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,sBAAsB;IACtB,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,IAAI,WAAW,IAAI,MAAM,GAAG,SAAS,CAEpC;IAED;;;;OAIG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY;IAe9D;;;;OAIG;IACH,SAAS,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,SAAI,GAAG,MAAM,IAAI;IAqB5D,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAY3B,kDAAkD;IAClD,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAOvC;;;OAGG;IACH,SAAS,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAQnC,yDAAyD;IACzD,SAAS,CAAC,IAAI,IAAI,IAAI;IAatB,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAM5C,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;CAWxC"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { errMessage } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* 새 뷰어에게 다시 보낼 앞부분의 상한. 넘으면 오래된 것부터 버린다. 공유는 최대 7일
|
|
4
|
+
* 열려 있고 도구 출력이 크면 events가 끝없이 는다.
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_MAX_BACKLOG_BYTES = 32 * 1024 * 1024;
|
|
7
|
+
/**
|
|
8
|
+
* 바꾼 이벤트를 직렬화한 글자로 상한까지 들고 붙은 뷰어에 흘린다. 뷰어 서버는 이것만 안다.
|
|
9
|
+
* 내 대화 기록을 읽는 ShareStream과 원격 공유를 받아 옮기는 MirrorFeed가 이걸 물려받는다.
|
|
10
|
+
*
|
|
11
|
+
* seq는 공유가 연 뒤로 나온 이벤트의 번호다. 앞부분을 버려도 번호는 안 당긴다.
|
|
12
|
+
* 배열 위치로 매기면 버린 만큼 번호가 밀려 Last-Event-ID로 이어 받을 때 겹치거나 빠진다.
|
|
13
|
+
*/
|
|
14
|
+
export class ShareFeed {
|
|
15
|
+
id;
|
|
16
|
+
/**
|
|
17
|
+
* `events[head]`부터가 살아 있다. 앞에서 하나씩 shift하면 큰 배열에서 매번 다 옮긴다.
|
|
18
|
+
* head 앞 칸은 버릴 때 비워 둔다. 압축은 head가 1024를 넘어야 돌아서 칸에 참조를
|
|
19
|
+
* 남기면 버린 이벤트를 GC가 못 가져간다.
|
|
20
|
+
*/
|
|
21
|
+
events = [];
|
|
22
|
+
head = 0;
|
|
23
|
+
/** `events[0]` 앞에서 잘라 낸 이벤트 수. `events[i]`의 seq는 `base + i + 1`이다 */
|
|
24
|
+
base = 0;
|
|
25
|
+
backlogBytes = 0;
|
|
26
|
+
maxBacklogBytes;
|
|
27
|
+
listeners = new Set();
|
|
28
|
+
status = 'live';
|
|
29
|
+
closedReason;
|
|
30
|
+
constructor(id, maxBacklogBytes = DEFAULT_MAX_BACKLOG_BYTES) {
|
|
31
|
+
this.id = id;
|
|
32
|
+
this.maxBacklogBytes = maxBacklogBytes;
|
|
33
|
+
}
|
|
34
|
+
get currentStatus() {
|
|
35
|
+
return this.status;
|
|
36
|
+
}
|
|
37
|
+
get unknownLines() {
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
get eventCount() {
|
|
41
|
+
return this.base + this.events.length;
|
|
42
|
+
}
|
|
43
|
+
/** 상한에 밀려 버린 이벤트 수 */
|
|
44
|
+
get droppedEvents() {
|
|
45
|
+
return this.base + this.head;
|
|
46
|
+
}
|
|
47
|
+
get viewerCount() {
|
|
48
|
+
return this.listeners.size;
|
|
49
|
+
}
|
|
50
|
+
get isClosed() {
|
|
51
|
+
return this.closedReason !== undefined;
|
|
52
|
+
}
|
|
53
|
+
get closeReason() {
|
|
54
|
+
return this.closedReason;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 쌓인 것 중 `afterSeq` 뒤를 `maxBytes`쯤까지 꺼낸다. 이벤트 하나가 상한보다 커도 하나는
|
|
58
|
+
* 꺼낸다. 뷰어가 소켓이 비는 만큼씩 앞부분을 보내려고 쓴다. 한 번에 다 넘기면 안 읽는
|
|
59
|
+
* 뷰어마다 앞부분 전체가 소켓 버퍼에 쌓인다.
|
|
60
|
+
*/
|
|
61
|
+
backlogAfter(afterSeq, maxBytes) {
|
|
62
|
+
if (this.closedReason !== undefined)
|
|
63
|
+
return { dropped: 0, items: [] };
|
|
64
|
+
const droppedTotal = this.droppedEvents;
|
|
65
|
+
const after = Math.max(0, afterSeq);
|
|
66
|
+
const items = [];
|
|
67
|
+
let bytes = 0;
|
|
68
|
+
for (let i = Math.max(after, droppedTotal) - this.base; i < this.events.length; i++) {
|
|
69
|
+
const held = this.events[i];
|
|
70
|
+
if (items.length > 0 && bytes + held.bytes > maxBytes)
|
|
71
|
+
break;
|
|
72
|
+
items.push({ seq: this.base + i + 1, json: held.json });
|
|
73
|
+
bytes += held.bytes;
|
|
74
|
+
}
|
|
75
|
+
return { dropped: after < droppedTotal ? droppedTotal - after : 0, items };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 뷰어를 붙인다. 지금 상태와 `afterSeq` 뒤 앞부분을 한 번에 보내고 이후는 생기는 대로.
|
|
79
|
+
* 떼어내는 함수를 돌려준다. 앞부분이 큰 뷰어는 backlogAfter로 먼저 따라잡은 뒤 이벤트
|
|
80
|
+
* 루프로 돌아가기 전에 바로 붙어야 사이에 poll이 끼어 이벤트가 빠지지 않는다.
|
|
81
|
+
*/
|
|
82
|
+
subscribe(listener, afterSeq = 0) {
|
|
83
|
+
listener({ type: 'state', status: this.status });
|
|
84
|
+
listener({ type: 'stats', unknownLines: this.unknownLines });
|
|
85
|
+
const { dropped, items } = this.backlogAfter(afterSeq, Infinity);
|
|
86
|
+
if (dropped > 0)
|
|
87
|
+
listener({ type: 'trimmed', dropped });
|
|
88
|
+
for (const item of items)
|
|
89
|
+
listener({ type: 'event', ...item });
|
|
90
|
+
if (this.closedReason !== undefined) {
|
|
91
|
+
// close가 events를 비워도 base를 늘려 두므로 eventCount는 그때까지 나온 전체 개수다
|
|
92
|
+
listener({
|
|
93
|
+
type: 'closed',
|
|
94
|
+
reason: this.closedReason,
|
|
95
|
+
missed: Math.max(0, this.eventCount - afterSeq),
|
|
96
|
+
});
|
|
97
|
+
return () => { };
|
|
98
|
+
}
|
|
99
|
+
this.listeners.add(listener);
|
|
100
|
+
return () => {
|
|
101
|
+
this.listeners.delete(listener);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
close(reason) {
|
|
105
|
+
if (this.closedReason !== undefined)
|
|
106
|
+
return;
|
|
107
|
+
this.closedReason = reason;
|
|
108
|
+
this.emit({ type: 'closed', reason });
|
|
109
|
+
this.listeners.clear();
|
|
110
|
+
// 종료된 공유의 내용을 메모리에 붙잡아 둘 이유가 없다
|
|
111
|
+
this.base += this.events.length;
|
|
112
|
+
this.events = [];
|
|
113
|
+
this.head = 0;
|
|
114
|
+
this.backlogBytes = 0;
|
|
115
|
+
}
|
|
116
|
+
/** 이벤트 하나를 쌓고 흘린다. 상한 정리는 부른 쪽이 trim으로 한 번에 한다 */
|
|
117
|
+
pushEvent(json) {
|
|
118
|
+
const bytes = Buffer.byteLength(json);
|
|
119
|
+
this.events.push({ json, bytes });
|
|
120
|
+
this.backlogBytes += bytes;
|
|
121
|
+
this.emit({ type: 'event', seq: this.eventCount, json });
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 들고 있던 걸 다 버리고 다음 이벤트 번호를 `seq + 1`부터 잇는다. 원격 공유에서 주인
|
|
125
|
+
* 쪽이 버린 구간을 건너뛸 때 쓴다. 버린 개수는 droppedEvents에 들어간다.
|
|
126
|
+
*/
|
|
127
|
+
skipTo(seq) {
|
|
128
|
+
if (seq <= this.eventCount)
|
|
129
|
+
return;
|
|
130
|
+
this.base = seq;
|
|
131
|
+
this.events = [];
|
|
132
|
+
this.head = 0;
|
|
133
|
+
this.backlogBytes = 0;
|
|
134
|
+
}
|
|
135
|
+
/** 가장 최근 이벤트 하나는 상한을 넘어도 남긴다. 다 버리면 새 뷰어가 지금 상태를 못 본다 */
|
|
136
|
+
trim() {
|
|
137
|
+
while (this.backlogBytes > this.maxBacklogBytes && this.events.length - this.head > 1) {
|
|
138
|
+
this.backlogBytes -= this.events[this.head].bytes;
|
|
139
|
+
this.events[this.head] = undefined;
|
|
140
|
+
this.head++;
|
|
141
|
+
}
|
|
142
|
+
if (this.head > 1024 && this.head * 2 > this.events.length) {
|
|
143
|
+
this.base += this.head;
|
|
144
|
+
this.events = this.events.slice(this.head);
|
|
145
|
+
this.head = 0;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
setStatus(next) {
|
|
149
|
+
if (next === this.status)
|
|
150
|
+
return;
|
|
151
|
+
this.status = next;
|
|
152
|
+
this.emit({ type: 'state', status: next });
|
|
153
|
+
}
|
|
154
|
+
emit(frame) {
|
|
155
|
+
for (const listener of [...this.listeners]) {
|
|
156
|
+
try {
|
|
157
|
+
listener(frame);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
// 뷰어 하나가 끊겨 쓰기에 실패해도 다른 뷰어와 데몬은 계속 돈다
|
|
161
|
+
this.listeners.delete(listener);
|
|
162
|
+
process.stderr.write(`[peer] viewer listener failed: ${errMessage(err)}\n`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=feed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feed.js","sourceRoot":"","sources":["../../../src/peer/feed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAuBzC;;;GAGG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAa1D;;;;;;GAMG;AACH,MAAM,OAAO,SAAS;IACX,EAAE,CAAS;IAEpB;;;;OAIG;IACK,MAAM,GAAyB,EAAE,CAAC;IAClC,IAAI,GAAG,CAAC,CAAC;IACjB,qEAAqE;IAC7D,IAAI,GAAG,CAAC,CAAC;IACT,YAAY,GAAG,CAAC,CAAC;IACR,eAAe,CAAS;IACjC,SAAS,GAAG,IAAI,GAAG,EAAiB,CAAC;IACnC,MAAM,GAAgB,MAAM,CAAC;IAC/B,YAAY,CAAqB;IAEzC,YAAY,EAAU,EAAE,eAAe,GAAG,yBAAyB;QACjE,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;IACzC,CAAC;IAED,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,IAAI,YAAY;QACd,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IACxC,CAAC;IAED,sBAAsB;IACtB,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IAC/B,CAAC;IAED,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;IAC7B,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,YAAY,KAAK,SAAS,CAAC;IACzC,CAAC;IAED,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED;;;;OAIG;IACH,YAAY,CAAC,QAAgB,EAAE,QAAgB;QAC7C,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;YAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;QACtE,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,CAAC;QACxC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QACpC,MAAM,KAAK,GAA0B,EAAE,CAAC;QACxC,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,YAAY,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACpF,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC;YAC7B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,QAAQ;gBAAE,MAAM;YAC7D,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YACxD,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC;QACtB,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,YAAY,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;IAC7E,CAAC;IAED;;;;OAIG;IACH,SAAS,CAAC,QAAuB,EAAE,QAAQ,GAAG,CAAC;QAC7C,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QACjD,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;QAC7D,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QACjE,IAAI,OAAO,GAAG,CAAC;YAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;QACxD,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;QAC/D,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;YACpC,6DAA6D;YAC7D,QAAQ,CAAC;gBACP,IAAI,EAAE,QAAQ;gBACd,MAAM,EAAE,IAAI,CAAC,YAAY;gBACzB,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAC;aAChD,CAAC,CAAC;YACH,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;QAClB,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,MAAc;QAClB,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;YAAE,OAAO;QAC5C,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QACtC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QACvB,gCAAgC;QAChC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;QAChC,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;IACxB,CAAC;IAED,kDAAkD;IACxC,SAAS,CAAC,IAAY;QAC9B,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAClC,IAAI,CAAC,YAAY,IAAI,KAAK,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED;;;OAGG;IACO,MAAM,CAAC,GAAW;QAC1B,IAAI,GAAG,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QACnC,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;QAChB,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;IACxB,CAAC;IAED,yDAAyD;IAC/C,IAAI;QACZ,OAAO,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YACtF,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAE,CAAC,KAAK,CAAC;YACnD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC;YACnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,IAAI,CAAC,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YAC3D,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC;YACvB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3C,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QAChB,CAAC;IACH,CAAC;IAES,SAAS,CAAC,IAAiB;QACnC,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM;YAAE,OAAO;QACjC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IAES,IAAI,CAAC,KAAiB;QAC9B,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3C,IAAI,CAAC;gBACH,QAAQ,CAAC,KAAK,CAAC,CAAC;YAClB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,sCAAsC;gBACtC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kCAAkC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9E,CAAC;QACH,CAAC;IACH,CAAC;CACF"}
|
package/dist/src/peer/index.d.ts
CHANGED
|
@@ -11,4 +11,14 @@ export { TranscriptTransformer, stripSystemReminders } from './transform.js';
|
|
|
11
11
|
export type { PeerEvent } from './transform.js';
|
|
12
12
|
export { PeerState } from './state.js';
|
|
13
13
|
export type { DaemonStatus, PeerRequest, PeerResponse, ShareInfo } from './protocol.js';
|
|
14
|
+
export { RelayServer } from './relay.js';
|
|
15
|
+
export type { RelayServerOptions } from './relay.js';
|
|
16
|
+
export { RelayTransport, RelaySendError } from './transport.js';
|
|
17
|
+
export type { PeerTransport, TransportState } from './transport.js';
|
|
18
|
+
export { PeerNetwork } from './network.js';
|
|
19
|
+
export type { MemberInfo, TeamStatus } from './network.js';
|
|
20
|
+
export { loadOrCreateMachineKey, machineKeyPath } from './keys.js';
|
|
21
|
+
export type { MachineKey } from './keys.js';
|
|
22
|
+
export { checkRosterUpdate, formatShareLink, parseShareLink } from './roster.js';
|
|
23
|
+
export type { Roster, RosterMember, SignedRoster } from './roster.js';
|
|
14
24
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/peer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EACL,eAAe,EACf,OAAO,EACP,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC7F,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC7E,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/peer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EACL,eAAe,EACf,OAAO,EACP,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC7F,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC7E,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACxF,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACnE,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACjF,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/src/peer/index.js
CHANGED
|
@@ -6,4 +6,9 @@ export { redactText, redactValue, truncateBytes, isToolsLevel, REDACTED } from '
|
|
|
6
6
|
export { TranscriptTail } from './tail.js';
|
|
7
7
|
export { TranscriptTransformer, stripSystemReminders } from './transform.js';
|
|
8
8
|
export { PeerState } from './state.js';
|
|
9
|
+
export { RelayServer } from './relay.js';
|
|
10
|
+
export { RelayTransport, RelaySendError } from './transport.js';
|
|
11
|
+
export { PeerNetwork } from './network.js';
|
|
12
|
+
export { loadOrCreateMachineKey, machineKeyPath } from './keys.js';
|
|
13
|
+
export { checkRosterUpdate, formatShareLink, parseShareLink } from './roster.js';
|
|
9
14
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/peer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EACL,eAAe,EACf,OAAO,EACP,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE7F,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE7E,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/peer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EACL,eAAe,EACf,OAAO,EACP,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE7F,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE7E,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEhE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEnE,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { KeyObject } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* 기기 키. Ed25519로 서명하고 X25519로 봉인을 연다. 이 기기를 가리키는 이름(`id`)은
|
|
4
|
+
* Ed25519 공개키 32바이트를 base64url로 쓴 43자다.
|
|
5
|
+
*/
|
|
6
|
+
export interface MachineKey {
|
|
7
|
+
id: string;
|
|
8
|
+
/** X25519 공개키 32바이트 base64url */
|
|
9
|
+
boxKey: string;
|
|
10
|
+
signPrivate: KeyObject;
|
|
11
|
+
boxPrivate: KeyObject;
|
|
12
|
+
}
|
|
13
|
+
export declare function machineKeyPath(root: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* 키 파일을 읽고 없으면 만든다. 파일은 만드는 순간부터 0600이다 — 쓰고 나서 chmod하면
|
|
16
|
+
* 그 사이에 기본 권한(umask)으로 열려 있다.
|
|
17
|
+
*
|
|
18
|
+
* 권한이 넓거나 심링크거나 남의 파일이면 읽지 않고 던진다. 고치는 건 사람 몫이다.
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadOrCreateMachineKey(root: string): MachineKey;
|
|
21
|
+
export declare function loadMachineKey(path: string): MachineKey;
|
|
22
|
+
/** 32바이트 공개키를 base64url로 쓴 꼴인지 */
|
|
23
|
+
export declare function isRawKey(value: unknown): value is string;
|
|
24
|
+
export declare function signPublicKey(id: string): KeyObject;
|
|
25
|
+
export declare function boxPublicKey(raw: string): KeyObject;
|
|
26
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../../src/peer/keys.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAc,SAAS,EAAE,MAAM,aAAa,CAAC;AAIzD;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,SAAS,CAAC;IACvB,UAAU,EAAE,SAAS,CAAC;CACvB;AAQD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAyB/D;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAmBvD;AAwBD,kCAAkC;AAClC,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAExD;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAEnD;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAEnD"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync } from 'node:crypto';
|
|
2
|
+
import { closeSync, fsyncSync, lstatSync, openSync, readFileSync, writeSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export function machineKeyPath(root) {
|
|
5
|
+
return join(root, 'machine.key');
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 키 파일을 읽고 없으면 만든다. 파일은 만드는 순간부터 0600이다 — 쓰고 나서 chmod하면
|
|
9
|
+
* 그 사이에 기본 권한(umask)으로 열려 있다.
|
|
10
|
+
*
|
|
11
|
+
* 권한이 넓거나 심링크거나 남의 파일이면 읽지 않고 던진다. 고치는 건 사람 몫이다.
|
|
12
|
+
*/
|
|
13
|
+
export function loadOrCreateMachineKey(root) {
|
|
14
|
+
const path = machineKeyPath(root);
|
|
15
|
+
try {
|
|
16
|
+
return loadMachineKey(path);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
if (err.code !== 'ENOENT')
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
const sign = generateKeyPairSync('ed25519').privateKey.export({ format: 'jwk' });
|
|
23
|
+
const box = generateKeyPairSync('x25519').privateKey.export({ format: 'jwk' });
|
|
24
|
+
const body = { v: 1, sign, box };
|
|
25
|
+
let fd;
|
|
26
|
+
try {
|
|
27
|
+
fd = openSync(path, 'wx', 0o600);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
// 다른 데몬이 먼저 만들었다. 그쪽 키를 쓴다
|
|
31
|
+
if (err.code === 'EEXIST')
|
|
32
|
+
return loadMachineKey(path);
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
writeSync(fd, JSON.stringify(body) + '\n');
|
|
37
|
+
fsyncSync(fd);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
closeSync(fd);
|
|
41
|
+
}
|
|
42
|
+
return fromFile(body, path);
|
|
43
|
+
}
|
|
44
|
+
export function loadMachineKey(path) {
|
|
45
|
+
const st = lstatSync(path);
|
|
46
|
+
if (st.isSymbolicLink())
|
|
47
|
+
throw new Error(`기기 키가 심링크예요: ${path}`);
|
|
48
|
+
if (!st.isFile())
|
|
49
|
+
throw new Error(`기기 키 자리에 다른 파일이 있어요: ${path}`);
|
|
50
|
+
const uid = process.getuid?.();
|
|
51
|
+
if (uid !== undefined && st.uid !== uid)
|
|
52
|
+
throw new Error(`기기 키의 주인이 내가 아니에요: ${path}`);
|
|
53
|
+
if ((st.mode & 0o077) !== 0) {
|
|
54
|
+
throw new Error(`기기 키 권한이 넓어요(${(st.mode & 0o777).toString(8)}). chmod 600 ${path} 뒤에 다시 띄워 주세요`);
|
|
55
|
+
}
|
|
56
|
+
let body;
|
|
57
|
+
try {
|
|
58
|
+
body = JSON.parse(readFileSync(path, 'utf8'));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new Error(`기기 키 파일을 못 읽었어요: ${path}`);
|
|
62
|
+
}
|
|
63
|
+
return fromFile(body, path);
|
|
64
|
+
}
|
|
65
|
+
function fromFile(body, path) {
|
|
66
|
+
if (body.v !== 1 || body.sign?.crv !== 'Ed25519' || body.box?.crv !== 'X25519') {
|
|
67
|
+
throw new Error(`기기 키 형식이 아니에요: ${path}`);
|
|
68
|
+
}
|
|
69
|
+
const signPrivate = createPrivateKey({ key: body.sign, format: 'jwk' });
|
|
70
|
+
const boxPrivate = createPrivateKey({ key: body.box, format: 'jwk' });
|
|
71
|
+
return {
|
|
72
|
+
id: rawPublic(signPrivate),
|
|
73
|
+
boxKey: rawPublic(boxPrivate),
|
|
74
|
+
signPrivate,
|
|
75
|
+
boxPrivate,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function rawPublic(priv) {
|
|
79
|
+
const jwk = createPublicKey(priv).export({ format: 'jwk' });
|
|
80
|
+
if (typeof jwk.x !== 'string')
|
|
81
|
+
throw new Error('공개키를 못 꺼냈어요');
|
|
82
|
+
return jwk.x;
|
|
83
|
+
}
|
|
84
|
+
const RAW_KEY = /^[A-Za-z0-9_-]{43}$/;
|
|
85
|
+
/** 32바이트 공개키를 base64url로 쓴 꼴인지 */
|
|
86
|
+
export function isRawKey(value) {
|
|
87
|
+
return typeof value === 'string' && RAW_KEY.test(value);
|
|
88
|
+
}
|
|
89
|
+
export function signPublicKey(id) {
|
|
90
|
+
return createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: id }, format: 'jwk' });
|
|
91
|
+
}
|
|
92
|
+
export function boxPublicKey(raw) {
|
|
93
|
+
return createPublicKey({ key: { kty: 'OKP', crv: 'X25519', x: raw }, format: 'jwk' });
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../../../src/peer/keys.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
|