granttap-mcp 0.8.59 → 0.8.61
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/apps/bridge/src/managed-admission/README.md +4 -0
- package/apps/bridge/src/managed-admission/index.ts +5 -2
- package/apps/mcp/src/desktop/delivery/task-handoff.ts +3 -1
- package/apps/mcp/src/mcp-tools/connect/relay.ts +3 -1
- package/apps/mcp/src/mcp-tools/mesh/relay.ts +2 -1
- package/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/package.json +1 -1
- package/packages/core/relay-client-types.ts +2 -0
- package/packages/core/relay-client.ts +9 -50
- package/packages/core/socket-lifecycle/README.md +11 -0
- package/packages/core/socket-lifecycle/index.ts +2 -0
- package/packages/core/socket-lifecycle/managed-relay-lease.ts +49 -0
- package/packages/core/socket-lifecycle/relay-socket-health.ts +31 -0
|
@@ -6,6 +6,10 @@ chooses the stable computer identity and enforces Personal seats; QR rooms and
|
|
|
6
6
|
native agent sessions do not take additional seats. Own-relay connections never
|
|
7
7
|
call billing. Tickets are transport headers and are not prompt content.
|
|
8
8
|
|
|
9
|
+
The background monitor, MCP tool connection, desktop Task handoff, agent hooks
|
|
10
|
+
and machine-role Mesh connections all use this entry point. Their leases renew
|
|
11
|
+
on the existing socket; the provider conversation and task keys remain intact.
|
|
12
|
+
|
|
9
13
|
Legacy free beta servers may not issue leases. Commercial relay mode requires
|
|
10
14
|
a valid signed lease independently, including when a modified client omits it.
|
|
11
15
|
Expiry/refund stops hosted delivery; local provider approval remains available,
|
|
@@ -3,10 +3,12 @@ import { loadAccountLink } from "../account-recovery/link";
|
|
|
3
3
|
|
|
4
4
|
/** Account computer identity is stable across QR rooms and provider sessions. */
|
|
5
5
|
export function managedAdmissionOptions(config: PeerConfig,
|
|
6
|
-
request: typeof fetch = fetch): Pick<RelayClientOptions, "upgradeHeaders"> {
|
|
6
|
+
request: typeof fetch = fetch): Pick<RelayClientOptions, "upgradeHeaders" | "connectionLeaseExpiresAt"> {
|
|
7
7
|
const url = new URL(config.relayUrl);
|
|
8
8
|
if (config.role !== "machine" || url.protocol !== "wss:" || url.hostname !== "relay.granttap.com") return {};
|
|
9
|
-
|
|
9
|
+
let expiresAt: number | null = null;
|
|
10
|
+
return { connectionLeaseExpiresAt: () => expiresAt,
|
|
11
|
+
upgradeHeaders: async (): Promise<Record<string, string>> => {
|
|
10
12
|
const account = loadAccountLink();
|
|
11
13
|
if (!account) return {};
|
|
12
14
|
const response = await request("https://granttap.com/api/account/machine/admission", {
|
|
@@ -25,6 +27,7 @@ export function managedAdmissionOptions(config: PeerConfig,
|
|
|
25
27
|
|| body.expiresAt <= Date.now() || body.expiresAt > Date.now() + 360_000) {
|
|
26
28
|
throw new Error("Managed admission returned an invalid lease.");
|
|
27
29
|
}
|
|
30
|
+
expiresAt = body.expiresAt;
|
|
28
31
|
return { "X-GrantTap-Admission": body.ticket };
|
|
29
32
|
} };
|
|
30
33
|
}
|
|
@@ -7,12 +7,14 @@ import { observedComputerId } from "../../../../bridge/src/mesh/identity/compute
|
|
|
7
7
|
import { prepareMeshHandoff } from "../../../../bridge/src/mesh/runtime";
|
|
8
8
|
import { readStoreState } from "../../../../bridge/src/mesh/store/state";
|
|
9
9
|
import { controllerPeerGate } from "../../../../bridge/src/pairing/controllers";
|
|
10
|
+
import { managedAdmissionOptions } from "../../../../bridge/src/managed-admission";
|
|
10
11
|
|
|
11
12
|
type Prepare = (request: HandoffRequest) => Promise<boolean>;
|
|
12
13
|
|
|
13
14
|
async function prepareThroughRelay(request: HandoffRequest): Promise<boolean> {
|
|
14
15
|
const config = applyNetworkRoute(loadConfig(machineConfigPath()));
|
|
15
|
-
const client = new RelayClient(config, {
|
|
16
|
+
const client = new RelayClient(config, { ...managedAdmissionOptions(config),
|
|
17
|
+
peerAllowed: controllerPeerGate(config) });
|
|
16
18
|
try {
|
|
17
19
|
await client.connect();
|
|
18
20
|
return await prepareMeshHandoff(client, request);
|
|
@@ -14,6 +14,7 @@ import { completeEnrollment } from "../../connection-center/enrollment";
|
|
|
14
14
|
import { confirmPendingController, controllerPeerGate } from "../../../../bridge/src/pairing/controllers";
|
|
15
15
|
import { applyNetworkRoute } from "../../../../bridge/src/device-network/settings";
|
|
16
16
|
import { startEndpointPublisher } from "../../../../bridge/src/device-network/directory";
|
|
17
|
+
import { managedAdmissionOptions } from "../../../../bridge/src/managed-admission";
|
|
17
18
|
|
|
18
19
|
const ASK_TIMEOUT_MS = Number(
|
|
19
20
|
process.env.GRANTTAP_ASK_TIMEOUT_MS ?? process.env.NODVOX_ASK_TIMEOUT_MS ?? 180_000,
|
|
@@ -35,7 +36,8 @@ export async function relay(): Promise<RelayClient | null> {
|
|
|
35
36
|
try {
|
|
36
37
|
if (!client) {
|
|
37
38
|
const config = applyNetworkRoute(loadConfig(machineConfigPath()));
|
|
38
|
-
client = new RelayClient(config, {
|
|
39
|
+
client = new RelayClient(config, { ...managedAdmissionOptions(config),
|
|
40
|
+
autoReconnect: true, peerAllowed: controllerPeerGate(config) });
|
|
39
41
|
client.onMessage((payload, peerPublicKey) => {
|
|
40
42
|
if (payload.type === "pairing.join") {
|
|
41
43
|
const result = applyPairingJoin(payload);
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
loadGrokBotEndpoint,
|
|
7
7
|
} from "../../../../bridge/src/mesh/identity/endpoint";
|
|
8
8
|
import { localMeshStore } from "../../../../bridge/src/mesh/local-remote/local";
|
|
9
|
+
import { managedAdmissionOptions } from "../../../../bridge/src/managed-admission";
|
|
9
10
|
|
|
10
11
|
let client: RelayClient | null = null;
|
|
11
12
|
let credentialId: string | null = null;
|
|
@@ -43,7 +44,7 @@ export async function grokBotRelay(): Promise<RelayClient> {
|
|
|
43
44
|
});
|
|
44
45
|
if (!client || credentialId !== bundle.credential.credentialId) {
|
|
45
46
|
client?.close();
|
|
46
|
-
client = new RelayClient(bundle.pairing, { autoReconnect: true });
|
|
47
|
+
client = new RelayClient(bundle.pairing, { ...managedAdmissionOptions(bundle.pairing), autoReconnect: true });
|
|
47
48
|
credentialId = bundle.credential.credentialId;
|
|
48
49
|
client.onMessage(receive);
|
|
49
50
|
}
|
package/cursor-plugin/README.md
CHANGED
|
@@ -10,7 +10,7 @@ keeps each phone response tied to the exact originating chat and prompt.
|
|
|
10
10
|
Install the supported CLI on this computer, then run:
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npm install -g granttap-mcp@0.8.
|
|
13
|
+
npm install -g granttap-mcp@0.8.61
|
|
14
14
|
granttap setup
|
|
15
15
|
granttap status
|
|
16
16
|
```
|
package/cursor-plugin/mcp.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"command": "node",
|
|
5
5
|
"args": [
|
|
6
6
|
"-e",
|
|
7
|
-
"\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.
|
|
7
|
+
"\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.61\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 20+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
|
|
8
8
|
]
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -7,7 +7,7 @@ const { spawn } = require("node:child_process");
|
|
|
7
7
|
const { existsSync } = require("node:fs");
|
|
8
8
|
const { delimiter, dirname, join } = require("node:path");
|
|
9
9
|
|
|
10
|
-
const PACKAGE = "granttap-mcp@0.8.
|
|
10
|
+
const PACKAGE = "granttap-mcp@0.8.61";
|
|
11
11
|
const dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === "1";
|
|
12
12
|
const platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM
|
|
13
13
|
? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;
|
package/package.json
CHANGED
|
@@ -22,6 +22,8 @@ export type PeerConfig = {
|
|
|
22
22
|
export type RelayClientOptions = {
|
|
23
23
|
/** Fresh managed admission at each socket upgrade; own relays need no ticket. */
|
|
24
24
|
upgradeHeaders?: () => Promise<Record<string, string>>;
|
|
25
|
+
/** Verified server lease deadline, refreshed without reconnecting the socket. */
|
|
26
|
+
connectionLeaseExpiresAt?: () => number | null;
|
|
25
27
|
autoReconnect?: boolean;
|
|
26
28
|
minReconnectMs?: number;
|
|
27
29
|
maxReconnectMs?: number;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* the Cloudflare Durable Object deployment.
|
|
11
11
|
*/
|
|
12
12
|
import WebSocket from "ws";
|
|
13
|
+
import { ManagedRelayLease, RelaySocketHealth } from "./socket-lifecycle";
|
|
13
14
|
import { OutboundFrameQueue, sendEncryptedPayload } from "./outbound-scheduler";
|
|
14
15
|
import { createHash } from "node:crypto";
|
|
15
16
|
import { Envelope, Payload, type Role } from "../protocol/schema";
|
|
@@ -31,8 +32,8 @@ export class RelayClient {
|
|
|
31
32
|
private connectPromise?: Promise<void>;
|
|
32
33
|
private reconnectTimer?: NodeJS.Timeout;
|
|
33
34
|
private reconnectDelay: number;
|
|
34
|
-
private
|
|
35
|
-
private
|
|
35
|
+
private readonly socketHealth = new RelaySocketHealth();
|
|
36
|
+
private readonly admissionLease = new ManagedRelayLease();
|
|
36
37
|
private readonly sessionKeys = new Map<string, string>();
|
|
37
38
|
/**
|
|
38
39
|
* Delivery ids are relay-controlled metadata, so replay protection is bound
|
|
@@ -96,14 +97,12 @@ export class RelayClient {
|
|
|
96
97
|
reject(new Error(`relay connect timeout (${this.cfg.relayUrl})`));
|
|
97
98
|
}, timeoutMs);
|
|
98
99
|
|
|
99
|
-
ws.on("pong", () => {
|
|
100
|
-
if (this.ws === ws) this.awaitingPong = false;
|
|
101
|
-
});
|
|
102
100
|
ws.on("open", () => {
|
|
103
101
|
opened = true;
|
|
104
102
|
clearTimeout(timer);
|
|
105
103
|
this.reconnectDelay = this.opts.minReconnectMs ?? 1_000;
|
|
106
|
-
this.
|
|
104
|
+
this.socketHealth.start(ws, this.opts.pingIntervalMs ?? 20_000);
|
|
105
|
+
this.admissionLease.start(ws, this.opts);
|
|
107
106
|
// Announce ourselves. The envelope's cleartext (room, from) is what the
|
|
108
107
|
// relay uses to register this socket; the Hello body stays encrypted.
|
|
109
108
|
void this
|
|
@@ -122,7 +121,8 @@ export class RelayClient {
|
|
|
122
121
|
ws.on("close", () => {
|
|
123
122
|
frames.cancel();
|
|
124
123
|
clearTimeout(timer);
|
|
125
|
-
this.
|
|
124
|
+
this.socketHealth.stop(ws);
|
|
125
|
+
this.admissionLease.stop(ws);
|
|
126
126
|
if (this.ws === ws) this.ws = undefined;
|
|
127
127
|
if (!opened) reject(new Error(`relay connection closed (${this.cfg.relayUrl})`));
|
|
128
128
|
this.scheduleReconnect();
|
|
@@ -317,7 +317,8 @@ export class RelayClient {
|
|
|
317
317
|
|
|
318
318
|
close(): void {
|
|
319
319
|
this.frames?.cancel();
|
|
320
|
-
this.
|
|
320
|
+
this.socketHealth.stop();
|
|
321
|
+
this.admissionLease.stop();
|
|
321
322
|
this.intentionalClose = true;
|
|
322
323
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
323
324
|
this.reconnectTimer = undefined;
|
|
@@ -329,48 +330,6 @@ export class RelayClient {
|
|
|
329
330
|
return this.ws?.readyState === WebSocket.OPEN;
|
|
330
331
|
}
|
|
331
332
|
|
|
332
|
-
/**
|
|
333
|
-
* Prove the socket is still there, rather than trusting `readyState`.
|
|
334
|
-
*
|
|
335
|
-
* A machine that changes network — joining a hotspot, waking on another
|
|
336
|
-
* Wi-Fi — leaves its relay socket half-open. No close event arrives, so
|
|
337
|
-
* nothing reconnects, and the computer keeps reporting itself as connected
|
|
338
|
-
* while everything it sends goes nowhere. A ping with a deadline is the only
|
|
339
|
-
* thing that tells the two apart.
|
|
340
|
-
*/
|
|
341
|
-
private startLivenessChecks(ws: WebSocket): void {
|
|
342
|
-
this.stopLivenessChecks();
|
|
343
|
-
const interval = this.opts.pingIntervalMs ?? 20_000;
|
|
344
|
-
this.awaitingPong = false;
|
|
345
|
-
this.livenessTimer = setInterval(() => {
|
|
346
|
-
if (this.ws !== ws || ws.readyState !== WebSocket.OPEN) {
|
|
347
|
-
this.stopLivenessChecks();
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (this.awaitingPong) {
|
|
351
|
-
// The previous ping was never answered. Terminating fires `close`,
|
|
352
|
-
// which is what schedules the reconnect.
|
|
353
|
-
this.stopLivenessChecks();
|
|
354
|
-
ws.terminate();
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
this.awaitingPong = true;
|
|
358
|
-
try {
|
|
359
|
-
ws.ping();
|
|
360
|
-
} catch {
|
|
361
|
-
this.stopLivenessChecks();
|
|
362
|
-
ws.terminate();
|
|
363
|
-
}
|
|
364
|
-
}, interval);
|
|
365
|
-
this.livenessTimer.unref?.();
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
private stopLivenessChecks(): void {
|
|
369
|
-
if (this.livenessTimer) clearInterval(this.livenessTimer);
|
|
370
|
-
this.livenessTimer = undefined;
|
|
371
|
-
this.awaitingPong = false;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
333
|
private scheduleReconnect(): void {
|
|
375
334
|
if (!this.opts.autoReconnect || this.intentionalClose || this.reconnectTimer) return;
|
|
376
335
|
const delay = this.reconnectDelay;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Relay socket lifecycle
|
|
2
|
+
|
|
3
|
+
Public entry point: `index.ts`. `RelayClient` uses this module for network health
|
|
4
|
+
and short managed admission leases. Admission refresh stays on the existing
|
|
5
|
+
socket and does not rotate task keys or reload transcript state. A failed lookup
|
|
6
|
+
cannot extend the server's signed expiry. Closing or replacing a socket cancels
|
|
7
|
+
only its own timers.
|
|
8
|
+
|
|
9
|
+
The real WebSocket integration is in `tests/core/managed-relay-renewal.test.ts`.
|
|
10
|
+
|
|
11
|
+
Licensed under the MIT License in the repository root.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import type { RelayClientOptions } from "../relay-client-types";
|
|
3
|
+
|
|
4
|
+
/** Renew a computer's signed admission on its existing socket; no chat keys change. */
|
|
5
|
+
export class ManagedRelayLease {
|
|
6
|
+
private socket?: WebSocket;
|
|
7
|
+
private timer?: NodeJS.Timeout;
|
|
8
|
+
|
|
9
|
+
start(socket: WebSocket, options: RelayClientOptions): void {
|
|
10
|
+
this.stop();
|
|
11
|
+
this.socket = socket;
|
|
12
|
+
socket.once("close", () => this.stop(socket));
|
|
13
|
+
this.schedule(socket, options);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
stop(socket?: WebSocket): void {
|
|
17
|
+
if (socket && this.socket !== socket) return;
|
|
18
|
+
if (this.timer) clearTimeout(this.timer);
|
|
19
|
+
this.timer = undefined;
|
|
20
|
+
this.socket = undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private schedule(socket: WebSocket, options: RelayClientOptions, retry = false): void {
|
|
24
|
+
const expiry = options.connectionLeaseExpiresAt?.();
|
|
25
|
+
if (!expiry || !Number.isFinite(expiry) || !options.upgradeHeaders) return;
|
|
26
|
+
const remaining = expiry - Date.now();
|
|
27
|
+
const margin = Math.min(30_000, Math.max(10, remaining * 0.15));
|
|
28
|
+
const delay = retry ? 10_000 : Math.max(10, remaining - margin);
|
|
29
|
+
this.timer = setTimeout(() => void this.renew(socket, options), delay);
|
|
30
|
+
this.timer.unref();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private async renew(socket: WebSocket, options: RelayClientOptions): Promise<void> {
|
|
34
|
+
this.timer = undefined;
|
|
35
|
+
if (this.socket !== socket || socket.readyState !== WebSocket.OPEN) return;
|
|
36
|
+
try {
|
|
37
|
+
const headers = await options.upgradeHeaders!();
|
|
38
|
+
if (this.socket !== socket || socket.readyState !== WebSocket.OPEN) return;
|
|
39
|
+
const ticket = headers["X-GrantTap-Admission"];
|
|
40
|
+
if (!ticket) throw new Error("No refreshed relay admission");
|
|
41
|
+
await new Promise<void>((resolve, reject) => socket.send(
|
|
42
|
+
JSON.stringify({ type: "relay.admission", ticket }), error => error ? reject(error) : resolve()));
|
|
43
|
+
this.schedule(socket, options);
|
|
44
|
+
} catch {
|
|
45
|
+
// The relay's signed expiry remains authoritative; a failed lookup cannot extend it.
|
|
46
|
+
if (this.socket === socket && socket.readyState === WebSocket.OPEN) this.schedule(socket, options, true);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
|
|
3
|
+
/** Detect half-open network sockets independently of managed authorization. */
|
|
4
|
+
export class RelaySocketHealth {
|
|
5
|
+
private socket?: WebSocket;
|
|
6
|
+
private timer?: NodeJS.Timeout;
|
|
7
|
+
private awaitingPong = false;
|
|
8
|
+
|
|
9
|
+
start(socket: WebSocket, interval: number): void {
|
|
10
|
+
this.stop();
|
|
11
|
+
this.socket = socket;
|
|
12
|
+
socket.on("pong", () => { if (this.socket === socket) this.awaitingPong = false; });
|
|
13
|
+
this.timer = setInterval(() => {
|
|
14
|
+
if (this.socket !== socket || socket.readyState !== WebSocket.OPEN) { this.stop(socket); return; }
|
|
15
|
+
if (this.awaitingPong) { this.stop(socket); socket.terminate(); return; }
|
|
16
|
+
this.awaitingPong = true;
|
|
17
|
+
try { socket.ping(); }
|
|
18
|
+
catch { this.stop(socket); socket.terminate(); }
|
|
19
|
+
}, interval);
|
|
20
|
+
this.timer.unref();
|
|
21
|
+
socket.once("close", () => this.stop(socket));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
stop(socket?: WebSocket): void {
|
|
25
|
+
if (socket && this.socket !== socket) return;
|
|
26
|
+
if (this.timer) clearInterval(this.timer);
|
|
27
|
+
this.timer = undefined;
|
|
28
|
+
this.socket = undefined;
|
|
29
|
+
this.awaitingPong = false;
|
|
30
|
+
}
|
|
31
|
+
}
|