@forge-glance/sdk 0.1.0
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/dist/ActionCableClient.d.ts +41 -0
- package/dist/ActionCableClient.js +189 -0
- package/dist/GitHubProvider.d.ts +50 -0
- package/dist/GitHubProvider.js +361 -0
- package/dist/GitLabProvider.d.ts +34 -0
- package/dist/GitLabProvider.js +359 -0
- package/dist/GitProvider.d.ts +50 -0
- package/dist/GitProvider.js +12 -0
- package/dist/MRDetailFetcher.d.ts +18 -0
- package/dist/MRDetailFetcher.js +74 -0
- package/dist/NoteMutator.d.ts +37 -0
- package/dist/NoteMutator.js +54 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +972 -0
- package/dist/logger.d.ts +20 -0
- package/dist/logger.js +10 -0
- package/dist/providers.d.ts +23 -0
- package/dist/providers.js +722 -0
- package/dist/types.d.ts +176 -0
- package/dist/types.js +0 -0
- package/package.json +38 -0
- package/src/ActionCableClient.ts +237 -0
- package/src/GitHubProvider.ts +639 -0
- package/src/GitLabProvider.ts +471 -0
- package/src/GitProvider.ts +77 -0
- package/src/MRDetailFetcher.ts +133 -0
- package/src/NoteMutator.ts +108 -0
- package/src/index.ts +54 -0
- package/src/logger.ts +26 -0
- package/src/providers.ts +40 -0
- package/src/types.ts +196 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-agnostic domain types.
|
|
3
|
+
*
|
|
4
|
+
* These are the canonical types sent to Swift clients via the WebSocket protocol.
|
|
5
|
+
* No raw provider payloads leave the provider layer — only these types.
|
|
6
|
+
*
|
|
7
|
+
* Field names match the Swift DomainXxx structs in Sources/GlanceLib/Models/Domain/.
|
|
8
|
+
*/
|
|
9
|
+
export interface UserRef {
|
|
10
|
+
/** Scoped provider ID, e.g. "gitlab:12345". */
|
|
11
|
+
id: string;
|
|
12
|
+
username: string;
|
|
13
|
+
name: string;
|
|
14
|
+
avatarUrl: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface PipelineJob {
|
|
17
|
+
/** Scoped provider ID, e.g. "gitlab:12345". */
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
stage: string;
|
|
21
|
+
/** Normalized status: "success" | "failed" | "running" | "pending" | "canceled" | "skipped" | "manual" | etc. */
|
|
22
|
+
status: string;
|
|
23
|
+
allowFailure: boolean;
|
|
24
|
+
webUrl: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface Pipeline {
|
|
27
|
+
/** Scoped provider ID, e.g. "gitlab:pipeline:12345". */
|
|
28
|
+
id: string;
|
|
29
|
+
/** Normalized status. */
|
|
30
|
+
status: string;
|
|
31
|
+
createdAt: string | null;
|
|
32
|
+
webUrl: string | null;
|
|
33
|
+
jobs: PipelineJob[];
|
|
34
|
+
}
|
|
35
|
+
export interface DiffStats {
|
|
36
|
+
additions: number;
|
|
37
|
+
deletions: number;
|
|
38
|
+
filesChanged: number;
|
|
39
|
+
}
|
|
40
|
+
export interface PullRequest {
|
|
41
|
+
/** Scoped provider ID, e.g. "gitlab:12345". */
|
|
42
|
+
id: string;
|
|
43
|
+
/** Numeric MR/PR number within the project. */
|
|
44
|
+
iid: number;
|
|
45
|
+
/** Scoped repository ID, e.g. "gitlab:42". */
|
|
46
|
+
repositoryId: string;
|
|
47
|
+
title: string;
|
|
48
|
+
/** Full MR description body. Used by AI summarization and the MR header. */
|
|
49
|
+
description: string | null;
|
|
50
|
+
/** "opened" | "merged" | "closed" */
|
|
51
|
+
state: string;
|
|
52
|
+
draft: boolean;
|
|
53
|
+
conflicts: boolean;
|
|
54
|
+
webUrl: string | null;
|
|
55
|
+
sourceBranch: string;
|
|
56
|
+
targetBranch: string;
|
|
57
|
+
createdAt: string | null;
|
|
58
|
+
updatedAt: string | null;
|
|
59
|
+
/** Head commit SHA. */
|
|
60
|
+
sha: string | null;
|
|
61
|
+
author: UserRef;
|
|
62
|
+
assignees: UserRef[];
|
|
63
|
+
reviewers: UserRef[];
|
|
64
|
+
/** Roles the current authenticated user has on this MR. Values: "author" | "reviewer" | "assignee". */
|
|
65
|
+
roles: string[];
|
|
66
|
+
pipeline: Pipeline | null;
|
|
67
|
+
unresolvedThreadCount: number;
|
|
68
|
+
approvalsLeft: number;
|
|
69
|
+
/** True when the MR has met its approval requirements. */
|
|
70
|
+
approved: boolean;
|
|
71
|
+
approvedBy: UserRef[];
|
|
72
|
+
diffStats: DiffStats | null;
|
|
73
|
+
/**
|
|
74
|
+
* Raw GitLab detailedMergeStatus value, e.g. "mergeable", "conflict",
|
|
75
|
+
* "not_approved", "discussions_not_resolved". Null for non-GitLab providers.
|
|
76
|
+
*/
|
|
77
|
+
detailedMergeStatus: string | null;
|
|
78
|
+
}
|
|
79
|
+
/** Snapshot payload sent when a client first connects. */
|
|
80
|
+
export interface PullRequestsSnapshot {
|
|
81
|
+
items: PullRequest[];
|
|
82
|
+
}
|
|
83
|
+
/** Feed event emitted as `feed_event` (incremental) or inside `feed_snapshot` (initial batch). */
|
|
84
|
+
export interface FeedEvent {
|
|
85
|
+
/** Stable event ID, e.g. "note-1234" or "projEvent-5678". */
|
|
86
|
+
id: string;
|
|
87
|
+
/** MR IID within the project. */
|
|
88
|
+
mrIid: number;
|
|
89
|
+
/** Scoped repository ID, e.g. "gitlab:42". */
|
|
90
|
+
repositoryId: string;
|
|
91
|
+
/** Actor's GitLab username. */
|
|
92
|
+
actor: string;
|
|
93
|
+
actorAvatarUrl: string | null;
|
|
94
|
+
body: string | null;
|
|
95
|
+
/** ISO 8601 timestamp. */
|
|
96
|
+
createdAt: string;
|
|
97
|
+
/**
|
|
98
|
+
* View scope the event was fetched under.
|
|
99
|
+
* "authored_by_me" | "reviewing" | "assigned_to_me" | "any"
|
|
100
|
+
*/
|
|
101
|
+
scope: string;
|
|
102
|
+
/** Normalized action: "commented" | "approved" | "merged" | "closed" | etc. */
|
|
103
|
+
action: string;
|
|
104
|
+
/** Normalized target type, e.g. "merge_request". */
|
|
105
|
+
targetType: string;
|
|
106
|
+
/** Note ID when the event originates from a note; null for action-only events. */
|
|
107
|
+
noteId: number | null;
|
|
108
|
+
/** MR title — the primary title shown in the Swift feed row. */
|
|
109
|
+
title: string;
|
|
110
|
+
/** Short human-readable line, e.g. "username commented". */
|
|
111
|
+
subtitle: string;
|
|
112
|
+
/** MR web URL for deep-linking. */
|
|
113
|
+
webUrl: string | null;
|
|
114
|
+
/**
|
|
115
|
+
* True for events within the initial history window (not new since last poll).
|
|
116
|
+
* Swift uses this to set event state to `.read` on insertion.
|
|
117
|
+
*/
|
|
118
|
+
isHistorical: boolean;
|
|
119
|
+
}
|
|
120
|
+
/** Payload for a `feed_snapshot` event sent on first connect. */
|
|
121
|
+
export interface FeedSnapshot {
|
|
122
|
+
items: FeedEvent[];
|
|
123
|
+
}
|
|
124
|
+
export interface NoteAuthor {
|
|
125
|
+
/** Scoped provider ID, e.g. "gitlab:user:12345". */
|
|
126
|
+
id: string;
|
|
127
|
+
username: string;
|
|
128
|
+
name: string;
|
|
129
|
+
avatarUrl: string | null;
|
|
130
|
+
}
|
|
131
|
+
export interface NotePosition {
|
|
132
|
+
newPath: string | null;
|
|
133
|
+
oldPath: string | null;
|
|
134
|
+
newLine: number | null;
|
|
135
|
+
oldLine: number | null;
|
|
136
|
+
positionType: string | null;
|
|
137
|
+
}
|
|
138
|
+
export interface Note {
|
|
139
|
+
id: number;
|
|
140
|
+
body: string;
|
|
141
|
+
author: NoteAuthor;
|
|
142
|
+
createdAt: string;
|
|
143
|
+
system: boolean;
|
|
144
|
+
/** "DiffNote" | "DiscussionNote" | null */
|
|
145
|
+
type: string | null;
|
|
146
|
+
resolvable: boolean | null;
|
|
147
|
+
resolved: boolean | null;
|
|
148
|
+
position: NotePosition | null;
|
|
149
|
+
}
|
|
150
|
+
export interface Discussion {
|
|
151
|
+
id: string;
|
|
152
|
+
resolvable: boolean | null;
|
|
153
|
+
resolved: boolean | null;
|
|
154
|
+
notes: Note[];
|
|
155
|
+
}
|
|
156
|
+
export interface MRDetail {
|
|
157
|
+
mrIid: number;
|
|
158
|
+
/** Scoped repository ID, e.g. "gitlab:42". */
|
|
159
|
+
repositoryId: string;
|
|
160
|
+
discussions: Discussion[];
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Payload for a `notification` event emitted by the server to connected clients.
|
|
164
|
+
*
|
|
165
|
+
* Rules are intentionally simple for Phase A6 (prefs stubbed).
|
|
166
|
+
* When per-user preferences land (Phase B), the server will evaluate a proper
|
|
167
|
+
* rules engine before deciding whether to emit this event.
|
|
168
|
+
*/
|
|
169
|
+
export interface ServerNotification {
|
|
170
|
+
title: string;
|
|
171
|
+
body: string;
|
|
172
|
+
/** Scoped PR id, e.g. "gitlab:mr:12345". Omitted for non-MR notifications. */
|
|
173
|
+
mrId?: string;
|
|
174
|
+
/** Deep-link URL to open when the notification is tapped. */
|
|
175
|
+
webUrl?: string | null;
|
|
176
|
+
}
|
package/dist/types.js
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forge-glance/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "GitHub & GitLab API client — REST, GraphQL, and real-time ActionCable subscriptions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"bun": "./src/index.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"dist",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "bun build src/index.ts src/types.ts src/logger.ts src/GitProvider.ts src/GitLabProvider.ts src/GitHubProvider.ts src/ActionCableClient.ts src/MRDetailFetcher.ts src/NoteMutator.ts src/providers.ts --outdir dist --root src --target node && tsc -p tsconfig.build.json",
|
|
23
|
+
"check": "tsc --noEmit -p tsconfig.json",
|
|
24
|
+
"prepublishOnly": "bun run check && bun run build"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/m4ttheweric/Glance",
|
|
30
|
+
"directory": "packages/sdk"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"typescript": "^5.9.3"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActionCableClient — outgoing WebSocket client for GitLab's ActionCable endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Connects to wss://{baseURL}/-/cable, implements the ActionCable protocol
|
|
5
|
+
* (subscribe/unsubscribe/ping/confirm), and auto-reconnects with exponential backoff.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors Swift's ActionCableClient.swift.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { type ForgeLogger, noopLogger } from "./logger.ts";
|
|
11
|
+
|
|
12
|
+
export interface ActionCableCallbacks {
|
|
13
|
+
onConnected(): void;
|
|
14
|
+
onDisconnected(intentional: boolean, reason: string): void;
|
|
15
|
+
onMessage(identifier: string, message: unknown): void;
|
|
16
|
+
onConfirm(identifier: string): void;
|
|
17
|
+
onReject(identifier: string): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const BASE_RECONNECT_DELAY_MS = 1_000;
|
|
21
|
+
const MAX_RECONNECT_DELAY_MS = 120_000;
|
|
22
|
+
const MAX_RECONNECT_ATTEMPTS = 8;
|
|
23
|
+
|
|
24
|
+
export class ActionCableClient {
|
|
25
|
+
private ws: WebSocket | null = null;
|
|
26
|
+
private reconnectAttempt = 0;
|
|
27
|
+
private intentionalDisconnect = false;
|
|
28
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
29
|
+
private readonly wsUrl: string;
|
|
30
|
+
private readonly originUrl: string;
|
|
31
|
+
private readonly log: ForgeLogger;
|
|
32
|
+
private readonly logContext: string;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
baseURL: string,
|
|
36
|
+
private readonly token: string,
|
|
37
|
+
private readonly callbacks: ActionCableCallbacks,
|
|
38
|
+
options: { logger?: ForgeLogger; logContext?: string } = {},
|
|
39
|
+
) {
|
|
40
|
+
this.log = options.logger ?? noopLogger;
|
|
41
|
+
this.logContext = options.logContext ?? "";
|
|
42
|
+
const stripped = baseURL.replace(/\/$/, "");
|
|
43
|
+
this.originUrl = stripped;
|
|
44
|
+
this.wsUrl =
|
|
45
|
+
stripped.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") + "/-/cable";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
connect(): void {
|
|
49
|
+
this.intentionalDisconnect = false;
|
|
50
|
+
this.reconnectAttempt = 0;
|
|
51
|
+
this.performConnect();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
disconnect(): void {
|
|
55
|
+
this.intentionalDisconnect = true;
|
|
56
|
+
this.cleanup();
|
|
57
|
+
this.log.info("ActionCable intentionally disconnected", {
|
|
58
|
+
url: this.wsUrl,
|
|
59
|
+
ctx: this.logContext,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
subscribe(identifier: string): void {
|
|
64
|
+
this.send({ command: "subscribe", identifier });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
unsubscribe(identifier: string): void {
|
|
68
|
+
this.send({ command: "unsubscribe", identifier });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private performConnect(): void {
|
|
72
|
+
this.cleanup();
|
|
73
|
+
|
|
74
|
+
let ws: WebSocket;
|
|
75
|
+
try {
|
|
76
|
+
// Bun extends the standard WebSocket constructor to accept an options object
|
|
77
|
+
// with a `headers` field (Bun-specific, not in the browser WebSocket API).
|
|
78
|
+
ws = new WebSocket(
|
|
79
|
+
this.wsUrl,
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
{
|
|
82
|
+
headers: {
|
|
83
|
+
Authorization: `Bearer ${this.token}`,
|
|
84
|
+
Origin: this.originUrl,
|
|
85
|
+
},
|
|
86
|
+
} as any,
|
|
87
|
+
);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
this.log.error("ActionCable failed to create WebSocket", {
|
|
91
|
+
url: this.wsUrl,
|
|
92
|
+
message,
|
|
93
|
+
ctx: this.logContext,
|
|
94
|
+
});
|
|
95
|
+
this.scheduleReconnect();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.ws = ws;
|
|
100
|
+
|
|
101
|
+
ws.onmessage = (event: MessageEvent) => {
|
|
102
|
+
const raw = typeof event.data === "string" ? event.data : String(event.data);
|
|
103
|
+
this.handleMessage(raw);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
ws.onclose = (event: CloseEvent) => {
|
|
107
|
+
if (this.intentionalDisconnect) {
|
|
108
|
+
this.callbacks.onDisconnected(true, "intentional disconnect");
|
|
109
|
+
} else {
|
|
110
|
+
const reason = event.reason || `code ${event.code}`;
|
|
111
|
+
this.log.warn("ActionCable disconnected", {
|
|
112
|
+
url: this.wsUrl,
|
|
113
|
+
reason,
|
|
114
|
+
ctx: this.logContext,
|
|
115
|
+
});
|
|
116
|
+
this.callbacks.onDisconnected(false, reason);
|
|
117
|
+
this.scheduleReconnect();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
ws.onerror = () => {
|
|
122
|
+
// onclose always fires after onerror and handles the reconnect schedule.
|
|
123
|
+
this.log.warn("ActionCable WebSocket error", { url: this.wsUrl, ctx: this.logContext });
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
this.log.info("ActionCable connecting", { url: this.wsUrl, ctx: this.logContext });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private handleMessage(raw: string): void {
|
|
130
|
+
let msg: {
|
|
131
|
+
type?: string;
|
|
132
|
+
identifier?: string;
|
|
133
|
+
message?: unknown;
|
|
134
|
+
reason?: string;
|
|
135
|
+
reconnect?: boolean;
|
|
136
|
+
};
|
|
137
|
+
try {
|
|
138
|
+
msg = JSON.parse(raw) as typeof msg;
|
|
139
|
+
} catch {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!msg.type) {
|
|
144
|
+
// Data message — no "type" field present.
|
|
145
|
+
if (typeof msg.identifier === "string" && msg.message !== undefined) {
|
|
146
|
+
this.callbacks.onMessage(msg.identifier, msg.message);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
switch (msg.type) {
|
|
152
|
+
case "welcome":
|
|
153
|
+
this.reconnectAttempt = 0;
|
|
154
|
+
this.log.info("ActionCable connected (welcome)", { url: this.wsUrl, ctx: this.logContext });
|
|
155
|
+
this.callbacks.onConnected();
|
|
156
|
+
break;
|
|
157
|
+
|
|
158
|
+
case "ping":
|
|
159
|
+
// Server heartbeat — no response needed.
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case "confirm_subscription":
|
|
163
|
+
if (typeof msg.identifier === "string") {
|
|
164
|
+
this.log.debug("ActionCable subscription confirmed", { ctx: this.logContext });
|
|
165
|
+
this.callbacks.onConfirm(msg.identifier);
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case "reject_subscription":
|
|
170
|
+
if (typeof msg.identifier === "string") {
|
|
171
|
+
this.log.warn("ActionCable subscription rejected", { ctx: this.logContext });
|
|
172
|
+
this.callbacks.onReject(msg.identifier);
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case "disconnect": {
|
|
177
|
+
const shouldReconnect = msg.reconnect !== false;
|
|
178
|
+
this.log.info("ActionCable server disconnect", {
|
|
179
|
+
reason: msg.reason,
|
|
180
|
+
reconnect: shouldReconnect,
|
|
181
|
+
ctx: this.logContext,
|
|
182
|
+
});
|
|
183
|
+
if (!shouldReconnect) this.intentionalDisconnect = true;
|
|
184
|
+
this.callbacks.onDisconnected(!shouldReconnect, msg.reason ?? "server disconnect");
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private send(obj: Record<string, unknown>): void {
|
|
191
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
192
|
+
this.ws.send(JSON.stringify(obj));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private cleanup(): void {
|
|
197
|
+
if (this.reconnectTimer !== null) {
|
|
198
|
+
clearTimeout(this.reconnectTimer);
|
|
199
|
+
this.reconnectTimer = null;
|
|
200
|
+
}
|
|
201
|
+
if (this.ws !== null) {
|
|
202
|
+
this.ws.onmessage = null;
|
|
203
|
+
this.ws.onclose = null;
|
|
204
|
+
this.ws.onerror = null;
|
|
205
|
+
this.ws.close();
|
|
206
|
+
this.ws = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private scheduleReconnect(): void {
|
|
211
|
+
if (this.reconnectAttempt >= MAX_RECONNECT_ATTEMPTS) {
|
|
212
|
+
this.log.error("ActionCable max reconnect attempts reached", {
|
|
213
|
+
url: this.wsUrl,
|
|
214
|
+
ctx: this.logContext,
|
|
215
|
+
});
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const base = BASE_RECONNECT_DELAY_MS * Math.pow(2, this.reconnectAttempt);
|
|
220
|
+
const jitter = Math.random() * base * 0.3;
|
|
221
|
+
const delayMs = Math.min(base + jitter, MAX_RECONNECT_DELAY_MS);
|
|
222
|
+
this.reconnectAttempt++;
|
|
223
|
+
|
|
224
|
+
this.log.info("ActionCable scheduling reconnect", {
|
|
225
|
+
attempt: this.reconnectAttempt,
|
|
226
|
+
delayMs: Math.round(delayMs),
|
|
227
|
+
ctx: this.logContext,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
this.reconnectTimer = setTimeout(() => {
|
|
231
|
+
this.reconnectTimer = null;
|
|
232
|
+
if (!this.intentionalDisconnect) {
|
|
233
|
+
this.performConnect();
|
|
234
|
+
}
|
|
235
|
+
}, delayMs);
|
|
236
|
+
}
|
|
237
|
+
}
|