viveworker 0.8.1 → 0.8.2
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 +59 -0
- package/package.json +1 -1
- package/plugins/viveworker-control-plane/.codex-plugin/plugin.json +1 -1
- package/plugins/viveworker-control-plane/DISTRIBUTION.md +96 -0
- package/scripts/a2a-executor.mjs +261 -7
- package/scripts/a2a-handler.mjs +88 -0
- package/scripts/a2a-relay-client.mjs +6 -0
- package/scripts/viveworker-bridge.mjs +1113 -65
- package/web/app.css +114 -1
- package/web/app.js +1129 -115
- package/web/build-id.js +1 -1
- package/web/i18n.js +123 -15
- package/web/index.html +1 -1
- package/web/remote-pairing/api-router.js +84 -0
- package/web/remote-pairing/transport.js +67 -2
- package/web/sw.js +14 -4
|
@@ -84,6 +84,10 @@ const DEFAULT_BACKOFF_MS = [1_000, 2_000, 4_000, 8_000, 16_000, 30_000];
|
|
|
84
84
|
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 30_000;
|
|
85
85
|
const MAX_PRE_CONNECT_BACKOFF_MS = 4_000;
|
|
86
86
|
const RELAY_RESET_RECONNECT_MS = 250;
|
|
87
|
+
const DEFAULT_FAILURE_WINDOW_MS = 60_000;
|
|
88
|
+
const DEFAULT_FAILURE_THRESHOLD = 4;
|
|
89
|
+
const DEFAULT_CIRCUIT_BREAKER_MS = 60_000;
|
|
90
|
+
const DEFAULT_MAX_CIRCUIT_BREAKER_MS = 10 * 60_000;
|
|
87
91
|
const DEFAULT_PROLOGUE = new TextEncoder().encode("viveworker/remote-pairing/v1");
|
|
88
92
|
|
|
89
93
|
// CloseEvent codes we emit. 1000 is normal; 4xxx is application-defined.
|
|
@@ -113,6 +117,10 @@ const CLOSE_RELAY_RESET_SESSION = 4004;
|
|
|
113
117
|
* @property {number} [pingIntervalMs]
|
|
114
118
|
* @property {number[]} [backoffMs]
|
|
115
119
|
* @property {number} [handshakeTimeoutMs]
|
|
120
|
+
* @property {number} [failureWindowMs]
|
|
121
|
+
* @property {number} [failureThreshold]
|
|
122
|
+
* @property {number} [circuitBreakerMs]
|
|
123
|
+
* @property {number} [maxCircuitBreakerMs]
|
|
116
124
|
* @property {typeof WebSocket} [WebSocketImpl] injectable for Node-side tests
|
|
117
125
|
* @property {{debug?: Function, warn?: Function}} [logger]
|
|
118
126
|
*/
|
|
@@ -154,6 +162,10 @@ export class RemotePairingTransport {
|
|
|
154
162
|
this._backoffMs = (opts.backoffMs ?? DEFAULT_BACKOFF_MS).slice();
|
|
155
163
|
if (this._backoffMs.length === 0) this._backoffMs = [1_000];
|
|
156
164
|
this._handshakeTimeoutMs = opts.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
|
|
165
|
+
this._failureWindowMs = opts.failureWindowMs ?? DEFAULT_FAILURE_WINDOW_MS;
|
|
166
|
+
this._failureThreshold = opts.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD;
|
|
167
|
+
this._circuitBreakerMs = opts.circuitBreakerMs ?? DEFAULT_CIRCUIT_BREAKER_MS;
|
|
168
|
+
this._maxCircuitBreakerMs = opts.maxCircuitBreakerMs ?? DEFAULT_MAX_CIRCUIT_BREAKER_MS;
|
|
157
169
|
|
|
158
170
|
this._WebSocketImpl = opts.WebSocketImpl ?? globalThis.WebSocket;
|
|
159
171
|
if (typeof this._WebSocketImpl !== "function") {
|
|
@@ -188,6 +200,12 @@ export class RemotePairingTransport {
|
|
|
188
200
|
this._pingTimer = null;
|
|
189
201
|
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
190
202
|
this._handshakeTimer = null;
|
|
203
|
+
/** @type {number[]} recent reconnect-triggering failures. */
|
|
204
|
+
this._recentFailureAtMs = [];
|
|
205
|
+
/** If > Date.now(), reconnect attempts are intentionally delayed. */
|
|
206
|
+
this._circuitOpenUntilMs = 0;
|
|
207
|
+
/** Consecutive circuit openings, used to lengthen persistent outages. */
|
|
208
|
+
this._circuitOpenCount = 0;
|
|
191
209
|
|
|
192
210
|
// ---- crypto state (persists across reconnects until RESUME_FAIL) ----
|
|
193
211
|
/** @type {import("../remote-pairing.bundle.js").NoiseSession | null} */
|
|
@@ -320,6 +338,12 @@ export class RemotePairingTransport {
|
|
|
320
338
|
|
|
321
339
|
_open() {
|
|
322
340
|
this._cancelReconnectTimer();
|
|
341
|
+
const circuitDelay = this._circuitDelayMs();
|
|
342
|
+
if (circuitDelay > 0) {
|
|
343
|
+
this._log.warn?.(`relay reconnect circuit open for ${circuitDelay}ms`);
|
|
344
|
+
this._scheduleReconnect();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
323
347
|
this._setState(STATE.OPENING);
|
|
324
348
|
|
|
325
349
|
const url =
|
|
@@ -484,6 +508,12 @@ export class RemotePairingTransport {
|
|
|
484
508
|
|
|
485
509
|
this._log.debug?.(`ws closed code=${evt?.code} reason=${evt?.reason}`);
|
|
486
510
|
this._setState(STATE.DISCONNECTED, { code: evt?.code, reason: evt?.reason });
|
|
511
|
+
if (Number(evt?.code) !== CLOSE_NORMAL) {
|
|
512
|
+
this._recordReconnectFailure({
|
|
513
|
+
code: Number(evt?.code) || 0,
|
|
514
|
+
reason: String(evt?.reason || ""),
|
|
515
|
+
});
|
|
516
|
+
}
|
|
487
517
|
if (isRelayReset) {
|
|
488
518
|
this._scheduleRelayResetReconnect();
|
|
489
519
|
} else {
|
|
@@ -619,9 +649,10 @@ export class RemotePairingTransport {
|
|
|
619
649
|
const idx = Math.min(this._reconnectAttempt, this._backoffMs.length - 1);
|
|
620
650
|
const waitingForFirstConnect = this._connectPromise != null && this._session == null;
|
|
621
651
|
const rawDelay = this._backoffMs[idx];
|
|
622
|
-
const
|
|
652
|
+
const backoffDelay = waitingForFirstConnect
|
|
623
653
|
? Math.min(rawDelay, MAX_PRE_CONNECT_BACKOFF_MS)
|
|
624
654
|
: rawDelay;
|
|
655
|
+
const delay = Math.max(backoffDelay, this._circuitDelayMs());
|
|
625
656
|
this._reconnectAttempt += 1;
|
|
626
657
|
this._log.debug?.(`reconnect in ${delay}ms (attempt ${this._reconnectAttempt})`);
|
|
627
658
|
this._reconnectTimer = setTimeout(() => {
|
|
@@ -636,7 +667,7 @@ export class RemotePairingTransport {
|
|
|
636
667
|
// rendezvous again. Treat it as a protocol reset, not a network failure,
|
|
637
668
|
// so app boot does not sit behind the exponential backoff ladder.
|
|
638
669
|
this._reconnectAttempt = 0;
|
|
639
|
-
const delay = RELAY_RESET_RECONNECT_MS;
|
|
670
|
+
const delay = Math.max(RELAY_RESET_RECONNECT_MS, this._circuitDelayMs());
|
|
640
671
|
this._log.debug?.(`reconnect in ${delay}ms (relay reset)`);
|
|
641
672
|
this._reconnectTimer = setTimeout(() => {
|
|
642
673
|
this._reconnectTimer = null;
|
|
@@ -713,6 +744,9 @@ export class RemotePairingTransport {
|
|
|
713
744
|
this._state = newState;
|
|
714
745
|
if (newState === STATE.CONNECTED) {
|
|
715
746
|
this._reconnectAttempt = 0;
|
|
747
|
+
this._recentFailureAtMs = [];
|
|
748
|
+
this._circuitOpenUntilMs = 0;
|
|
749
|
+
this._circuitOpenCount = 0;
|
|
716
750
|
}
|
|
717
751
|
try {
|
|
718
752
|
this._onStateChange(newState, prev, info);
|
|
@@ -729,6 +763,10 @@ export class RemotePairingTransport {
|
|
|
729
763
|
}
|
|
730
764
|
|
|
731
765
|
_fail(err) {
|
|
766
|
+
this._recordReconnectFailure({
|
|
767
|
+
code: CLOSE_FATAL,
|
|
768
|
+
reason: err?.message || "fatal-error",
|
|
769
|
+
});
|
|
732
770
|
this._setState(STATE.FAILED, { error: err });
|
|
733
771
|
this._stopPing();
|
|
734
772
|
this._stopHandshakeTimer();
|
|
@@ -748,6 +786,33 @@ export class RemotePairingTransport {
|
|
|
748
786
|
this._session = null;
|
|
749
787
|
this._handshake = null;
|
|
750
788
|
}
|
|
789
|
+
|
|
790
|
+
_recordReconnectFailure(info = {}) {
|
|
791
|
+
const now = Date.now();
|
|
792
|
+
const windowMs = Math.max(1_000, Number(this._failureWindowMs) || DEFAULT_FAILURE_WINDOW_MS);
|
|
793
|
+
this._recentFailureAtMs = this._recentFailureAtMs.filter((at) => now - at <= windowMs);
|
|
794
|
+
this._recentFailureAtMs.push(now);
|
|
795
|
+
const threshold = Math.max(2, Math.floor(Number(this._failureThreshold) || DEFAULT_FAILURE_THRESHOLD));
|
|
796
|
+
if (this._recentFailureAtMs.length < threshold) return;
|
|
797
|
+
|
|
798
|
+
const baseCooldownMs = Math.max(1_000, Number(this._circuitBreakerMs) || DEFAULT_CIRCUIT_BREAKER_MS);
|
|
799
|
+
const maxCooldownMs = Math.max(baseCooldownMs, Number(this._maxCircuitBreakerMs) || DEFAULT_MAX_CIRCUIT_BREAKER_MS);
|
|
800
|
+
const multiplier = Math.min(16, 2 ** this._circuitOpenCount);
|
|
801
|
+
const cooldownMs = Math.min(maxCooldownMs, baseCooldownMs * multiplier);
|
|
802
|
+
this._circuitOpenCount += 1;
|
|
803
|
+
this._circuitOpenUntilMs = Math.max(this._circuitOpenUntilMs, now + cooldownMs);
|
|
804
|
+
this._recentFailureAtMs = [];
|
|
805
|
+
this._dropNoiseState();
|
|
806
|
+
this._log.warn?.(
|
|
807
|
+
`relay reconnect circuit opened for ${cooldownMs}ms` +
|
|
808
|
+
`${info?.code ? ` after close=${info.code}` : ""}` +
|
|
809
|
+
`${info?.reason ? ` (${String(info.reason).slice(0, 64)})` : ""}`,
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
_circuitDelayMs() {
|
|
814
|
+
return Math.max(0, this._circuitOpenUntilMs - Date.now());
|
|
815
|
+
}
|
|
751
816
|
}
|
|
752
817
|
|
|
753
818
|
// ---------------------------------------------------------------------------
|
package/web/sw.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const APP_BUILD_ID = "__VIVEWORKER_APP_BUILD_ID__";
|
|
2
2
|
const CACHE_NAME = `viveworker-${APP_BUILD_ID}`;
|
|
3
3
|
const APP_SCRIPT_URL = `/app.js?v=${APP_BUILD_ID}`;
|
|
4
|
+
const APP_STYLE_URL = `/app.css?v=${APP_BUILD_ID}`;
|
|
4
5
|
const API_ROUTER_URL = `/remote-pairing/api-router.js?v=${APP_BUILD_ID}`;
|
|
5
6
|
const NOTIFICATION_INTENT_CACHE = "viveworker-notification-intent-v1";
|
|
6
7
|
const NOTIFICATION_INTENT_PATH = "/__viveworker_notification_intent__";
|
|
@@ -14,7 +15,7 @@ const NOTIFICATION_INTENT_PATH = "/__viveworker_notification_intent__";
|
|
|
14
15
|
const APP_ASSETS = [
|
|
15
16
|
"/app",
|
|
16
17
|
APP_SCRIPT_URL,
|
|
17
|
-
|
|
18
|
+
APP_STYLE_URL,
|
|
18
19
|
"/app.js",
|
|
19
20
|
"/i18n.js",
|
|
20
21
|
"/icons/viveworker-v-pulse.svg",
|
|
@@ -31,10 +32,12 @@ const APP_ROUTES = new Set(["/", "/app", "/app/"]);
|
|
|
31
32
|
const CACHED_PATHS = new Set(APP_ASSETS.map((asset) => new URL(asset, self.location.origin).pathname));
|
|
32
33
|
const VERSIONED_CACHE_PATHS = new Set([
|
|
33
34
|
"/app.js",
|
|
35
|
+
"/app.css",
|
|
34
36
|
"/remote-pairing/api-router.js",
|
|
35
37
|
]);
|
|
36
38
|
const NETWORK_FIRST_PATHS = new Set([
|
|
37
39
|
"/app.js",
|
|
40
|
+
"/app.css",
|
|
38
41
|
"/remote-pairing/api-router.js",
|
|
39
42
|
]);
|
|
40
43
|
const APP_NAVIGATION_NETWORK_TIMEOUT_MS = 1800;
|
|
@@ -49,7 +52,7 @@ const APP_SHELL_FALLBACK_HTML = `<!doctype html>
|
|
|
49
52
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
50
53
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
|
51
54
|
<link rel="icon" type="image/png" sizes="192x192" href="/icons/viveworker-icon-192.png">
|
|
52
|
-
<link rel="stylesheet" href="
|
|
55
|
+
<link rel="stylesheet" href="${APP_STYLE_URL}">
|
|
53
56
|
<style>
|
|
54
57
|
html, body { min-height: 100%; margin: 0; background: #081015; color: #f5fbff; }
|
|
55
58
|
.boot-splash { position: fixed; inset: 0; display: grid; place-items: center; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; background: radial-gradient(circle at 50% 18%, rgba(47, 143, 103, 0.22), transparent 30%), linear-gradient(180deg, #081015 0%, #091015 100%); }
|
|
@@ -150,14 +153,14 @@ self.addEventListener("push", (event) => {
|
|
|
150
153
|
} catch {
|
|
151
154
|
payload = {
|
|
152
155
|
title: "viveworker",
|
|
153
|
-
body: event.data ? event.data.text() :
|
|
156
|
+
body: event.data ? event.data.text() : fallbackNotificationBody(),
|
|
154
157
|
data: { url: "/app" },
|
|
155
158
|
};
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
const title = payload.title || "viveworker";
|
|
159
162
|
const options = {
|
|
160
|
-
body: payload.body ||
|
|
163
|
+
body: payload.body || fallbackNotificationBody(),
|
|
161
164
|
tag: payload.tag || "",
|
|
162
165
|
data: payload.data || { url: "/app" },
|
|
163
166
|
};
|
|
@@ -175,6 +178,13 @@ self.addEventListener("push", (event) => {
|
|
|
175
178
|
})());
|
|
176
179
|
});
|
|
177
180
|
|
|
181
|
+
function fallbackNotificationBody() {
|
|
182
|
+
const language = String(self.navigator?.language || "").toLowerCase();
|
|
183
|
+
return language.startsWith("ja")
|
|
184
|
+
? "viveworker を開いて確認してください。"
|
|
185
|
+
: "Open viveworker for details.";
|
|
186
|
+
}
|
|
187
|
+
|
|
178
188
|
self.addEventListener("notificationclick", (event) => {
|
|
179
189
|
event.preventDefault?.();
|
|
180
190
|
event.notification.close();
|