@linxin666/dsh-remote-web-ui 0.2.6 → 0.2.8

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 (86) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +8 -1
  3. package/README.zh.md +2 -1
  4. package/lib/client.js +55 -10
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +12 -8
  7. package/lib/mobile.js +37 -18
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/FenceNotice.d.ts +3 -2
  10. package/lib/types/client/FenceNotice.d.ts.map +1 -1
  11. package/lib/types/client/FooterRemoteEntry.js +1 -1
  12. package/lib/types/client/PluginSettingsCard.d.ts +1 -1
  13. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  14. package/lib/types/client/RemoteEntry.js +8 -8
  15. package/lib/types/client/RemotePanel.js +3 -3
  16. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  17. package/lib/types/client/RemoteSettingsCard.js +2 -2
  18. package/lib/types/client/UpdateEntry.d.ts.map +1 -1
  19. package/lib/types/client/UpdateEntry.js +2 -2
  20. package/lib/types/client/UpdatePanel.js +1 -1
  21. package/lib/types/client/deep-link.js +1 -1
  22. package/lib/types/client/index.d.ts.map +1 -1
  23. package/lib/types/client/index.js +45 -23
  24. package/lib/types/client/pair-api.d.ts +6 -0
  25. package/lib/types/client/pair-api.d.ts.map +1 -1
  26. package/lib/types/client/pair-api.js +11 -0
  27. package/lib/types/client/remote-channel.d.ts +25 -0
  28. package/lib/types/client/remote-channel.d.ts.map +1 -1
  29. package/lib/types/client/remote-channel.js +24 -0
  30. package/lib/types/client/settings-form.d.ts.map +1 -1
  31. package/lib/types/client/update-api.d.ts.map +1 -1
  32. package/lib/types/gate.js +1 -1
  33. package/lib/types/index.js +15 -15
  34. package/lib/types/invariant.d.ts.map +1 -1
  35. package/lib/types/loopback-proxy.js +1 -1
  36. package/lib/types/mobile/PairRequiredView.js +1 -1
  37. package/lib/types/mobile/PairRequiredView.test.js +1 -1
  38. package/lib/types/mobile/api.js +1 -1
  39. package/lib/types/mobile/index.js +5 -5
  40. package/lib/types/mobile/markdown.test.js +1 -1
  41. package/lib/types/mobile/messages.d.ts.map +1 -1
  42. package/lib/types/mobile/messages.js +5 -3
  43. package/lib/types/mobile/messages.test.js +21 -1
  44. package/lib/types/mobile/mobile-theme.test.js +2 -2
  45. package/lib/types/mobile/mux.d.ts +7 -4
  46. package/lib/types/mobile/mux.d.ts.map +1 -1
  47. package/lib/types/mobile/mux.js +44 -16
  48. package/lib/types/mobile/mux.test.js +58 -1
  49. package/lib/types/mobile/rpc.d.ts.map +1 -1
  50. package/lib/types/mobile/theme-toggle.js +1 -1
  51. package/lib/types/mobile/views/App.js +7 -7
  52. package/lib/types/mobile/views/App.test.js +2 -2
  53. package/lib/types/mobile/views/ChatView.js +6 -6
  54. package/lib/types/mobile/views/ChatView.test.js +3 -3
  55. package/lib/types/mobile/views/SessionListView.js +3 -3
  56. package/lib/types/mobile/views/SessionListView.test.js +2 -2
  57. package/lib/types/mobile/views/WorkspaceView.js +3 -3
  58. package/lib/types/mobile/views/WorkspaceView.test.js +3 -3
  59. package/lib/types/mobile-api.d.ts +1 -1
  60. package/lib/types/mobile-api.d.ts.map +1 -1
  61. package/lib/types/mobile-api.js +2 -2
  62. package/lib/types/pairing-access.d.ts.map +1 -1
  63. package/lib/types/pairing.d.ts.map +1 -1
  64. package/lib/types/pairing.persist.test.js +2 -2
  65. package/lib/types/remote-api.js +6 -6
  66. package/lib/types/remote-methods.d.ts.map +1 -1
  67. package/lib/types/routes.d.ts +9 -7
  68. package/lib/types/routes.d.ts.map +1 -1
  69. package/lib/types/routes.js +8 -5
  70. package/lib/types/tunnel.d.ts.map +1 -1
  71. package/lib/types/update-routes.d.ts +2 -2
  72. package/lib/types/update-routes.d.ts.map +1 -1
  73. package/lib/types/update-routes.js +1 -1
  74. package/package.json +15 -12
  75. package/src/client/FenceNotice.tsx +3 -2
  76. package/src/client/RemoteEntry.tsx +3 -2
  77. package/src/client/UpdateEntry.tsx +1 -0
  78. package/src/client/index.ts +40 -15
  79. package/src/client/pair-api.ts +16 -0
  80. package/src/client/remote-channel.ts +42 -0
  81. package/src/index.ts +1 -1
  82. package/src/mobile/messages.test.ts +23 -0
  83. package/src/mobile/messages.ts +5 -3
  84. package/src/mobile/mux.test.ts +64 -0
  85. package/src/mobile/mux.ts +44 -17
  86. package/src/routes.ts +7 -2
