@shakudo/opencode-mattermost-control 0.3.45
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/.opencode/command/mattermost-connect.md +5 -0
- package/.opencode/command/mattermost-disconnect.md +5 -0
- package/.opencode/command/mattermost-monitor.md +12 -0
- package/.opencode/command/mattermost-status.md +5 -0
- package/.opencode/command/speckit.analyze.md +184 -0
- package/.opencode/command/speckit.checklist.md +294 -0
- package/.opencode/command/speckit.clarify.md +181 -0
- package/.opencode/command/speckit.constitution.md +82 -0
- package/.opencode/command/speckit.implement.md +135 -0
- package/.opencode/command/speckit.plan.md +89 -0
- package/.opencode/command/speckit.specify.md +258 -0
- package/.opencode/command/speckit.tasks.md +137 -0
- package/.opencode/command/speckit.taskstoissues.md +30 -0
- package/.opencode/plugin/mattermost-control/event-handlers/compaction.ts +61 -0
- package/.opencode/plugin/mattermost-control/event-handlers/file.ts +36 -0
- package/.opencode/plugin/mattermost-control/event-handlers/index.ts +14 -0
- package/.opencode/plugin/mattermost-control/event-handlers/message.ts +124 -0
- package/.opencode/plugin/mattermost-control/event-handlers/permission.ts +34 -0
- package/.opencode/plugin/mattermost-control/event-handlers/question.ts +92 -0
- package/.opencode/plugin/mattermost-control/event-handlers/session.ts +100 -0
- package/.opencode/plugin/mattermost-control/event-handlers/todo.ts +33 -0
- package/.opencode/plugin/mattermost-control/event-handlers/tool.ts +76 -0
- package/.opencode/plugin/mattermost-control/formatters.ts +202 -0
- package/.opencode/plugin/mattermost-control/index.ts +964 -0
- package/.opencode/plugin/mattermost-control/package.json +12 -0
- package/.opencode/plugin/mattermost-control/state.ts +180 -0
- package/.opencode/plugin/mattermost-control/timers.ts +96 -0
- package/.opencode/plugin/mattermost-control/tools/connect.ts +563 -0
- package/.opencode/plugin/mattermost-control/tools/file.ts +41 -0
- package/.opencode/plugin/mattermost-control/tools/index.ts +12 -0
- package/.opencode/plugin/mattermost-control/tools/monitor.ts +183 -0
- package/.opencode/plugin/mattermost-control/tools/schedule.ts +253 -0
- package/.opencode/plugin/mattermost-control/tools/session.ts +120 -0
- package/.opencode/plugin/mattermost-control/types.ts +107 -0
- package/LICENSE +21 -0
- package/README.md +1280 -0
- package/opencode-shared +359 -0
- package/opencode-shared-restart +495 -0
- package/opencode-shared-stop +90 -0
- package/package.json +65 -0
- package/src/clients/mattermost-client.ts +221 -0
- package/src/clients/websocket-client.ts +199 -0
- package/src/command-handler.ts +1035 -0
- package/src/config.ts +170 -0
- package/src/context-builder.ts +309 -0
- package/src/file-completion-handler.ts +521 -0
- package/src/file-handler.ts +242 -0
- package/src/guest-approval-handler.ts +223 -0
- package/src/logger.ts +73 -0
- package/src/merge-handler.ts +335 -0
- package/src/message-router.ts +151 -0
- package/src/models/index.ts +197 -0
- package/src/models/routing.ts +50 -0
- package/src/models/thread-mapping.ts +40 -0
- package/src/monitor-service.ts +222 -0
- package/src/notification-service.ts +118 -0
- package/src/opencode-session-registry.ts +370 -0
- package/src/persistence/team-store.ts +396 -0
- package/src/persistence/thread-mapping-store.ts +258 -0
- package/src/question-handler.ts +401 -0
- package/src/reaction-handler.ts +111 -0
- package/src/response-streamer.ts +364 -0
- package/src/scheduler/schedule-store.ts +261 -0
- package/src/scheduler/scheduler-service.ts +349 -0
- package/src/session-manager.ts +142 -0
- package/src/session-ownership-handler.ts +253 -0
- package/src/status-indicator.ts +279 -0
- package/src/thread-manager.ts +231 -0
- package/src/todo-manager.ts +162 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { MattermostConfig } from "../config.js";
|
|
2
|
+
import type { User, Team, Channel, Post, PostList, FileInfo } from "../models/index.js";
|
|
3
|
+
import { log as fileLog } from "../logger.js";
|
|
4
|
+
|
|
5
|
+
interface RequestOptions {
|
|
6
|
+
method?: string;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
body?: string | FormData;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class MattermostClient {
|
|
12
|
+
private config: MattermostConfig;
|
|
13
|
+
private baseUrl: string;
|
|
14
|
+
private token: string | null = null;
|
|
15
|
+
private debug: boolean;
|
|
16
|
+
|
|
17
|
+
constructor(config: MattermostConfig) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.token = config.token || null;
|
|
20
|
+
this.debug = config.debug || false;
|
|
21
|
+
|
|
22
|
+
if (!this.config.baseUrl.startsWith("http://") && !this.config.baseUrl.startsWith("https://")) {
|
|
23
|
+
throw new Error(`Invalid baseUrl format: ${this.config.baseUrl} - URL must start with http:// or https://`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.baseUrl = this.config.baseUrl.replace(/\/$/, "");
|
|
27
|
+
this.log("Mattermost client initialized with base URL:", this.baseUrl);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private log(...args: unknown[]): void {
|
|
31
|
+
if (this.debug) {
|
|
32
|
+
fileLog.debug(`[Client] ${args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private getHeaders(contentType?: string): Record<string, string> {
|
|
37
|
+
const headers: Record<string, string> = {
|
|
38
|
+
"Accept": "application/json",
|
|
39
|
+
};
|
|
40
|
+
if (contentType) {
|
|
41
|
+
headers["Content-Type"] = contentType;
|
|
42
|
+
}
|
|
43
|
+
if (this.token) {
|
|
44
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
45
|
+
}
|
|
46
|
+
return headers;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
50
|
+
const url = `${this.baseUrl}${path}`;
|
|
51
|
+
const isFormData = options.body instanceof FormData;
|
|
52
|
+
|
|
53
|
+
const response = await fetch(url, {
|
|
54
|
+
method: options.method || "GET",
|
|
55
|
+
headers: {
|
|
56
|
+
...this.getHeaders(isFormData ? undefined : "application/json"),
|
|
57
|
+
...options.headers,
|
|
58
|
+
},
|
|
59
|
+
body: options.body,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (response.status >= 400) {
|
|
63
|
+
const errorData = await response.text();
|
|
64
|
+
throw new Error(`Request failed (${response.status}): ${errorData}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const contentType = response.headers.get("content-type");
|
|
68
|
+
if (contentType?.includes("application/json")) {
|
|
69
|
+
return response.json() as Promise<T>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return response.text() as unknown as T;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async requestArrayBuffer(path: string): Promise<Buffer> {
|
|
76
|
+
const url = `${this.baseUrl}${path}`;
|
|
77
|
+
const response = await fetch(url, {
|
|
78
|
+
method: "GET",
|
|
79
|
+
headers: this.getHeaders(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (response.status >= 400) {
|
|
83
|
+
const errorData = await response.text();
|
|
84
|
+
throw new Error(`Request failed (${response.status}): ${errorData}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
88
|
+
return Buffer.from(arrayBuffer);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async testConnection(): Promise<any> {
|
|
92
|
+
return this.request("/system/ping");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async getCurrentUser(): Promise<User> {
|
|
96
|
+
return this.request<User>("/users/me");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getUserByUsername(username: string): Promise<User> {
|
|
100
|
+
return this.request<User>(`/users/username/${username}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getUserById(userId: string): Promise<User> {
|
|
104
|
+
return this.request<User>(`/users/${userId}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async getTeams(): Promise<Team[]> {
|
|
108
|
+
return this.request<Team[]>("/teams");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async getChannel(channelId: string): Promise<Channel> {
|
|
112
|
+
return this.request<Channel>(`/channels/${channelId}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async createDirectChannel(userId: string): Promise<Channel> {
|
|
116
|
+
const currentUser = await this.getCurrentUser();
|
|
117
|
+
return this.request<Channel>("/channels/direct", {
|
|
118
|
+
method: "POST",
|
|
119
|
+
body: JSON.stringify([currentUser.id, userId]),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async createPost(channelId: string, message: string, rootId?: string, fileIds?: string[]): Promise<Post> {
|
|
124
|
+
const payload: any = {
|
|
125
|
+
channel_id: channelId,
|
|
126
|
+
message,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (rootId) payload.root_id = rootId;
|
|
130
|
+
if (fileIds) payload.file_ids = fileIds;
|
|
131
|
+
|
|
132
|
+
this.log(`Creating post in channel: ${channelId}`);
|
|
133
|
+
return this.request<Post>("/posts", {
|
|
134
|
+
method: "POST",
|
|
135
|
+
body: JSON.stringify(payload),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async updatePost(postId: string, message: string): Promise<Post> {
|
|
140
|
+
this.log(`Updating post: ${postId}`);
|
|
141
|
+
return this.request<Post>(`/posts/${postId}`, {
|
|
142
|
+
method: "PUT",
|
|
143
|
+
body: JSON.stringify({ id: postId, message }),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async deletePost(postId: string): Promise<void> {
|
|
148
|
+
this.log(`Deleting post: ${postId}`);
|
|
149
|
+
await this.request(`/posts/${postId}`, { method: "DELETE" });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async getPosts(channelId: string, page = 0, perPage = 60): Promise<PostList> {
|
|
153
|
+
return this.request<PostList>(`/channels/${channelId}/posts?page=${page}&per_page=${perPage}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async getPost(postId: string): Promise<Post> {
|
|
157
|
+
return this.request<Post>(`/posts/${postId}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async getPostThread(postId: string): Promise<PostList> {
|
|
161
|
+
return this.request<PostList>(`/posts/${postId}/thread`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async addReaction(postId: string, emojiName: string): Promise<any> {
|
|
165
|
+
const currentUser = await this.getCurrentUser();
|
|
166
|
+
return this.request("/reactions", {
|
|
167
|
+
method: "POST",
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
user_id: currentUser.id,
|
|
170
|
+
post_id: postId,
|
|
171
|
+
emoji_name: emojiName,
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async getReactions(postId: string): Promise<any[]> {
|
|
177
|
+
const result = await this.request<any[]>(`/posts/${postId}/reactions`);
|
|
178
|
+
return result || [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async uploadFile(
|
|
182
|
+
channelId: string,
|
|
183
|
+
fileName: string,
|
|
184
|
+
fileData: Buffer,
|
|
185
|
+
contentType: string
|
|
186
|
+
): Promise<{ file_infos: FileInfo[]; client_ids: string[] }> {
|
|
187
|
+
this.log(`Uploading file ${fileName} to channel ${channelId}`);
|
|
188
|
+
|
|
189
|
+
const formData = new FormData();
|
|
190
|
+
formData.append("files", new Blob([new Uint8Array(fileData)], { type: contentType }), fileName);
|
|
191
|
+
formData.append("channel_id", channelId);
|
|
192
|
+
|
|
193
|
+
const url = `${this.baseUrl}/files`;
|
|
194
|
+
const response = await fetch(url, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: {
|
|
197
|
+
"Authorization": `Bearer ${this.token}`,
|
|
198
|
+
},
|
|
199
|
+
body: formData,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (response.status >= 400) {
|
|
203
|
+
const errorData = await response.text();
|
|
204
|
+
throw new Error(`Failed to upload file: ${errorData}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return response.json();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async downloadFile(fileId: string): Promise<Buffer> {
|
|
211
|
+
return this.requestArrayBuffer(`/files/${fileId}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async getFileInfo(fileId: string): Promise<FileInfo> {
|
|
215
|
+
return this.request<FileInfo>(`/files/${fileId}/info`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async getChannelMembers(channelId: string): Promise<{ user_id: string; channel_id: string }[]> {
|
|
219
|
+
return this.request<{ user_id: string; channel_id: string }[]>(`/channels/${channelId}/members`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
import type { MattermostConfig } from "../config.js";
|
|
4
|
+
import type { WebSocketEvent } from "../models/index.js";
|
|
5
|
+
import { log as fileLog } from "../logger.js";
|
|
6
|
+
|
|
7
|
+
export class MattermostWebSocketClient extends EventEmitter {
|
|
8
|
+
private config: MattermostConfig;
|
|
9
|
+
private ws: WebSocket | null = null;
|
|
10
|
+
private connected = false;
|
|
11
|
+
private reconnectAttempts = 0;
|
|
12
|
+
private reconnectTimeout: NodeJS.Timeout | null = null;
|
|
13
|
+
private eventBuffer: WebSocketEvent[] = [];
|
|
14
|
+
private maxBufferSize = 1000;
|
|
15
|
+
private channelSubscriptions: Set<string> = new Set();
|
|
16
|
+
private debug: boolean;
|
|
17
|
+
private seq: number = 0;
|
|
18
|
+
|
|
19
|
+
constructor(config: MattermostConfig) {
|
|
20
|
+
super();
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.debug = config.debug || false;
|
|
23
|
+
this.log("WebSocket client initialized");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private log(...args: unknown[]): void {
|
|
27
|
+
if (this.debug) {
|
|
28
|
+
fileLog.debug(`[WebSocket] ${args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async connect(): Promise<void> {
|
|
33
|
+
if (this.ws) {
|
|
34
|
+
this.log("WebSocket already connected");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!this.config.token) {
|
|
39
|
+
throw new Error("Cannot connect WebSocket: No authentication token provided");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
try {
|
|
44
|
+
this.log(`Connecting to WebSocket at ${this.config.wsUrl}`);
|
|
45
|
+
|
|
46
|
+
const connectionTimeout = setTimeout(() => {
|
|
47
|
+
this.log("WebSocket connection timeout");
|
|
48
|
+
if (this.ws) {
|
|
49
|
+
this.ws.terminate();
|
|
50
|
+
this.ws = null;
|
|
51
|
+
}
|
|
52
|
+
this.connected = false;
|
|
53
|
+
reject(new Error("WebSocket connection timeout after 10 seconds"));
|
|
54
|
+
}, 10000);
|
|
55
|
+
|
|
56
|
+
this.ws = new WebSocket(this.config.wsUrl);
|
|
57
|
+
|
|
58
|
+
this.ws.on("open", () => {
|
|
59
|
+
this.log("WebSocket connection established");
|
|
60
|
+
clearTimeout(connectionTimeout);
|
|
61
|
+
this.reconnectAttempts = 0;
|
|
62
|
+
|
|
63
|
+
if (this.ws) {
|
|
64
|
+
try {
|
|
65
|
+
this.seq++;
|
|
66
|
+
const authMessage = {
|
|
67
|
+
seq: this.seq,
|
|
68
|
+
action: "authentication_challenge",
|
|
69
|
+
data: {
|
|
70
|
+
token: this.config.token,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
this.log("Sending auth:", JSON.stringify(authMessage));
|
|
74
|
+
this.ws.send(JSON.stringify(authMessage));
|
|
75
|
+
this.log("Sent authentication challenge");
|
|
76
|
+
this.connected = true;
|
|
77
|
+
resolve();
|
|
78
|
+
} catch (sendError) {
|
|
79
|
+
this.log("Error sending authentication challenge:", sendError);
|
|
80
|
+
reject(sendError);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
this.ws.on("message", (data: WebSocket.Data) => {
|
|
86
|
+
try {
|
|
87
|
+
const raw = data.toString();
|
|
88
|
+
this.log("Raw message:", raw.substring(0, 200));
|
|
89
|
+
const event = JSON.parse(raw) as WebSocketEvent;
|
|
90
|
+
this.handleEvent(event);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
this.log("Error parsing WebSocket message:", error);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.ws.on("close", (code, reason) => {
|
|
97
|
+
clearTimeout(connectionTimeout);
|
|
98
|
+
this.log(`WebSocket connection closed with code ${code}, reason: ${reason?.toString() || 'none'}`);
|
|
99
|
+
this.connected = false;
|
|
100
|
+
this.ws = null;
|
|
101
|
+
if (code !== 1000) {
|
|
102
|
+
this.scheduleReconnect();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
this.ws.on("error", (error) => {
|
|
107
|
+
clearTimeout(connectionTimeout);
|
|
108
|
+
this.log("WebSocket error:", error);
|
|
109
|
+
this.connected = false;
|
|
110
|
+
});
|
|
111
|
+
} catch (error) {
|
|
112
|
+
this.log("Failed to connect to WebSocket:", error);
|
|
113
|
+
this.scheduleReconnect();
|
|
114
|
+
reject(error);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private handleEvent(event: WebSocketEvent): void {
|
|
120
|
+
this.log(`Received WebSocket event: ${event.event}`);
|
|
121
|
+
|
|
122
|
+
this.eventBuffer.push(event);
|
|
123
|
+
if (this.eventBuffer.length > this.maxBufferSize) {
|
|
124
|
+
this.eventBuffer.shift();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.emit(event.event, event);
|
|
128
|
+
this.emit("message", event);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private scheduleReconnect(): void {
|
|
132
|
+
if (this.reconnectTimeout) {
|
|
133
|
+
clearTimeout(this.reconnectTimeout);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const maxAttempts = this.config.maxReconnectAttempts || 10;
|
|
137
|
+
if (this.reconnectAttempts >= maxAttempts) {
|
|
138
|
+
this.log(`Maximum reconnection attempts (${maxAttempts}) reached`);
|
|
139
|
+
this.emit("reconnect_failed");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const baseInterval = this.config.reconnectInterval || 5000;
|
|
144
|
+
const delay = Math.min(baseInterval * Math.pow(1.5, this.reconnectAttempts), 60000);
|
|
145
|
+
|
|
146
|
+
this.log(`Scheduling reconnect in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);
|
|
147
|
+
|
|
148
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
149
|
+
this.reconnectAttempts++;
|
|
150
|
+
this.connect().catch(() => {});
|
|
151
|
+
}, delay);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
subscribeToChannel(channelId: string): void {
|
|
155
|
+
this.channelSubscriptions.add(channelId);
|
|
156
|
+
this.log(`Subscribed to channel: ${channelId}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
unsubscribeFromChannel(channelId: string): void {
|
|
160
|
+
this.channelSubscriptions.delete(channelId);
|
|
161
|
+
this.log(`Unsubscribed from channel: ${channelId}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
getChannelEvents(channelId: string, eventType?: string, limit = 50): WebSocketEvent[] {
|
|
165
|
+
return this.eventBuffer
|
|
166
|
+
.filter(
|
|
167
|
+
(event) =>
|
|
168
|
+
event.broadcast?.channel_id === channelId && (!eventType || event.event === eventType)
|
|
169
|
+
)
|
|
170
|
+
.slice(-limit);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
disconnect(): void {
|
|
174
|
+
this.log("Disconnecting WebSocket");
|
|
175
|
+
|
|
176
|
+
if (this.reconnectTimeout) {
|
|
177
|
+
clearTimeout(this.reconnectTimeout);
|
|
178
|
+
this.reconnectTimeout = null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (this.ws) {
|
|
182
|
+
this.ws.close();
|
|
183
|
+
this.ws = null;
|
|
184
|
+
this.connected = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
isConnected(): boolean {
|
|
189
|
+
return this.connected;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
getAllEvents(limit = 100): WebSocketEvent[] {
|
|
193
|
+
return this.eventBuffer.slice(-limit);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
clearEventBuffer(): void {
|
|
197
|
+
this.eventBuffer = [];
|
|
198
|
+
}
|
|
199
|
+
}
|