@linxin666/dsh-remote-web-ui 0.3.6 → 0.3.9
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.i18n.yaml +2 -2
- package/README.md +77 -468
- package/README.zh.md +83 -113
- package/lib/client.js +955 -203
- package/lib/client.js.map +1 -1
- package/lib/index.js +1136 -1466
- package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
- package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
- package/lib/types/client/FooterRemoteEntry.js +1 -1
- package/lib/types/client/RemoteEntry.d.ts +7 -4
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +7 -10
- package/lib/types/client/RemotePanel.d.ts +2 -2
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +56 -3
- package/lib/types/client/deep-link.d.ts +10 -13
- package/lib/types/client/deep-link.d.ts.map +1 -1
- package/lib/types/client/deep-link.js +14 -74
- package/lib/types/client/index.d.ts +2 -6
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +73 -39
- package/lib/types/client/locales.d.ts +26 -12
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +36 -22
- package/lib/types/client/mobile-adapt.d.ts +40 -0
- package/lib/types/client/mobile-adapt.d.ts.map +1 -0
- package/lib/types/client/mobile-adapt.js +967 -0
- package/lib/types/client/pair-api.d.ts +24 -6
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +9 -14
- package/lib/types/client/remote-channel.d.ts +3 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +35 -3
- package/lib/types/client/settings-form.d.ts +35 -43
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +60 -53
- package/lib/types/firewall.d.ts +75 -0
- package/lib/types/firewall.d.ts.map +1 -0
- package/lib/types/firewall.js +184 -0
- package/lib/types/gate.d.ts +11 -5
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +11 -5
- package/lib/types/index.d.ts +39 -10
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +206 -58
- package/lib/types/inner-auth.d.ts +32 -0
- package/lib/types/inner-auth.d.ts.map +1 -0
- package/lib/types/inner-auth.js +72 -0
- package/lib/types/lan-bind-plan.d.ts +55 -0
- package/lib/types/lan-bind-plan.d.ts.map +1 -0
- package/lib/types/lan-bind-plan.js +60 -0
- package/lib/types/lan-bind.d.ts +69 -0
- package/lib/types/lan-bind.d.ts.map +1 -0
- package/lib/types/lan-bind.js +128 -0
- package/lib/types/loopback-proxy.d.ts +9 -2
- package/lib/types/loopback-proxy.d.ts.map +1 -1
- package/lib/types/loopback-proxy.js +24 -2
- package/lib/types/pairing.d.ts +15 -3
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +20 -6
- package/lib/types/remote-api.d.ts +30 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +55 -42
- package/lib/types/remote-channel-boot.d.ts +21 -2
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +65 -3
- package/lib/types/remote-channel-rules.d.ts +8 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +9 -2
- package/lib/types/remote-methods.d.ts +45 -11
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +56 -27
- package/lib/types/remote-presence-pet.d.ts +51 -0
- package/lib/types/remote-presence-pet.d.ts.map +1 -0
- package/lib/types/remote-presence-pet.js +101 -0
- package/lib/types/routes.d.ts +76 -4
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +336 -19
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -1
- package/package.json +20 -18
- package/src/client/FooterRemoteEntry.tsx +9 -10
- package/src/client/RemoteEntry.tsx +13 -14
- package/src/client/RemotePanel.tsx +5 -16
- package/src/client/RemoteSettingsCard.tsx +72 -13
- package/src/client/deep-link.ts +14 -75
- package/src/client/index.ts +77 -39
- package/src/client/locales.ts +36 -22
- package/src/client/mobile-adapt.ts +913 -0
- package/src/client/pair-api.ts +26 -15
- package/src/client/remote-channel.ts +31 -3
- package/src/client/settings-form.ts +78 -92
- package/src/dsh-home.ts +1 -1
- package/src/firewall.ts +230 -0
- package/src/gate.ts +11 -5
- package/src/index.ts +235 -65
- package/src/inner-auth.ts +85 -0
- package/src/lan-bind-plan.ts +81 -0
- package/src/lan-bind.ts +145 -0
- package/src/loopback-proxy.ts +34 -0
- package/src/pairing.persist.test.ts +1 -1
- package/src/pairing.ts +20 -6
- package/src/remote-api.ts +67 -45
- package/src/remote-channel-boot.ts +70 -3
- package/src/remote-channel-rules.ts +17 -2
- package/src/remote-methods.ts +58 -27
- package/src/remote-presence-pet.ts +129 -0
- package/src/routes.ts +358 -19
- package/src/tunnel.ts +7 -1
- package/assets/apple-touch-icon.png +0 -0
- package/assets/mobile-icon-192.png +0 -0
- package/assets/mobile-icon-512.png +0 -0
- package/assets/mobile-service-worker.js +0 -63
- package/lib/mobile.js +0 -17658
- package/lib/mobile.js.map +0 -1
- package/lib/types/mobile/PairRequiredView.d.ts +0 -7
- package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.js +0 -28
- package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
- package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.test.js +0 -24
- package/lib/types/mobile/api.d.ts +0 -145
- package/lib/types/mobile/api.d.ts.map +0 -1
- package/lib/types/mobile/api.js +0 -103
- package/lib/types/mobile/index.d.ts +0 -10
- package/lib/types/mobile/index.d.ts.map +0 -1
- package/lib/types/mobile/index.js +0 -38
- package/lib/types/mobile/markdown.d.ts +0 -34
- package/lib/types/mobile/markdown.d.ts.map +0 -1
- package/lib/types/mobile/markdown.js +0 -285
- package/lib/types/mobile/markdown.test.d.ts +0 -2
- package/lib/types/mobile/markdown.test.d.ts.map +0 -1
- package/lib/types/mobile/markdown.test.js +0 -79
- package/lib/types/mobile/messages.d.ts +0 -149
- package/lib/types/mobile/messages.d.ts.map +0 -1
- package/lib/types/mobile/messages.js +0 -561
- package/lib/types/mobile/messages.test.d.ts +0 -2
- package/lib/types/mobile/messages.test.d.ts.map +0 -1
- package/lib/types/mobile/messages.test.js +0 -356
- package/lib/types/mobile/mobile-styles.d.ts +0 -6
- package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
- package/lib/types/mobile/mobile-styles.js +0 -1438
- package/lib/types/mobile/mobile-theme.d.ts +0 -22
- package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.js +0 -70
- package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
- package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.test.js +0 -71
- package/lib/types/mobile/mux.d.ts +0 -128
- package/lib/types/mobile/mux.d.ts.map +0 -1
- package/lib/types/mobile/mux.js +0 -305
- package/lib/types/mobile/mux.test.d.ts +0 -2
- package/lib/types/mobile/mux.test.d.ts.map +0 -1
- package/lib/types/mobile/mux.test.js +0 -267
- package/lib/types/mobile/pairing.d.ts +0 -31
- package/lib/types/mobile/pairing.d.ts.map +0 -1
- package/lib/types/mobile/pairing.js +0 -62
- package/lib/types/mobile/rpc.d.ts +0 -38
- package/lib/types/mobile/rpc.d.ts.map +0 -1
- package/lib/types/mobile/rpc.js +0 -83
- package/lib/types/mobile/theme-toggle.d.ts +0 -12
- package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
- package/lib/types/mobile/theme-toggle.js +0 -26
- package/lib/types/mobile/views/App.d.ts +0 -57
- package/lib/types/mobile/views/App.d.ts.map +0 -1
- package/lib/types/mobile/views/App.js +0 -150
- package/lib/types/mobile/views/App.test.d.ts +0 -2
- package/lib/types/mobile/views/App.test.d.ts.map +0 -1
- package/lib/types/mobile/views/App.test.js +0 -52
- package/lib/types/mobile/views/ChatView.d.ts +0 -63
- package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.js +0 -821
- package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.stream.test.js +0 -236
- package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.test.js +0 -718
- package/lib/types/mobile/views/SessionListView.d.ts +0 -27
- package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.js +0 -139
- package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
- package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.test.js +0 -167
- package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.js +0 -93
- package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
- package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
- package/lib/types/mobile-api.d.ts +0 -65
- package/lib/types/mobile-api.d.ts.map +0 -1
- package/lib/types/mobile-api.js +0 -404
- package/lib/types/mobile-pending.d.ts +0 -38
- package/lib/types/mobile-pending.d.ts.map +0 -1
- package/lib/types/mobile-pending.js +0 -59
- package/lib/types/mobile-pwa.d.ts +0 -6
- package/lib/types/mobile-pwa.d.ts.map +0 -1
- package/lib/types/mobile-pwa.js +0 -17
- package/lib/types/mobile-routes.d.ts +0 -13
- package/lib/types/mobile-routes.d.ts.map +0 -1
- package/lib/types/mobile-routes.js +0 -217
- package/lib/types/paired-model-catalog.d.ts +0 -17
- package/lib/types/paired-model-catalog.d.ts.map +0 -1
- package/lib/types/paired-model-catalog.js +0 -311
- package/src/mobile/PairRequiredView.test.tsx +0 -28
- package/src/mobile/PairRequiredView.tsx +0 -58
- package/src/mobile/api.ts +0 -229
- package/src/mobile/index.tsx +0 -41
- package/src/mobile/markdown.test.ts +0 -91
- package/src/mobile/markdown.ts +0 -291
- package/src/mobile/messages.test.ts +0 -402
- package/src/mobile/messages.ts +0 -685
- package/src/mobile/mobile-styles.ts +0 -1439
- package/src/mobile/mobile-theme.test.ts +0 -84
- package/src/mobile/mobile-theme.ts +0 -76
- package/src/mobile/mux.test.ts +0 -314
- package/src/mobile/mux.ts +0 -336
- package/src/mobile/pairing.ts +0 -77
- package/src/mobile/rpc.ts +0 -90
- package/src/mobile/theme-toggle.tsx +0 -47
- package/src/mobile/views/App.test.tsx +0 -62
- package/src/mobile/views/App.tsx +0 -213
- package/src/mobile/views/ChatView.stream.test.tsx +0 -268
- package/src/mobile/views/ChatView.test.tsx +0 -816
- package/src/mobile/views/ChatView.tsx +0 -1342
- package/src/mobile/views/SessionListView.test.tsx +0 -207
- package/src/mobile/views/SessionListView.tsx +0 -244
- package/src/mobile/views/WorkspaceView.test.tsx +0 -227
- package/src/mobile/views/WorkspaceView.tsx +0 -245
- package/src/mobile-api.ts +0 -418
- package/src/mobile-pending.ts +0 -88
- package/src/mobile-pwa.ts +0 -19
- package/src/mobile-routes.ts +0 -231
- package/src/paired-model-catalog.ts +0 -272
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mobile-surface theme: light by default, dark via an explicit persisted
|
|
3
|
-
* toggle. The standalone page boots without the shell, so there is no theme
|
|
4
|
-
* system to inherit — the choice is stored in localStorage and applied as a
|
|
5
|
-
* `data-theme` attribute on <html>; the stylesheet defines both palettes
|
|
6
|
-
* under `:root` (light) / `:root[data-theme='dark']` (dark).
|
|
7
|
-
*
|
|
8
|
-
* A tiny module store (subscribe/get) keeps the toggle button and the boot
|
|
9
|
-
* path in sync without threading props through the three view levels.
|
|
10
|
-
*/
|
|
11
|
-
export type MobileTheme = 'light' | 'dark';
|
|
12
|
-
/** Current theme (light unless the user explicitly toggled to dark). */
|
|
13
|
-
export declare function getMobileTheme(): MobileTheme;
|
|
14
|
-
/** Subscribe to theme changes; returns the unsubscribe function. */
|
|
15
|
-
export declare function subscribeMobileTheme(listener: () => void): () => void;
|
|
16
|
-
/** Set the theme explicitly (persisted + applied to the document). */
|
|
17
|
-
export declare function setMobileTheme(theme: MobileTheme): void;
|
|
18
|
-
/** Flip light/dark and return the new theme. */
|
|
19
|
-
export declare function toggleMobileTheme(): MobileTheme;
|
|
20
|
-
/** Apply the persisted (or default) theme once at boot, before first paint. */
|
|
21
|
-
export declare function initMobileTheme(): void;
|
|
22
|
-
//# sourceMappingURL=mobile-theme.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-theme.d.ts","sourceRoot":"","sources":["../../../src/mobile/mobile-theme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,CAAA;AAmC1C,wEAAwE;AACxE,wBAAgB,cAAc,IAAI,WAAW,CAE5C;AAED,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAGrE;AAED,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAMvD;AAED,gDAAgD;AAChD,wBAAgB,iBAAiB,IAAI,WAAW,CAG/C;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,IAAI,IAAI,CAEtC"}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mobile-surface theme: light by default, dark via an explicit persisted
|
|
3
|
-
* toggle. The standalone page boots without the shell, so there is no theme
|
|
4
|
-
* system to inherit — the choice is stored in localStorage and applied as a
|
|
5
|
-
* `data-theme` attribute on <html>; the stylesheet defines both palettes
|
|
6
|
-
* under `:root` (light) / `:root[data-theme='dark']` (dark).
|
|
7
|
-
*
|
|
8
|
-
* A tiny module store (subscribe/get) keeps the toggle button and the boot
|
|
9
|
-
* path in sync without threading props through the three view levels.
|
|
10
|
-
*/
|
|
11
|
-
const STORAGE_KEY = 'dsh.remote.theme';
|
|
12
|
-
const THEME_COLOR_META_NAME = 'theme-color';
|
|
13
|
-
const LIGHT_THEME_COLOR = '#f3f5f9';
|
|
14
|
-
const DARK_THEME_COLOR = '#111418';
|
|
15
|
-
let current = readStored() ?? 'light';
|
|
16
|
-
const listeners = new Set();
|
|
17
|
-
function readStored() {
|
|
18
|
-
try {
|
|
19
|
-
const value = localStorage.getItem(STORAGE_KEY);
|
|
20
|
-
return value === 'dark' || value === 'light' ? value : undefined;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return undefined;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
function persist(theme) {
|
|
27
|
-
try {
|
|
28
|
-
localStorage.setItem(STORAGE_KEY, theme);
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
// Private-mode storage failures are non-fatal; the session keeps the theme.
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
/** Apply the theme to the document: data-theme attribute + browser chrome color. */
|
|
35
|
-
function applyToDocument(theme) {
|
|
36
|
-
if (typeof document === 'undefined')
|
|
37
|
-
return;
|
|
38
|
-
document.documentElement.dataset.theme = theme;
|
|
39
|
-
const meta = document.querySelector(`meta[name="${THEME_COLOR_META_NAME}"]`);
|
|
40
|
-
if (meta !== null)
|
|
41
|
-
meta.content = theme === 'dark' ? DARK_THEME_COLOR : LIGHT_THEME_COLOR;
|
|
42
|
-
}
|
|
43
|
-
/** Current theme (light unless the user explicitly toggled to dark). */
|
|
44
|
-
export function getMobileTheme() {
|
|
45
|
-
return current;
|
|
46
|
-
}
|
|
47
|
-
/** Subscribe to theme changes; returns the unsubscribe function. */
|
|
48
|
-
export function subscribeMobileTheme(listener) {
|
|
49
|
-
listeners.add(listener);
|
|
50
|
-
return () => { listeners.delete(listener); };
|
|
51
|
-
}
|
|
52
|
-
/** Set the theme explicitly (persisted + applied to the document). */
|
|
53
|
-
export function setMobileTheme(theme) {
|
|
54
|
-
if (theme === current)
|
|
55
|
-
return;
|
|
56
|
-
current = theme;
|
|
57
|
-
persist(theme);
|
|
58
|
-
applyToDocument(theme);
|
|
59
|
-
for (const listener of [...listeners])
|
|
60
|
-
listener();
|
|
61
|
-
}
|
|
62
|
-
/** Flip light/dark and return the new theme. */
|
|
63
|
-
export function toggleMobileTheme() {
|
|
64
|
-
setMobileTheme(current === 'light' ? 'dark' : 'light');
|
|
65
|
-
return current;
|
|
66
|
-
}
|
|
67
|
-
/** Apply the persisted (or default) theme once at boot, before first paint. */
|
|
68
|
-
export function initMobileTheme() {
|
|
69
|
-
applyToDocument(current);
|
|
70
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-theme.test.d.ts","sourceRoot":"","sources":["../../../src/mobile/mobile-theme.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
/** mobile-theme: light default, persisted dark toggle, document wiring. */
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { getMobileTheme, setMobileTheme, subscribeMobileTheme, toggleMobileTheme, } from "./mobile-theme.js";
|
|
5
|
-
const STORAGE_KEY = 'dsh.remote.theme';
|
|
6
|
-
/** jsdom in this setup ships a bare localStorage object; install a real fake. */
|
|
7
|
-
function makeStorage() {
|
|
8
|
-
const map = new Map();
|
|
9
|
-
return {
|
|
10
|
-
getItem: (key) => map.get(key) ?? null,
|
|
11
|
-
setItem: (key, value) => { map.set(key, value); },
|
|
12
|
-
removeItem: (key) => { map.delete(key); },
|
|
13
|
-
clear: () => { map.clear(); },
|
|
14
|
-
key: (index) => [...map.keys()][index] ?? null,
|
|
15
|
-
get length() { return map.size; },
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
let storage;
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
storage = makeStorage();
|
|
21
|
-
vi.stubGlobal('localStorage', storage);
|
|
22
|
-
setMobileTheme('light');
|
|
23
|
-
document.documentElement.removeAttribute('data-theme');
|
|
24
|
-
});
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
vi.unstubAllGlobals();
|
|
27
|
-
});
|
|
28
|
-
describe('mobile-theme', () => {
|
|
29
|
-
it('defaults to light with nothing stored', () => {
|
|
30
|
-
expect(getMobileTheme()).toBe('light');
|
|
31
|
-
});
|
|
32
|
-
it('toggles to dark, persists it, and wires the document', () => {
|
|
33
|
-
expect(toggleMobileTheme()).toBe('dark');
|
|
34
|
-
expect(getMobileTheme()).toBe('dark');
|
|
35
|
-
expect(document.documentElement.dataset.theme).toBe('dark');
|
|
36
|
-
expect(storage.getItem(STORAGE_KEY)).toBe('dark');
|
|
37
|
-
});
|
|
38
|
-
it('toggles back to light', () => {
|
|
39
|
-
toggleMobileTheme();
|
|
40
|
-
expect(toggleMobileTheme()).toBe('light');
|
|
41
|
-
expect(document.documentElement.dataset.theme).toBe('light');
|
|
42
|
-
expect(storage.getItem(STORAGE_KEY)).toBe('light');
|
|
43
|
-
});
|
|
44
|
-
it('updates the browser chrome color along with the theme', () => {
|
|
45
|
-
const meta = document.createElement('meta');
|
|
46
|
-
meta.name = 'theme-color';
|
|
47
|
-
document.head.appendChild(meta);
|
|
48
|
-
setMobileTheme('dark');
|
|
49
|
-
expect(meta.content).toBe('#111418');
|
|
50
|
-
setMobileTheme('light');
|
|
51
|
-
expect(meta.content).toBe('#f3f5f9');
|
|
52
|
-
meta.remove();
|
|
53
|
-
});
|
|
54
|
-
it('boot reads the stored theme and initMobileTheme applies it', async () => {
|
|
55
|
-
storage.setItem(STORAGE_KEY, 'dark');
|
|
56
|
-
vi.resetModules();
|
|
57
|
-
const fresh = await import("./mobile-theme.js");
|
|
58
|
-
expect(fresh.getMobileTheme()).toBe('dark');
|
|
59
|
-
fresh.initMobileTheme();
|
|
60
|
-
expect(document.documentElement.dataset.theme).toBe('dark');
|
|
61
|
-
});
|
|
62
|
-
it('notifies subscribers on change', () => {
|
|
63
|
-
const seen = [];
|
|
64
|
-
const unsubscribe = subscribeMobileTheme(() => { seen.push(getMobileTheme()); });
|
|
65
|
-
setMobileTheme('dark');
|
|
66
|
-
setMobileTheme('light');
|
|
67
|
-
unsubscribe();
|
|
68
|
-
setMobileTheme('dark');
|
|
69
|
-
expect(seen).toEqual(['dark', 'light']);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mobile-surface live-event client: the plugin's `/m/api/events.mux` SSE
|
|
3
|
-
* channel (Server-Sent Events — the host bridges the mux stream onto it, so
|
|
4
|
-
* no WebSocket handshake or framing is needed on this side). The host
|
|
5
|
-
* pushes mux frames (subscribed baselines, session events, approvals,
|
|
6
|
-
* questions, queue snapshots, tasks, projections) as soon as the stream
|
|
7
|
-
* opens — no subscription handshake is needed. Frames arrive as
|
|
8
|
-
* server-request envelopes whose payload is the mux frame; unknown frame
|
|
9
|
-
* types are dropped so a newer host never breaks this client.
|
|
10
|
-
*
|
|
11
|
-
* EventSource reconnects automatically — but only over a tunnel that
|
|
12
|
-
* actually forwards frames. Public quick tunnels (Cloudflare quick tunnel /
|
|
13
|
-
* Tailscale Serve) do not transparently pass Server-Sent Events: ordinary
|
|
14
|
-
* HTTP works, yet the SSE connection stays open or reconnects with zero
|
|
15
|
-
* bytes, so no live frame ever arrives. That is a transport-layer limit of
|
|
16
|
-
* the tunnel, not something the host can fix. This client therefore
|
|
17
|
-
* degrades gracefully: once the SSE channel has silently stalled (no frame
|
|
18
|
-
* for {@link MuxClientOptions.stallThresholdMs}, or the EventSource reports
|
|
19
|
-
* an error), it starts polling the open session's history over plain HTTP
|
|
20
|
-
* (the `/m/api/session.history` RPC — unaffected by the SSE limitation),
|
|
21
|
-
* and re-emits freshly appended events as `session/event` frames through
|
|
22
|
-
* the same subscriber contract, so listeners (and the message fold) behave
|
|
23
|
-
* exactly as if the frames had arrived over SSE. When the SSE channel
|
|
24
|
-
* delivers again, fallback polling stops and the live stream takes over.
|
|
25
|
-
*/
|
|
26
|
-
import type { MuxFrame } from '@deepseek-ai/dsh-host-apiproxy/api/events';
|
|
27
|
-
import { type HistoryPage } from './api.ts';
|
|
28
|
-
/** Injectable seams for tests. */
|
|
29
|
-
export interface MuxClientOptions {
|
|
30
|
-
/** EventSource factory (defaults to the browser EventSource). */
|
|
31
|
-
sourceFactory?: (url: string) => EventSourceLike;
|
|
32
|
-
/**
|
|
33
|
-
* Fetch one history page (tail) for a session — the polling fallback's
|
|
34
|
-
* data source. Defaults to the mobile `session.history` RPC, which rides
|
|
35
|
-
* the ordinary HTTP channel (unaffected by SSE-impairing tunnels).
|
|
36
|
-
*/
|
|
37
|
-
pollLatest?: (sessionId: string) => Promise<HistoryPage>;
|
|
38
|
-
/** Initial poll cadence while SSE is stalled (default 3000 ms). Empty polls back off to 60000 ms. */
|
|
39
|
-
pollIntervalMs?: number;
|
|
40
|
-
/** Initial SSE stall window (default 12000 ms); a previously-live stream gets three windows before fallback. */
|
|
41
|
-
stallThresholdMs?: number;
|
|
42
|
-
/** Clock seam for tests (defaults to Date.now). */
|
|
43
|
-
now?: () => number;
|
|
44
|
-
}
|
|
45
|
-
/** The EventSource subset this client uses (browser EventSource fits). */
|
|
46
|
-
export interface EventSourceLike {
|
|
47
|
-
onmessage: ((event: {
|
|
48
|
-
data: string;
|
|
49
|
-
}) => void) | null;
|
|
50
|
-
onerror: ((event: unknown) => void) | null;
|
|
51
|
-
close(): void;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Keep one SSE subscription open, fanning validated frames out to
|
|
55
|
-
* subscribers. EventSource owns reconnection (with its own backoff); this
|
|
56
|
-
* class only manages the subscription lifecycle, plus a polling fallback
|
|
57
|
-
* that keeps the open session live when the SSE channel cannot deliver.
|
|
58
|
-
*/
|
|
59
|
-
export declare class MuxClient {
|
|
60
|
-
private readonly sourceFactory;
|
|
61
|
-
private readonly pollLatest;
|
|
62
|
-
private readonly pollIntervalMs;
|
|
63
|
-
private readonly stallThresholdMs;
|
|
64
|
-
private readonly now;
|
|
65
|
-
private readonly listeners;
|
|
66
|
-
private source;
|
|
67
|
-
private stopped;
|
|
68
|
-
private readonly url;
|
|
69
|
-
/** The session to keep live via fallback polling (undefined = none). */
|
|
70
|
-
private observeSessionId;
|
|
71
|
-
/** Last epoch ms the SSE channel produced a frame (or the stream opened). */
|
|
72
|
-
private lastDataAt;
|
|
73
|
-
/**
|
|
74
|
-
* Whether the SSE channel has ever delivered a frame in this stream (a
|
|
75
|
-
* delivered frame proves the tunnel can forward SSE; sustained silence may
|
|
76
|
-
* still mean a suspended mobile tunnel, so polling re-arms after 3 windows).
|
|
77
|
-
*/
|
|
78
|
-
private sseAlive;
|
|
79
|
-
/** Per-session highest event seq already emitted, for poll dedup. */
|
|
80
|
-
private readonly pollWatermark;
|
|
81
|
-
/** Single scheduler tick: both the stall check and the poll cadence ride this one interval. */
|
|
82
|
-
private tickTimer;
|
|
83
|
-
private polling;
|
|
84
|
-
/** Epoch ms of the next due poll while polling (kept on the same tick timer). */
|
|
85
|
-
private nextPollAt;
|
|
86
|
-
/** Adaptive delay: productive polls reset it; empty/error polls add one base interval up to one minute. */
|
|
87
|
-
private pollDelayMs;
|
|
88
|
-
/**
|
|
89
|
-
* @param url - the mobile events endpoint (browser-relative).
|
|
90
|
-
* @param options - seams.
|
|
91
|
-
*/
|
|
92
|
-
constructor(url?: string, options?: MuxClientOptions);
|
|
93
|
-
/** Open the stream (idempotent; EventSource reconnects until {@link stop}). */
|
|
94
|
-
start(): void;
|
|
95
|
-
/** Close for good. */
|
|
96
|
-
stop(): void;
|
|
97
|
-
/** Subscribe to validated frames; returns an unsubscribe function. */
|
|
98
|
-
onFrame(listener: (frame: MuxFrame) => void): () => void;
|
|
99
|
-
/**
|
|
100
|
-
* Point the fallback at one open session (or `undefined` to stop it).
|
|
101
|
-
* While the SSE channel is stalled this client polls that session's
|
|
102
|
-
* history and re-emits new events as `session/event` frames.
|
|
103
|
-
*/
|
|
104
|
-
observe(sessionId: string | undefined): void;
|
|
105
|
-
private connect;
|
|
106
|
-
/**
|
|
107
|
-
* The single scheduler tick is the only interval this client owns. One
|
|
108
|
-
* timer (at the finer of the stall-check and poll cadences) both arms the
|
|
109
|
-
* polling fallback once the stall threshold passes and drives each poll at
|
|
110
|
-
* {@link MuxClientOptions.pollIntervalMs} — one timer instead of two.
|
|
111
|
-
*/
|
|
112
|
-
private startTick;
|
|
113
|
-
private stopTick;
|
|
114
|
-
private tick;
|
|
115
|
-
private isSseStalled;
|
|
116
|
-
private startPolling;
|
|
117
|
-
private stopPolling;
|
|
118
|
-
/**
|
|
119
|
-
* Fetch the latest history page for the observed session and re-emit any
|
|
120
|
-
* event above the per-session watermark as a `session/event` frame.
|
|
121
|
-
* Idempotent by seq: listeners (and the fold) never see a duplicate.
|
|
122
|
-
*/
|
|
123
|
-
private pollTick;
|
|
124
|
-
private handleMessage;
|
|
125
|
-
private emit;
|
|
126
|
-
private closeSource;
|
|
127
|
-
}
|
|
128
|
-
//# sourceMappingURL=mux.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mux.d.ts","sourceRoot":"","sources":["../../../src/mobile/mux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2CAA2C,CAAA;AAGzE,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAEpE,kCAAkC;AAClC,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,eAAe,CAAA;IAChD;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IACxD,qGAAqG;IACrG,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,gHAAgH;IAChH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IACrD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IAC1C,KAAK,IAAI,IAAI,CAAA;CACd;AAyBD;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6C;IACxE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuC;IACjE,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAE5B,wEAAwE;IACxE,OAAO,CAAC,gBAAgB,CAAoB;IAC5C,6EAA6E;IAC7E,OAAO,CAAC,UAAU,CAAI;IACtB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAQ;IACxB,qEAAqE;IACrE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4B;IAC1D,+FAA+F;IAC/F,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,OAAO,CAAQ;IACvB,iFAAiF;IACjF,OAAO,CAAC,UAAU,CAAI;IACtB,2GAA2G;IAC3G,OAAO,CAAC,WAAW,CAAQ;IAE3B;;;OAGG;gBACS,GAAG,SAAsB,EAAE,OAAO,GAAE,gBAAqB;IAUrE,+EAA+E;IAC/E,KAAK,IAAI,IAAI;IAOb,sBAAsB;IACtB,IAAI,IAAI,IAAI;IASZ,sEAAsE;IACtE,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxD;;;;OAIG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAU5C,OAAO,CAAC,OAAO;IAqBf;;;;;OAKG;IACH,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,IAAI;IAgBZ,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,WAAW;IAMnB;;;;OAIG;YACW,QAAQ;IAsCtB,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,IAAI;IAUZ,OAAO,CAAC,WAAW;CAapB"}
|
package/lib/types/mobile/mux.js
DELETED
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mobile-surface live-event client: the plugin's `/m/api/events.mux` SSE
|
|
3
|
-
* channel (Server-Sent Events — the host bridges the mux stream onto it, so
|
|
4
|
-
* no WebSocket handshake or framing is needed on this side). The host
|
|
5
|
-
* pushes mux frames (subscribed baselines, session events, approvals,
|
|
6
|
-
* questions, queue snapshots, tasks, projections) as soon as the stream
|
|
7
|
-
* opens — no subscription handshake is needed. Frames arrive as
|
|
8
|
-
* server-request envelopes whose payload is the mux frame; unknown frame
|
|
9
|
-
* types are dropped so a newer host never breaks this client.
|
|
10
|
-
*
|
|
11
|
-
* EventSource reconnects automatically — but only over a tunnel that
|
|
12
|
-
* actually forwards frames. Public quick tunnels (Cloudflare quick tunnel /
|
|
13
|
-
* Tailscale Serve) do not transparently pass Server-Sent Events: ordinary
|
|
14
|
-
* HTTP works, yet the SSE connection stays open or reconnects with zero
|
|
15
|
-
* bytes, so no live frame ever arrives. That is a transport-layer limit of
|
|
16
|
-
* the tunnel, not something the host can fix. This client therefore
|
|
17
|
-
* degrades gracefully: once the SSE channel has silently stalled (no frame
|
|
18
|
-
* for {@link MuxClientOptions.stallThresholdMs}, or the EventSource reports
|
|
19
|
-
* an error), it starts polling the open session's history over plain HTTP
|
|
20
|
-
* (the `/m/api/session.history` RPC — unaffected by the SSE limitation),
|
|
21
|
-
* and re-emits freshly appended events as `session/event` frames through
|
|
22
|
-
* the same subscriber contract, so listeners (and the message fold) behave
|
|
23
|
-
* exactly as if the frames had arrived over SSE. When the SSE channel
|
|
24
|
-
* delivers again, fallback polling stops and the live stream takes over.
|
|
25
|
-
*/
|
|
26
|
-
import { muxFrameSchema } from '@deepseek-ai/dsh-host-apiproxy/api/events.schema';
|
|
27
|
-
import { serverRequestSchema } from '@deepseek-ai/dsh-host-apiproxy/api/rpc.schema';
|
|
28
|
-
import { history as fetchHistory } from "./api.js";
|
|
29
|
-
/** Browser default source factory. */
|
|
30
|
-
function browserSource(url) {
|
|
31
|
-
// The DOM EventSource is structurally compatible; the `this`-typed handler
|
|
32
|
-
// signatures differ, so the narrow face takes it through an adapter cast.
|
|
33
|
-
return new EventSource(url);
|
|
34
|
-
}
|
|
35
|
-
const DEFAULT_POLL_INTERVAL_MS = 3000;
|
|
36
|
-
const DEFAULT_STALL_THRESHOLD_MS = 12000;
|
|
37
|
-
const LIVE_SSE_STALL_MULTIPLIER = 3;
|
|
38
|
-
const MAX_POLL_BACKOFF_MS = 60000;
|
|
39
|
-
/**
|
|
40
|
-
* Stall-check granularity: the single scheduler tick runs at least this
|
|
41
|
-
* often so fallback arms within a second of the stall threshold passing,
|
|
42
|
-
* while the poll cadence itself stays {@link MuxClientOptions.pollIntervalMs}.
|
|
43
|
-
*/
|
|
44
|
-
const STALL_CHECK_MS = 1000;
|
|
45
|
-
/** Poll window: enough recent events to cover a few seconds of agent output. */
|
|
46
|
-
const DEFAULT_POLL_PAGE_SIZE = 50;
|
|
47
|
-
/**
|
|
48
|
-
* Keep one SSE subscription open, fanning validated frames out to
|
|
49
|
-
* subscribers. EventSource owns reconnection (with its own backoff); this
|
|
50
|
-
* class only manages the subscription lifecycle, plus a polling fallback
|
|
51
|
-
* that keeps the open session live when the SSE channel cannot deliver.
|
|
52
|
-
*/
|
|
53
|
-
export class MuxClient {
|
|
54
|
-
sourceFactory;
|
|
55
|
-
pollLatest;
|
|
56
|
-
pollIntervalMs;
|
|
57
|
-
stallThresholdMs;
|
|
58
|
-
now;
|
|
59
|
-
listeners = new Set();
|
|
60
|
-
source;
|
|
61
|
-
stopped = false;
|
|
62
|
-
url;
|
|
63
|
-
/** The session to keep live via fallback polling (undefined = none). */
|
|
64
|
-
observeSessionId;
|
|
65
|
-
/** Last epoch ms the SSE channel produced a frame (or the stream opened). */
|
|
66
|
-
lastDataAt = 0;
|
|
67
|
-
/**
|
|
68
|
-
* Whether the SSE channel has ever delivered a frame in this stream (a
|
|
69
|
-
* delivered frame proves the tunnel can forward SSE; sustained silence may
|
|
70
|
-
* still mean a suspended mobile tunnel, so polling re-arms after 3 windows).
|
|
71
|
-
*/
|
|
72
|
-
sseAlive = false;
|
|
73
|
-
/** Per-session highest event seq already emitted, for poll dedup. */
|
|
74
|
-
pollWatermark = new Map();
|
|
75
|
-
/** Single scheduler tick: both the stall check and the poll cadence ride this one interval. */
|
|
76
|
-
tickTimer;
|
|
77
|
-
polling = false;
|
|
78
|
-
/** Epoch ms of the next due poll while polling (kept on the same tick timer). */
|
|
79
|
-
nextPollAt = 0;
|
|
80
|
-
/** Adaptive delay: productive polls reset it; empty/error polls add one base interval up to one minute. */
|
|
81
|
-
pollDelayMs;
|
|
82
|
-
/**
|
|
83
|
-
* @param url - the mobile events endpoint (browser-relative).
|
|
84
|
-
* @param options - seams.
|
|
85
|
-
*/
|
|
86
|
-
constructor(url = '/m/api/events.mux', options = {}) {
|
|
87
|
-
this.url = url;
|
|
88
|
-
this.sourceFactory = options.sourceFactory ?? browserSource;
|
|
89
|
-
this.pollLatest = options.pollLatest ?? ((sessionId) => fetchHistory(sessionId, undefined, DEFAULT_POLL_PAGE_SIZE));
|
|
90
|
-
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
91
|
-
this.pollDelayMs = this.pollIntervalMs;
|
|
92
|
-
this.stallThresholdMs = options.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS;
|
|
93
|
-
this.now = options.now ?? (() => Date.now());
|
|
94
|
-
}
|
|
95
|
-
/** Open the stream (idempotent; EventSource reconnects until {@link stop}). */
|
|
96
|
-
start() {
|
|
97
|
-
this.stopped = false;
|
|
98
|
-
this.lastDataAt = this.now();
|
|
99
|
-
if (this.source === undefined)
|
|
100
|
-
this.connect();
|
|
101
|
-
this.startTick();
|
|
102
|
-
}
|
|
103
|
-
/** Close for good. */
|
|
104
|
-
stop() {
|
|
105
|
-
this.stopped = true;
|
|
106
|
-
this.stopTick();
|
|
107
|
-
this.stopPolling();
|
|
108
|
-
this.closeSource();
|
|
109
|
-
this.observeSessionId = undefined;
|
|
110
|
-
this.nextPollAt = 0;
|
|
111
|
-
}
|
|
112
|
-
/** Subscribe to validated frames; returns an unsubscribe function. */
|
|
113
|
-
onFrame(listener) {
|
|
114
|
-
this.listeners.add(listener);
|
|
115
|
-
return () => { this.listeners.delete(listener); };
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Point the fallback at one open session (or `undefined` to stop it).
|
|
119
|
-
* While the SSE channel is stalled this client polls that session's
|
|
120
|
-
* history and re-emits new events as `session/event` frames.
|
|
121
|
-
*/
|
|
122
|
-
observe(sessionId) {
|
|
123
|
-
this.observeSessionId = sessionId;
|
|
124
|
-
if (sessionId === undefined) {
|
|
125
|
-
this.stopPolling();
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
// If SSE is already stalled for this session, start patching right away.
|
|
129
|
-
if (!this.polling && !this.stopped && this.isSseStalled())
|
|
130
|
-
this.startPolling();
|
|
131
|
-
}
|
|
132
|
-
connect() {
|
|
133
|
-
// A fresh stream starts unknown; only a delivered frame proves it works.
|
|
134
|
-
this.sseAlive = false;
|
|
135
|
-
const source = this.sourceFactory(this.url);
|
|
136
|
-
this.source = source;
|
|
137
|
-
source.onmessage = (event) => {
|
|
138
|
-
this.handleMessage(event.data);
|
|
139
|
-
};
|
|
140
|
-
source.onerror = () => {
|
|
141
|
-
// EventSource reconnects by itself; when we are closing, detach first
|
|
142
|
-
// so the native reconnect cannot outlive stop(). Otherwise an error is
|
|
143
|
-
// a strong signal the transport is not delivering — degrade to polling.
|
|
144
|
-
if (this.stopped && this.source === source) {
|
|
145
|
-
this.closeSource();
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
this.sseAlive = false;
|
|
149
|
-
if (this.observeSessionId !== undefined)
|
|
150
|
-
this.startPolling();
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* The single scheduler tick is the only interval this client owns. One
|
|
155
|
-
* timer (at the finer of the stall-check and poll cadences) both arms the
|
|
156
|
-
* polling fallback once the stall threshold passes and drives each poll at
|
|
157
|
-
* {@link MuxClientOptions.pollIntervalMs} — one timer instead of two.
|
|
158
|
-
*/
|
|
159
|
-
startTick() {
|
|
160
|
-
if (this.tickTimer !== undefined)
|
|
161
|
-
return;
|
|
162
|
-
const cadence = Math.min(this.pollIntervalMs, STALL_CHECK_MS);
|
|
163
|
-
this.tickTimer = setInterval(() => { this.tick(); }, cadence);
|
|
164
|
-
}
|
|
165
|
-
stopTick() {
|
|
166
|
-
if (this.tickTimer !== undefined) {
|
|
167
|
-
clearInterval(this.tickTimer);
|
|
168
|
-
this.tickTimer = undefined;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
tick() {
|
|
172
|
-
if (this.stopped)
|
|
173
|
-
return;
|
|
174
|
-
if (this.observeSessionId === undefined)
|
|
175
|
-
return;
|
|
176
|
-
if (this.polling) {
|
|
177
|
-
// The stall phase has ended; the same tick now paces the adaptive polls.
|
|
178
|
-
if (this.now() >= this.nextPollAt) {
|
|
179
|
-
// pollTick schedules the next run after it settles, so slow requests
|
|
180
|
-
// cannot overlap with another scheduler tick.
|
|
181
|
-
this.nextPollAt = Number.POSITIVE_INFINITY;
|
|
182
|
-
void this.pollTick();
|
|
183
|
-
}
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
if (this.isSseStalled())
|
|
187
|
-
this.startPolling();
|
|
188
|
-
}
|
|
189
|
-
isSseStalled() {
|
|
190
|
-
const windowMs = this.sseAlive
|
|
191
|
-
? this.stallThresholdMs * LIVE_SSE_STALL_MULTIPLIER
|
|
192
|
-
: this.stallThresholdMs;
|
|
193
|
-
return (this.now() - this.lastDataAt) > windowMs;
|
|
194
|
-
}
|
|
195
|
-
startPolling() {
|
|
196
|
-
if (this.polling || this.stopped)
|
|
197
|
-
return;
|
|
198
|
-
this.polling = true;
|
|
199
|
-
this.pollDelayMs = this.pollIntervalMs;
|
|
200
|
-
this.nextPollAt = Number.POSITIVE_INFINITY;
|
|
201
|
-
void this.pollTick();
|
|
202
|
-
}
|
|
203
|
-
stopPolling() {
|
|
204
|
-
this.polling = false;
|
|
205
|
-
this.pollDelayMs = this.pollIntervalMs;
|
|
206
|
-
this.nextPollAt = 0;
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Fetch the latest history page for the observed session and re-emit any
|
|
210
|
-
* event above the per-session watermark as a `session/event` frame.
|
|
211
|
-
* Idempotent by seq: listeners (and the fold) never see a duplicate.
|
|
212
|
-
*/
|
|
213
|
-
async pollTick() {
|
|
214
|
-
const sessionId = this.observeSessionId;
|
|
215
|
-
if (sessionId === undefined) {
|
|
216
|
-
this.stopPolling();
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
let emitted = 0;
|
|
220
|
-
try {
|
|
221
|
-
const page = await this.pollLatest(sessionId);
|
|
222
|
-
let maxSeq = this.pollWatermark.get(sessionId) ?? -1;
|
|
223
|
-
const ordered = [...page.events].sort((left, right) => {
|
|
224
|
-
const leftSeq = typeof left.event?.seq === 'number' ? left.event.seq : -1;
|
|
225
|
-
const rightSeq = typeof right.event?.seq === 'number' ? right.event.seq : -1;
|
|
226
|
-
return leftSeq - rightSeq;
|
|
227
|
-
});
|
|
228
|
-
for (const entry of ordered) {
|
|
229
|
-
const event = entry.event;
|
|
230
|
-
const seq = typeof event?.seq === 'number' ? event.seq : -1;
|
|
231
|
-
if (seq <= maxSeq)
|
|
232
|
-
continue;
|
|
233
|
-
maxSeq = seq;
|
|
234
|
-
emitted += 1;
|
|
235
|
-
this.emit({ type: 'session/event', sessionId: sessionId, event });
|
|
236
|
-
}
|
|
237
|
-
this.pollWatermark.set(sessionId, maxSeq);
|
|
238
|
-
}
|
|
239
|
-
catch {
|
|
240
|
-
// Transient (network, pairing, history paging); retry with backoff.
|
|
241
|
-
}
|
|
242
|
-
finally {
|
|
243
|
-
if (emitted > 0) {
|
|
244
|
-
this.pollDelayMs = this.pollIntervalMs;
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
this.pollDelayMs = Math.min(MAX_POLL_BACKOFF_MS, this.pollDelayMs + this.pollIntervalMs);
|
|
248
|
-
}
|
|
249
|
-
if (this.polling && this.observeSessionId === sessionId) {
|
|
250
|
-
this.nextPollAt = this.now() + this.pollDelayMs;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
handleMessage(data) {
|
|
255
|
-
if (typeof data !== 'string' || data === '')
|
|
256
|
-
return;
|
|
257
|
-
let parsed;
|
|
258
|
-
try {
|
|
259
|
-
parsed = JSON.parse(data);
|
|
260
|
-
}
|
|
261
|
-
catch {
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
// The SSE channel carries server-request envelopes whose payload is the
|
|
265
|
-
// mux frame (same wire shape as the desktop mux channel).
|
|
266
|
-
const envelope = serverRequestSchema.safeParse(parsed);
|
|
267
|
-
if (!envelope.success)
|
|
268
|
-
return;
|
|
269
|
-
const frame = muxFrameSchema.safeParse(envelope.data.payload);
|
|
270
|
-
if (!frame.success)
|
|
271
|
-
return;
|
|
272
|
-
// A delivered frame proves the SSE channel is live (the tunnel forwards
|
|
273
|
-
// it) and delivers again — drop any fallback polling so the live stream
|
|
274
|
-
// takes over without double delivery.
|
|
275
|
-
this.sseAlive = true;
|
|
276
|
-
this.lastDataAt = this.now();
|
|
277
|
-
if (this.polling)
|
|
278
|
-
this.stopPolling();
|
|
279
|
-
this.emit(frame.data);
|
|
280
|
-
}
|
|
281
|
-
emit(frame) {
|
|
282
|
-
for (const listener of this.listeners) {
|
|
283
|
-
try {
|
|
284
|
-
listener(frame);
|
|
285
|
-
}
|
|
286
|
-
catch {
|
|
287
|
-
// A throwing subscriber must not break the emit loop.
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
closeSource() {
|
|
292
|
-
const source = this.source;
|
|
293
|
-
this.source = undefined;
|
|
294
|
-
if (source !== undefined) {
|
|
295
|
-
source.onmessage = null;
|
|
296
|
-
source.onerror = null;
|
|
297
|
-
try {
|
|
298
|
-
source.close();
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
// Already closed.
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mux.test.d.ts","sourceRoot":"","sources":["../../../src/mobile/mux.test.ts"],"names":[],"mappings":""}
|