package/src/mobile/mux.ts CHANGED
@@ -39,9 +39,9 @@ export interface MuxClientOptions {
39
39
  * the ordinary HTTP channel (unaffected by SSE-impairing tunnels).
40
40
  */
41
41
  pollLatest?: (sessionId: string) => Promise<HistoryPage>
42
- /** Poll cadence while SSE is stalled (default 3000 ms). */
42
+ /** Initial poll cadence while SSE is stalled (default 3000 ms). Empty polls back off to 60000 ms. */
43
43
  pollIntervalMs?: number
44
- /** How long SSE must go without a frame before fallback kicks in (default 12000 ms). */
44
+ /** Initial SSE stall window (default 12000 ms); a previously-live stream gets three windows before fallback. */
45
45
  stallThresholdMs?: number
46
46
  /** Clock seam for tests (defaults to Date.now). */
47
47
  now?: () => number
@@ -66,6 +66,8 @@ type SessionEventFrame = Extract<MuxFrame, { type: 'session/event' }>
66
66
 
67
67
  const DEFAULT_POLL_INTERVAL_MS = 3000
68
68
  const DEFAULT_STALL_THRESHOLD_MS = 12000
69
+ const LIVE_SSE_STALL_MULTIPLIER = 3
70
+ const MAX_POLL_BACKOFF_MS = 60000
69
71
  /**
70
72
  * Stall-check granularity: the single scheduler tick runs at least this
71
73
  * often so fallback arms within a second of the stall threshold passing,
@@ -98,8 +100,8 @@ export class MuxClient {
98
100
  private lastDataAt = 0
99
101
  /**
100
102
  * Whether the SSE channel has ever delivered a frame in this stream (a
101
- * delivered frame proves the tunnel forwards SSE; silence alone then means
102
- * the agent idle, not a dead channel — only an onerror re-arms fallback).
103
+ * delivered frame proves the tunnel can forward SSE; sustained silence may
104
+ * still mean a suspended mobile tunnel, so polling re-arms after 3 windows).
103
105
  */
104
106
  private sseAlive = false
105
107
  /** Per-session highest event seq already emitted, for poll dedup. */
@@ -109,6 +111,8 @@ export class MuxClient {
109
111
  private polling = false
110
112
  /** Epoch ms of the next due poll while polling (kept on the same tick timer). */
111
113
  private nextPollAt = 0
114
+ /** Adaptive delay: productive polls reset it; empty/error polls add one base interval up to one minute. */
115
+ private pollDelayMs: number
112
116
 
113
117
  /**
114
118
  * @param url - the mobile events endpoint (browser-relative).
@@ -119,6 +123,7 @@ export class MuxClient {
119
123
  this.sourceFactory = options.sourceFactory ?? browserSource
120
124
  this.pollLatest = options.pollLatest ?? ((sessionId) => fetchHistory(sessionId, undefined, DEFAULT_POLL_PAGE_SIZE))
121
125
  this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS
126
+ this.pollDelayMs = this.pollIntervalMs
122
127
  this.stallThresholdMs = options.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS
123
128
  this.now = options.now ?? (() => Date.now())
124
129
  }
@@ -159,9 +164,7 @@ export class MuxClient {
159
164
  return
160
165
  }
161
166
  // If SSE is already stalled for this session, start patching right away.
162
- if (!this.polling && !this.stopped && !this.sseAlive && (this.now() - this.lastDataAt) > this.stallThresholdMs) {
163
- this.startPolling()
164
- }
167
+ if (!this.polling && !this.stopped && this.isSseStalled()) this.startPolling()
165
168
  }
166
169
 
167
170
  private connect(): void {
@@ -208,29 +211,37 @@ export class MuxClient {
208
211
  if (this.stopped) return
209
212
  if (this.observeSessionId === undefined) return
210
213
  if (this.polling) {
211
- // The stall phase has ended; the same tick now paces the polls.
214
+ // The stall phase has ended; the same tick now paces the adaptive polls.
212
215
  if (this.now() >= this.nextPollAt) {
213
- this.nextPollAt = this.now() + this.pollIntervalMs
216
+ // pollTick schedules the next run after it settles, so slow requests
217
+ // cannot overlap with another scheduler tick.
218
+ this.nextPollAt = Number.POSITIVE_INFINITY
214
219
  void this.pollTick()
215
220
  }
216
221
  return
217
222
  }
218
- // A live SSE channel only goes silent while the agent idles; never poll
219
- // against it. Fallback arms again only via onerror or a stream that has
220
- // never delivered.
221
- if (this.sseAlive) return
222
- if ((this.now() - this.lastDataAt) > this.stallThresholdMs) this.startPolling()
223
+ if (this.isSseStalled()) this.startPolling()
224
+ }
225
+
226
+ private isSseStalled(): boolean {
227
+ const windowMs = this.sseAlive
228
+ ? this.stallThresholdMs * LIVE_SSE_STALL_MULTIPLIER
229
+ : this.stallThresholdMs
230
+ return (this.now() - this.lastDataAt) > windowMs
223
231
  }
224
232
 
225
233
  private startPolling(): void {
226
234
  if (this.polling || this.stopped) return
227
235
  this.polling = true
228
- this.nextPollAt = this.now() + this.pollIntervalMs
236
+ this.pollDelayMs = this.pollIntervalMs
237
+ this.nextPollAt = Number.POSITIVE_INFINITY
229
238
  void this.pollTick()
230
239
  }
231
240
 
232
241
  private stopPolling(): void {
233
242
  this.polling = false
243
+ this.pollDelayMs = this.pollIntervalMs
244
+ this.nextPollAt = 0
234
245
  }
235
246
 
236
247
  /**
@@ -244,19 +255,35 @@ export class MuxClient {
244
255
  this.stopPolling()
245
256
  return
246
257
  }
258
+ let emitted = 0
247
259
  try {
248
260
  const page = await this.pollLatest(sessionId)
249
261
  let maxSeq = this.pollWatermark.get(sessionId) ?? -1
250
- for (const entry of page.events) {
262
+ const ordered = [...page.events].sort((left, right) => {
263
+ const leftSeq = typeof left.event?.seq === 'number' ? left.event.seq : -1
264
+ const rightSeq = typeof right.event?.seq === 'number' ? right.event.seq : -1
265
+ return leftSeq - rightSeq
266
+ })
267
+ for (const entry of ordered) {
251
268
  const event = entry.event
252
269
  const seq = typeof event?.seq === 'number' ? event.seq : -1
253
270
  if (seq <= maxSeq) continue
254
271
  maxSeq = seq
272
+ emitted += 1
255
273
  this.emit({ type: 'session/event', sessionId: sessionId as SessionEventFrame['sessionId'], event } as SessionEventFrame)
256
274
  }
257
275
  this.pollWatermark.set(sessionId, maxSeq)
258
276
  } catch {
259
- // Transient (network, pairing, history paging); the next tick retries.
277
+ // Transient (network, pairing, history paging); retry with backoff.
278
+ } finally {
279
+ if (emitted > 0) {
280
+ this.pollDelayMs = this.pollIntervalMs
281
+ } else {
282
+ this.pollDelayMs = Math.min(MAX_POLL_BACKOFF_MS, this.pollDelayMs + this.pollIntervalMs)
283
+ }
284
+ if (this.polling && this.observeSessionId === sessionId) {
285
+ this.nextPollAt = this.now() + this.pollDelayMs
286
+ }
260
287
  }
261
288
  }
262
289
 
package/src/routes.ts CHANGED
@@ -195,6 +195,8 @@ export interface PairRoutesDeps {
195
195
  service: PairingService
196
196
  /** The LAN IP literals the fence accepts (derived from the bind host). */
197
197
  lanAddresses: readonly string[]
198
+ /** Current desktop gate policy, re-read for every status response. */
199
+ requirePairingForLan?: boolean | (() => boolean)
198
200
  }
199
201
 
200
202
  /**
@@ -203,7 +205,10 @@ export interface PairRoutesDeps {
203
205
  * @returns the exact routes to register on webServer.
204
206
  */
205
207
  export function makeRoutes(deps: PairRoutesDeps): WebRoute[] {
206
- const { service, lanAddresses } = deps
208
+ const { service, lanAddresses, requirePairingForLan = true } = deps
209
+ const pairingRequired = (): boolean => typeof requirePairingForLan === 'function'
210
+ ? requirePairingForLan()
211
+ : requirePairingForLan
207
212
  const events = new PairingEventsStream(service)
208
213
 
209
214
  /** Loopback-only fence: the desktop panel's control endpoints. */
@@ -404,7 +409,7 @@ export function makeRoutes(deps: PairRoutesDeps): WebRoute[] {
404
409
  const visible = paired
405
410
  ? rest
406
411
  : { phase: snapshot.phase, lanAvailable: snapshot.lanAvailable, lanAddresses: snapshot.lanAddresses }
407
- writeJson(res, 200, { ok: true, paired, ...visible })
412
+ writeJson(res, 200, { ok: true, paired, requirePairingForLan: pairingRequired(), ...visible })
408
413
  }
409
414
 
410
415
  const handleEvents = (req: IncomingMessage, res: ServerResponse): void => {