@desklink/react-native 0.5.3 → 0.5.4
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/README.md +40 -11
- package/android/build.gradle +0 -1
- package/android/src/main/java/expo/modules/desklink/DesktopSession.kt +20 -12
- package/package.json +1 -1
- package/src/DesktopView.ios.tsx +10 -4
- package/src/native.ios.ts +12 -7
- package/src/native.web.ts +3 -2
- package/src/protocol.ts +5 -2
- package/src/useDesktopSession.ts +393 -137
- package/src/DesktopView.ios.spec.tsx +0 -530
- package/src/controls.spec.tsx +0 -205
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<a href="https://www.npmjs.com/package/@desklink/react-native"><img alt="npm" src="https://img.shields.io/npm/v/@desklink/react-native?style=flat&label=npm" /></a>
|
|
5
5
|
<a href="https://github.com/umeranjum17/desklink/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/umeranjum17/desklink/ci.yml?style=flat&branch=main" /></a>
|
|
6
|
-
<a href="LICENSE"><img alt="Apache 2.0" src="https://img.shields.io/badge/license-Apache--2.0-666?style=flat" /></a>
|
|
6
|
+
<a href="https://github.com/umeranjum17/desklink/blob/main/packages/desktop-client/LICENSE"><img alt="Apache 2.0" src="https://img.shields.io/badge/license-Apache--2.0-666?style=flat" /></a>
|
|
7
7
|
<img alt="Android, iOS and web" src="https://img.shields.io/badge/Android%20%7C%20iOS%20%7C%20web-111?style=flat" />
|
|
8
8
|
</p>
|
|
9
9
|
|
|
@@ -20,7 +20,7 @@ the application.
|
|
|
20
20
|
## Install
|
|
21
21
|
|
|
22
22
|
The npm badge at the top of this page tracks the registry version, and npm
|
|
23
|
-
verifies package integrity on install. See the [changelog](
|
|
23
|
+
verifies package integrity on install. See the [changelog](https://github.com/umeranjum17/desklink/blob/main/CHANGELOG.md)
|
|
24
24
|
for release changes. Supported platforms: Android, iOS and web.
|
|
25
25
|
|
|
26
26
|
The package contains native code and is not an Expo Go package. Inside this
|
|
@@ -241,8 +241,12 @@ view.
|
|
|
241
241
|
`disconnected` path reads as `reconnecting` within about a second of the
|
|
242
242
|
drop — never a frozen picture that still says live — and a recovered path
|
|
243
243
|
reads as `live` again once the picture is advancing again (ICE `connected`
|
|
244
|
-
alone does not restore `live` while the picture is frozen).
|
|
245
|
-
`failed`
|
|
244
|
+
alone does not restore `live` while the picture is frozen). The peer's own ICE
|
|
245
|
+
`failed` — a lost media path, not a verdict on the session — holds the session
|
|
246
|
+
and its handle for ~30 s (the engine keeps it for about 28 s after the path
|
|
247
|
+
dies), so a roam longer than the client's own ~15 s `failed` still returns to
|
|
248
|
+
the same session; an SDP or candidate failure, which means the negotiation
|
|
249
|
+
itself is broken, reopens with fresh authority instead of staying stuck.
|
|
246
250
|
Every event carries its engine session id where the carrier preserves it,
|
|
247
251
|
and the session ignores anything naming a session it no longer holds, so a
|
|
248
252
|
previous generation's queued offer or revocation cannot corrupt or kill a
|
|
@@ -272,7 +276,28 @@ view.
|
|
|
272
276
|
reopens back off across ~60 s too instead of spending one attempt while the
|
|
273
277
|
network is still down. Renegotiation transients from a restart the session
|
|
274
278
|
asked for itself are expected, not a new outage: they neither flip the
|
|
275
|
-
status nor trigger another restart for a short grace window.
|
|
279
|
+
status nor trigger another restart for a short grace window. A picture
|
|
280
|
+
that keeps moving holds off the heartbeat watchdog for up to ~20 s, since
|
|
281
|
+
the control channel lags the video after an outage.
|
|
282
|
+
- **Roaming keeps the session.** The signalling carrier is not the session:
|
|
283
|
+
when it closes, or a restart request on it gets no reply within 1.5 s, the
|
|
284
|
+
session drops that carrier for good (calling its optional `close()`) and
|
|
285
|
+
keeps calling `authorize()` — one dial at a time, 250 ms after a failure,
|
|
286
|
+
5 s each, with no budget and no backoff. The lost carrier does not change the
|
|
287
|
+
status by itself: the status follows the picture and the peer's transport, so
|
|
288
|
+
it reads `reconnecting` only once the picture stops or the stall watchdog
|
|
289
|
+
fires. While the loop runs, a session that has shown a picture never reports
|
|
290
|
+
`failed` or a transport failure; one that never showed a picture reports the
|
|
291
|
+
first-frame transport failure after 20 s and the loop stops. A new carrier
|
|
292
|
+
picks up the same session after a 300 ms grace for the old ICE pair: if the
|
|
293
|
+
picture advanced during it, there is no restart (and a `reconnecting`
|
|
294
|
+
session returns to `live`); otherwise the session moves to `reconnecting`
|
|
295
|
+
and asks for an ICE restart. Failures caused
|
|
296
|
+
by the dead carrier wait for the next one rather than ending the session.
|
|
297
|
+
A carrier that comes back after the host's 30 s reattach window opens a new
|
|
298
|
+
session at once. A `session` or `generation` refusal (the desktop no longer
|
|
299
|
+
holds the session) also reopens at once. An `authorize()` refusal still ends
|
|
300
|
+
the session: a revoked pairing cannot roam back.
|
|
276
301
|
- **Input starts disarmed.** This is a local arm against accidental touches,
|
|
277
302
|
not a permission: the session already has input. `setInputEnabled(true)` arms
|
|
278
303
|
pointer, keyboard and clipboard input; `setInputEnabled(false)` blocks new
|
|
@@ -299,7 +324,7 @@ view.
|
|
|
299
324
|
The one thing the app must supply. It carries the engine's local protocol over
|
|
300
325
|
whatever authenticated channel the app already has.
|
|
301
326
|
|
|
302
|
-
See the [BYOKit integration guidance](
|
|
327
|
+
See the [BYOKit integration guidance](https://github.com/umeranjum17/desklink/blob/main/README.md#why-desklink-exists)
|
|
303
328
|
for the application's pairing, identity and signaling transport.
|
|
304
329
|
|
|
305
330
|
```ts
|
|
@@ -315,9 +340,13 @@ candidates, state and revocation. Pixels and input do **not** use this
|
|
|
315
340
|
channel — they are the desktop's own WebRTC session.
|
|
316
341
|
|
|
317
342
|
`authorize()` must wait for the authenticated carrier to open before returning
|
|
318
|
-
its `Signaling`.
|
|
319
|
-
|
|
320
|
-
|
|
343
|
+
its `Signaling`. It rejects with `code: 'transport'` for a network or carrier
|
|
344
|
+
failure; any other rejection is a refusal that ends the session, so a roam only
|
|
345
|
+
keeps the session when the adapter tags its outages. When that carrier is gone,
|
|
346
|
+
reject pending and future requests with `code: 'transport'`; do not queue them
|
|
347
|
+
indefinitely. An adapter may emit
|
|
348
|
+
`{ kind: 'carrier-closed' }` once to notify subscribers of that loss, and may
|
|
349
|
+
offer `close()`, which the package calls on a carrier it has given up on. This is
|
|
321
350
|
not the native peer's `closed` event or an engine revocation. Preserve the
|
|
322
351
|
engine's `session.revoked` code: `transport` indicates path loss; other codes
|
|
323
352
|
(or no code) are final. The demo adapter bounds connection setup to five seconds
|
|
@@ -334,7 +363,7 @@ What the consuming app — not this package — must get right before review.
|
|
|
334
363
|
`NSBonjourServices` is only needed if the app adds its own Bonjour
|
|
335
364
|
discovery — desklink discovers no hosts. Relay-only sessions never prompt.
|
|
336
365
|
For app-supplied routes, see the
|
|
337
|
-
[BYOKit integration guidance](
|
|
366
|
+
[BYOKit integration guidance](https://github.com/umeranjum17/desklink/blob/main/README.md#why-desklink-exists).
|
|
338
367
|
See [TN3179](https://developer.apple.com/documentation/technotes/tn3179-understanding-local-network-privacy).
|
|
339
368
|
- **Export compliance.** The viewer's DTLS-SRTP comes from libwebrtc bundled
|
|
340
369
|
with `react-native-webrtc`, not from the OS, so the app must check the
|
|
@@ -395,7 +424,7 @@ the run builds or caches stays in `~/desklink-ios` there. The run creates a
|
|
|
395
424
|
uniquely named simulator and deletes it during cleanup. Set
|
|
396
425
|
`DESKLINK_IOS_DEVICE` to the name or udid of one of the Mac's own simulators to
|
|
397
426
|
run on that instead and leave it in place. For host desktop isolation, see the
|
|
398
|
-
[private-lab guidance](
|
|
427
|
+
[private-lab guidance](https://github.com/umeranjum17/desklink/blob/main/README.md#develop). When the selected
|
|
399
428
|
Xcode has no simulator platform, point `DESKLINK_IOS_XCODE` at one that does;
|
|
400
429
|
`DESKLINK_IOS_RUNTIME` picks the iOS version (for example `18.6`); the
|
|
401
430
|
script's header lists the other settings. To capture the cursor tip proof, set
|
package/android/build.gradle
CHANGED
|
@@ -33,7 +33,6 @@ dependencies {
|
|
|
33
33
|
implementation project(':expo-modules-core')
|
|
34
34
|
implementation 'androidx.core:core-ktx:1.17.0'
|
|
35
35
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}"
|
|
36
|
-
testImplementation 'junit:junit:4.13.2'
|
|
37
36
|
// The app's existing WebRTC binding, so the org.webrtc classes and the single
|
|
38
37
|
// native library are shared rather than a second copy being linked in. This
|
|
39
38
|
// package never mutates that binding's global decoder factory.
|
|
@@ -111,7 +111,7 @@ class DesktopSession(
|
|
|
111
111
|
emit("ready", emptyMap())
|
|
112
112
|
} catch (error: Throwable) {
|
|
113
113
|
Log.w(TAG, "Could not start the desktop session", error)
|
|
114
|
-
fail("transport", error.message ?: "the desktop session could not start")
|
|
114
|
+
fail("transport", error.message ?: "the desktop session could not start", "session")
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
}
|
|
@@ -146,7 +146,7 @@ class DesktopSession(
|
|
|
146
146
|
val connection = peer ?: return@execute
|
|
147
147
|
val target = epoch
|
|
148
148
|
if (type != "offer") {
|
|
149
|
-
fail("transport", "the engine must send an offer")
|
|
149
|
+
fail("transport", "the engine must send an offer", "sdp")
|
|
150
150
|
return@execute
|
|
151
151
|
}
|
|
152
152
|
connection.setRemoteDescription(
|
|
@@ -171,23 +171,23 @@ class DesktopSession(
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
override fun onCreateFailure(error: String?) = fail("transport", error)
|
|
175
|
-
override fun onSetFailure(error: String?) = fail("transport", error)
|
|
174
|
+
override fun onCreateFailure(error: String?) = fail("transport", error, "sdp")
|
|
175
|
+
override fun onSetFailure(error: String?) = fail("transport", error, "sdp")
|
|
176
176
|
},
|
|
177
177
|
answer,
|
|
178
178
|
)
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
override fun onSetSuccess() = Unit
|
|
182
|
-
override fun onCreateFailure(error: String?) = fail("transport", error)
|
|
183
|
-
override fun onSetFailure(error: String?) = fail("transport", error)
|
|
182
|
+
override fun onCreateFailure(error: String?) = fail("transport", error, "sdp")
|
|
183
|
+
override fun onSetFailure(error: String?) = fail("transport", error, "sdp")
|
|
184
184
|
},
|
|
185
185
|
MediaConstraints(),
|
|
186
186
|
)
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
override fun onCreateFailure(error: String?) = fail("transport", error)
|
|
190
|
-
override fun onSetFailure(error: String?) = fail("transport", error)
|
|
189
|
+
override fun onCreateFailure(error: String?) = fail("transport", error, "sdp")
|
|
190
|
+
override fun onSetFailure(error: String?) = fail("transport", error, "sdp")
|
|
191
191
|
},
|
|
192
192
|
SessionDescription(SessionDescription.Type.OFFER, sdp),
|
|
193
193
|
)
|
|
@@ -233,7 +233,7 @@ class DesktopSession(
|
|
|
233
233
|
ByteBuffer.wrap(message.toString().toByteArray(StandardCharsets.UTF_8)), false,
|
|
234
234
|
))
|
|
235
235
|
if (!sent) {
|
|
236
|
-
fail("transport", "the desktop control channel could not send")
|
|
236
|
+
fail("transport", "the desktop control channel could not send", "control")
|
|
237
237
|
close()
|
|
238
238
|
}
|
|
239
239
|
}
|
|
@@ -425,7 +425,7 @@ class DesktopSession(
|
|
|
425
425
|
ui.postDelayed(sample, 2000)
|
|
426
426
|
}
|
|
427
427
|
if (state == PeerConnection.IceConnectionState.FAILED) {
|
|
428
|
-
fail("transport", "the connection to the desktop was lost")
|
|
428
|
+
fail("transport", "the connection to the desktop was lost", "ice")
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
431
|
|
|
@@ -495,8 +495,16 @@ class DesktopSession(
|
|
|
495
495
|
ui.post { onEvent(name, payload) }
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
-
|
|
499
|
-
|
|
498
|
+
/**
|
|
499
|
+
* One native failure, with its `reason` so the hook can tell an ICE failure
|
|
500
|
+
* (the peer's path died, which the session can be held across) from an SDP,
|
|
501
|
+
* candidate or control failure (the session itself is broken, which stays
|
|
502
|
+
* terminal).
|
|
503
|
+
*/
|
|
504
|
+
private fun fail(code: String, message: String?, reason: String? = null) {
|
|
505
|
+
val payload = mutableMapOf<String, Any?>("code" to code, "message" to (message ?: "unknown"))
|
|
506
|
+
if (reason != null) payload["reason"] = reason
|
|
507
|
+
ui.post { onEvent("failure", payload) }
|
|
500
508
|
}
|
|
501
509
|
|
|
502
510
|
companion object {
|
package/package.json
CHANGED
package/src/DesktopView.ios.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { Image, Keyboard, StyleSheet, TextInput, View, type GestureResponderEvent, type LayoutChangeEvent, type ViewProps } from 'react-native';
|
|
2
|
+
import { Image, Keyboard, StyleSheet, TextInput, View, useWindowDimensions, type GestureResponderEvent, type LayoutChangeEvent, type ViewProps } from 'react-native';
|
|
3
3
|
import pointerArrow from './pointer.png';
|
|
4
4
|
import { requireNativeViewManager, requireOptionalNativeModule } from 'expo-modules-core';
|
|
5
5
|
import type { DesktopViewProps } from './DesktopView';
|
|
@@ -155,11 +155,17 @@ export function DesktopView({ sessionId, style, placeholder, accessibilityLabel,
|
|
|
155
155
|
const scale = zoom === null ? fill : fit * zoom;
|
|
156
156
|
/** The zoom over the whole-desktop fit the picture is at now. */
|
|
157
157
|
const zoomed = fit > 0 ? scale / fit : 1;
|
|
158
|
-
// A new desktop size fills the view again, and a
|
|
159
|
-
//
|
|
158
|
+
// A new desktop size fills the view again, and a rotation starts again at
|
|
159
|
+
// the desktop's top-left corner, where a page's first words are. Only an
|
|
160
|
+
// orientation change resets it: the application making room for the
|
|
161
|
+
// keyboard also changes the view, by taking height away, and that must
|
|
162
|
+
// move the picture (below) rather than snap it back to the corner and
|
|
163
|
+
// leave the field the tap just put the caret in hidden under the keyboard.
|
|
164
|
+
const window = useWindowDimensions();
|
|
165
|
+
const portrait = window.width <= window.height;
|
|
160
166
|
React.useEffect(() => { setZoom(null); setOffset(TOP_LEFT); }, [size.width, size.height]);
|
|
161
167
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
162
|
-
React.useEffect(() => { if (zoom === null) setOffset(TOP_LEFT); }, [
|
|
168
|
+
React.useEffect(() => { if (zoom === null) setOffset(TOP_LEFT); }, [portrait]);
|
|
163
169
|
const pictureWidth = size.width * scale;
|
|
164
170
|
const pictureHeight = size.height * scale;
|
|
165
171
|
const point = (x: number, y: number, clamp = false): Point | null => {
|
package/src/native.ios.ts
CHANGED
|
@@ -95,8 +95,13 @@ export function preferH264(sdp: string): string {
|
|
|
95
95
|
return lines.join('\r\n');
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
/**
|
|
99
|
+
* One native failure, with its `reason` so the hook can tell an ICE failure
|
|
100
|
+
* (the peer's path died, which the session can be held across) from an SDP or
|
|
101
|
+
* candidate failure (the negotiation itself is broken, which stays terminal).
|
|
102
|
+
*/
|
|
103
|
+
function failure(id: string, error: unknown, reason: 'ice' | 'sdp' | 'candidate' | 'control') {
|
|
104
|
+
emit(id, 'failure', { code: 'transport', reason, message: error instanceof Error ? error.message : String(error) });
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
export const nativeDesklink: NativeDesklinkModule = {
|
|
@@ -121,7 +126,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
121
126
|
};
|
|
122
127
|
peer.oniceconnectionstatechange = () => {
|
|
123
128
|
emit(id, 'ice', { state: peer.iceConnectionState.toUpperCase() });
|
|
124
|
-
if (peer.iceConnectionState === 'failed') failure(id, 'the connection to the desktop was lost');
|
|
129
|
+
if (peer.iceConnectionState === 'failed') failure(id, 'the connection to the desktop was lost', 'ice');
|
|
125
130
|
};
|
|
126
131
|
peer.ontrack = (event) => {
|
|
127
132
|
if (session.closed || event.track.kind !== 'video') return;
|
|
@@ -148,7 +153,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
148
153
|
setRemoteDescription(id, type, sdp) {
|
|
149
154
|
const session = sessions.get(id);
|
|
150
155
|
if (!session) return false;
|
|
151
|
-
if (type !== 'offer') { failure(id, 'the engine must send an offer'); return false; }
|
|
156
|
+
if (type !== 'offer') { failure(id, 'the engine must send an offer', 'sdp'); return false; }
|
|
152
157
|
void (async () => {
|
|
153
158
|
try {
|
|
154
159
|
await session.peer.setRemoteDescription({ type: 'offer', sdp: preferH264(sdp.replace(PLAYOUT_DELAY, '')) });
|
|
@@ -158,7 +163,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
158
163
|
const answer = await session.peer.createAnswer();
|
|
159
164
|
await session.peer.setLocalDescription(answer);
|
|
160
165
|
if (!session.closed) emit(id, 'answer', { sdp: session.peer.localDescription?.sdp ?? answer.sdp });
|
|
161
|
-
} catch (error) { if (!session.closed) failure(id, error); }
|
|
166
|
+
} catch (error) { if (!session.closed) failure(id, error, 'sdp'); }
|
|
162
167
|
})();
|
|
163
168
|
return true;
|
|
164
169
|
},
|
|
@@ -167,7 +172,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
167
172
|
if (!session) return false;
|
|
168
173
|
const ice = { candidate, sdpMid, sdpMLineIndex: sdpMLineIndex ?? 0 };
|
|
169
174
|
if (!session.remoteSet) session.candidates.push(ice);
|
|
170
|
-
else void session.peer.addIceCandidate(ice).catch((error) => failure(id, error));
|
|
175
|
+
else void session.peer.addIceCandidate(ice).catch((error) => failure(id, error, 'candidate'));
|
|
171
176
|
return true;
|
|
172
177
|
},
|
|
173
178
|
sendControl(id, message) {
|
|
@@ -178,7 +183,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
178
183
|
if (!session.input && ['pointer', 'wheel', 'key', 'text', 'clipboard_read', 'clipboard_write'].includes(String(control.kind))) return false;
|
|
179
184
|
session.channel.send(JSON.stringify({ ...control, seq: ++session.seq }));
|
|
180
185
|
return true;
|
|
181
|
-
} catch (error) { failure(id, error); return false; }
|
|
186
|
+
} catch (error) { failure(id, error, 'control'); return false; }
|
|
182
187
|
},
|
|
183
188
|
setInputEnabled(id, enabled) {
|
|
184
189
|
const session = sessions.get(id);
|
package/src/native.web.ts
CHANGED
|
@@ -1018,7 +1018,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
1018
1018
|
peer.onconnectionstatechange = () => {
|
|
1019
1019
|
if (sessions.get(id) !== session) return;
|
|
1020
1020
|
if (peer.connectionState === 'failed') {
|
|
1021
|
-
emit(id, 'failure', { code: 'transport', message: 'the connection to the desktop was lost' });
|
|
1021
|
+
emit(id, 'failure', { code: 'transport', reason: 'ice', message: 'the connection to the desktop was lost' });
|
|
1022
1022
|
}
|
|
1023
1023
|
};
|
|
1024
1024
|
peer.oniceconnectionstatechange = () => {
|
|
@@ -1035,7 +1035,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
1035
1035
|
const session = sessions.get(id);
|
|
1036
1036
|
if (session === undefined) return false;
|
|
1037
1037
|
if (type !== 'offer') {
|
|
1038
|
-
emit(id, 'failure', { code: 'transport', message: 'the engine must send an offer' });
|
|
1038
|
+
emit(id, 'failure', { code: 'transport', reason: 'sdp', message: 'the engine must send an offer' });
|
|
1039
1039
|
return false;
|
|
1040
1040
|
}
|
|
1041
1041
|
void (async () => {
|
|
@@ -1052,6 +1052,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
1052
1052
|
} catch (error) {
|
|
1053
1053
|
emit(id, 'failure', {
|
|
1054
1054
|
code: 'transport',
|
|
1055
|
+
reason: 'sdp',
|
|
1055
1056
|
message: error instanceof Error ? error.message : 'the desktop refused our answer',
|
|
1056
1057
|
});
|
|
1057
1058
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -122,7 +122,8 @@ export type SessionEvent =
|
|
|
122
122
|
* The package calls these and nothing else, so an application can carry them
|
|
123
123
|
* over its existing encrypted request path, a socket, or an in-process bridge.
|
|
124
124
|
* `subscribe` delivers engine notifications until the returned function runs.
|
|
125
|
-
* The application's authorize() resolves only once this carrier is open.
|
|
125
|
+
* The application's authorize() resolves only once this carrier is open. Its
|
|
126
|
+
* rejection rules (transport vs. refusal) are in the README's Signaling section.
|
|
126
127
|
*/
|
|
127
128
|
export interface Signaling {
|
|
128
129
|
/**
|
|
@@ -134,6 +135,8 @@ export interface Signaling {
|
|
|
134
135
|
*/
|
|
135
136
|
request<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
136
137
|
subscribe(handler: (event: SessionEvent) => void): () => void;
|
|
138
|
+
/** Optional: end this carrier now. Called once the package has declared it dead. */
|
|
139
|
+
close?(): void;
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
export type SessionStatus =
|
|
@@ -178,7 +181,7 @@ export interface SessionSnapshot {
|
|
|
178
181
|
}
|
|
179
182
|
|
|
180
183
|
/**
|
|
181
|
-
* Control-channel messages. `seq` is monotonic per
|
|
184
|
+
* Control-channel messages. `seq` is monotonic per session; the engine
|
|
182
185
|
* refuses a repeat, so a replayed gesture cannot move the pointer twice.
|
|
183
186
|
*/
|
|
184
187
|
export type ControlMessage =
|