@sockethub/client 5.0.0-alpha.12 → 5.0.0-alpha.14
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/README.md +47 -21
- package/dist/sockethub-client.browser.js +404 -728
- package/dist/sockethub-client.d.ts +280 -0
- package/dist/sockethub-client.js +398 -715
- package/dist/sockethub-client.min.js +21 -36
- package/package.json +6 -7
- package/src/sockethub-client.test.ts +0 -797
- package/src/sockethub-client.ts +0 -1095
package/src/sockethub-client.ts
DELETED
|
@@ -1,1095 +0,0 @@
|
|
|
1
|
-
import { ASFactory, type ASManager } from "@sockethub/activity-streams";
|
|
2
|
-
import type {
|
|
3
|
-
ActivityObject,
|
|
4
|
-
ActivityStream,
|
|
5
|
-
BaseActivityObject,
|
|
6
|
-
} from "@sockethub/schemas";
|
|
7
|
-
import {
|
|
8
|
-
addPlatformContext,
|
|
9
|
-
addPlatformSchema,
|
|
10
|
-
validateActivityStream,
|
|
11
|
-
validateCredentials,
|
|
12
|
-
} from "@sockethub/schemas";
|
|
13
|
-
import EventEmitter from "eventemitter3";
|
|
14
|
-
import type { Socket } from "socket.io-client";
|
|
15
|
-
|
|
16
|
-
export interface EventMapping {
|
|
17
|
-
credentials: Map<string, ActivityStream>;
|
|
18
|
-
"activity-object": Map<string, BaseActivityObject>;
|
|
19
|
-
connect: Map<string, ActivityStream>;
|
|
20
|
-
join: Map<string, ActivityStream>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type ReplayEventMap = {
|
|
24
|
-
"activity-object": BaseActivityObject;
|
|
25
|
-
credentials: ActivityStream;
|
|
26
|
-
message: ActivityStream;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
type InitState = "idle" | "initializing" | "ready" | "init_error" | "closed";
|
|
30
|
-
|
|
31
|
-
type ReadyReason = "initial-connect" | "reconnect" | "schemas-update";
|
|
32
|
-
|
|
33
|
-
type InitErrorPhase = "schemas-request" | "schemas-apply" | "timeout";
|
|
34
|
-
|
|
35
|
-
interface PendingReadyWaiter {
|
|
36
|
-
resolve: (info: ClientReadyInfo) => void;
|
|
37
|
-
reject: (err: Error) => void;
|
|
38
|
-
timer?: ReturnType<typeof setTimeout>;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface QueuedOutboundEvent {
|
|
42
|
-
event: string;
|
|
43
|
-
content: unknown;
|
|
44
|
-
callback?: unknown;
|
|
45
|
-
enqueuedAt: number;
|
|
46
|
-
sequence: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface InitializationCycle {
|
|
50
|
-
token: number;
|
|
51
|
-
reason: ReadyReason;
|
|
52
|
-
startedAt: number;
|
|
53
|
-
replayOnReady: boolean;
|
|
54
|
-
timedOut: boolean;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface SockethubClientOptions {
|
|
58
|
-
initTimeoutMs?: number;
|
|
59
|
-
maxQueuedOutbound?: number;
|
|
60
|
-
maxQueuedAgeMs?: number;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface CustomEmitter extends EventEmitter {
|
|
64
|
-
_emit(s: string, o: unknown, c?: unknown): void;
|
|
65
|
-
connect(): void;
|
|
66
|
-
disconnect(): void;
|
|
67
|
-
connected: boolean;
|
|
68
|
-
id: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface PlatformRegistrySchemas {
|
|
72
|
-
credentials?: object;
|
|
73
|
-
messages?: object;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Server-declared platform metadata used by the client for context generation
|
|
78
|
-
* and runtime validation.
|
|
79
|
-
*/
|
|
80
|
-
export interface PlatformRegistryEntry {
|
|
81
|
-
id: string;
|
|
82
|
-
version: string;
|
|
83
|
-
contextUrl: string;
|
|
84
|
-
contextVersion: string;
|
|
85
|
-
schemaVersion: string;
|
|
86
|
-
types: Array<string>;
|
|
87
|
-
schemas: PlatformRegistrySchemas;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface PlatformRegistryPayload {
|
|
91
|
-
version?: string;
|
|
92
|
-
contexts?: {
|
|
93
|
-
as?: string;
|
|
94
|
-
sockethub?: string;
|
|
95
|
-
};
|
|
96
|
-
platforms?: Array<PlatformRegistryEntry>;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export interface ClientReadyInfo {
|
|
100
|
-
state: "ready";
|
|
101
|
-
reason: ReadyReason;
|
|
102
|
-
sockethubVersion: string;
|
|
103
|
-
contexts: {
|
|
104
|
-
as: string;
|
|
105
|
-
sockethub: string;
|
|
106
|
-
};
|
|
107
|
-
platforms: Array<{
|
|
108
|
-
id: string;
|
|
109
|
-
version: string;
|
|
110
|
-
contextUrl: string;
|
|
111
|
-
contextVersion: string;
|
|
112
|
-
schemaVersion: string;
|
|
113
|
-
types: Array<string>;
|
|
114
|
-
}>;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export interface ClientInitError {
|
|
118
|
-
error: string;
|
|
119
|
-
phase: InitErrorPhase;
|
|
120
|
-
retrying: boolean;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* SockethubClient - Client library for Sockethub protocol gateway
|
|
125
|
-
*
|
|
126
|
-
* A JavaScript client for connecting to Sockethub servers. Provides a high-level
|
|
127
|
-
* API for sending and receiving ActivityStreams messages over Socket.IO, with
|
|
128
|
-
* automatic state management and reconnection handling.
|
|
129
|
-
*
|
|
130
|
-
* Sockethub acts as a protocol gateway, translating ActivityStreams messages into
|
|
131
|
-
* various protocols (XMPP, IRC, RSS, etc.). This client handles the communication
|
|
132
|
-
* with the Sockethub server, including credential management, connection state,
|
|
133
|
-
* and automatic reconnection.
|
|
134
|
-
*
|
|
135
|
-
* ## Automatic Reconnection & State Replay
|
|
136
|
-
*
|
|
137
|
-
* This client automatically handles transient network disconnections by storing
|
|
138
|
-
* connection state in memory and replaying it when the socket reconnects.
|
|
139
|
-
*
|
|
140
|
-
* ### Security Model
|
|
141
|
-
*
|
|
142
|
-
* **Storage Location:**
|
|
143
|
-
* - All credentials and state are stored ONLY in JavaScript memory (heap)
|
|
144
|
-
* - Nothing is persisted to localStorage, sessionStorage, cookies, or disk
|
|
145
|
-
* - Memory is cleared when the browser tab closes or page refreshes
|
|
146
|
-
*
|
|
147
|
-
* **Replay Triggers:**
|
|
148
|
-
* - Automatic replay occurs on Socket.IO reconnection events
|
|
149
|
-
* - Typically triggered by brief network interruptions (WiFi switching, mobile network blips)
|
|
150
|
-
* - Does NOT occur on page refresh (new SockethubClient instance = empty state)
|
|
151
|
-
*
|
|
152
|
-
* **Server Restart Behavior:**
|
|
153
|
-
* - If server restarts, client socket will reconnect and replay credentials
|
|
154
|
-
* - Server must handle replayed credentials appropriately (validate, reject stale sessions, etc.)
|
|
155
|
-
* - Applications should implement proper session validation server-side
|
|
156
|
-
*
|
|
157
|
-
* **Lifetime:**
|
|
158
|
-
* - Credentials exist only during the browser tab's lifetime
|
|
159
|
-
* - Cleared on page reload, tab close, or manual disconnect
|
|
160
|
-
* - Not accessible across tabs or after browser restart
|
|
161
|
-
*
|
|
162
|
-
* **What Gets Replayed:**
|
|
163
|
-
* - Credentials (username/password/tokens sent via credentials event)
|
|
164
|
-
* - Activity Objects (actor definitions)
|
|
165
|
-
* - Connect commands (platform connections)
|
|
166
|
-
* - Join commands (room/channel joins)
|
|
167
|
-
*
|
|
168
|
-
* @example
|
|
169
|
-
* ```typescript
|
|
170
|
-
* // Create client
|
|
171
|
-
* const socket = io('http://localhost:10550');
|
|
172
|
-
* const client = new SockethubClient(socket);
|
|
173
|
-
*
|
|
174
|
-
* // Wait for schema registry before sending messages
|
|
175
|
-
* await client.ready();
|
|
176
|
-
*
|
|
177
|
-
* // Build canonical @context for a platform
|
|
178
|
-
* const ctx = client.contextFor('irc');
|
|
179
|
-
*
|
|
180
|
-
* // Send credentials - these will be replayed on reconnection
|
|
181
|
-
* client.socket.emit('credentials', {
|
|
182
|
-
* '@context': ctx,
|
|
183
|
-
* type: 'credentials',
|
|
184
|
-
* actor: { id: 'user@example.com', type: 'person' },
|
|
185
|
-
* object: { type: 'credentials', username: 'user', password: 'pass' }
|
|
186
|
-
* });
|
|
187
|
-
* ```
|
|
188
|
-
*/
|
|
189
|
-
export default class SockethubClient {
|
|
190
|
-
/**
|
|
191
|
-
* In-memory storage for client state that should be replayed on reconnection.
|
|
192
|
-
*
|
|
193
|
-
* Security: Stored ONLY in JavaScript heap memory. Never persisted to disk,
|
|
194
|
-
* localStorage, or any permanent storage. Cleared on page reload.
|
|
195
|
-
*/
|
|
196
|
-
private events: EventMapping = {
|
|
197
|
-
credentials: new Map(),
|
|
198
|
-
"activity-object": new Map(),
|
|
199
|
-
connect: new Map(),
|
|
200
|
-
join: new Map(),
|
|
201
|
-
};
|
|
202
|
-
private _socket: Socket;
|
|
203
|
-
public ActivityStreams!: ASManager;
|
|
204
|
-
public socket!: CustomEmitter;
|
|
205
|
-
public debug = true;
|
|
206
|
-
private readonly options: Required<SockethubClientOptions>;
|
|
207
|
-
private platformRegistry = new Map<string, PlatformRegistryEntry>();
|
|
208
|
-
private asContextUrl?: string;
|
|
209
|
-
private sockethubContextUrl?: string;
|
|
210
|
-
private sockethubVersion?: string;
|
|
211
|
-
private initState: InitState = "idle";
|
|
212
|
-
private hasReadyOnce = false;
|
|
213
|
-
private initCycle?: InitializationCycle;
|
|
214
|
-
private initTokenCounter = 0;
|
|
215
|
-
private initTimeoutTimer?: ReturnType<typeof setTimeout>;
|
|
216
|
-
private waitingWarningTimer?: ReturnType<typeof setInterval>;
|
|
217
|
-
private waitingWarningIntervalMs = 10000;
|
|
218
|
-
private readyWaiters: Array<PendingReadyWaiter> = [];
|
|
219
|
-
private outboundQueue: Array<QueuedOutboundEvent> = [];
|
|
220
|
-
private outboundSequence = 0;
|
|
221
|
-
private registryFingerprint?: string;
|
|
222
|
-
private latestReadyInfo?: ClientReadyInfo;
|
|
223
|
-
|
|
224
|
-
constructor(socket: Socket, options: SockethubClientOptions = {}) {
|
|
225
|
-
if (!socket) {
|
|
226
|
-
throw new Error("SockethubClient requires a socket.io instance");
|
|
227
|
-
}
|
|
228
|
-
this._socket = socket;
|
|
229
|
-
this.options = {
|
|
230
|
-
initTimeoutMs: options.initTimeoutMs ?? 5000,
|
|
231
|
-
maxQueuedOutbound: options.maxQueuedOutbound ?? 1000,
|
|
232
|
-
maxQueuedAgeMs: options.maxQueuedAgeMs ?? 30000,
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
this.socket = this.createPublicEmitter();
|
|
236
|
-
this.registerSocketIOHandlers();
|
|
237
|
-
this.initActivityStreams();
|
|
238
|
-
|
|
239
|
-
this.ActivityStreams.on(
|
|
240
|
-
"activity-object-create",
|
|
241
|
-
(obj: ActivityObject) => {
|
|
242
|
-
this.socket.emit(
|
|
243
|
-
"activity-object",
|
|
244
|
-
obj,
|
|
245
|
-
(resp?: { error?: string }) => {
|
|
246
|
-
if (resp && typeof resp.error === "string") {
|
|
247
|
-
console.error(
|
|
248
|
-
"failed to create activity-object ",
|
|
249
|
-
resp.error,
|
|
250
|
-
);
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
this.eventActivityObject(obj);
|
|
254
|
-
},
|
|
255
|
-
);
|
|
256
|
-
},
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
socket.on("activity-object", (obj) => {
|
|
260
|
-
this.ActivityStreams.Object.create(obj);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
if (this._socket.connected) {
|
|
264
|
-
this.socket.connected = true;
|
|
265
|
-
(this.socket as unknown as { id?: string }).id = this._socket.id;
|
|
266
|
-
this.socket._emit("connect");
|
|
267
|
-
this.startInitialization("initial-connect", true);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
initActivityStreams() {
|
|
272
|
-
this.ActivityStreams = ASFactory({ specialObjs: ["credentials"] });
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Clear stored credentials to prevent automatic replay on reconnection.
|
|
277
|
-
*
|
|
278
|
-
* This method removes all stored credentials from memory. Useful for
|
|
279
|
-
* security-sensitive applications that want to prevent automatic credential
|
|
280
|
-
* replay when the socket reconnects.
|
|
281
|
-
*
|
|
282
|
-
* @example
|
|
283
|
-
* ```typescript
|
|
284
|
-
* // Clear credentials on disconnect
|
|
285
|
-
* sc.socket.on('disconnect', () => {
|
|
286
|
-
* sc.clearCredentials();
|
|
287
|
-
* });
|
|
288
|
-
* ```
|
|
289
|
-
*/
|
|
290
|
-
public clearCredentials(): void {
|
|
291
|
-
this.events.credentials.clear();
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Return the platform registry discovered from the server.
|
|
296
|
-
*/
|
|
297
|
-
public getRegisteredPlatforms(): Array<PlatformRegistryEntry> {
|
|
298
|
-
return Array.from(this.platformRegistry.values()).map((platform) => ({
|
|
299
|
-
...platform,
|
|
300
|
-
types: [...platform.types],
|
|
301
|
-
schemas: { ...platform.schemas },
|
|
302
|
-
}));
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Indicates whether server-provided schema/context registry data is loaded.
|
|
307
|
-
*/
|
|
308
|
-
public isSchemasReady(): boolean {
|
|
309
|
-
return this.isReady();
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Indicates whether the client has completed schema initialization.
|
|
314
|
-
*/
|
|
315
|
-
public isReady(): boolean {
|
|
316
|
-
return this.initState === "ready";
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Returns the current client initialization state.
|
|
321
|
-
*/
|
|
322
|
-
public getInitState(): InitState {
|
|
323
|
-
return this.initState;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Return the canonical base contexts learned from the server registry.
|
|
328
|
-
*/
|
|
329
|
-
public getRegisteredBaseContexts(): { as: string; sockethub: string } {
|
|
330
|
-
if (!this.asContextUrl || !this.sockethubContextUrl) {
|
|
331
|
-
throw new Error(
|
|
332
|
-
"Schema registry not loaded yet. Wait for client ready state after connect.",
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
return {
|
|
336
|
-
as: this.asContextUrl,
|
|
337
|
-
sockethub: this.sockethubContextUrl,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
public getPlatformSchema(
|
|
342
|
-
platform: string,
|
|
343
|
-
schemaType: "messages" | "credentials" = "messages",
|
|
344
|
-
): object | undefined {
|
|
345
|
-
const normalizedPlatform = platform?.trim();
|
|
346
|
-
if (!normalizedPlatform) {
|
|
347
|
-
return undefined;
|
|
348
|
-
}
|
|
349
|
-
return this.platformRegistry.get(normalizedPlatform)?.schemas?.[
|
|
350
|
-
schemaType
|
|
351
|
-
];
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Wait for schema registry data from the server and return the normalized payload.
|
|
356
|
-
* @deprecated Use ready(timeoutMs?) instead.
|
|
357
|
-
*/
|
|
358
|
-
public async waitForSchemas(
|
|
359
|
-
timeoutMs = 2000,
|
|
360
|
-
): Promise<PlatformRegistryPayload> {
|
|
361
|
-
await this.ready(timeoutMs);
|
|
362
|
-
return this.buildPlatformRegistryPayload();
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Wait until the client reaches a ready state.
|
|
367
|
-
*/
|
|
368
|
-
public ready(
|
|
369
|
-
timeoutMs = this.options.initTimeoutMs,
|
|
370
|
-
): Promise<ClientReadyInfo> {
|
|
371
|
-
if (this.isReady() && this.latestReadyInfo) {
|
|
372
|
-
return Promise.resolve(this.latestReadyInfo);
|
|
373
|
-
}
|
|
374
|
-
return new Promise((resolve, reject) => {
|
|
375
|
-
const waiter: PendingReadyWaiter = { resolve, reject };
|
|
376
|
-
if (timeoutMs > 0) {
|
|
377
|
-
waiter.timer = setTimeout(() => {
|
|
378
|
-
this.readyWaiters = this.readyWaiters.filter(
|
|
379
|
-
(entry) => entry !== waiter,
|
|
380
|
-
);
|
|
381
|
-
reject(
|
|
382
|
-
new Error(
|
|
383
|
-
`SockethubClient ready() timed out after ${timeoutMs}ms`,
|
|
384
|
-
),
|
|
385
|
-
);
|
|
386
|
-
}, timeoutMs);
|
|
387
|
-
}
|
|
388
|
-
this.readyWaiters.push(waiter);
|
|
389
|
-
if (this.socket.connected && this.initState === "idle") {
|
|
390
|
-
this.startInitialization(
|
|
391
|
-
this.hasReadyOnce ? "reconnect" : "initial-connect",
|
|
392
|
-
true,
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Validate an activity stream against currently registered platform schemas.
|
|
400
|
-
* Returns an empty string when valid.
|
|
401
|
-
*/
|
|
402
|
-
public validateActivity(activity: ActivityStream): string {
|
|
403
|
-
if (activity.type === "credentials") {
|
|
404
|
-
return validateCredentials(activity);
|
|
405
|
-
}
|
|
406
|
-
return validateActivityStream(activity);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Build canonical Sockethub contexts for a platform using server-provided schema metadata.
|
|
411
|
-
*/
|
|
412
|
-
public contextFor(platform: string): ActivityStream["@context"] {
|
|
413
|
-
if (typeof platform !== "string" || platform.trim().length === 0) {
|
|
414
|
-
throw new Error(
|
|
415
|
-
"SockethubClient.contextFor(platform) requires a non-empty platform string",
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (!this.asContextUrl || !this.sockethubContextUrl) {
|
|
420
|
-
throw new Error(
|
|
421
|
-
"Schema registry not loaded yet. Wait for client ready state after connect.",
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const normalizedPlatform = platform.trim();
|
|
426
|
-
const entry = this.platformRegistry.get(normalizedPlatform);
|
|
427
|
-
if (!entry) {
|
|
428
|
-
const names = Array.from(this.platformRegistry.keys()).sort();
|
|
429
|
-
throw new Error(
|
|
430
|
-
`unknown platform '${normalizedPlatform}'. Registered platforms: ${names.join(", ")}`,
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
return [this.asContextUrl, this.sockethubContextUrl, entry.contextUrl];
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
private createPublicEmitter(): CustomEmitter {
|
|
437
|
-
const socket = new EventEmitter() as CustomEmitter;
|
|
438
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
439
|
-
// @ts-expect-error
|
|
440
|
-
socket._emit = socket.emit;
|
|
441
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
442
|
-
// @ts-expect-error
|
|
443
|
-
socket.emit = (event, content, callback): void => {
|
|
444
|
-
this.handlePublicEmit(event as string, content, callback);
|
|
445
|
-
};
|
|
446
|
-
socket.connected = false;
|
|
447
|
-
socket.disconnect = () => {
|
|
448
|
-
this._socket.disconnect();
|
|
449
|
-
};
|
|
450
|
-
socket.connect = () => {
|
|
451
|
-
this._socket.connect();
|
|
452
|
-
};
|
|
453
|
-
return socket;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Ask server for the latest platform/context registry via ack callback.
|
|
458
|
-
* This keeps client context composition aligned with server schema state.
|
|
459
|
-
*/
|
|
460
|
-
private requestSchemaRegistry() {
|
|
461
|
-
const socketLike = this._socket as unknown as Record<string, unknown>;
|
|
462
|
-
if (!("io" in socketLike)) {
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
this._socket.emit("schemas", (payload: unknown) => {
|
|
466
|
-
this.handleSchemasPayload(payload);
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Apply server-provided registry metadata to local runtime state.
|
|
472
|
-
* Also registers platform contexts/schemas with @sockethub/schemas validators
|
|
473
|
-
* so local validation uses the same canonical sources as the server.
|
|
474
|
-
*/
|
|
475
|
-
private applyPlatformRegistry(
|
|
476
|
-
payload: unknown,
|
|
477
|
-
): PlatformRegistryPayload | undefined {
|
|
478
|
-
if (!payload || typeof payload !== "object") {
|
|
479
|
-
return undefined;
|
|
480
|
-
}
|
|
481
|
-
const registry = payload as PlatformRegistryPayload;
|
|
482
|
-
const asContextUrl = registry.contexts?.as;
|
|
483
|
-
const sockethubContextUrl = registry.contexts?.sockethub;
|
|
484
|
-
if (
|
|
485
|
-
typeof asContextUrl !== "string" ||
|
|
486
|
-
typeof sockethubContextUrl !== "string" ||
|
|
487
|
-
!Array.isArray(registry.platforms)
|
|
488
|
-
) {
|
|
489
|
-
return undefined;
|
|
490
|
-
}
|
|
491
|
-
this.sockethubVersion =
|
|
492
|
-
typeof registry.version === "string" ? registry.version : "unknown";
|
|
493
|
-
this.asContextUrl = asContextUrl;
|
|
494
|
-
this.sockethubContextUrl = sockethubContextUrl;
|
|
495
|
-
|
|
496
|
-
this.platformRegistry.clear();
|
|
497
|
-
for (const platform of registry.platforms) {
|
|
498
|
-
if (
|
|
499
|
-
!platform ||
|
|
500
|
-
typeof platform !== "object" ||
|
|
501
|
-
typeof platform.id !== "string" ||
|
|
502
|
-
typeof platform.version !== "string" ||
|
|
503
|
-
typeof platform.contextUrl !== "string"
|
|
504
|
-
) {
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
this.platformRegistry.set(platform.id, {
|
|
508
|
-
...platform,
|
|
509
|
-
version: platform.version,
|
|
510
|
-
types: Array.isArray(platform.types) ? platform.types : [],
|
|
511
|
-
schemas: platform.schemas || {},
|
|
512
|
-
});
|
|
513
|
-
addPlatformContext(platform.id, platform.contextUrl);
|
|
514
|
-
try {
|
|
515
|
-
const credSchema = platform.schemas?.credentials;
|
|
516
|
-
if (
|
|
517
|
-
credSchema &&
|
|
518
|
-
typeof credSchema === "object" &&
|
|
519
|
-
!Array.isArray(credSchema)
|
|
520
|
-
) {
|
|
521
|
-
addPlatformSchema(credSchema, `${platform.id}/credentials`);
|
|
522
|
-
}
|
|
523
|
-
const msgSchema = platform.schemas?.messages;
|
|
524
|
-
if (
|
|
525
|
-
msgSchema &&
|
|
526
|
-
typeof msgSchema === "object" &&
|
|
527
|
-
!Array.isArray(msgSchema)
|
|
528
|
-
) {
|
|
529
|
-
addPlatformSchema(msgSchema, `${platform.id}/messages`);
|
|
530
|
-
}
|
|
531
|
-
} catch (err) {
|
|
532
|
-
const message =
|
|
533
|
-
err instanceof Error ? err.message : String(err);
|
|
534
|
-
console.warn(
|
|
535
|
-
`[SockethubClient] Failed to register schemas for platform ${platform.id}: ${message}`,
|
|
536
|
-
);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
const normalizedPayload = this.buildPlatformRegistryPayload();
|
|
540
|
-
this.registryFingerprint =
|
|
541
|
-
this.computePayloadFingerprint(normalizedPayload);
|
|
542
|
-
// Emit normalized registry payload so app code receives a stable shape.
|
|
543
|
-
this.socket._emit("schemas", normalizedPayload);
|
|
544
|
-
return normalizedPayload;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
private eventActivityObject(content: ActivityObject) {
|
|
548
|
-
if (content.id) {
|
|
549
|
-
this.events["activity-object"].set(content.id, content);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
private eventCredentials(content: ActivityStream) {
|
|
554
|
-
if (content.object && content.object.type === "credentials") {
|
|
555
|
-
const key: string =
|
|
556
|
-
content.actor.id || (content.actor as unknown as string);
|
|
557
|
-
this.events.credentials.set(key, content);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
private eventMessage(content: BaseActivityObject) {
|
|
562
|
-
if (!this._socket.connected) {
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
// either stores or delete the specified content onto the storedJoins map,
|
|
566
|
-
// for reply once we're back online.
|
|
567
|
-
const key = SockethubClient.getKey(content as ActivityStream);
|
|
568
|
-
if (content.type === "join" || content.type === "connect") {
|
|
569
|
-
this.events[content.type].set(key, content as ActivityStream);
|
|
570
|
-
} else if (content.type === "leave") {
|
|
571
|
-
this.events.join.delete(key);
|
|
572
|
-
} else if (content.type === "disconnect") {
|
|
573
|
-
this.events.connect.delete(key);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
private static getKey(content: ActivityStream) {
|
|
578
|
-
const actor = content.actor?.id || content.actor;
|
|
579
|
-
if (!actor) {
|
|
580
|
-
throw new Error(
|
|
581
|
-
`actor property not present for message type: ${content?.type}`,
|
|
582
|
-
);
|
|
583
|
-
}
|
|
584
|
-
const target = content.target
|
|
585
|
-
? content.target.id || content.target
|
|
586
|
-
: "";
|
|
587
|
-
return `${actor}-${target}`;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
private buildPlatformRegistryPayload(): PlatformRegistryPayload {
|
|
591
|
-
return {
|
|
592
|
-
version: this.sockethubVersion,
|
|
593
|
-
contexts:
|
|
594
|
-
this.asContextUrl && this.sockethubContextUrl
|
|
595
|
-
? {
|
|
596
|
-
as: this.asContextUrl,
|
|
597
|
-
sockethub: this.sockethubContextUrl,
|
|
598
|
-
}
|
|
599
|
-
: undefined,
|
|
600
|
-
platforms: this.getRegisteredPlatforms(),
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
private buildReadyInfo(reason: ReadyReason): ClientReadyInfo | undefined {
|
|
605
|
-
if (
|
|
606
|
-
!this.sockethubVersion ||
|
|
607
|
-
!this.asContextUrl ||
|
|
608
|
-
!this.sockethubContextUrl
|
|
609
|
-
) {
|
|
610
|
-
return undefined;
|
|
611
|
-
}
|
|
612
|
-
return {
|
|
613
|
-
state: "ready",
|
|
614
|
-
reason,
|
|
615
|
-
sockethubVersion: this.sockethubVersion,
|
|
616
|
-
contexts: {
|
|
617
|
-
as: this.asContextUrl,
|
|
618
|
-
sockethub: this.sockethubContextUrl,
|
|
619
|
-
},
|
|
620
|
-
platforms: this.getRegisteredPlatforms().map((platform) => ({
|
|
621
|
-
id: platform.id,
|
|
622
|
-
version: platform.version,
|
|
623
|
-
contextUrl: platform.contextUrl,
|
|
624
|
-
contextVersion: platform.contextVersion,
|
|
625
|
-
schemaVersion: platform.schemaVersion,
|
|
626
|
-
types: [...platform.types],
|
|
627
|
-
})),
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
private resolveReadyWaiters(info: ClientReadyInfo) {
|
|
632
|
-
const waiters = this.readyWaiters;
|
|
633
|
-
this.readyWaiters = [];
|
|
634
|
-
for (const waiter of waiters) {
|
|
635
|
-
if (waiter.timer) {
|
|
636
|
-
clearTimeout(waiter.timer);
|
|
637
|
-
}
|
|
638
|
-
waiter.resolve(info);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
private rejectReadyWaiters(err: Error) {
|
|
643
|
-
const waiters = this.readyWaiters;
|
|
644
|
-
this.readyWaiters = [];
|
|
645
|
-
for (const waiter of waiters) {
|
|
646
|
-
if (waiter.timer) {
|
|
647
|
-
clearTimeout(waiter.timer);
|
|
648
|
-
}
|
|
649
|
-
waiter.reject(err);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
private emitInitError(
|
|
654
|
-
error: string,
|
|
655
|
-
phase: InitErrorPhase,
|
|
656
|
-
retrying: boolean,
|
|
657
|
-
) {
|
|
658
|
-
this.socket._emit("init_error", {
|
|
659
|
-
error,
|
|
660
|
-
phase,
|
|
661
|
-
retrying,
|
|
662
|
-
} satisfies ClientInitError);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
private emitClientError(
|
|
666
|
-
event: string,
|
|
667
|
-
callback: unknown,
|
|
668
|
-
errorMessage: string,
|
|
669
|
-
) {
|
|
670
|
-
if (typeof callback === "function") {
|
|
671
|
-
callback({ error: errorMessage });
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
this.socket._emit("client_error", {
|
|
675
|
-
event,
|
|
676
|
-
error: errorMessage,
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
private clearInitTimers() {
|
|
681
|
-
if (this.initTimeoutTimer) {
|
|
682
|
-
clearTimeout(this.initTimeoutTimer);
|
|
683
|
-
this.initTimeoutTimer = undefined;
|
|
684
|
-
}
|
|
685
|
-
if (this.waitingWarningTimer) {
|
|
686
|
-
clearInterval(this.waitingWarningTimer);
|
|
687
|
-
this.waitingWarningTimer = undefined;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
private startWaitingWarnings() {
|
|
692
|
-
if (this.waitingWarningTimer) {
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
this.waitingWarningTimer = setInterval(() => {
|
|
696
|
-
if (this.isReady() || this.initState === "closed") {
|
|
697
|
-
this.clearInitTimers();
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
const queueSize = this.outboundQueue.length;
|
|
701
|
-
const oldest = this.outboundQueue[0];
|
|
702
|
-
const oldestAgeSeconds = oldest
|
|
703
|
-
? ((Date.now() - oldest.enqueuedAt) / 1000).toFixed(1)
|
|
704
|
-
: "0.0";
|
|
705
|
-
console.warn(
|
|
706
|
-
`[SockethubClient] Still waiting for schemas; queued outbound messages: ${queueSize}; oldest queued age: ${oldestAgeSeconds}s.`,
|
|
707
|
-
);
|
|
708
|
-
}, this.waitingWarningIntervalMs);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
private startInitialization(reason: ReadyReason, replayOnReady: boolean) {
|
|
712
|
-
if (!this.socket.connected || this.initState === "closed") {
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const token = ++this.initTokenCounter;
|
|
717
|
-
this.initCycle = {
|
|
718
|
-
token,
|
|
719
|
-
reason,
|
|
720
|
-
startedAt: Date.now(),
|
|
721
|
-
replayOnReady,
|
|
722
|
-
timedOut: false,
|
|
723
|
-
};
|
|
724
|
-
this.initState = "initializing";
|
|
725
|
-
this.clearInitTimers();
|
|
726
|
-
|
|
727
|
-
this.initTimeoutTimer = setTimeout(() => {
|
|
728
|
-
if (!this.initCycle || this.initCycle.token !== token) {
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
this.initCycle.timedOut = true;
|
|
732
|
-
this.initState = "init_error";
|
|
733
|
-
const timeoutMsg = `Initialization timed out after ${this.options.initTimeoutMs}ms waiting for schemas`;
|
|
734
|
-
console.warn(
|
|
735
|
-
`[SockethubClient] ${timeoutMsg}; queued outbound messages: ${this.outboundQueue.length}. Waiting for schemas event from server.`,
|
|
736
|
-
);
|
|
737
|
-
this.emitInitError(timeoutMsg, "timeout", false);
|
|
738
|
-
this.startWaitingWarnings();
|
|
739
|
-
}, this.options.initTimeoutMs);
|
|
740
|
-
|
|
741
|
-
try {
|
|
742
|
-
// Pull the latest registry from the server for this init cycle.
|
|
743
|
-
this.requestSchemaRegistry();
|
|
744
|
-
} catch (err) {
|
|
745
|
-
this.initState = "init_error";
|
|
746
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
747
|
-
this.emitInitError(message, "schemas-request", false);
|
|
748
|
-
this.startWaitingWarnings();
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
private markReady(reason: ReadyReason) {
|
|
753
|
-
const cycle = this.initCycle;
|
|
754
|
-
const replayOnReady = Boolean(cycle?.replayOnReady);
|
|
755
|
-
this.initCycle = undefined;
|
|
756
|
-
this.clearInitTimers();
|
|
757
|
-
this.initState = "ready";
|
|
758
|
-
this.hasReadyOnce = true;
|
|
759
|
-
|
|
760
|
-
const info = this.buildReadyInfo(reason);
|
|
761
|
-
if (!info) {
|
|
762
|
-
const err = new Error("Failed to build ready payload");
|
|
763
|
-
this.initState = "init_error";
|
|
764
|
-
this.emitInitError(err.message, "schemas-apply", true);
|
|
765
|
-
this.rejectReadyWaiters(err);
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
this.socket._emit("ready", info);
|
|
769
|
-
this.latestReadyInfo = info;
|
|
770
|
-
this.resolveReadyWaiters(info);
|
|
771
|
-
|
|
772
|
-
if (replayOnReady) {
|
|
773
|
-
// Replay previously sent state before flushing newly queued outbound events.
|
|
774
|
-
this.replay("activity-object", this.events["activity-object"]);
|
|
775
|
-
this.replay("credentials", this.events.credentials);
|
|
776
|
-
this.replay("message", this.events.connect);
|
|
777
|
-
this.replay("message", this.events.join);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
this.flushOutboundQueue();
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
private computePayloadFingerprint(payload: unknown): string | undefined {
|
|
784
|
-
if (!payload || typeof payload !== "object") {
|
|
785
|
-
return undefined;
|
|
786
|
-
}
|
|
787
|
-
const registry = payload as PlatformRegistryPayload;
|
|
788
|
-
if (
|
|
789
|
-
typeof registry.contexts?.as !== "string" ||
|
|
790
|
-
typeof registry.contexts?.sockethub !== "string" ||
|
|
791
|
-
!Array.isArray(registry.platforms)
|
|
792
|
-
) {
|
|
793
|
-
return undefined;
|
|
794
|
-
}
|
|
795
|
-
const normalizedPlatforms = registry.platforms
|
|
796
|
-
.map((platform) => ({
|
|
797
|
-
id: platform.id,
|
|
798
|
-
version: platform.version,
|
|
799
|
-
contextUrl: platform.contextUrl,
|
|
800
|
-
contextVersion: platform.contextVersion,
|
|
801
|
-
schemaVersion: platform.schemaVersion,
|
|
802
|
-
}))
|
|
803
|
-
.sort((a, b) => a.id.localeCompare(b.id));
|
|
804
|
-
return JSON.stringify({
|
|
805
|
-
version: registry.version,
|
|
806
|
-
contexts: registry.contexts,
|
|
807
|
-
platforms: normalizedPlatforms,
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
private handleSchemasPayload(payload: unknown) {
|
|
812
|
-
if (!payload || typeof payload !== "object") {
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
const incomingFingerprint = this.computePayloadFingerprint(payload);
|
|
816
|
-
if (
|
|
817
|
-
this.initState === "ready" &&
|
|
818
|
-
!this.initCycle &&
|
|
819
|
-
incomingFingerprint &&
|
|
820
|
-
incomingFingerprint === this.registryFingerprint
|
|
821
|
-
) {
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
if (this.initState === "ready" && !this.initCycle) {
|
|
826
|
-
// A server-side schema update arrived while already running.
|
|
827
|
-
this.initState = "initializing";
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
const normalizedPayload = this.applyPlatformRegistry(payload);
|
|
831
|
-
if (!normalizedPayload) {
|
|
832
|
-
this.initState = "init_error";
|
|
833
|
-
this.emitInitError(
|
|
834
|
-
"Received invalid schemas payload from server",
|
|
835
|
-
"schemas-apply",
|
|
836
|
-
true,
|
|
837
|
-
);
|
|
838
|
-
this.startWaitingWarnings();
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
if (this.initCycle) {
|
|
843
|
-
this.markReady(this.initCycle.reason);
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
this.markReady("schemas-update");
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
private handlePublicEmit(
|
|
850
|
-
event: string,
|
|
851
|
-
content: unknown,
|
|
852
|
-
callback?: unknown,
|
|
853
|
-
) {
|
|
854
|
-
const queuedEvent: QueuedOutboundEvent = {
|
|
855
|
-
event,
|
|
856
|
-
content,
|
|
857
|
-
callback,
|
|
858
|
-
enqueuedAt: Date.now(),
|
|
859
|
-
sequence: this.outboundSequence++,
|
|
860
|
-
};
|
|
861
|
-
|
|
862
|
-
if (!this.isReady()) {
|
|
863
|
-
// Hold outbound until schemas/context metadata is loaded.
|
|
864
|
-
this.enqueueOutbound(queuedEvent);
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
|
-
this.sendOutbound(queuedEvent);
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
private enqueueOutbound(queuedEvent: QueuedOutboundEvent) {
|
|
871
|
-
this.outboundQueue.push(queuedEvent);
|
|
872
|
-
if (this.outboundQueue.length <= this.options.maxQueuedOutbound) {
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
const dropped = this.outboundQueue.shift();
|
|
876
|
-
if (!dropped) {
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
this.emitClientError(
|
|
880
|
-
dropped.event,
|
|
881
|
-
dropped.callback,
|
|
882
|
-
"SockethubClient queue overflow before ready",
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
private flushOutboundQueue() {
|
|
887
|
-
if (!this.isReady() || this.outboundQueue.length === 0) {
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
const now = Date.now();
|
|
891
|
-
const queued = this.outboundQueue.sort(
|
|
892
|
-
(a, b) => a.sequence - b.sequence,
|
|
893
|
-
);
|
|
894
|
-
this.outboundQueue = [];
|
|
895
|
-
for (const entry of queued) {
|
|
896
|
-
if (now - entry.enqueuedAt > this.options.maxQueuedAgeMs) {
|
|
897
|
-
this.emitClientError(
|
|
898
|
-
entry.event,
|
|
899
|
-
entry.callback,
|
|
900
|
-
`SockethubClient queued message expired after ${this.options.maxQueuedAgeMs}ms before initialization`,
|
|
901
|
-
);
|
|
902
|
-
continue;
|
|
903
|
-
}
|
|
904
|
-
this.sendOutbound(entry);
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
private sendOutbound(entry: QueuedOutboundEvent) {
|
|
909
|
-
let outgoing = entry.content;
|
|
910
|
-
try {
|
|
911
|
-
if (entry.event === "credentials" || entry.event === "message") {
|
|
912
|
-
// Run canonical expansion/normalization at send time so queued and
|
|
913
|
-
// immediate sends follow the exact same path.
|
|
914
|
-
outgoing = this.ActivityStreams.Stream(
|
|
915
|
-
entry.content as ActivityStream,
|
|
916
|
-
);
|
|
917
|
-
if (outgoing && typeof outgoing === "object") {
|
|
918
|
-
const activity = outgoing as ActivityStream;
|
|
919
|
-
if (
|
|
920
|
-
!activity["@context"] &&
|
|
921
|
-
typeof activity.platform === "string" &&
|
|
922
|
-
activity.platform.trim().length > 0
|
|
923
|
-
) {
|
|
924
|
-
activity["@context"] = this.contextFor(
|
|
925
|
-
activity.platform,
|
|
926
|
-
);
|
|
927
|
-
}
|
|
928
|
-
if (entry.event === "credentials" && !activity.type) {
|
|
929
|
-
activity.type = "credentials";
|
|
930
|
-
}
|
|
931
|
-
if (
|
|
932
|
-
activity.actor &&
|
|
933
|
-
typeof activity.actor === "object" &&
|
|
934
|
-
!activity.actor.type
|
|
935
|
-
) {
|
|
936
|
-
activity.actor.type = "person";
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
if (this.platformRegistry.size > 0) {
|
|
940
|
-
const validationError = this.validateActivity(
|
|
941
|
-
outgoing as ActivityStream,
|
|
942
|
-
);
|
|
943
|
-
if (validationError) {
|
|
944
|
-
this.emitClientError(
|
|
945
|
-
entry.event,
|
|
946
|
-
entry.callback,
|
|
947
|
-
`SockethubClient validation failed: ${validationError}`,
|
|
948
|
-
);
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
if (entry.event === "credentials") {
|
|
954
|
-
this.eventCredentials(outgoing as ActivityStream);
|
|
955
|
-
} else if (entry.event === "message") {
|
|
956
|
-
this.eventMessage(outgoing as BaseActivityObject);
|
|
957
|
-
}
|
|
958
|
-
if (entry.event === "activity-object") {
|
|
959
|
-
// Persist only after successful server ACK to avoid replaying
|
|
960
|
-
// rejected objects on reconnection.
|
|
961
|
-
const originalCallback = entry.callback;
|
|
962
|
-
const obj = outgoing as ActivityObject;
|
|
963
|
-
this._socket.emit(
|
|
964
|
-
entry.event,
|
|
965
|
-
outgoing,
|
|
966
|
-
(resp?: { error?: string }) => {
|
|
967
|
-
if (resp && typeof resp.error === "string") {
|
|
968
|
-
if (obj.id) {
|
|
969
|
-
this.events["activity-object"].delete(obj.id);
|
|
970
|
-
}
|
|
971
|
-
} else {
|
|
972
|
-
this.eventActivityObject(obj);
|
|
973
|
-
}
|
|
974
|
-
if (typeof originalCallback === "function") {
|
|
975
|
-
originalCallback(resp);
|
|
976
|
-
}
|
|
977
|
-
},
|
|
978
|
-
);
|
|
979
|
-
} else {
|
|
980
|
-
this._socket.emit(entry.event, outgoing, entry.callback);
|
|
981
|
-
}
|
|
982
|
-
} catch (err) {
|
|
983
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
984
|
-
this.emitClientError(entry.event, entry.callback, message);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
private log(msg: string, obj?: unknown) {
|
|
989
|
-
if (this.debug) {
|
|
990
|
-
console.log(msg, obj);
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
private registerSocketIOHandlers() {
|
|
995
|
-
// register for events that give us information on connection status
|
|
996
|
-
this._socket.on("connect", () => {
|
|
997
|
-
this.socket.id = this._socket.id;
|
|
998
|
-
this.socket.connected = true;
|
|
999
|
-
this.socket._emit("connect");
|
|
1000
|
-
this.startInitialization(
|
|
1001
|
-
this.hasReadyOnce ? "reconnect" : "initial-connect",
|
|
1002
|
-
true,
|
|
1003
|
-
);
|
|
1004
|
-
});
|
|
1005
|
-
this._socket.on("connect_error", (obj?: unknown) => {
|
|
1006
|
-
this.socket._emit("connect_error", obj);
|
|
1007
|
-
});
|
|
1008
|
-
this._socket.on("disconnect", (obj?: unknown) => {
|
|
1009
|
-
this.socket.connected = false;
|
|
1010
|
-
if (this.initState !== "closed") {
|
|
1011
|
-
this.initState = "idle";
|
|
1012
|
-
}
|
|
1013
|
-
this.clearInitTimers();
|
|
1014
|
-
this.socket._emit("disconnect", obj);
|
|
1015
|
-
});
|
|
1016
|
-
this._socket.on("schemas", (payload: unknown) => {
|
|
1017
|
-
this.handleSchemasPayload(payload);
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
// use as middleware to receive incoming Sockethub messages and unpack them
|
|
1021
|
-
// using the ActivityStreams library before passing them along to the app.
|
|
1022
|
-
this._socket.on("message", (obj) => {
|
|
1023
|
-
this.socket._emit("message", this.ActivityStreams.Stream(obj));
|
|
1024
|
-
});
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
/**
|
|
1028
|
-
* Type guard to check if an object is an ActivityStream with a valid actor.id.
|
|
1029
|
-
*/
|
|
1030
|
-
private hasActorId(
|
|
1031
|
-
obj: ActivityStream | ActivityObject,
|
|
1032
|
-
): obj is ActivityStream {
|
|
1033
|
-
return (
|
|
1034
|
-
"actor" in obj &&
|
|
1035
|
-
obj.actor !== null &&
|
|
1036
|
-
typeof obj.actor === "object" &&
|
|
1037
|
-
"id" in obj.actor &&
|
|
1038
|
-
typeof obj.actor.id === "string"
|
|
1039
|
-
);
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
/**
|
|
1043
|
-
* Replays previously sent events to the server after reconnection.
|
|
1044
|
-
*
|
|
1045
|
-
* This method is called automatically when the Socket.IO connection is
|
|
1046
|
-
* re-established after a transient network interruption. It resends
|
|
1047
|
-
* credentials and connection state so the user doesn't need to manually
|
|
1048
|
-
* re-authenticate or rejoin channels.
|
|
1049
|
-
*
|
|
1050
|
-
* Security considerations:
|
|
1051
|
-
* - Only replays events stored in memory during this session
|
|
1052
|
-
* - Does not replay after page refresh (memory cleared)
|
|
1053
|
-
* - Server should validate replayed credentials (may be stale/revoked)
|
|
1054
|
-
*
|
|
1055
|
-
* @param name - Event name to emit ("credentials", "activity-object", "message")
|
|
1056
|
-
* @param asMap - Map of events to replay
|
|
1057
|
-
*/
|
|
1058
|
-
private replay<K extends keyof ReplayEventMap>(
|
|
1059
|
-
name: K,
|
|
1060
|
-
asMap: Map<string, ReplayEventMap[K]>,
|
|
1061
|
-
): void {
|
|
1062
|
-
for (const obj of asMap.values()) {
|
|
1063
|
-
// activity-objects are raw objects, don't pass through Stream()
|
|
1064
|
-
// which is designed for activity streams with actor/object structure
|
|
1065
|
-
const isActivityObject = name === "activity-object";
|
|
1066
|
-
if (isActivityObject) {
|
|
1067
|
-
const expandedObj = obj as BaseActivityObject;
|
|
1068
|
-
const id = expandedObj?.id;
|
|
1069
|
-
this.log(`replaying ${name} for ${id}`);
|
|
1070
|
-
this._socket.emit(name, expandedObj);
|
|
1071
|
-
continue;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
const expandedObj = this.ActivityStreams.Stream(
|
|
1075
|
-
obj as ActivityStream,
|
|
1076
|
-
);
|
|
1077
|
-
let id = expandedObj?.id;
|
|
1078
|
-
if (this.hasActorId(expandedObj)) {
|
|
1079
|
-
const actor = (expandedObj as ActivityStream).actor;
|
|
1080
|
-
// actor can be a string (JID) or an object with an id field
|
|
1081
|
-
id = typeof actor === "string" ? actor : actor.id;
|
|
1082
|
-
}
|
|
1083
|
-
this.log(`replaying ${name} for ${id}`);
|
|
1084
|
-
this._socket.emit(name, expandedObj);
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
((global: Record<string, unknown>) => {
|
|
1090
|
-
global.SockethubClient = SockethubClient;
|
|
1091
|
-
})(
|
|
1092
|
-
typeof globalThis === "object"
|
|
1093
|
-
? (globalThis as Record<string, unknown>)
|
|
1094
|
-
: {},
|
|
1095
|
-
);
|