@desklink/react-native 0.5.2 → 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/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
- function failure(id: string, error: unknown) {
99
- emit(id, 'failure', { code: 'transport', message: error instanceof Error ? error.message : String(error) });
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
@@ -127,6 +127,8 @@ interface WebSession {
127
127
  chordsDown: Set<string>;
128
128
  /** Re-measure what the keyboard covers; null until a surface is attached. */
129
129
  followKeyboard: (() => void) | null;
130
+ /** Whether the first frame was already reported; renegotiation must not re-mark a stale picture. */
131
+ presented: boolean;
130
132
  /** Candidates that arrived before the offer; applied once it is set. */
131
133
  remoteDescriptionSet: boolean;
132
134
  pendingCandidates: RTCIceCandidateInit[];
@@ -960,6 +962,7 @@ export const nativeDesklink: NativeDesklinkModule = {
960
962
  compositionSent: '',
961
963
  composing: false,
962
964
  chordsDown: new Set<string>(),
965
+ presented: false,
963
966
  remoteDescriptionSet: false,
964
967
  pendingCandidates: [],
965
968
  detach: null,
@@ -982,6 +985,12 @@ export const nativeDesklink: NativeDesklinkModule = {
982
985
  // track and produce nothing.
983
986
  const presented = (): void => {
984
987
  if (sessions.get(id) !== session) return;
988
+ // Readiness is the first frame. Renegotiation re-fires ontrack
989
+ // and the compositor repaints the frame it already holds, and a
990
+ // repaint is not a new frame: a second mark would read a stale
991
+ // picture as live and reset the recovery budget mid-outage.
992
+ if (session.presented) return;
993
+ session.presented = true;
985
994
  video.style.opacity = '1';
986
995
  emit(id, 'presented', {});
987
996
  };
@@ -1009,7 +1018,7 @@ export const nativeDesklink: NativeDesklinkModule = {
1009
1018
  peer.onconnectionstatechange = () => {
1010
1019
  if (sessions.get(id) !== session) return;
1011
1020
  if (peer.connectionState === 'failed') {
1012
- 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' });
1013
1022
  }
1014
1023
  };
1015
1024
  peer.oniceconnectionstatechange = () => {
@@ -1026,7 +1035,7 @@ export const nativeDesklink: NativeDesklinkModule = {
1026
1035
  const session = sessions.get(id);
1027
1036
  if (session === undefined) return false;
1028
1037
  if (type !== 'offer') {
1029
- 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' });
1030
1039
  return false;
1031
1040
  }
1032
1041
  void (async () => {
@@ -1043,6 +1052,7 @@ export const nativeDesklink: NativeDesklinkModule = {
1043
1052
  } catch (error) {
1044
1053
  emit(id, 'failure', {
1045
1054
  code: 'transport',
1055
+ reason: 'sdp',
1046
1056
  message: error instanceof Error ? error.message : 'the desktop refused our answer',
1047
1057
  });
1048
1058
  }
package/src/protocol.ts CHANGED
@@ -103,6 +103,8 @@ export interface CursorSample {
103
103
  * the new session corrupts or kills a healthy recovery.
104
104
  */
105
105
  export type SessionEvent =
106
+ /** Optional carrier notification, distinct from the native peer's `closed`. */
107
+ | { kind: 'carrier-closed'; sessionId?: never }
106
108
  | { kind: 'description'; description: RtcDescription; sessionId?: string }
107
109
  | { kind: 'candidate'; candidate: RtcCandidate; sessionId?: string }
108
110
  | ({ kind: 'cursor' } & CursorSample)
@@ -120,15 +122,21 @@ export type SessionEvent =
120
122
  * The package calls these and nothing else, so an application can carry them
121
123
  * over its existing encrypted request path, a socket, or an in-process bridge.
122
124
  * `subscribe` delivers engine notifications until the returned function runs.
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.
123
127
  */
124
128
  export interface Signaling {
125
129
  /**
126
- * Send one request. A rejection may carry a stable `code` — the engine's or
130
+ * Send one request. Reject pending and future requests with `code: 'transport'`
131
+ * once the carrier is gone; never queue requests on a dead carrier.
132
+ * A rejection may carry a stable `code` — the engine's or
127
133
  * the host's own token — which the package reads to classify a refusal, so a
128
134
  * refusal is not retried as though it were a network failure.
129
135
  */
130
136
  request<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
131
137
  subscribe(handler: (event: SessionEvent) => void): () => void;
138
+ /** Optional: end this carrier now. Called once the package has declared it dead. */
139
+ close?(): void;
132
140
  }
133
141
 
134
142
  export type SessionStatus =
@@ -173,7 +181,7 @@ export interface SessionSnapshot {
173
181
  }
174
182
 
175
183
  /**
176
- * Control-channel messages. `seq` is monotonic per generation; the engine
184
+ * Control-channel messages. `seq` is monotonic per session; the engine
177
185
  * refuses a repeat, so a replayed gesture cannot move the pointer twice.
178
186
  */
179
187
  export type ControlMessage =