oysterun 1.5.7-beta.2 → 1.5.7-beta.3
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 +5 -3
- package/bin/oysterun.mjs +3 -0
- package/dev/client/connectivity/data-channel-adapter.mjs +146 -0
- package/dev/client/connectivity/host-connection-feedback.mjs +193 -0
- package/dev/client/connectivity/logical-host-connection.mjs +427 -0
- package/dev/client/connectivity/managed-host-browser.d.mts +50 -0
- package/dev/client/connectivity/managed-host-browser.mjs +1929 -0
- package/dev/client/connectivity/random-id.mjs +28 -0
- package/dev/client/connectivity/webrtc-frame.mjs +87 -0
- package/dev/client/connectivity/webrtc-logical-transport.mjs +576 -0
- package/dev/client/web/index.html +6230 -642
- package/dev/client/web-chat/dist/assets/{ReactPrism-pByE_IEa.js → ReactPrism-DXWjWRSc.js} +1 -1
- package/dev/client/web-chat/dist/assets/ReactPrism-DXWjWRSc.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{index-AdSozXW7.js → index-6PAeuTVW.js} +2 -2
- package/dev/client/web-chat/dist/assets/index-6PAeuTVW.js.map +1 -0
- package/dev/client/web-chat/dist/assets/index-B9fWrEk-.js.map +1 -1
- package/dev/client/web-chat/dist/assets/{index-BNznPZkv.js → index-CMTuzF_U.js} +1 -1
- package/dev/client/web-chat/dist/assets/index-CMTuzF_U.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{index-BFaINJYC.css → index-DyTV0z1z.css} +1 -1
- package/dev/client/web-chat/dist/assets/{index-BjpMgToA.js → index-c-mXm6Vw.js} +83 -83
- package/dev/client/web-chat/dist/assets/index-c-mXm6Vw.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{pdf-cTY9JS8Q.js → pdf-DCR-4G9y.js} +3 -3
- package/dev/client/web-chat/dist/assets/pdf-DCR-4G9y.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{web-CQIq3Kfx.js → web-dsaC3bqP.js} +1 -1
- package/dev/client/web-chat/dist/assets/web-dsaC3bqP.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{web-CpdV1b9l.js → web-rNZ6ony7.js} +1 -1
- package/dev/client/web-chat/dist/assets/web-rNZ6ony7.js.map +1 -0
- package/dev/client/web-chat/dist/index.html +2 -2
- package/docs/routec/config_hierachy.md +21 -2
- package/docs/routec/dev/cloud_deplyments.md +228 -18
- package/docs/routec/dev/npm_native_dependency_install.md +33 -14
- package/docs/routec/dev/npmjs_publish.md +81 -15
- package/docs/routec/dev/provider_startup_diagnostics.md +20 -11
- package/docs/routec/form_UI_fields.md +42 -4
- package/docs/routec/host2_turn_started_idempotency_diagnostics.md +140 -0
- package/docs/routec/host_cloud_app_auth.md +5 -0
- package/docs/routec/host_setup.md +94 -64
- package/docs/routec/oysterun_operation_interface_spec.md +20 -0
- package/docs/routec/provider_contract.md +30 -10
- package/host-service/adapters/claude-code-adapter.mjs +194 -38
- package/host-service/adapters/codex-app-server-adapter.mjs +15 -0
- package/host-service/adapters/debug-fixture-provider-adapter.mjs +35 -4
- package/host-service/adapters/debug-large-tool-spillover-provider-adapter.mjs +35 -4
- package/host-service/adapters/debug-p135-codex-replay-provider-adapter.mjs +35 -4
- package/host-service/adapters/debug-routec-structural-replay-provider-adapter.mjs +27 -1
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-sessions/SKILL.md +5 -1
- package/host-service/assets/product-skills/oysterun-sessions/SKILL.md +5 -1
- package/host-service/browser-root-project-index.mjs +63 -6
- package/host-service/cli/product-cli.mjs +53 -32
- package/host-service/config.mjs +132 -1
- package/host-service/connectivity/data-channel-adapter.mjs +1 -0
- package/host-service/connectivity/logical-host-connection.mjs +1 -0
- package/host-service/connectivity/managed-tunnel-runtime-status.mjs +101 -0
- package/host-service/connectivity/node-webrtc-runtime.mjs +181 -0
- package/host-service/connectivity/random-id.mjs +1 -0
- package/host-service/connectivity/webrtc-frame.mjs +1 -0
- package/host-service/connectivity/webrtc-host-dispatcher.mjs +171 -0
- package/host-service/connectivity/webrtc-logical-transport.mjs +1 -0
- package/host-service/connectivity/webrtc-signaling-service.mjs +1394 -0
- package/host-service/frp-agent.mjs +861 -409
- package/host-service/host-login-bootstrap-tokens.mjs +45 -0
- package/host-service/matrix-event-correlator.mjs +199 -7
- package/host-service/matrix-facade.mjs +454 -59
- package/host-service/matrix-read-authority.mjs +48 -44
- package/host-service/matrix-transcript-read-adapter.mjs +22 -39
- package/host-service/package.json +17 -4
- package/host-service/provider-registry.mjs +10 -3
- package/host-service/provider-resume-contract.mjs +95 -0
- package/host-service/provider-startup-preflight.mjs +60 -15
- package/host-service/routec-host2-intake-diagnostics.mjs +146 -0
- package/host-service/routec-matrix-media-store.mjs +91 -3
- package/host-service/routec-matrix-sqlite-store.mjs +561 -149
- package/host-service/routec-matrix-storage-adapter.mjs +305 -371
- package/host-service/routec-matrix-stream-seq-repair.mjs +554 -0
- package/host-service/scheduler-service.mjs +68 -15
- package/host-service/server.mjs +975 -164
- package/host-service/session-history.mjs +2 -0
- package/host-service/session-lifecycle-snapshot.mjs +162 -0
- package/host-service/session-loop-definitions.mjs +1605 -112
- package/host-service/session-manager.mjs +271 -71
- package/host-service/setup.mjs +611 -105
- package/host-service/telegram-adapter.mjs +267 -0
- package/host-service/telegram-bridge.mjs +1061 -15
- package/host-service/telegram-media.mjs +319 -0
- package/package.json +6 -3
- package/static/site/assets/site.css +1213 -2
- package/static/site/content/AGENTS.md +150 -0
- package/static/site/content/blog/drafts/20260718-24hr-agent-team/assets/article.css +52 -0
- package/static/site/content/blog/drafts/20260718-24hr-agent-team/content.json +16 -0
- package/static/site/content/blog/drafts/20260718-24hr-agent-team/zh/index.html +35 -0
- package/static/site/content/blog/drafts/voice-input-model-experiment/assets/audio/A1.wav +0 -0
- package/static/site/content/blog/drafts/voice-input-model-experiment/assets/audio/A4.wav +0 -0
- package/static/site/content/blog/drafts/voice-input-model-experiment/assets/audio/B3.wav +0 -0
- package/static/site/content/blog/drafts/voice-input-model-experiment/assets/audio/C2.wav +0 -0
- package/static/site/content/blog/drafts/voice-input-model-experiment/assets/audio/D3.wav +0 -0
- package/static/site/content/blog/drafts/voice-input-model-experiment/assets/audio/G3.wav +0 -0
- package/static/site/content/blog/drafts/voice-input-model-experiment/assets/oysterun_logo_v5.PNG +0 -0
- package/static/site/content/blog/drafts/voice-input-model-experiment/assets/site.css +2436 -0
- package/static/site/content/blog/drafts/voice-input-model-experiment/content.json +21 -0
- package/static/site/content/blog/drafts/voice-input-model-experiment/en/index.html +820 -0
- package/static/site/content/blog/drafts/voice-input-model-experiment/zh/index.html +1665 -0
- package/static/site/content/tutorials/drafts/getting-started/assets/AppStore.png +0 -0
- package/static/site/content/tutorials/drafts/getting-started/assets/article.css +66 -0
- package/static/site/content/tutorials/drafts/getting-started/assets/chat.png +0 -0
- package/static/site/content/tutorials/drafts/getting-started/content.json +16 -0
- package/static/site/content/tutorials/drafts/getting-started/en/index.html +161 -0
- package/tool_scripts/oysterun_npm_package_manifest.mjs +88 -0
- package/tool_scripts/oysterun_release_receipt_lib.mjs +47 -0
- package/tool_scripts/verify_oysterun_package_layout.mjs +172 -0
- package/tool_scripts/verify_oysterun_release_receipt.mjs +96 -0
- package/tool_scripts/verify_oysterun_runtime.mjs +348 -8
- package/dev/client/web-chat/dist/assets/ReactPrism-pByE_IEa.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-AdSozXW7.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-BNznPZkv.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-BjpMgToA.js.map +0 -1
- package/dev/client/web-chat/dist/assets/pdf-cTY9JS8Q.js.map +0 -1
- package/dev/client/web-chat/dist/assets/web-CQIq3Kfx.js.map +0 -1
- package/dev/client/web-chat/dist/assets/web-CpdV1b9l.js.map +0 -1
package/README.md
CHANGED
|
@@ -248,8 +248,9 @@ npm start
|
|
|
248
248
|
Useful commands:
|
|
249
249
|
|
|
250
250
|
```bash
|
|
251
|
-
npm run setup # first-run
|
|
252
|
-
|
|
251
|
+
npm run setup # first-run Managed Tunnel Host setup
|
|
252
|
+
node host-service/setup.mjs # source-local Direct Host setup
|
|
253
|
+
node bin/oysterun.mjs setup --local # explicit local-only Direct setup
|
|
253
254
|
npm start # plain start, no watch mode
|
|
254
255
|
npm run dev # watch mode for Host JS files
|
|
255
256
|
npm run cli # local CLI client
|
|
@@ -272,7 +273,8 @@ Port notes:
|
|
|
272
273
|
- direct mode can also store an optional `public_base_url` there for manual client connection
|
|
273
274
|
- fresh Session Setup runtime defaults, including interface style plus Claude/Codex provider defaults, are stored there under `session_defaults`
|
|
274
275
|
- Host Preferences shows the active config path, so `OYSTERUN_CONFIG_DIR` overrides are visible in the UI
|
|
275
|
-
-
|
|
276
|
+
- release setup defaults to Oysterun Managed Tunnel
|
|
277
|
+
- source-local setup and `oysterun setup --local` retain explicit Direct mode
|
|
276
278
|
|
|
277
279
|
### Optional: start backend + Host together for repo development
|
|
278
280
|
|
package/bin/oysterun.mjs
CHANGED
|
@@ -191,6 +191,9 @@ Product modules:
|
|
|
191
191
|
|
|
192
192
|
First run:
|
|
193
193
|
If Host config is missing or incomplete, 'oysterun' enters setup.
|
|
194
|
+
Release setup uses Oysterun Managed Tunnel by default.
|
|
195
|
+
Use 'oysterun setup --local' for local-only Direct Host setup.
|
|
196
|
+
Use '--enable-cloud-direct' only for advanced Direct Cloud registration.
|
|
194
197
|
|
|
195
198
|
Install contract:
|
|
196
199
|
npm install -g oysterun installs files only. It does not run setup or start
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
function toUint8Array(value) {
|
|
2
|
+
if (value instanceof Uint8Array) return value;
|
|
3
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
4
|
+
if (globalThis.Buffer?.isBuffer(value)) {
|
|
5
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
6
|
+
}
|
|
7
|
+
throw new Error("DataChannel message must be binary");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function adaptBrowserDataChannel(channel) {
|
|
11
|
+
const messageListeners = new Set();
|
|
12
|
+
const openListeners = new Set();
|
|
13
|
+
const bufferedLowListeners = new Set();
|
|
14
|
+
const closeListeners = new Set();
|
|
15
|
+
const errorListeners = new Set();
|
|
16
|
+
channel.binaryType = "arraybuffer";
|
|
17
|
+
|
|
18
|
+
channel.addEventListener("message", (event) => {
|
|
19
|
+
try {
|
|
20
|
+
const bytes = toUint8Array(event.data);
|
|
21
|
+
for (const listener of messageListeners) listener(bytes);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
for (const listener of errorListeners) listener(error);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
channel.addEventListener("open", () => {
|
|
27
|
+
for (const listener of openListeners) listener();
|
|
28
|
+
});
|
|
29
|
+
channel.addEventListener("bufferedamountlow", () => {
|
|
30
|
+
for (const listener of bufferedLowListeners) listener();
|
|
31
|
+
});
|
|
32
|
+
channel.addEventListener("close", () => {
|
|
33
|
+
for (const listener of closeListeners) listener();
|
|
34
|
+
});
|
|
35
|
+
channel.addEventListener("error", (event) => {
|
|
36
|
+
const error = new Error(event?.error?.message || "Browser DataChannel error");
|
|
37
|
+
for (const listener of errorListeners) listener(error);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
label: channel.label,
|
|
42
|
+
isOpen: () => channel.readyState === "open",
|
|
43
|
+
bufferedAmount: () => channel.bufferedAmount,
|
|
44
|
+
setBufferedAmountLowThreshold(bytes) {
|
|
45
|
+
channel.bufferedAmountLowThreshold = bytes;
|
|
46
|
+
},
|
|
47
|
+
sendBinary(bytes) {
|
|
48
|
+
channel.send(bytes);
|
|
49
|
+
return { buffered: channel.bufferedAmount > 0 };
|
|
50
|
+
},
|
|
51
|
+
onMessage(listener) {
|
|
52
|
+
messageListeners.add(listener);
|
|
53
|
+
return () => messageListeners.delete(listener);
|
|
54
|
+
},
|
|
55
|
+
onOpen(listener) {
|
|
56
|
+
openListeners.add(listener);
|
|
57
|
+
return () => openListeners.delete(listener);
|
|
58
|
+
},
|
|
59
|
+
onBufferedAmountLow(listener) {
|
|
60
|
+
bufferedLowListeners.add(listener);
|
|
61
|
+
return () => bufferedLowListeners.delete(listener);
|
|
62
|
+
},
|
|
63
|
+
onClose(listener) {
|
|
64
|
+
closeListeners.add(listener);
|
|
65
|
+
return () => closeListeners.delete(listener);
|
|
66
|
+
},
|
|
67
|
+
onError(listener) {
|
|
68
|
+
errorListeners.add(listener);
|
|
69
|
+
return () => errorListeners.delete(listener);
|
|
70
|
+
},
|
|
71
|
+
close() {
|
|
72
|
+
channel.close();
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function adaptNodeDataChannel(channel) {
|
|
78
|
+
const messageListeners = new Set();
|
|
79
|
+
const openListeners = new Set();
|
|
80
|
+
const bufferedLowListeners = new Set();
|
|
81
|
+
const closeListeners = new Set();
|
|
82
|
+
const errorListeners = new Set();
|
|
83
|
+
|
|
84
|
+
channel.onMessage((message) => {
|
|
85
|
+
if (typeof message === "string") {
|
|
86
|
+
for (const listener of errorListeners) {
|
|
87
|
+
listener(new Error("Oysterun transport rejects text DataChannel frames"));
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const bytes = toUint8Array(message);
|
|
92
|
+
for (const listener of messageListeners) listener(bytes);
|
|
93
|
+
});
|
|
94
|
+
channel.onOpen(() => {
|
|
95
|
+
for (const listener of openListeners) listener();
|
|
96
|
+
});
|
|
97
|
+
channel.onBufferedAmountLow(() => {
|
|
98
|
+
for (const listener of bufferedLowListeners) listener();
|
|
99
|
+
});
|
|
100
|
+
channel.onClosed(() => {
|
|
101
|
+
for (const listener of closeListeners) listener();
|
|
102
|
+
});
|
|
103
|
+
channel.onError((message) => {
|
|
104
|
+
const error = new Error(String(message));
|
|
105
|
+
for (const listener of errorListeners) listener(error);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
label: channel.getLabel(),
|
|
110
|
+
isOpen: () => channel.isOpen(),
|
|
111
|
+
bufferedAmount: () => channel.bufferedAmount(),
|
|
112
|
+
setBufferedAmountLowThreshold: (bytes) =>
|
|
113
|
+
channel.setBufferedAmountLowThreshold(bytes),
|
|
114
|
+
sendBinary(bytes) {
|
|
115
|
+
// False means the message was accepted into the native send buffer.
|
|
116
|
+
// Retrying on false duplicates the frame.
|
|
117
|
+
return {
|
|
118
|
+
buffered:
|
|
119
|
+
channel.sendMessageBinary(globalThis.Buffer.from(bytes)) === false,
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
onMessage(listener) {
|
|
123
|
+
messageListeners.add(listener);
|
|
124
|
+
return () => messageListeners.delete(listener);
|
|
125
|
+
},
|
|
126
|
+
onOpen(listener) {
|
|
127
|
+
openListeners.add(listener);
|
|
128
|
+
return () => openListeners.delete(listener);
|
|
129
|
+
},
|
|
130
|
+
onBufferedAmountLow(listener) {
|
|
131
|
+
bufferedLowListeners.add(listener);
|
|
132
|
+
return () => bufferedLowListeners.delete(listener);
|
|
133
|
+
},
|
|
134
|
+
onClose(listener) {
|
|
135
|
+
closeListeners.add(listener);
|
|
136
|
+
return () => closeListeners.delete(listener);
|
|
137
|
+
},
|
|
138
|
+
onError(listener) {
|
|
139
|
+
errorListeners.add(listener);
|
|
140
|
+
return () => errorListeners.delete(listener);
|
|
141
|
+
},
|
|
142
|
+
close() {
|
|
143
|
+
channel.close();
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
export const HOST_CONNECTION_RECONNECT_WARNING_DELAY_MS = 1_500;
|
|
2
|
+
export const HOST_CONNECTION_LOST_DELAY_MS = 30_000;
|
|
3
|
+
export const PAGE_REFRESH_PENDING_RESET_MS = 1_500;
|
|
4
|
+
|
|
5
|
+
function boundedString(value) {
|
|
6
|
+
return typeof value === "string" ? value.trim() : "";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeHostConnectionStatus(status = {}) {
|
|
10
|
+
const originKind =
|
|
11
|
+
status.origin_kind === "managed_endpoint"
|
|
12
|
+
? "managed_endpoint"
|
|
13
|
+
: "direct_host";
|
|
14
|
+
const activePathKind = boundedString(status.active_path_kind);
|
|
15
|
+
const unavailable =
|
|
16
|
+
status.closed === true ||
|
|
17
|
+
status.browser_online === false ||
|
|
18
|
+
status.managed_host_unavailable === true ||
|
|
19
|
+
status.last_request_failed === true ||
|
|
20
|
+
["closed", "disconnected", "recovering", "unavailable"].includes(
|
|
21
|
+
status.connection_state
|
|
22
|
+
);
|
|
23
|
+
let qualifier = "Direct Host";
|
|
24
|
+
if (originKind === "managed_endpoint") {
|
|
25
|
+
qualifier =
|
|
26
|
+
activePathKind === "webrtc" ? "Managed WebRTC" : "Managed Endpoint";
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
connected: !unavailable,
|
|
30
|
+
qualifier,
|
|
31
|
+
origin_kind: originKind,
|
|
32
|
+
active_path_kind: activePathKind || null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function connectionFeedbackPresentation({
|
|
37
|
+
signal,
|
|
38
|
+
disconnectedAt = null,
|
|
39
|
+
now = Date.now(),
|
|
40
|
+
} = {}) {
|
|
41
|
+
if (!signal || signal.connected === true) {
|
|
42
|
+
return {
|
|
43
|
+
visible: true,
|
|
44
|
+
state: "connected",
|
|
45
|
+
label: "Connected",
|
|
46
|
+
qualifier: signal?.qualifier || "",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const elapsedMs = Math.max(0, now - (disconnectedAt ?? now));
|
|
50
|
+
if (elapsedMs < HOST_CONNECTION_RECONNECT_WARNING_DELAY_MS) {
|
|
51
|
+
return {
|
|
52
|
+
visible: false,
|
|
53
|
+
state: "transient",
|
|
54
|
+
label: "",
|
|
55
|
+
qualifier: signal.qualifier,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (elapsedMs < HOST_CONNECTION_LOST_DELAY_MS) {
|
|
59
|
+
return {
|
|
60
|
+
visible: true,
|
|
61
|
+
state: "reconnecting",
|
|
62
|
+
label: "Reconnecting...",
|
|
63
|
+
qualifier: signal.qualifier,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
visible: true,
|
|
68
|
+
state: "lost",
|
|
69
|
+
label: "Connection Lost. Still retrying.",
|
|
70
|
+
qualifier: signal.qualifier,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function presentationKey(presentation) {
|
|
75
|
+
return JSON.stringify([
|
|
76
|
+
presentation.visible,
|
|
77
|
+
presentation.state,
|
|
78
|
+
presentation.label,
|
|
79
|
+
presentation.qualifier,
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createHostConnectionFeedbackController({
|
|
84
|
+
onPresentation,
|
|
85
|
+
now = () => Date.now(),
|
|
86
|
+
setTimeoutImpl = globalThis.setTimeout.bind(globalThis),
|
|
87
|
+
clearTimeoutImpl = globalThis.clearTimeout.bind(globalThis),
|
|
88
|
+
} = {}) {
|
|
89
|
+
if (typeof onPresentation !== "function") {
|
|
90
|
+
throw new Error("Host connection feedback listener is required");
|
|
91
|
+
}
|
|
92
|
+
let signal = null;
|
|
93
|
+
let disconnectedAt = null;
|
|
94
|
+
let warningTimer = null;
|
|
95
|
+
let lostTimer = null;
|
|
96
|
+
let lastPresentationKey = "";
|
|
97
|
+
|
|
98
|
+
const emit = () => {
|
|
99
|
+
const presentation = connectionFeedbackPresentation({
|
|
100
|
+
signal,
|
|
101
|
+
disconnectedAt,
|
|
102
|
+
now: now(),
|
|
103
|
+
});
|
|
104
|
+
const nextKey = presentationKey(presentation);
|
|
105
|
+
if (nextKey === lastPresentationKey) return;
|
|
106
|
+
lastPresentationKey = nextKey;
|
|
107
|
+
onPresentation(presentation);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const clearTimers = () => {
|
|
111
|
+
if (warningTimer !== null) clearTimeoutImpl(warningTimer);
|
|
112
|
+
if (lostTimer !== null) clearTimeoutImpl(lostTimer);
|
|
113
|
+
warningTimer = null;
|
|
114
|
+
lostTimer = null;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const scheduleThresholds = () => {
|
|
118
|
+
warningTimer = setTimeoutImpl(() => {
|
|
119
|
+
warningTimer = null;
|
|
120
|
+
emit();
|
|
121
|
+
}, HOST_CONNECTION_RECONNECT_WARNING_DELAY_MS);
|
|
122
|
+
lostTimer = setTimeoutImpl(() => {
|
|
123
|
+
lostTimer = null;
|
|
124
|
+
emit();
|
|
125
|
+
}, HOST_CONNECTION_LOST_DELAY_MS);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
handleStatus(status) {
|
|
130
|
+
const nextSignal = normalizeHostConnectionStatus(status);
|
|
131
|
+
const wasConnected = signal?.connected !== false;
|
|
132
|
+
signal = nextSignal;
|
|
133
|
+
if (nextSignal.connected) {
|
|
134
|
+
disconnectedAt = null;
|
|
135
|
+
clearTimers();
|
|
136
|
+
emit();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (wasConnected || disconnectedAt === null) {
|
|
140
|
+
disconnectedAt = now();
|
|
141
|
+
clearTimers();
|
|
142
|
+
scheduleThresholds();
|
|
143
|
+
}
|
|
144
|
+
emit();
|
|
145
|
+
},
|
|
146
|
+
render() {
|
|
147
|
+
emit();
|
|
148
|
+
},
|
|
149
|
+
close() {
|
|
150
|
+
clearTimers();
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function createPageRefreshController({
|
|
156
|
+
reload,
|
|
157
|
+
onPendingChange = () => {},
|
|
158
|
+
setTimeoutImpl = globalThis.setTimeout.bind(globalThis),
|
|
159
|
+
clearTimeoutImpl = globalThis.clearTimeout.bind(globalThis),
|
|
160
|
+
pendingResetMs = PAGE_REFRESH_PENDING_RESET_MS,
|
|
161
|
+
} = {}) {
|
|
162
|
+
if (typeof reload !== "function") {
|
|
163
|
+
throw new Error("Page reload callback is required");
|
|
164
|
+
}
|
|
165
|
+
let pending = false;
|
|
166
|
+
let resetTimer = null;
|
|
167
|
+
|
|
168
|
+
const setPending = (nextPending) => {
|
|
169
|
+
pending = nextPending;
|
|
170
|
+
onPendingChange(pending);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
request() {
|
|
175
|
+
if (pending) return false;
|
|
176
|
+
setPending(true);
|
|
177
|
+
resetTimer = setTimeoutImpl(() => {
|
|
178
|
+
resetTimer = null;
|
|
179
|
+
setPending(false);
|
|
180
|
+
}, pendingResetMs);
|
|
181
|
+
reload();
|
|
182
|
+
return true;
|
|
183
|
+
},
|
|
184
|
+
isPending() {
|
|
185
|
+
return pending;
|
|
186
|
+
},
|
|
187
|
+
close() {
|
|
188
|
+
if (resetTimer !== null) clearTimeoutImpl(resetTimer);
|
|
189
|
+
resetTimer = null;
|
|
190
|
+
if (pending) setPending(false);
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|