@sockethub/client 5.0.0-alpha.11 → 5.0.0-alpha.12
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 +128 -31
- package/dist/sockethub-client.browser.js +17452 -288
- package/dist/sockethub-client.js +17456 -275
- package/dist/sockethub-client.min.js +32 -4
- package/package.json +4 -4
- package/src/sockethub-client.test.ts +433 -25
- package/src/sockethub-client.ts +832 -75
package/src/sockethub-client.ts
CHANGED
|
@@ -4,6 +4,12 @@ import type {
|
|
|
4
4
|
ActivityStream,
|
|
5
5
|
BaseActivityObject,
|
|
6
6
|
} from "@sockethub/schemas";
|
|
7
|
+
import {
|
|
8
|
+
addPlatformContext,
|
|
9
|
+
addPlatformSchema,
|
|
10
|
+
validateActivityStream,
|
|
11
|
+
validateCredentials,
|
|
12
|
+
} from "@sockethub/schemas";
|
|
7
13
|
import EventEmitter from "eventemitter3";
|
|
8
14
|
import type { Socket } from "socket.io-client";
|
|
9
15
|
|
|
@@ -14,6 +20,46 @@ export interface EventMapping {
|
|
|
14
20
|
join: Map<string, ActivityStream>;
|
|
15
21
|
}
|
|
16
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
|
+
|
|
17
63
|
interface CustomEmitter extends EventEmitter {
|
|
18
64
|
_emit(s: string, o: unknown, c?: unknown): void;
|
|
19
65
|
connect(): void;
|
|
@@ -22,6 +68,58 @@ interface CustomEmitter extends EventEmitter {
|
|
|
22
68
|
id: string;
|
|
23
69
|
}
|
|
24
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
|
+
|
|
25
123
|
/**
|
|
26
124
|
* SockethubClient - Client library for Sockethub protocol gateway
|
|
27
125
|
*
|
|
@@ -73,14 +171,19 @@ interface CustomEmitter extends EventEmitter {
|
|
|
73
171
|
* const socket = io('http://localhost:10550');
|
|
74
172
|
* const client = new SockethubClient(socket);
|
|
75
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
|
+
*
|
|
76
180
|
* // Send credentials - these will be replayed on reconnection
|
|
77
181
|
* client.socket.emit('credentials', {
|
|
78
|
-
*
|
|
79
|
-
*
|
|
182
|
+
* '@context': ctx,
|
|
183
|
+
* type: 'credentials',
|
|
184
|
+
* actor: { id: 'user@example.com', type: 'person' },
|
|
185
|
+
* object: { type: 'credentials', username: 'user', password: 'pass' }
|
|
80
186
|
* });
|
|
81
|
-
*
|
|
82
|
-
* // If network disconnects and reconnects, credentials are automatically replayed
|
|
83
|
-
* // If page refreshes, credentials are lost and must be resent
|
|
84
187
|
* ```
|
|
85
188
|
*/
|
|
86
189
|
export default class SockethubClient {
|
|
@@ -97,15 +200,37 @@ export default class SockethubClient {
|
|
|
97
200
|
join: new Map(),
|
|
98
201
|
};
|
|
99
202
|
private _socket: Socket;
|
|
100
|
-
public ActivityStreams
|
|
101
|
-
public socket
|
|
203
|
+
public ActivityStreams!: ASManager;
|
|
204
|
+
public socket!: CustomEmitter;
|
|
102
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;
|
|
103
223
|
|
|
104
|
-
constructor(socket: Socket) {
|
|
224
|
+
constructor(socket: Socket, options: SockethubClientOptions = {}) {
|
|
105
225
|
if (!socket) {
|
|
106
226
|
throw new Error("SockethubClient requires a socket.io instance");
|
|
107
227
|
}
|
|
108
228
|
this._socket = socket;
|
|
229
|
+
this.options = {
|
|
230
|
+
initTimeoutMs: options.initTimeoutMs ?? 5000,
|
|
231
|
+
maxQueuedOutbound: options.maxQueuedOutbound ?? 1000,
|
|
232
|
+
maxQueuedAgeMs: options.maxQueuedAgeMs ?? 30000,
|
|
233
|
+
};
|
|
109
234
|
|
|
110
235
|
this.socket = this.createPublicEmitter();
|
|
111
236
|
this.registerSocketIOHandlers();
|
|
@@ -114,19 +239,33 @@ export default class SockethubClient {
|
|
|
114
239
|
this.ActivityStreams.on(
|
|
115
240
|
"activity-object-create",
|
|
116
241
|
(obj: ActivityObject) => {
|
|
117
|
-
socket.emit(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
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
|
+
}
|
|
121
253
|
this.eventActivityObject(obj);
|
|
122
|
-
}
|
|
123
|
-
|
|
254
|
+
},
|
|
255
|
+
);
|
|
124
256
|
},
|
|
125
257
|
);
|
|
126
258
|
|
|
127
259
|
socket.on("activity-object", (obj) => {
|
|
128
260
|
this.ActivityStreams.Object.create(obj);
|
|
129
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
|
+
}
|
|
130
269
|
}
|
|
131
270
|
|
|
132
271
|
initActivityStreams() {
|
|
@@ -152,22 +291,157 @@ export default class SockethubClient {
|
|
|
152
291
|
this.events.credentials.clear();
|
|
153
292
|
}
|
|
154
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
|
+
|
|
155
436
|
private createPublicEmitter(): CustomEmitter {
|
|
156
437
|
const socket = new EventEmitter() as CustomEmitter;
|
|
157
438
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
158
|
-
// @ts-
|
|
439
|
+
// @ts-expect-error
|
|
159
440
|
socket._emit = socket.emit;
|
|
160
441
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
161
|
-
// @ts-
|
|
442
|
+
// @ts-expect-error
|
|
162
443
|
socket.emit = (event, content, callback): void => {
|
|
163
|
-
|
|
164
|
-
this.eventCredentials(content);
|
|
165
|
-
} else if (event === "activity-object") {
|
|
166
|
-
this.eventActivityObject(content);
|
|
167
|
-
} else if (event === "message") {
|
|
168
|
-
this.eventMessage(content);
|
|
169
|
-
}
|
|
170
|
-
this._socket.emit(event as string, content, callback);
|
|
444
|
+
this.handlePublicEmit(event as string, content, callback);
|
|
171
445
|
};
|
|
172
446
|
socket.connected = false;
|
|
173
447
|
socket.disconnect = () => {
|
|
@@ -179,6 +453,97 @@ export default class SockethubClient {
|
|
|
179
453
|
return socket;
|
|
180
454
|
}
|
|
181
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
|
+
|
|
182
547
|
private eventActivityObject(content: ActivityObject) {
|
|
183
548
|
if (content.id) {
|
|
184
549
|
this.events["activity-object"].set(content.id, content);
|
|
@@ -222,6 +587,404 @@ export default class SockethubClient {
|
|
|
222
587
|
return `${actor}-${target}`;
|
|
223
588
|
}
|
|
224
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
|
+
|
|
225
988
|
private log(msg: string, obj?: unknown) {
|
|
226
989
|
if (this.debug) {
|
|
227
990
|
console.log(msg, obj);
|
|
@@ -229,46 +992,30 @@ export default class SockethubClient {
|
|
|
229
992
|
}
|
|
230
993
|
|
|
231
994
|
private registerSocketIOHandlers() {
|
|
232
|
-
// middleware for events which don't deal in AS objects
|
|
233
|
-
const callHandler = (event: string) => {
|
|
234
|
-
return async (obj?: unknown) => {
|
|
235
|
-
if (event === "connect") {
|
|
236
|
-
this.socket.id = this._socket.id;
|
|
237
|
-
this.socket.connected = true;
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Automatic state replay on reconnection.
|
|
241
|
-
*
|
|
242
|
-
* When Socket.IO reconnects after a network interruption, we automatically
|
|
243
|
-
* replay all stored state to restore the session seamlessly:
|
|
244
|
-
*
|
|
245
|
-
* 1. Activity Objects (actor definitions)
|
|
246
|
-
* 2. Credentials (authentication)
|
|
247
|
-
* 3. Connect commands (platform connections)
|
|
248
|
-
* 4. Join commands (room/channel memberships)
|
|
249
|
-
*
|
|
250
|
-
* This allows the client to survive brief network blips without requiring
|
|
251
|
-
* user intervention. However, the server must properly validate replayed
|
|
252
|
-
* credentials as they may be stale or revoked.
|
|
253
|
-
*/
|
|
254
|
-
this.replay(
|
|
255
|
-
"activity-object",
|
|
256
|
-
this.events["activity-object"],
|
|
257
|
-
);
|
|
258
|
-
this.replay("credentials", this.events.credentials);
|
|
259
|
-
this.replay("message", this.events.connect);
|
|
260
|
-
this.replay("message", this.events.join);
|
|
261
|
-
} else if (event === "disconnect") {
|
|
262
|
-
this.socket.connected = false;
|
|
263
|
-
}
|
|
264
|
-
this.socket._emit(event, obj);
|
|
265
|
-
};
|
|
266
|
-
};
|
|
267
|
-
|
|
268
995
|
// register for events that give us information on connection status
|
|
269
|
-
this._socket.on("connect",
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
});
|
|
272
1019
|
|
|
273
1020
|
// use as middleware to receive incoming Sockethub messages and unpack them
|
|
274
1021
|
// using the ActivityStreams library before passing them along to the app.
|
|
@@ -308,19 +1055,27 @@ export default class SockethubClient {
|
|
|
308
1055
|
* @param name - Event name to emit ("credentials", "activity-object", "message")
|
|
309
1056
|
* @param asMap - Map of events to replay
|
|
310
1057
|
*/
|
|
311
|
-
private replay(
|
|
312
|
-
name:
|
|
313
|
-
asMap: Map<string,
|
|
314
|
-
) {
|
|
1058
|
+
private replay<K extends keyof ReplayEventMap>(
|
|
1059
|
+
name: K,
|
|
1060
|
+
asMap: Map<string, ReplayEventMap[K]>,
|
|
1061
|
+
): void {
|
|
315
1062
|
for (const obj of asMap.values()) {
|
|
316
1063
|
// activity-objects are raw objects, don't pass through Stream()
|
|
317
1064
|
// which is designed for activity streams with actor/object structure
|
|
318
1065
|
const isActivityObject = name === "activity-object";
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
+
);
|
|
322
1077
|
let id = expandedObj?.id;
|
|
323
|
-
if (
|
|
1078
|
+
if (this.hasActorId(expandedObj)) {
|
|
324
1079
|
const actor = (expandedObj as ActivityStream).actor;
|
|
325
1080
|
// actor can be a string (JID) or an object with an id field
|
|
326
1081
|
id = typeof actor === "string" ? actor : actor.id;
|
|
@@ -331,8 +1086,10 @@ export default class SockethubClient {
|
|
|
331
1086
|
}
|
|
332
1087
|
}
|
|
333
1088
|
|
|
334
|
-
|
|
335
|
-
((global: any) => {
|
|
1089
|
+
((global: Record<string, unknown>) => {
|
|
336
1090
|
global.SockethubClient = SockethubClient;
|
|
337
|
-
|
|
338
|
-
|
|
1091
|
+
})(
|
|
1092
|
+
typeof globalThis === "object"
|
|
1093
|
+
? (globalThis as Record<string, unknown>)
|
|
1094
|
+
: {},
|
|
1095
|
+
);
|