@relayfile/sdk 0.1.0 → 0.1.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.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Abstract integration provider interface.
3
+ *
4
+ * Relayfile supports multiple integration providers (Nango, Composio, etc.)
5
+ * for syncing external service data into the Relayfile filesystem.
6
+ *
7
+ * Each provider maps its own webhook/event format into Relayfile's canonical
8
+ * path + semantics model.
9
+ */
10
+ import type { RelayFileClient } from "./client.js";
11
+ import type { FileQueryItem, FilesystemEvent, QueuedResponse } from "./types.js";
12
+ /** Normalized webhook input from any provider */
13
+ export interface WebhookInput {
14
+ /** Provider name (e.g., "github", "slack", "zendesk") */
15
+ provider: string;
16
+ /** Object type / model (e.g., "tickets", "commits", "messages") */
17
+ objectType: string;
18
+ /** Unique object ID within the provider */
19
+ objectId: string;
20
+ /** Event type (e.g., "created", "updated", "deleted") */
21
+ eventType: string;
22
+ /** Raw payload data */
23
+ payload: Record<string, unknown>;
24
+ /** Optional relations to other objects */
25
+ relations?: string[];
26
+ /** Provider-specific metadata (connection IDs, user IDs, etc.) */
27
+ metadata?: Record<string, string>;
28
+ }
29
+ /** Options for listing files from a specific provider */
30
+ export interface ListProviderFilesOptions {
31
+ provider: string;
32
+ objectType?: string;
33
+ status?: string;
34
+ limit?: number;
35
+ signal?: AbortSignal;
36
+ }
37
+ /** Options for watching provider events */
38
+ export interface WatchProviderEventsOptions {
39
+ provider: string;
40
+ pollIntervalMs?: number;
41
+ cursor?: string;
42
+ signal?: AbortSignal;
43
+ }
44
+ export declare function computeCanonicalPath(provider: string, objectType: string, objectId: string): string;
45
+ export declare abstract class IntegrationProvider {
46
+ protected readonly client: RelayFileClient;
47
+ abstract readonly name: string;
48
+ constructor(client: RelayFileClient);
49
+ /**
50
+ * Ingest a webhook event from this provider into Relayfile.
51
+ * Each provider implementation normalizes its event format to WebhookInput,
52
+ * then writes it to the canonical path.
53
+ */
54
+ abstract ingestWebhook(workspaceId: string, rawInput: unknown, signal?: AbortSignal): Promise<QueuedResponse>;
55
+ /**
56
+ * Query files from a specific provider.
57
+ */
58
+ getProviderFiles(workspaceId: string, options: ListProviderFilesOptions): Promise<FileQueryItem[]>;
59
+ /**
60
+ * Watch for file events from a specific provider.
61
+ */
62
+ watchProviderEvents(workspaceId: string, options: WatchProviderEventsOptions): AsyncGenerator<FilesystemEvent, void, unknown>;
63
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Abstract integration provider interface.
3
+ *
4
+ * Relayfile supports multiple integration providers (Nango, Composio, etc.)
5
+ * for syncing external service data into the Relayfile filesystem.
6
+ *
7
+ * Each provider maps its own webhook/event format into Relayfile's canonical
8
+ * path + semantics model.
9
+ */
10
+ // ---------------------------------------------------------------------------
11
+ // Provider path mapping
12
+ // ---------------------------------------------------------------------------
13
+ const DEFAULT_PATH_PREFIXES = {
14
+ zendesk: "/zendesk",
15
+ shopify: "/shopify",
16
+ github: "/github",
17
+ stripe: "/stripe",
18
+ slack: "/slack",
19
+ linear: "/linear",
20
+ jira: "/jira",
21
+ hubspot: "/hubspot",
22
+ salesforce: "/salesforce",
23
+ gmail: "/gmail",
24
+ notion: "/notion",
25
+ asana: "/asana",
26
+ trello: "/trello",
27
+ intercom: "/intercom",
28
+ freshdesk: "/freshdesk",
29
+ discord: "/discord",
30
+ twilio: "/twilio",
31
+ };
32
+ export function computeCanonicalPath(provider, objectType, objectId) {
33
+ const prefix = DEFAULT_PATH_PREFIXES[provider] ?? `/${provider}`;
34
+ return `${prefix}/${objectType}/${objectId}.json`;
35
+ }
36
+ // ---------------------------------------------------------------------------
37
+ // Abstract provider
38
+ // ---------------------------------------------------------------------------
39
+ export class IntegrationProvider {
40
+ client;
41
+ constructor(client) {
42
+ this.client = client;
43
+ }
44
+ /**
45
+ * Query files from a specific provider.
46
+ */
47
+ async getProviderFiles(workspaceId, options) {
48
+ const prefix = DEFAULT_PATH_PREFIXES[options.provider] ?? `/${options.provider}`;
49
+ const pathFilter = options.objectType
50
+ ? `${prefix}/${options.objectType}/`
51
+ : `${prefix}/`;
52
+ const properties = {
53
+ provider: options.provider,
54
+ };
55
+ if (options.objectType) {
56
+ properties["provider.object_type"] = options.objectType;
57
+ }
58
+ if (options.status) {
59
+ properties["provider.status"] = options.status;
60
+ }
61
+ const allItems = [];
62
+ let cursor;
63
+ for (;;) {
64
+ const response = await this.client.queryFiles(workspaceId, {
65
+ path: pathFilter,
66
+ properties,
67
+ cursor,
68
+ limit: options.limit,
69
+ signal: options.signal,
70
+ });
71
+ allItems.push(...response.items);
72
+ if (!response.nextCursor ||
73
+ (options.limit && allItems.length >= options.limit)) {
74
+ break;
75
+ }
76
+ cursor = response.nextCursor;
77
+ }
78
+ return options.limit ? allItems.slice(0, options.limit) : allItems;
79
+ }
80
+ /**
81
+ * Watch for file events from a specific provider.
82
+ */
83
+ async *watchProviderEvents(workspaceId, options) {
84
+ const pollIntervalMs = options.pollIntervalMs ?? 5000;
85
+ let cursor = options.cursor;
86
+ for (;;) {
87
+ if (options.signal?.aborted)
88
+ return;
89
+ const response = await this.client.getEvents(workspaceId, {
90
+ provider: options.provider,
91
+ cursor,
92
+ signal: options.signal,
93
+ });
94
+ for (const event of response.events) {
95
+ yield event;
96
+ }
97
+ if (response.nextCursor) {
98
+ cursor = response.nextCursor;
99
+ }
100
+ if (options.signal?.aborted)
101
+ return;
102
+ await new Promise((resolve) => {
103
+ const timer = setTimeout(() => {
104
+ options.signal?.removeEventListener("abort", onAbort);
105
+ resolve();
106
+ }, pollIntervalMs);
107
+ const onAbort = () => {
108
+ clearTimeout(timer);
109
+ resolve();
110
+ };
111
+ options.signal?.addEventListener("abort", onAbort, { once: true });
112
+ });
113
+ }
114
+ }
115
+ }
package/dist/sync.d.ts ADDED
@@ -0,0 +1,87 @@
1
+ import type { RelayFileClient } from "./client.js";
2
+ import type { FilesystemEvent } from "./types.js";
3
+ export type RelayFileSyncState = "idle" | "connecting" | "open" | "polling" | "reconnecting" | "closed";
4
+ export interface RelayFileSyncPong {
5
+ type: "pong";
6
+ timestamp?: string;
7
+ }
8
+ export interface RelayFileSyncReconnectOptions {
9
+ enabled?: boolean;
10
+ minDelayMs?: number;
11
+ maxDelayMs?: number;
12
+ }
13
+ export interface RelayFileSyncOptions {
14
+ client: RelayFileClient;
15
+ workspaceId: string;
16
+ baseUrl?: string;
17
+ token?: string;
18
+ cursor?: string;
19
+ preferPolling?: boolean;
20
+ pollIntervalMs?: number;
21
+ pingIntervalMs?: number;
22
+ reconnect?: boolean | RelayFileSyncReconnectOptions;
23
+ signal?: AbortSignal;
24
+ webSocketFactory?: (url: string) => RelayFileSyncSocket;
25
+ onEvent?: (event: FilesystemEvent) => void;
26
+ }
27
+ export interface RelayFileSyncSocket {
28
+ addEventListener(type: "open", handler: (event: Event) => void): void;
29
+ addEventListener(type: "message", handler: (event: MessageEvent) => void): void;
30
+ addEventListener(type: "error", handler: (event: Event | ErrorEvent) => void): void;
31
+ addEventListener(type: "close", handler: (event: CloseEvent) => void): void;
32
+ close(code?: number, reason?: string): void;
33
+ send(data: string): void;
34
+ }
35
+ type RelayFileSyncEventName = "event" | "error" | "state" | "open" | "close" | "pong";
36
+ type RelayFileSyncHandlerMap = {
37
+ event: (event: FilesystemEvent) => void;
38
+ error: (error: Event | Error) => void;
39
+ state: (state: RelayFileSyncState) => void;
40
+ open: (event: Event) => void;
41
+ close: (event: CloseEvent) => void;
42
+ pong: (event: RelayFileSyncPong) => void;
43
+ };
44
+ export declare class RelayFileSync {
45
+ private readonly client;
46
+ private readonly workspaceId;
47
+ private readonly baseUrl?;
48
+ private readonly token?;
49
+ private readonly pollIntervalMs;
50
+ private readonly pingIntervalMs;
51
+ private readonly reconnect;
52
+ private readonly preferPolling;
53
+ private readonly signal?;
54
+ private readonly webSocketFactory;
55
+ private readonly handlers;
56
+ private state;
57
+ private cursor?;
58
+ private socket?;
59
+ private started;
60
+ private stopped;
61
+ private pollingPromise?;
62
+ private reconnectTimer?;
63
+ private pingTimer?;
64
+ private reconnectAttempts;
65
+ private readonly abortHandler?;
66
+ constructor(options: RelayFileSyncOptions);
67
+ static connect(options: RelayFileSyncOptions): RelayFileSync;
68
+ getState(): RelayFileSyncState;
69
+ start(): void;
70
+ stop(): Promise<void>;
71
+ on<TEventName extends RelayFileSyncEventName>(event: TEventName, handler: RelayFileSyncHandlerMap[TEventName]): () => void;
72
+ private shouldUsePolling;
73
+ private openWebSocket;
74
+ private startPolling;
75
+ private pollLoop;
76
+ private handleSocketMessage;
77
+ private startPingLoop;
78
+ private scheduleReconnect;
79
+ private computeReconnectDelayMs;
80
+ private sleep;
81
+ private isAbortError;
82
+ private setState;
83
+ private clearReconnectTimer;
84
+ private clearPingTimer;
85
+ private emit;
86
+ }
87
+ export {};
package/dist/sync.js ADDED
@@ -0,0 +1,394 @@
1
+ const DEFAULT_POLL_INTERVAL_MS = 5000;
2
+ const DEFAULT_PING_INTERVAL_MS = 30000;
3
+ const DEFAULT_RECONNECT_MIN_DELAY_MS = 250;
4
+ const DEFAULT_RECONNECT_MAX_DELAY_MS = 5000;
5
+ function normalizeReconnectOptions(reconnect) {
6
+ if (reconnect === false) {
7
+ return {
8
+ enabled: false,
9
+ minDelayMs: DEFAULT_RECONNECT_MIN_DELAY_MS,
10
+ maxDelayMs: DEFAULT_RECONNECT_MAX_DELAY_MS
11
+ };
12
+ }
13
+ if (reconnect === true || reconnect === undefined) {
14
+ return {
15
+ enabled: true,
16
+ minDelayMs: DEFAULT_RECONNECT_MIN_DELAY_MS,
17
+ maxDelayMs: DEFAULT_RECONNECT_MAX_DELAY_MS
18
+ };
19
+ }
20
+ const minDelayMs = Math.max(1, Math.floor(reconnect.minDelayMs ?? DEFAULT_RECONNECT_MIN_DELAY_MS));
21
+ const maxDelayMs = Math.max(minDelayMs, Math.floor(reconnect.maxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS));
22
+ return {
23
+ enabled: reconnect.enabled ?? true,
24
+ minDelayMs,
25
+ maxDelayMs
26
+ };
27
+ }
28
+ function createAbortError() {
29
+ const err = new Error("The operation was aborted.");
30
+ err.name = "AbortError";
31
+ return err;
32
+ }
33
+ function buildEventId(message) {
34
+ return [
35
+ "ws",
36
+ message.type,
37
+ message.path ?? "",
38
+ message.revision ?? "",
39
+ message.timestamp ?? message.ts ?? ""
40
+ ].join(":");
41
+ }
42
+ function normalizeFilesystemEvent(message) {
43
+ return {
44
+ eventId: message.eventId ?? buildEventId(message),
45
+ type: message.type,
46
+ path: message.path ?? "",
47
+ revision: message.revision ?? "",
48
+ origin: message.origin,
49
+ provider: message.provider,
50
+ correlationId: message.correlationId,
51
+ timestamp: message.timestamp ?? message.ts ?? new Date().toISOString()
52
+ };
53
+ }
54
+ function normalizeError(event) {
55
+ return event instanceof ErrorEvent && event.error instanceof Error ? event.error : event;
56
+ }
57
+ export class RelayFileSync {
58
+ client;
59
+ workspaceId;
60
+ baseUrl;
61
+ token;
62
+ pollIntervalMs;
63
+ pingIntervalMs;
64
+ reconnect;
65
+ preferPolling;
66
+ signal;
67
+ webSocketFactory;
68
+ handlers = {
69
+ event: new Set(),
70
+ error: new Set(),
71
+ state: new Set(),
72
+ open: new Set(),
73
+ close: new Set(),
74
+ pong: new Set()
75
+ };
76
+ state = "idle";
77
+ cursor;
78
+ socket;
79
+ started = false;
80
+ stopped = false;
81
+ pollingPromise;
82
+ reconnectTimer;
83
+ pingTimer;
84
+ reconnectAttempts = 0;
85
+ abortHandler;
86
+ constructor(options) {
87
+ this.client = options.client;
88
+ this.workspaceId = options.workspaceId;
89
+ this.baseUrl = options.baseUrl?.replace(/\/+$/, "");
90
+ this.token = options.token;
91
+ this.cursor = options.cursor;
92
+ this.pollIntervalMs = Math.max(1, Math.floor(options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS));
93
+ this.pingIntervalMs = Math.max(1, Math.floor(options.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS));
94
+ this.reconnect = normalizeReconnectOptions(options.reconnect);
95
+ this.preferPolling = options.preferPolling ?? false;
96
+ this.signal = options.signal;
97
+ this.webSocketFactory =
98
+ options.webSocketFactory ??
99
+ ((url) => {
100
+ if (typeof WebSocket !== "function") {
101
+ throw new Error("WebSocket is not available in this environment.");
102
+ }
103
+ return new WebSocket(url);
104
+ });
105
+ if (options.onEvent) {
106
+ this.handlers.event.add(options.onEvent);
107
+ }
108
+ if (this.signal) {
109
+ this.abortHandler = () => {
110
+ this.stop();
111
+ };
112
+ if (this.signal.aborted) {
113
+ this.stopped = true;
114
+ this.state = "closed";
115
+ }
116
+ else {
117
+ this.signal.addEventListener("abort", this.abortHandler, { once: true });
118
+ }
119
+ }
120
+ }
121
+ static connect(options) {
122
+ const sync = new RelayFileSync(options);
123
+ sync.start();
124
+ return sync;
125
+ }
126
+ getState() {
127
+ return this.state;
128
+ }
129
+ start() {
130
+ if (this.started || this.stopped) {
131
+ return;
132
+ }
133
+ this.started = true;
134
+ if (this.shouldUsePolling()) {
135
+ this.startPolling();
136
+ return;
137
+ }
138
+ this.openWebSocket(false);
139
+ }
140
+ async stop() {
141
+ if (this.stopped) {
142
+ return;
143
+ }
144
+ this.stopped = true;
145
+ this.clearReconnectTimer();
146
+ this.clearPingTimer();
147
+ const socket = this.socket;
148
+ this.socket = undefined;
149
+ if (socket) {
150
+ socket.close(1000, "client stopped");
151
+ }
152
+ if (this.abortHandler && this.signal) {
153
+ this.signal.removeEventListener("abort", this.abortHandler);
154
+ }
155
+ if (this.pollingPromise) {
156
+ await this.pollingPromise.catch(() => undefined);
157
+ }
158
+ this.setState("closed");
159
+ }
160
+ on(event, handler) {
161
+ this.handlers[event].add(handler);
162
+ return () => {
163
+ this.handlers[event].delete(handler);
164
+ };
165
+ }
166
+ shouldUsePolling() {
167
+ return this.preferPolling || !this.baseUrl || !this.token;
168
+ }
169
+ openWebSocket(isReconnect) {
170
+ if (this.stopped) {
171
+ return;
172
+ }
173
+ const url = new URL(`${this.baseUrl}/v1/workspaces/${encodeURIComponent(this.workspaceId)}/fs/ws`);
174
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
175
+ // Pass token in query string — the server authenticates during the HTTP
176
+ // upgrade handshake via r.URL.Query().Get("token").
177
+ if (this.token) {
178
+ url.searchParams.set("token", this.token);
179
+ }
180
+ this.setState(isReconnect ? "reconnecting" : "connecting");
181
+ let socket;
182
+ try {
183
+ socket = this.webSocketFactory(url.toString());
184
+ }
185
+ catch (error) {
186
+ this.emit("error", error instanceof Error ? error : new Error("Failed to create WebSocket connection."));
187
+ this.startPolling();
188
+ return;
189
+ }
190
+ this.socket = socket;
191
+ socket.addEventListener("open", (event) => {
192
+ if (this.socket !== socket || this.stopped) {
193
+ return;
194
+ }
195
+ this.reconnectAttempts = 0;
196
+ this.setState("open");
197
+ this.startPingLoop(socket);
198
+ this.emit("open", event);
199
+ });
200
+ socket.addEventListener("message", (event) => {
201
+ if (this.socket !== socket || this.stopped) {
202
+ return;
203
+ }
204
+ this.handleSocketMessage(event);
205
+ });
206
+ socket.addEventListener("error", (event) => {
207
+ if (this.socket !== socket || this.stopped) {
208
+ return;
209
+ }
210
+ this.emit("error", normalizeError(event));
211
+ });
212
+ socket.addEventListener("close", (event) => {
213
+ if (this.socket === socket) {
214
+ this.socket = undefined;
215
+ }
216
+ this.clearPingTimer();
217
+ this.emit("close", event);
218
+ if (this.stopped) {
219
+ this.setState("closed");
220
+ return;
221
+ }
222
+ if (!this.reconnect.enabled) {
223
+ this.startPolling();
224
+ return;
225
+ }
226
+ this.scheduleReconnect();
227
+ });
228
+ }
229
+ startPolling() {
230
+ if (this.pollingPromise || this.stopped) {
231
+ return;
232
+ }
233
+ this.setState("polling");
234
+ this.pollingPromise = this.pollLoop().finally(() => {
235
+ this.pollingPromise = undefined;
236
+ if (!this.stopped && !this.shouldUsePolling()) {
237
+ this.scheduleReconnect();
238
+ }
239
+ });
240
+ }
241
+ async pollLoop() {
242
+ let retryAttempt = 0;
243
+ while (!this.stopped) {
244
+ if (this.signal?.aborted) {
245
+ throw createAbortError();
246
+ }
247
+ try {
248
+ const response = await this.client.getEvents(this.workspaceId, {
249
+ cursor: this.cursor,
250
+ signal: this.signal
251
+ });
252
+ retryAttempt = 0;
253
+ for (const event of response.events) {
254
+ this.emit("event", event);
255
+ }
256
+ if (response.nextCursor) {
257
+ this.cursor = response.nextCursor;
258
+ }
259
+ await this.sleep(this.pollIntervalMs);
260
+ }
261
+ catch (error) {
262
+ if (this.isAbortError(error)) {
263
+ return;
264
+ }
265
+ retryAttempt += 1;
266
+ this.emit("error", error instanceof Error ? error : new Error("Polling failed."));
267
+ const delayMs = this.computeReconnectDelayMs(retryAttempt);
268
+ await this.sleep(delayMs);
269
+ }
270
+ }
271
+ }
272
+ handleSocketMessage(event) {
273
+ if (typeof event.data !== "string") {
274
+ return;
275
+ }
276
+ let parsed;
277
+ try {
278
+ parsed = JSON.parse(event.data);
279
+ }
280
+ catch (error) {
281
+ this.emit("error", error instanceof Error ? error : new Error("Failed to parse WebSocket payload."));
282
+ return;
283
+ }
284
+ if (typeof parsed !== "object" || parsed === null || typeof parsed.type !== "string") {
285
+ this.emit("error", new Error("Invalid WebSocket payload: missing required 'type' field."));
286
+ return;
287
+ }
288
+ if (parsed.path !== undefined && typeof parsed.path !== "string") {
289
+ this.emit("error", new Error("Invalid WebSocket payload: 'path' must be a string."));
290
+ return;
291
+ }
292
+ if (parsed.revision !== undefined && typeof parsed.revision !== "string") {
293
+ this.emit("error", new Error("Invalid WebSocket payload: 'revision' must be a string."));
294
+ return;
295
+ }
296
+ if (parsed.timestamp !== undefined && typeof parsed.timestamp !== "string") {
297
+ this.emit("error", new Error("Invalid WebSocket payload: 'timestamp' must be a string."));
298
+ return;
299
+ }
300
+ if (parsed.ts !== undefined && typeof parsed.ts !== "string") {
301
+ this.emit("error", new Error("Invalid WebSocket payload: 'ts' must be a string."));
302
+ return;
303
+ }
304
+ if (parsed.type === "pong") {
305
+ this.emit("pong", {
306
+ type: "pong",
307
+ timestamp: parsed.timestamp ?? parsed.ts
308
+ });
309
+ return;
310
+ }
311
+ this.emit("event", normalizeFilesystemEvent(parsed));
312
+ }
313
+ startPingLoop(socket) {
314
+ this.clearPingTimer();
315
+ this.pingTimer = setInterval(() => {
316
+ if (this.socket !== socket || this.stopped) {
317
+ this.clearPingTimer();
318
+ return;
319
+ }
320
+ try {
321
+ socket.send(JSON.stringify({ type: "ping" }));
322
+ }
323
+ catch (error) {
324
+ this.emit("error", error instanceof Error ? error : new Error("Failed to send WebSocket ping."));
325
+ }
326
+ }, this.pingIntervalMs);
327
+ }
328
+ scheduleReconnect() {
329
+ if (this.stopped || this.reconnectTimer) {
330
+ return;
331
+ }
332
+ this.reconnectAttempts += 1;
333
+ this.setState("reconnecting");
334
+ const delayMs = this.computeReconnectDelayMs(this.reconnectAttempts);
335
+ this.reconnectTimer = setTimeout(() => {
336
+ this.reconnectTimer = undefined;
337
+ if (this.stopped) {
338
+ return;
339
+ }
340
+ if (this.pollingPromise) {
341
+ return;
342
+ }
343
+ this.openWebSocket(true);
344
+ }, delayMs);
345
+ }
346
+ computeReconnectDelayMs(attempt) {
347
+ const uncapped = this.reconnect.minDelayMs * Math.pow(2, Math.max(0, attempt - 1));
348
+ return Math.min(this.reconnect.maxDelayMs, uncapped);
349
+ }
350
+ async sleep(delayMs) {
351
+ if (delayMs <= 0) {
352
+ return;
353
+ }
354
+ await new Promise((resolve, reject) => {
355
+ const timer = setTimeout(() => {
356
+ this.signal?.removeEventListener("abort", onAbort);
357
+ resolve();
358
+ }, delayMs);
359
+ const onAbort = () => {
360
+ clearTimeout(timer);
361
+ reject(createAbortError());
362
+ };
363
+ this.signal?.addEventListener("abort", onAbort, { once: true });
364
+ });
365
+ }
366
+ isAbortError(error) {
367
+ return this.signal?.aborted || (error instanceof Error && error.name === "AbortError");
368
+ }
369
+ setState(state) {
370
+ if (this.state === state) {
371
+ return;
372
+ }
373
+ this.state = state;
374
+ this.emit("state", state);
375
+ }
376
+ clearReconnectTimer() {
377
+ if (this.reconnectTimer) {
378
+ clearTimeout(this.reconnectTimer);
379
+ this.reconnectTimer = undefined;
380
+ }
381
+ }
382
+ clearPingTimer() {
383
+ if (this.pingTimer) {
384
+ clearInterval(this.pingTimer);
385
+ this.pingTimer = undefined;
386
+ }
387
+ }
388
+ emit(event, payload) {
389
+ const handlers = this.handlers[event];
390
+ for (const handler of handlers) {
391
+ handler(payload);
392
+ }
393
+ }
394
+ }