virlen-remote 0.1.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +192 -0
  4. package/dist/index.d.ts +44 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +34 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/protocol/answer.d.ts +48 -0
  9. package/dist/protocol/answer.d.ts.map +1 -0
  10. package/dist/protocol/answer.js +65 -0
  11. package/dist/protocol/answer.js.map +1 -0
  12. package/dist/protocol/api.d.ts +576 -0
  13. package/dist/protocol/api.d.ts.map +1 -0
  14. package/dist/protocol/api.js +38 -0
  15. package/dist/protocol/api.js.map +1 -0
  16. package/dist/protocol/endpoint.d.ts +76 -0
  17. package/dist/protocol/endpoint.d.ts.map +1 -0
  18. package/dist/protocol/endpoint.js +307 -0
  19. package/dist/protocol/endpoint.js.map +1 -0
  20. package/dist/protocol/errors.d.ts +32 -0
  21. package/dist/protocol/errors.d.ts.map +1 -0
  22. package/dist/protocol/errors.js +56 -0
  23. package/dist/protocol/errors.js.map +1 -0
  24. package/dist/protocol/frame.d.ts +51 -0
  25. package/dist/protocol/frame.d.ts.map +1 -0
  26. package/dist/protocol/frame.js +154 -0
  27. package/dist/protocol/frame.js.map +1 -0
  28. package/dist/protocol/hello.d.ts +73 -0
  29. package/dist/protocol/hello.d.ts.map +1 -0
  30. package/dist/protocol/hello.js +37 -0
  31. package/dist/protocol/hello.js.map +1 -0
  32. package/dist/protocol/host.d.ts +116 -0
  33. package/dist/protocol/host.d.ts.map +1 -0
  34. package/dist/protocol/host.js +52 -0
  35. package/dist/protocol/host.js.map +1 -0
  36. package/dist/protocol/identity.d.ts +107 -0
  37. package/dist/protocol/identity.d.ts.map +1 -0
  38. package/dist/protocol/identity.js +143 -0
  39. package/dist/protocol/identity.js.map +1 -0
  40. package/dist/protocol/ids.d.ts +2 -0
  41. package/dist/protocol/ids.d.ts.map +1 -0
  42. package/dist/protocol/ids.js +20 -0
  43. package/dist/protocol/ids.js.map +1 -0
  44. package/dist/protocol/pairing.d.ts +43 -0
  45. package/dist/protocol/pairing.d.ts.map +1 -0
  46. package/dist/protocol/pairing.js +78 -0
  47. package/dist/protocol/pairing.js.map +1 -0
  48. package/dist/testing/index.d.ts +6 -0
  49. package/dist/testing/index.d.ts.map +1 -0
  50. package/dist/testing/index.js +5 -0
  51. package/dist/testing/index.js.map +1 -0
  52. package/dist/testing/mock-host.d.ts +66 -0
  53. package/dist/testing/mock-host.d.ts.map +1 -0
  54. package/dist/testing/mock-host.js +573 -0
  55. package/dist/testing/mock-host.js.map +1 -0
  56. package/dist/transport/broadcast.d.ts +26 -0
  57. package/dist/transport/broadcast.d.ts.map +1 -0
  58. package/dist/transport/broadcast.js +71 -0
  59. package/dist/transport/broadcast.js.map +1 -0
  60. package/dist/transport/ice.d.ts +130 -0
  61. package/dist/transport/ice.d.ts.map +1 -0
  62. package/dist/transport/ice.js +338 -0
  63. package/dist/transport/ice.js.map +1 -0
  64. package/dist/transport/memory.d.ts +52 -0
  65. package/dist/transport/memory.d.ts.map +1 -0
  66. package/dist/transport/memory.js +135 -0
  67. package/dist/transport/memory.js.map +1 -0
  68. package/dist/transport/rtc.d.ts +76 -0
  69. package/dist/transport/rtc.d.ts.map +1 -0
  70. package/dist/transport/rtc.js +310 -0
  71. package/dist/transport/rtc.js.map +1 -0
  72. package/dist/transport/signaling.d.ts +101 -0
  73. package/dist/transport/signaling.d.ts.map +1 -0
  74. package/dist/transport/signaling.js +235 -0
  75. package/dist/transport/signaling.js.map +1 -0
  76. package/dist/transport/types.d.ts +41 -0
  77. package/dist/transport/types.d.ts.map +1 -0
  78. package/dist/transport/types.js +11 -0
  79. package/dist/transport/types.js.map +1 -0
  80. package/package.json +69 -0
  81. package/src/index.ts +155 -0
  82. package/src/protocol/answer.ts +89 -0
  83. package/src/protocol/api.ts +517 -0
  84. package/src/protocol/endpoint.ts +405 -0
  85. package/src/protocol/errors.ts +90 -0
  86. package/src/protocol/frame.ts +196 -0
  87. package/src/protocol/hello.ts +112 -0
  88. package/src/protocol/host.ts +150 -0
  89. package/src/protocol/identity.ts +184 -0
  90. package/src/protocol/ids.ts +20 -0
  91. package/src/protocol/pairing.ts +95 -0
  92. package/src/testing/index.ts +5 -0
  93. package/src/testing/mock-host.ts +691 -0
  94. package/src/transport/broadcast.ts +81 -0
  95. package/src/transport/ice.ts +402 -0
  96. package/src/transport/memory.ts +164 -0
  97. package/src/transport/rtc.ts +344 -0
  98. package/src/transport/signaling.ts +317 -0
  99. package/src/transport/types.ts +50 -0
