portacode 1.3.32__py3-none-any.whl → 1.4.11.dev0__py3-none-any.whl
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.
Potentially problematic release.
This version of portacode might be problematic. Click here for more details.
- portacode/_version.py +2 -2
- portacode/cli.py +119 -14
- portacode/connection/client.py +127 -8
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +301 -4
- portacode/connection/handlers/__init__.py +10 -1
- portacode/connection/handlers/diff_handlers.py +603 -0
- portacode/connection/handlers/file_handlers.py +674 -17
- portacode/connection/handlers/project_aware_file_handlers.py +11 -0
- portacode/connection/handlers/project_state/file_system_watcher.py +31 -61
- portacode/connection/handlers/project_state/git_manager.py +139 -572
- portacode/connection/handlers/project_state/handlers.py +28 -14
- portacode/connection/handlers/project_state/manager.py +226 -101
- portacode/connection/handlers/proxmox_infra.py +307 -0
- portacode/connection/handlers/session.py +465 -84
- portacode/connection/handlers/system_handlers.py +140 -8
- portacode/connection/handlers/tab_factory.py +1 -47
- portacode/connection/handlers/update_handler.py +61 -0
- portacode/connection/terminal.py +51 -10
- portacode/keypair.py +63 -1
- portacode/link_capture/__init__.py +38 -0
- portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
- portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
- portacode/link_capture/bin/elinks +3 -0
- portacode/link_capture/bin/gio-open +3 -0
- portacode/link_capture/bin/gnome-open +3 -0
- portacode/link_capture/bin/gvfs-open +3 -0
- portacode/link_capture/bin/kde-open +3 -0
- portacode/link_capture/bin/kfmclient +3 -0
- portacode/link_capture/bin/link_capture_exec.sh +11 -0
- portacode/link_capture/bin/link_capture_wrapper.py +75 -0
- portacode/link_capture/bin/links +3 -0
- portacode/link_capture/bin/links2 +3 -0
- portacode/link_capture/bin/lynx +3 -0
- portacode/link_capture/bin/mate-open +3 -0
- portacode/link_capture/bin/netsurf +3 -0
- portacode/link_capture/bin/sensible-browser +3 -0
- portacode/link_capture/bin/w3m +3 -0
- portacode/link_capture/bin/x-www-browser +3 -0
- portacode/link_capture/bin/xdg-open +3 -0
- portacode/pairing.py +103 -0
- portacode/static/js/utils/ntp-clock.js +170 -79
- portacode/utils/diff_apply.py +456 -0
- portacode/utils/diff_renderer.py +371 -0
- portacode/utils/ntp_clock.py +45 -131
- {portacode-1.3.32.dist-info → portacode-1.4.11.dev0.dist-info}/METADATA +71 -3
- portacode-1.4.11.dev0.dist-info/RECORD +97 -0
- test_modules/test_device_online.py +1 -1
- test_modules/test_login_flow.py +8 -4
- test_modules/test_play_store_screenshots.py +294 -0
- testing_framework/.env.example +4 -1
- testing_framework/core/playwright_manager.py +63 -9
- portacode-1.3.32.dist-info/RECORD +0 -70
- {portacode-1.3.32.dist-info → portacode-1.4.11.dev0.dist-info}/WHEEL +0 -0
- {portacode-1.3.32.dist-info → portacode-1.4.11.dev0.dist-info}/entry_points.txt +0 -0
- {portacode-1.3.32.dist-info → portacode-1.4.11.dev0.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.3.32.dist-info → portacode-1.4.11.dev0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Simple link capture wrapper that never executes a native browser."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
LINK_CHANNEL_ENV = "PORTACODE_LINK_CHANNEL"
|
|
13
|
+
TERMINAL_ID_ENV = "PORTACODE_TERMINAL_ID"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _find_link_argument(args):
|
|
17
|
+
for arg in args:
|
|
18
|
+
if not isinstance(arg, str):
|
|
19
|
+
continue
|
|
20
|
+
parsed = urlparse(arg)
|
|
21
|
+
if parsed.scheme and parsed.netloc:
|
|
22
|
+
return arg
|
|
23
|
+
if arg.startswith("file://"):
|
|
24
|
+
return arg
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _write_capture_event(cmd_name, args, link):
|
|
29
|
+
channel = os.environ.get(LINK_CHANNEL_ENV)
|
|
30
|
+
terminal_id = os.environ.get(TERMINAL_ID_ENV)
|
|
31
|
+
if not channel or not link:
|
|
32
|
+
return
|
|
33
|
+
payload = {
|
|
34
|
+
"terminal_id": terminal_id,
|
|
35
|
+
"command": cmd_name,
|
|
36
|
+
"args": args,
|
|
37
|
+
"url": link,
|
|
38
|
+
"timestamp": time.time(),
|
|
39
|
+
}
|
|
40
|
+
directory = Path(channel)
|
|
41
|
+
try:
|
|
42
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
except Exception:
|
|
44
|
+
return
|
|
45
|
+
temp_file = directory / f".{uuid.uuid4().hex}.tmp"
|
|
46
|
+
term_label = terminal_id or "unknown"
|
|
47
|
+
base_name = f"{int(time.time() * 1000)}-{term_label}"
|
|
48
|
+
final_file = directory / f"{base_name}.json"
|
|
49
|
+
suffix = 0
|
|
50
|
+
while final_file.exists():
|
|
51
|
+
suffix += 1
|
|
52
|
+
final_file = directory / f"{base_name}-{suffix}.json"
|
|
53
|
+
try:
|
|
54
|
+
temp_file.write_text(json.dumps(payload), encoding="utf-8")
|
|
55
|
+
temp_file.replace(final_file)
|
|
56
|
+
except Exception:
|
|
57
|
+
if temp_file.exists():
|
|
58
|
+
temp_file.unlink(missing_ok=True)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main() -> None:
|
|
62
|
+
if len(sys.argv) < 2:
|
|
63
|
+
sys.stderr.write("link_capture: missing target command name\n")
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
cmd_name = sys.argv[1]
|
|
66
|
+
cmd_args = sys.argv[2:]
|
|
67
|
+
link = _find_link_argument(cmd_args)
|
|
68
|
+
if link:
|
|
69
|
+
_write_capture_event(cmd_name, cmd_args, link)
|
|
70
|
+
# Never run a real browser; capture and exit successfully.
|
|
71
|
+
sys.exit(0)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
portacode/pairing.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import socket
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import websockets
|
|
11
|
+
|
|
12
|
+
PAIRING_URL_ENV = "PORTACODE_PAIRING_URL"
|
|
13
|
+
DEFAULT_PAIRING_URL = "wss://portacode.com/ws/pairing/device/"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PairingError(Exception):
|
|
17
|
+
"""Raised when pairing fails or is rejected."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class PairingResult:
|
|
22
|
+
status: str
|
|
23
|
+
payload: dict
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def _pair_device(
|
|
27
|
+
url: str,
|
|
28
|
+
public_key_b64: str,
|
|
29
|
+
pairing_code: str,
|
|
30
|
+
device_name: str,
|
|
31
|
+
project_paths: list[str] | None = None,
|
|
32
|
+
timeout: float = 300.0,
|
|
33
|
+
) -> PairingResult:
|
|
34
|
+
async with websockets.connect(url) as ws:
|
|
35
|
+
# Initial ready/event
|
|
36
|
+
try:
|
|
37
|
+
ready = await asyncio.wait_for(ws.recv(), timeout=10)
|
|
38
|
+
except asyncio.TimeoutError as exc:
|
|
39
|
+
raise PairingError("Gateway did not acknowledge pairing request") from exc
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
data = json.loads(ready)
|
|
43
|
+
if data.get("event") != "pairing_ready":
|
|
44
|
+
raise PairingError(f"Unexpected response from pairing gateway: {ready}")
|
|
45
|
+
except json.JSONDecodeError:
|
|
46
|
+
raise PairingError(f"Unexpected response from pairing gateway: {ready}") from None
|
|
47
|
+
|
|
48
|
+
payload = {
|
|
49
|
+
"code": pairing_code,
|
|
50
|
+
"device_name": device_name,
|
|
51
|
+
"public_key": public_key_b64,
|
|
52
|
+
}
|
|
53
|
+
if project_paths:
|
|
54
|
+
payload["project_paths"] = project_paths
|
|
55
|
+
await ws.send(json.dumps(payload))
|
|
56
|
+
|
|
57
|
+
while True:
|
|
58
|
+
try:
|
|
59
|
+
message = await asyncio.wait_for(ws.recv(), timeout=timeout)
|
|
60
|
+
except asyncio.TimeoutError as exc:
|
|
61
|
+
raise PairingError("Pairing timed out waiting for approval") from exc
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
data = json.loads(message)
|
|
65
|
+
except json.JSONDecodeError:
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
if data.get("event") != "pairing_status":
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
status = data.get("status")
|
|
72
|
+
if status == "pending":
|
|
73
|
+
continue
|
|
74
|
+
if status == "approved":
|
|
75
|
+
return PairingResult(status="approved", payload=data)
|
|
76
|
+
reason = data.get("reason") or status or "unknown_error"
|
|
77
|
+
raise PairingError(reason)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def pair_device_with_code(
|
|
81
|
+
keypair,
|
|
82
|
+
pairing_code: str,
|
|
83
|
+
device_name: Optional[str] = None,
|
|
84
|
+
project_paths: list[str] | None = None,
|
|
85
|
+
*,
|
|
86
|
+
timeout: float = 300.0,
|
|
87
|
+
) -> PairingResult:
|
|
88
|
+
"""Run the pairing workflow synchronously."""
|
|
89
|
+
pairing_url = os.getenv(PAIRING_URL_ENV, DEFAULT_PAIRING_URL)
|
|
90
|
+
normalized_url = pairing_url if pairing_url.startswith("ws") else f"wss://{pairing_url.lstrip('/')}"
|
|
91
|
+
friendly_name = device_name or socket.gethostname() or "Portacode Device"
|
|
92
|
+
|
|
93
|
+
public_key_b64 = keypair.public_key_der_b64()
|
|
94
|
+
return asyncio.run(
|
|
95
|
+
_pair_device(
|
|
96
|
+
normalized_url,
|
|
97
|
+
public_key_b64=public_key_b64,
|
|
98
|
+
pairing_code=pairing_code,
|
|
99
|
+
device_name=friendly_name,
|
|
100
|
+
project_paths=project_paths,
|
|
101
|
+
timeout=timeout,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
@@ -1,86 +1,74 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* NTP Clock - Synchronized time source for distributed tracing
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* IMPORTANT: All entities (client, server, device) MUST sync to time.cloudflare.com
|
|
8
|
-
* If sync fails, timestamps will be null to indicate sync failure.
|
|
4
|
+
* Provides synchronization by requesting the server clock across the WebSocket
|
|
5
|
+
* channel defined in `WEBSOCKET_PROTOCOL`. The client measures round-trip time
|
|
6
|
+
* and applies an offset that compensates for latency before exposing timestamps.
|
|
9
7
|
*/
|
|
10
8
|
class NTPClock {
|
|
11
9
|
constructor() {
|
|
12
|
-
this.ntpServer = '
|
|
13
|
-
this.offset = null;
|
|
10
|
+
this.ntpServer = 'portacode-gateway';
|
|
11
|
+
this.offset = null;
|
|
14
12
|
this.lastSync = null;
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
13
|
+
this.lastLatencyMs = null;
|
|
14
|
+
this.syncInterval = 60 * 1000;
|
|
15
|
+
this.clockSyncTimeout = 20 * 1000;
|
|
16
|
+
this.maxSyncFailures = 3;
|
|
17
|
+
|
|
18
|
+
this._clockSyncSender = null;
|
|
19
|
+
this._failureCallback = null;
|
|
20
|
+
this._clockSyncTimer = null;
|
|
21
|
+
this._clockSyncTimeoutHandle = null;
|
|
22
|
+
this._pendingRequest = null;
|
|
23
|
+
this._autoSyncStarted = false;
|
|
24
|
+
this._clockSyncFailures = 0;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
/**
|
|
22
|
-
*
|
|
28
|
+
* Register the function responsible for piping `clock_sync_request`
|
|
29
|
+
* packets over the dashboard WebSocket.
|
|
23
30
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
31
|
+
setClockSyncSender(sender) {
|
|
32
|
+
this._clockSyncSender = sender;
|
|
33
|
+
if (this._autoSyncStarted && !this._pendingRequest) {
|
|
34
|
+
this._scheduleSync(0);
|
|
28
35
|
}
|
|
29
|
-
throw new Error('Failed to parse Cloudflare timestamp');
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
/**
|
|
33
|
-
*
|
|
39
|
+
* Callback invoked when multiple sync failures occur in a row.
|
|
40
|
+
* Useful for triggering a transport reconnect.
|
|
34
41
|
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
this._syncInProgress = true;
|
|
42
|
-
this._syncAttempts++;
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
// Capture local time BEFORE the fetch to avoid timing drift
|
|
46
|
-
const localTimeBeforeFetch = Date.now();
|
|
47
|
-
const t0 = performance.now();
|
|
48
|
-
const response = await fetch('https://cloudflare.com/cdn-cgi/trace');
|
|
49
|
-
const t1 = performance.now();
|
|
50
|
-
|
|
51
|
-
const text = await response.text();
|
|
52
|
-
const serverTime = this._parseCloudflareTime(text);
|
|
53
|
-
|
|
54
|
-
const latency = (t1 - t0) / 2; // Estimate one-way latency
|
|
55
|
-
|
|
56
|
-
// Calculate offset: server generated timestamp at local time (localTimeBeforeFetch + latency)
|
|
57
|
-
// So offset = serverTime - (localTimeBeforeFetch + latency)
|
|
58
|
-
this.offset = serverTime - (localTimeBeforeFetch + latency);
|
|
59
|
-
this.lastSync = Date.now();
|
|
60
|
-
|
|
61
|
-
console.log(
|
|
62
|
-
`✅ NTP sync successful: offset=${this.offset.toFixed(2)}ms, ` +
|
|
63
|
-
`latency=${latency.toFixed(2)}ms, server=${this.ntpServer}`
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
this._syncAttempts = 0; // Reset on success
|
|
67
|
-
return true;
|
|
68
|
-
|
|
69
|
-
} catch (error) {
|
|
70
|
-
console.warn(`❌ NTP sync failed (attempt ${this._syncAttempts}/${this._maxSyncAttempts}):`, error);
|
|
42
|
+
onClockSyncFailure(callback) {
|
|
43
|
+
this._failureCallback = callback;
|
|
44
|
+
}
|
|
71
45
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Start the auto-sync loop (idempotent). Will skip sending until a sender
|
|
48
|
+
* has been registered.
|
|
49
|
+
*/
|
|
50
|
+
startAutoSync() {
|
|
51
|
+
if (this._autoSyncStarted) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
this._autoSyncStarted = true;
|
|
55
|
+
this._scheduleSync(0);
|
|
56
|
+
}
|
|
79
57
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Stop the auto-sync loop (used primarily in tests).
|
|
60
|
+
*/
|
|
61
|
+
stopAutoSync() {
|
|
62
|
+
this._autoSyncStarted = false;
|
|
63
|
+
if (this._clockSyncTimer) {
|
|
64
|
+
clearTimeout(this._clockSyncTimer);
|
|
65
|
+
this._clockSyncTimer = null;
|
|
83
66
|
}
|
|
67
|
+
this._clearPendingRequest();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
sync() {
|
|
71
|
+
return this._performClockSync();
|
|
84
72
|
}
|
|
85
73
|
|
|
86
74
|
/**
|
|
@@ -106,31 +94,134 @@ class NTPClock {
|
|
|
106
94
|
return new Date(ts).toISOString();
|
|
107
95
|
}
|
|
108
96
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
97
|
+
handleServerSync(payload) {
|
|
98
|
+
if (!payload) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const receiveTime = Date.now();
|
|
102
|
+
let roundTrip = 0;
|
|
103
|
+
if (
|
|
104
|
+
payload.request_id &&
|
|
105
|
+
this._pendingRequest &&
|
|
106
|
+
payload.request_id === this._pendingRequest.requestId
|
|
107
|
+
) {
|
|
108
|
+
roundTrip = Math.max(receiveTime - this._pendingRequest.sentAt, 0);
|
|
109
|
+
this._clockSyncFailures = 0;
|
|
110
|
+
this._clearPendingRequest();
|
|
111
|
+
}
|
|
112
|
+
const serverSend = typeof payload.server_send_time === 'number' ? payload.server_send_time : payload.server_time;
|
|
113
|
+
const serverReceive = payload.server_receive_time;
|
|
114
|
+
const serverAvg = (typeof serverReceive === 'number' && typeof serverSend === 'number')
|
|
115
|
+
? (serverReceive + serverSend) / 2
|
|
116
|
+
: typeof serverSend === 'number'
|
|
117
|
+
? serverSend
|
|
118
|
+
: undefined;
|
|
119
|
+
if (typeof serverAvg === 'number') {
|
|
120
|
+
this.updateFromServer(serverAvg, roundTrip);
|
|
121
|
+
}
|
|
122
|
+
if (payload.server_time_iso) {
|
|
123
|
+
this.serverTimeIso = payload.server_time_iso;
|
|
124
|
+
}
|
|
125
|
+
this._scheduleSync(this.syncInterval);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
applyServerSync(payload) {
|
|
129
|
+
this.handleServerSync(payload);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
updateFromServer(serverTimeMs, roundTripMs = 0) {
|
|
133
|
+
const receiveTime = Date.now();
|
|
134
|
+
const halfLatency = (roundTripMs || 0) / 2;
|
|
135
|
+
const estimatedServerReceived = receiveTime - halfLatency;
|
|
136
|
+
this.offset = serverTimeMs - estimatedServerReceived;
|
|
137
|
+
this.lastLatencyMs = roundTripMs;
|
|
138
|
+
this.lastSync = receiveTime;
|
|
139
|
+
}
|
|
140
|
+
|
|
112
141
|
getStatus() {
|
|
113
142
|
return {
|
|
114
143
|
server: this.ntpServer,
|
|
115
144
|
offset: this.offset,
|
|
116
145
|
lastSync: this.lastSync ? new Date(this.lastSync).toISOString() : null,
|
|
146
|
+
lastLatencyMs: this.lastLatencyMs,
|
|
117
147
|
timeSinceSync: this.lastSync ? Date.now() - this.lastSync : null,
|
|
118
148
|
isSynced: this.offset !== null
|
|
119
149
|
};
|
|
120
150
|
}
|
|
121
151
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
152
|
+
_scheduleSync(delay) {
|
|
153
|
+
if (!this._autoSyncStarted) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (this._clockSyncTimer) {
|
|
157
|
+
clearTimeout(this._clockSyncTimer);
|
|
158
|
+
}
|
|
159
|
+
this._clockSyncTimer = setTimeout(() => this._performClockSync(), delay);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_performClockSync() {
|
|
163
|
+
if (!this._autoSyncStarted) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
if (!this._clockSyncSender) {
|
|
167
|
+
this._scheduleSync(Math.min(this.syncInterval, 3000));
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
if (this._pendingRequest) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
const requestId = this._generateRequestId();
|
|
174
|
+
const payload = {
|
|
175
|
+
event: 'clock_sync_request',
|
|
176
|
+
request_id: requestId,
|
|
177
|
+
};
|
|
178
|
+
this._pendingRequest = {
|
|
179
|
+
requestId,
|
|
180
|
+
sentAt: Date.now(),
|
|
181
|
+
};
|
|
182
|
+
const sent = this._clockSyncSender(payload);
|
|
183
|
+
if (!sent) {
|
|
184
|
+
this._handleClockSyncFailure();
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
this._clockSyncTimeoutHandle = setTimeout(() => this._handleClockSyncTimeout(), this.clockSyncTimeout);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
_handleClockSyncTimeout() {
|
|
192
|
+
this._clockSyncFailures += 1;
|
|
193
|
+
this._clearPendingRequest();
|
|
194
|
+
if (
|
|
195
|
+
this._clockSyncFailures >= this.maxSyncFailures &&
|
|
196
|
+
typeof this._failureCallback === 'function'
|
|
197
|
+
) {
|
|
198
|
+
this._failureCallback();
|
|
199
|
+
}
|
|
200
|
+
this._scheduleSync(this.syncInterval);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_handleClockSyncFailure() {
|
|
204
|
+
this._clockSyncFailures += 1;
|
|
205
|
+
this._clearPendingRequest();
|
|
206
|
+
if (
|
|
207
|
+
this._clockSyncFailures >= this.maxSyncFailures &&
|
|
208
|
+
typeof this._failureCallback === 'function'
|
|
209
|
+
) {
|
|
210
|
+
this._failureCallback();
|
|
211
|
+
}
|
|
212
|
+
this._scheduleSync(this.syncInterval);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
_clearPendingRequest() {
|
|
216
|
+
if (this._clockSyncTimeoutHandle) {
|
|
217
|
+
clearTimeout(this._clockSyncTimeoutHandle);
|
|
218
|
+
this._clockSyncTimeoutHandle = null;
|
|
219
|
+
}
|
|
220
|
+
this._pendingRequest = null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_generateRequestId() {
|
|
224
|
+
return `clock_sync:${Date.now()}:${Math.floor(Math.random() * 1000000)}`;
|
|
134
225
|
}
|
|
135
226
|
}
|
|
136
227
|
|