@@ -0,0 +1,135 @@
1
+ export class MemoryTransport {
2
+ constructor() {
3
+ this._state = 'connecting';
4
+ /** 对端;由 `createMemoryPair()` 建立双向链接。 */
5
+ this.peer = null;
6
+ this.messageListeners = new Set();
7
+ this.stateListeners = new Set();
8
+ this.metrics = { sent: 0, received: 0, dropped: 0 };
9
+ /** 随机丢包率 0..1(测试用)。 */
10
+ this.lossRate = 0;
11
+ /** 确定性丢包:>0 时下一次 send 丢弃该帧并自减。 */
12
+ this.dropNextOutgoing = 0;
13
+ /** 确定性丢包:>0 时下一次投递丢弃该帧并自减。 */
14
+ this.dropNextIncoming = 0;
15
+ }
16
+ get state() {
17
+ return this._state;
18
+ }
19
+ get bufferedAmount() {
20
+ return 0;
21
+ }
22
+ /** 累计统计(测试断言用)。 */
23
+ get stats() {
24
+ return { ...this.metrics };
25
+ }
26
+ send(bytes) {
27
+ this.metrics.sent++;
28
+ if (this._state !== 'open' || !this.peer) {
29
+ this.metrics.dropped++;
30
+ return;
31
+ }
32
+ if (this.dropNextOutgoing > 0) {
33
+ this.dropNextOutgoing--;
34
+ this.metrics.dropped++;
35
+ return;
36
+ }
37
+ if (this.lossRate > 0 && Math.random() < this.lossRate) {
38
+ this.metrics.dropped++;
39
+ return;
40
+ }
41
+ this.peer.deliver(bytes);
42
+ }
43
+ close() {
44
+ this.setState('closed');
45
+ }
46
+ onMessage(listener) {
47
+ this.messageListeners.add(listener);
48
+ return () => {
49
+ this.messageListeners.delete(listener);
50
+ };
51
+ }
52
+ onStateChange(listener) {
53
+ this.stateListeners.add(listener);
54
+ return () => {
55
+ this.stateListeners.delete(listener);
56
+ };
57
+ }
58
+ // ───────────────────────── 测试辅助 ─────────────────────────
59
+ open() {
60
+ this.setState('open');
61
+ }
62
+ /** 模拟链路断开(双向,原子:先写两端状态再通知)。 */
63
+ disconnect() {
64
+ this.transition('closed');
65
+ }
66
+ /** 模拟链路恢复(双向,原子)。 */
67
+ reconnect() {
68
+ this.transition('open');
69
+ }
70
+ /** 让接下来 n 次 send 静默丢弃(验证幂等/重放)。 */
71
+ dropOutgoing(n) {
72
+ this.dropNextOutgoing = n;
73
+ }
74
+ /** 让接下来 n 次投递静默丢弃(验证超时/重放)。 */
75
+ dropIncoming(n) {
76
+ this.dropNextIncoming = n;
77
+ }
78
+ deliver(bytes) {
79
+ if (this._state !== 'open') {
80
+ this.metrics.dropped++;
81
+ return;
82
+ }
83
+ if (this.dropNextIncoming > 0) {
84
+ this.dropNextIncoming--;
85
+ this.metrics.dropped++;
86
+ return;
87
+ }
88
+ // 异步投递:模拟真实链路的异步边界;queueMicrotask 保证先进先出(顺序性)
89
+ queueMicrotask(() => {
90
+ if (this._state !== 'open')
91
+ return;
92
+ this.metrics.received++;
93
+ for (const listener of this.messageListeners)
94
+ listener(bytes);
95
+ });
96
+ }
97
+ transition(state) {
98
+ const changed = [];
99
+ if (this._state !== state) {
100
+ this._state = state;
101
+ changed.push(this);
102
+ }
103
+ const peer = this.peer;
104
+ if (peer && peer._state !== state) {
105
+ peer._state = state;
106
+ changed.push(peer);
107
+ }
108
+ // 关键:**先把两端状态写完,再逐个通知**。
109
+ // 否则「重放」的一侧会在通知回调里立即发送,而对端此刻仍是 closed
110
+ // → 帧被 deliver() 静默丢弃,重放失效(真实链路恢复时同样会踩到)。
111
+ for (const t of changed)
112
+ t.notifyState();
113
+ }
114
+ notifyState() {
115
+ for (const listener of [...this.stateListeners])
116
+ listener(this._state);
117
+ }
118
+ setState(state) {
119
+ if (this._state === state)
120
+ return;
121
+ this._state = state;
122
+ this.notifyState();
123
+ }
124
+ }
125
+ /** 建立一对互连的 transport(双方初始即 open)。 */
126
+ export function createMemoryPair() {
127
+ const a = new MemoryTransport();
128
+ const b = new MemoryTransport();
129
+ a.peer = b;
130
+ b.peer = a;
131
+ a.open();
132
+ b.open();
133
+ return [a, b];
134
+ }
135
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/transport/memory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * RTC transport —— 真实链路(WebRTC DataChannel)。
3
+ *
4
+ * 只依赖两个注入点,因此**可单测**(不需要真实 WebRTC):
5
+ * - `signaling`:信令通道(`SignalingChannel`,M3 用 SSE 实现;测试用环回实现)
6
+ * - `createPeerConnection`:`RTCPeerConnection` 工厂(测试用假实现)
7
+ *
8
+ * 角色(见 §8 拍板):**电脑(host) 发起 offer**,手机(guest) 应答。
9
+ * 电脑常驻房间,手机加入后由电脑发 offer —— 手机可能在后台切换,桌面是稳定进程。
10
+ *
11
+ * 状态映射:`connected → open`;`failed/closed → closed`;`disconnected → connecting`(等重连)。
12
+ */
13
+ import type { Transport, TransportState } from './types.js';
14
+ import type { SignalingChannel, SignalingRole } from './signaling.js';
15
+ import type { IceServerInit } from './ice.js';
16
+ export interface RtcTransportOptions {
17
+ role: SignalingRole;
18
+ signaling: SignalingChannel;
19
+ /**
20
+ * ICE 服务器列表。**不再有内置默认值**(M7,§31):两端都由
21
+ * `resolveIceServers()` 决定(自定义 > 服务端下发 > 缓存 > 空)。
22
+ */
23
+ iceServers?: IceServerInit[];
24
+ /** 数据通道 label(对两端须一致)。 */
25
+ channelLabel?: string;
26
+ /** 注入 `RTCPeerConnection` 工厂(测试用;默认取全局构造器)。 */
27
+ createPeerConnection?: (config: RTCConfiguration) => RTCPeerConnection;
28
+ }
29
+ export declare class RtcTransport implements Transport {
30
+ private readonly options;
31
+ private _state;
32
+ private readonly messageListeners;
33
+ private readonly stateListeners;
34
+ private pc;
35
+ private dc;
36
+ private pendingCandidates;
37
+ private started;
38
+ /** 被顶号(致命):置位后一切重建/重连动作都无意义。 */
39
+ private replaced;
40
+ private readonly readyPromise;
41
+ private readyResolve;
42
+ private readyReject;
43
+ constructor(options: RtcTransportOptions);
44
+ get state(): TransportState;
45
+ get bufferedAmount(): number;
46
+ /** 链路真正可用的 Promise(DataChannel open 时 resolve);失败/关闭时 reject。 */
47
+ whenReady(): Promise<void>;
48
+ /** 加入信令房间并开始协商。使用方创建后调用一次(不需要 await,靠状态/`whenReady` 驱动)。 */
49
+ start(): Promise<void>;
50
+ send(bytes: Uint8Array): void;
51
+ close(): void;
52
+ onMessage(listener: (bytes: Uint8Array) => void): () => void;
53
+ onStateChange(listener: (state: TransportState) => void): () => void;
54
+ private onPeer;
55
+ /**
56
+ * 被服务端顶号(同一台电脑已有别的手机接入)。
57
+ *
58
+ * 顺序是刻意的:**先把错误告知上层,再改状态**。上层(手机端 `connectionStore`)在
59
+ * 看到 `closed` 时会安排自动重连 —— 而重连会把刚接入的那台手机再顶掉(乒乓)。
60
+ * 所以「被顶号」这个事实必须先于状态变化到达。
61
+ */
62
+ private onKicked;
63
+ private ensurePC;
64
+ private startOffer;
65
+ private handleRemote;
66
+ private flushCandidates;
67
+ private setupDataChannel;
68
+ private onConnectionState;
69
+ private teardownPeer;
70
+ private fail;
71
+ private onErrorListeners;
72
+ /** 订阅「链路级错误」(区别于状态变化);返回取消订阅函数。 */
73
+ onError(listener: (error: Error) => void): () => void;
74
+ private setState;
75
+ }
76
+ //# sourceMappingURL=rtc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rtc.d.ts","sourceRoot":"","sources":["../../src/transport/rtc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACxD,OAAO,KAAK,EAAc,gBAAgB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC9E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAG1C,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,aAAa,CAAA;IACnB,SAAS,EAAE,gBAAgB,CAAA;IAC3B;;;OAGG;IACH,UAAU,CAAC,EAAE,aAAa,EAAE,CAAA;IAC5B,0BAA0B;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,+CAA+C;IAC/C,oBAAoB,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,iBAAiB,CAAA;CACvE;AAOD,qBAAa,YAAa,YAAW,SAAS;IAgBhC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAfpC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAyC;IAC1E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6C;IAE5E,OAAO,CAAC,EAAE,CAAiC;IAC3C,OAAO,CAAC,EAAE,CAA8B;IACxC,OAAO,CAAC,iBAAiB,CAA4B;IACrD,OAAO,CAAC,OAAO,CAAQ;IACvB,gCAAgC;IAChC,OAAO,CAAC,QAAQ,CAAQ;IAExB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,YAAY,CAA4B;IAChD,OAAO,CAAC,WAAW,CAAwC;IAE3D,YAA6B,OAAO,EAAE,mBAAmB,EAYxD;IAED,IAAI,KAAK,IAAI,cAAc,CAE1B;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,iEAAiE;IACjE,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAEzB;IAED,4DAA4D;IACtD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ3B;IAED,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAM5B;IAED,KAAK,IAAI,IAAI,CAUZ;IAED,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI,CAK3D;IAED,aAAa,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,MAAM,IAAI,CAKnE;IAID,OAAO,CAAC,MAAM;IAgBd;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ;IAkBhB,OAAO,CAAC,QAAQ;YAeF,UAAU;YAeV,YAAY;YAkCZ,eAAe;IAU7B,OAAO,CAAC,gBAAgB;IAsBxB,OAAO,CAAC,iBAAiB;IAsBzB,OAAO,CAAC,YAAY;IAgBpB,OAAO,CAAC,IAAI;IAKZ,OAAO,CAAC,gBAAgB,CAAwC;IAEhE,mCAAmC;IACnC,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAKpD;IAED,OAAO,CAAC,QAAQ;CAKjB"}
@@ -0,0 +1,310 @@
1
+ import { BridgeError, toBridgeError } from '../protocol/errors.js';
2
+ export class RtcTransport {
3
+ constructor(options) {
4
+ this.options = options;
5
+ this._state = 'connecting';
6
+ this.messageListeners = new Set();
7
+ this.stateListeners = new Set();
8
+ this.pc = null;
9
+ this.dc = null;
10
+ this.pendingCandidates = [];
11
+ this.started = false;
12
+ /** 被顶号(致命):置位后一切重建/重连动作都无意义。 */
13
+ this.replaced = false;
14
+ this.readyResolve = null;
15
+ this.readyReject = null;
16
+ this.onErrorListeners = null;
17
+ this.readyPromise = new Promise((resolve, reject) => {
18
+ this.readyResolve = resolve;
19
+ this.readyReject = reject;
20
+ });
21
+ const sig = options.signaling;
22
+ sig.onPeer = (peerId) => this.onPeer(peerId);
23
+ sig.onData = (data) => {
24
+ void this.handleRemote(data);
25
+ };
26
+ sig.onError = (err) => this.fail(err);
27
+ sig.onKicked = (info) => this.onKicked(info);
28
+ }
29
+ get state() {
30
+ return this._state;
31
+ }
32
+ get bufferedAmount() {
33
+ return this.dc?.bufferedAmount ?? 0;
34
+ }
35
+ /** 链路真正可用的 Promise(DataChannel open 时 resolve);失败/关闭时 reject。 */
36
+ whenReady() {
37
+ return this.readyPromise;
38
+ }
39
+ /** 加入信令房间并开始协商。使用方创建后调用一次(不需要 await,靠状态/`whenReady` 驱动)。 */
40
+ async start() {
41
+ if (this.started)
42
+ return;
43
+ this.started = true;
44
+ try {
45
+ await this.options.signaling.join();
46
+ }
47
+ catch (err) {
48
+ this.fail(toBridgeError(err));
49
+ }
50
+ }
51
+ send(bytes) {
52
+ const dc = this.dc;
53
+ if (this._state !== 'open' || !dc || dc.readyState !== 'open')
54
+ return;
55
+ // 注:DOM 定义的 `send` 要求 `ArrayBufferView<ArrayBuffer>`,而 `Uint8Array` 泛型是
56
+ // `ArrayBufferLike`(可能含 SharedArrayBuffer)。实际数据永远是普通 buffer,安全 cast。
57
+ dc.send(bytes);
58
+ }
59
+ close() {
60
+ if (this._state === 'closed')
61
+ return;
62
+ this.teardownPeer();
63
+ try {
64
+ this.options.signaling.close();
65
+ }
66
+ catch {
67
+ /* 忽略 */
68
+ }
69
+ this.setState('closed');
70
+ this.readyReject?.(new Error('transport closed'));
71
+ }
72
+ onMessage(listener) {
73
+ this.messageListeners.add(listener);
74
+ return () => {
75
+ this.messageListeners.delete(listener);
76
+ };
77
+ }
78
+ onStateChange(listener) {
79
+ this.stateListeners.add(listener);
80
+ return () => {
81
+ this.stateListeners.delete(listener);
82
+ };
83
+ }
84
+ // ───────────────────────── 内部:信令 → 协商 ─────────────────────────
85
+ onPeer(peerId) {
86
+ // 被顶号后信令已停用:此时任何对端变化都不该再重建 PC(重建出来也是死链路)
87
+ if (this.replaced)
88
+ return;
89
+ if (peerId === null) {
90
+ // 对端离开:清掉旧连接,回到等待态(手机重连后会重新触发)
91
+ this.teardownPeer();
92
+ this.setState('connecting');
93
+ return;
94
+ }
95
+ // 只有 host 主动发起:guest 必须等 offer 到达才建 PC(否则会建两次 PC、上下文串味)
96
+ if (this.options.role === 'host') {
97
+ this.ensurePC();
98
+ void this.startOffer();
99
+ }
100
+ }
101
+ /**
102
+ * 被服务端顶号(同一台电脑已有别的手机接入)。
103
+ *
104
+ * 顺序是刻意的:**先把错误告知上层,再改状态**。上层(手机端 `connectionStore`)在
105
+ * 看到 `closed` 时会安排自动重连 —— 而重连会把刚接入的那台手机再顶掉(乒乓)。
106
+ * 所以「被顶号」这个事实必须先于状态变化到达。
107
+ */
108
+ onKicked(info) {
109
+ if (this.replaced)
110
+ return;
111
+ this.replaced = true;
112
+ this.fail(new BridgeError('E_REPLACED', '该电脑已被另一台手机接管连接', {
113
+ data: { reason: info.reason },
114
+ }));
115
+ this.teardownPeer();
116
+ try {
117
+ this.options.signaling.close();
118
+ }
119
+ catch {
120
+ /* 忽略 */
121
+ }
122
+ // 状态置 closed 收尾(`_state` 为 closed 时 `close()` 也不会重复处理)
123
+ this.setState('closed');
124
+ }
125
+ ensurePC() {
126
+ if (this.pc)
127
+ return this.pc;
128
+ const factory = this.options.createPeerConnection ?? defaultFactory();
129
+ const pc = factory({ iceServers: this.options.iceServers ?? [] });
130
+ this.pc = pc;
131
+ pc.onicecandidate = (e) => {
132
+ if (!e.candidate)
133
+ return;
134
+ void this.options.signaling.send({ kind: 'candidate', cand: candidateToJson(e.candidate) });
135
+ };
136
+ pc.onconnectionstatechange = () => this.onConnectionState(pc.connectionState);
137
+ pc.ondatachannel = (e) => this.setupDataChannel(e.channel);
138
+ return pc;
139
+ }
140
+ async startOffer() {
141
+ const pc = this.pc;
142
+ if (!pc)
143
+ return;
144
+ if (!this.dc) {
145
+ this.setupDataChannel(pc.createDataChannel(this.options.channelLabel ?? 'virlen'));
146
+ }
147
+ try {
148
+ const offer = await pc.createOffer();
149
+ await pc.setLocalDescription(offer);
150
+ await this.options.signaling.send({ kind: 'offer', sdp: pc.localDescription ?? offer });
151
+ }
152
+ catch (err) {
153
+ this.fail(toBridgeError(err));
154
+ }
155
+ }
156
+ async handleRemote(data) {
157
+ const msg = data;
158
+ if (!msg || typeof msg !== 'object' || typeof msg.kind !== 'string')
159
+ return;
160
+ try {
161
+ if (msg.kind === 'offer') {
162
+ // 每个 offer 视作新会话:清旧上下文,避免新旧 ICE 串味(同 rtc.js)
163
+ if (this.pc)
164
+ this.teardownPeer();
165
+ const pc = this.ensurePC();
166
+ await pc.setRemoteDescription(msg.sdp);
167
+ await this.flushCandidates();
168
+ const answer = await pc.createAnswer();
169
+ await pc.setLocalDescription(answer);
170
+ await this.options.signaling.send({ kind: 'answer', sdp: pc.localDescription ?? answer });
171
+ }
172
+ else if (msg.kind === 'answer') {
173
+ const pc = this.pc;
174
+ if (!pc)
175
+ return;
176
+ if (pc.signalingState === 'have-local-offer') {
177
+ await pc.setRemoteDescription(msg.sdp);
178
+ await this.flushCandidates();
179
+ }
180
+ }
181
+ else if (msg.kind === 'candidate') {
182
+ const pc = this.pc;
183
+ if (!pc || !msg.cand)
184
+ return;
185
+ if (pc.remoteDescription && pc.remoteDescription.type) {
186
+ await pc.addIceCandidate(msg.cand).catch(() => { });
187
+ }
188
+ else {
189
+ this.pendingCandidates.push(msg.cand);
190
+ }
191
+ }
192
+ }
193
+ catch (err) {
194
+ this.fail(toBridgeError(err));
195
+ }
196
+ }
197
+ async flushCandidates() {
198
+ const list = this.pendingCandidates;
199
+ this.pendingCandidates = [];
200
+ const pc = this.pc;
201
+ if (!pc)
202
+ return;
203
+ for (const cand of list) {
204
+ await pc.addIceCandidate(cand).catch(() => { });
205
+ }
206
+ }
207
+ setupDataChannel(channel) {
208
+ this.dc = channel;
209
+ try {
210
+ channel.binaryType = 'arraybuffer';
211
+ }
212
+ catch {
213
+ /* 某些实现只读 */
214
+ }
215
+ channel.onopen = () => {
216
+ this.setState('open');
217
+ this.readyResolve?.();
218
+ };
219
+ channel.onclose = () => {
220
+ // 通道关闭:等待对端重连(不永久 closed)
221
+ this.setState('connecting');
222
+ };
223
+ channel.onmessage = (e) => {
224
+ const bytes = toBytes(e.data);
225
+ if (!bytes)
226
+ return;
227
+ for (const listener of [...this.messageListeners])
228
+ listener(bytes);
229
+ };
230
+ }
231
+ onConnectionState(connState) {
232
+ switch (connState) {
233
+ case 'connected':
234
+ // ⚠️ **不在这里置 open**:`connectionState` 可能**早于** DataChannel 的 `open` 触发。
235
+ // 尤其 guest(手机)侧:dc 是 `ondatachannel` **异步交付**的,此刻 `this.dc` 可能还是 null
236
+ // → 上层 `whenReady` 已 resolve、`transport.state==='open'`,但 `send()` 因 dc 未就绪而
237
+ // **静默丢弃**(2026-09 真机踩过:手机“连接超时”,电脑端日志 open→4s→connecting)。
238
+ // 统一以 `dc.onopen` 为准 —— 回归用例在 rtc-transport.test.ts。
239
+ break;
240
+ case 'failed':
241
+ case 'closed':
242
+ this.setState('closed');
243
+ this.readyReject?.(new Error(`peer connection ${connState}`));
244
+ break;
245
+ case 'disconnected':
246
+ this.setState('connecting');
247
+ break;
248
+ default:
249
+ break;
250
+ }
251
+ }
252
+ teardownPeer() {
253
+ this.pendingCandidates = [];
254
+ try {
255
+ this.dc?.close();
256
+ }
257
+ catch {
258
+ /* 忽略 */
259
+ }
260
+ try {
261
+ this.pc?.close();
262
+ }
263
+ catch {
264
+ /* 忽略 */
265
+ }
266
+ this.dc = null;
267
+ this.pc = null;
268
+ }
269
+ fail(error) {
270
+ this.readyReject?.(error);
271
+ this.onErrorListeners?.(error);
272
+ }
273
+ /** 订阅「链路级错误」(区别于状态变化);返回取消订阅函数。 */
274
+ onError(listener) {
275
+ this.onErrorListeners = listener;
276
+ return () => {
277
+ if (this.onErrorListeners === listener)
278
+ this.onErrorListeners = null;
279
+ };
280
+ }
281
+ setState(state) {
282
+ if (this._state === state)
283
+ return;
284
+ this._state = state;
285
+ for (const listener of [...this.stateListeners])
286
+ listener(state);
287
+ }
288
+ }
289
+ function defaultFactory() {
290
+ return (config) => {
291
+ const Ctor = globalThis
292
+ .RTCPeerConnection;
293
+ if (!Ctor) {
294
+ throw new Error('当前环境没有 RTCPeerConnection,未注入 createPeerConnection');
295
+ }
296
+ return new Ctor(config);
297
+ };
298
+ }
299
+ function candidateToJson(candidate) {
300
+ const withJson = candidate;
301
+ return typeof withJson.toJSON === 'function' ? withJson.toJSON() : candidate;
302
+ }
303
+ function toBytes(data) {
304
+ if (data instanceof Uint8Array)
305
+ return data;
306
+ if (data instanceof ArrayBuffer)
307
+ return new Uint8Array(data);
308
+ return null;
309
+ }
310
+ //# sourceMappingURL=rtc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rtc.js","sourceRoot":"","sources":["../../src/transport/rtc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ import { type RoomStatus } from '../protocol/identity.js';
2
+ export type SignalingRole = 'host' | 'guest';
3
+ /** 被顶号的通知内容(服务器 `kicked` 事件)。 */
4
+ export interface KickedInfo {
5
+ /** `replaced` = 同一台电脑已有新的手机接入(本端被顶掉);`takeover` = 电脑端重入接管。 */
6
+ reason: string;
7
+ }
8
+ /** `EventSource` 的最小形状(注入测试替身用)。 */
9
+ export interface EventSourceLike {
10
+ onopen: ((event: unknown) => void) | null;
11
+ onerror: ((event: unknown) => void) | null;
12
+ onmessage: ((event: {
13
+ data: string;
14
+ }) => void) | null;
15
+ close(): void;
16
+ }
17
+ export interface SignalingChannel {
18
+ readonly selfId: string;
19
+ readonly peerId: string | null;
20
+ onPeer: ((peerId: string | null) => void) | null;
21
+ onData: ((data: unknown) => void) | null;
22
+ onError: ((error: Error) => void) | null;
23
+ /**
24
+ * 本端被服务端顶号(**不是**普通掉线)。
25
+ *
26
+ * 为什么必须独立成回调而不是兼做 `onError`:上层要做出**完全相反的动作** ——
27
+ * 普通掉线自动重连,被顶号**绝不能自动重连**(否则两台手机会互相顶来顶去)。
28
+ */
29
+ onKicked: ((info: KickedInfo) => void) | null;
30
+ /** 加入房间;返回本端 id。 */
31
+ join(): Promise<string>;
32
+ /** 把一条信令发给当前对端(无对端时 no-op)。 */
33
+ send(data: unknown): Promise<void>;
34
+ close(): void;
35
+ }
36
+ export interface SseSignalingOptions {
37
+ /** 信令基址,如 `https://virlen.cn/api/rtc/`(末尾斜杠可有可无)。 */
38
+ baseUrl: string;
39
+ room: string;
40
+ role: SignalingRole;
41
+ /** 本端设备 key(电脑 `dk-…` / 手机 `mk-…`);服务器用于日志与 host 房主校验。 */
42
+ deviceKey?: string;
43
+ /** 本端显示名(服务器日志可读性)。 */
44
+ clientName?: string;
45
+ /**
46
+ * 加入时要求房间内已有 host(手机端用)。
47
+ * 无 host 时 `join()` 直接抛错 —— 比「先占住 guest 位再等 15 秒超时」诚实得多。
48
+ */
49
+ requireHostOnline?: boolean;
50
+ /** 注入(测试用);默认取全局 `fetch`。 */
51
+ fetchImpl?: typeof fetch;
52
+ /** 注入(测试用);默认 `(url) => new EventSource(url)`。 */
53
+ eventSourceFactory?: (url: string) => EventSourceLike;
54
+ }
55
+ export declare class SseSignalingClient implements SignalingChannel {
56
+ private readonly options;
57
+ selfId: string;
58
+ peerId: string | null;
59
+ onPeer: ((peerId: string | null) => void) | null;
60
+ onData: ((data: unknown) => void) | null;
61
+ onError: ((error: Error) => void) | null;
62
+ onKicked: ((info: KickedInfo) => void) | null;
63
+ private readonly base;
64
+ private es;
65
+ private closed;
66
+ /** 被顶号后置位:此后一切信令发送都无意义(房间已把我们移除)。 */
67
+ private kicked;
68
+ constructor(options: SseSignalingOptions);
69
+ join(): Promise<string>;
70
+ send(data: unknown): Promise<void>;
71
+ close(): void;
72
+ private fetch;
73
+ private openEvents;
74
+ private route;
75
+ /**
76
+ * 被顶号:先通知上层(它要据此关掉自动重连),再关掉事件流。
77
+ *
78
+ * 顺序很重要:`onKicked` 里上层会去 `transport.close()`,而 reconnect 的判定
79
+ * 就在那条路径上 —— 先把「被顶号」这个事实告知,再关流,才不会出现「先判定为重连、后知道是顶号」。
80
+ */
81
+ private handleKicked;
82
+ private setPeer;
83
+ }
84
+ /** 批量查房间在线状态(**不加入房间**,不占 guest 位)。 */
85
+ export interface FetchRoomStatusOptions {
86
+ /** 信令基址,如 `https://virlen.cn/api/rtc/`。 */
87
+ baseUrl: string;
88
+ /** 要查的房间号(由 `roomFor(电脑 key)` 派生)。 */
89
+ rooms: string[];
90
+ fetchImpl?: typeof fetch;
91
+ }
92
+ /**
93
+ * 查询一批房间的在线状态。
94
+ *
95
+ * 失败时**返回空数组而不抛错**:手机端列表的「在线状态」只是提示,
96
+ * 信令服务不可用/版本过旧(无 `/status`)时应当退化为「未知」,而不是把整个列表打挂。
97
+ */
98
+ export declare function fetchRoomStatus(options: FetchRoomStatusOptions): Promise<RoomStatus[]>;
99
+ /** 便利封装:`电脑 key → 在线?`(手机端列表用)。 */
100
+ export declare function fetchHostOnlineMap(baseUrl: string, hostKeys: string[], fetchImpl?: typeof fetch): Promise<Map<string, boolean>>;
101
+ //# sourceMappingURL=signaling.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signaling.d.ts","sourceRoot":"","sources":["../../src/transport/signaling.ts"],"names":[],"mappings":"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"}