@turingfocus/chat-gateway-tfrobot 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dto.d.ts +1 -1
- package/dist/dto.js +1 -1
- package/dist/dto.js.map +1 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +8 -1
- package/dist/gateway.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/socket.d.ts.map +1 -1
- package/dist/socket.js +655 -134
- package/dist/socket.js.map +1 -1
- package/dist/types.d.ts +16 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/dist/socket.js
CHANGED
|
@@ -17,6 +17,123 @@ const KNOWN_EVENTS = new Set([
|
|
|
17
17
|
]);
|
|
18
18
|
const MAX_TIMER_DELAY = 2_147_483_647;
|
|
19
19
|
const RECONNECT_AUTH_TIMEOUT_MS = 10_000;
|
|
20
|
+
const RECONNECT_JOIN_TIMEOUT_MS = 10_000;
|
|
21
|
+
const parseJoinAcknowledgement = (value, reconnect) => {
|
|
22
|
+
if (value === undefined) {
|
|
23
|
+
return {
|
|
24
|
+
accepted: reconnect,
|
|
25
|
+
recoveryComplete: false,
|
|
26
|
+
...(reconnect ? {} : { rejectionCode: "validation" }),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (value === null) {
|
|
30
|
+
return {
|
|
31
|
+
accepted: false,
|
|
32
|
+
recoveryComplete: false,
|
|
33
|
+
rejectionCode: "validation",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (value === false) {
|
|
37
|
+
return {
|
|
38
|
+
accepted: false,
|
|
39
|
+
recoveryComplete: false,
|
|
40
|
+
rejectionCode: "validation",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (value === true) {
|
|
44
|
+
return { accepted: true, recoveryComplete: !reconnect };
|
|
45
|
+
}
|
|
46
|
+
if (typeof value !== "object") {
|
|
47
|
+
return {
|
|
48
|
+
accepted: false,
|
|
49
|
+
recoveryComplete: false,
|
|
50
|
+
rejectionCode: "validation",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const record = value;
|
|
54
|
+
const booleanAliases = ["ok", "accepted", "success"];
|
|
55
|
+
const hasInvalidBooleanAlias = booleanAliases.some((alias) => Object.hasOwn(record, alias) && typeof record[alias] !== "boolean");
|
|
56
|
+
const statusAliases = ["status", "statusCode", "code"];
|
|
57
|
+
const hasInvalidStatusAlias = statusAliases.some((alias) => Object.hasOwn(record, alias) && record[alias] === undefined);
|
|
58
|
+
const statuses = [record["status"], record["statusCode"], record["code"]]
|
|
59
|
+
.filter((status) => status !== undefined)
|
|
60
|
+
.map((status) => {
|
|
61
|
+
const numeric = typeof status === "number"
|
|
62
|
+
? status
|
|
63
|
+
: typeof status === "string" && /^\d{3}$/u.test(status)
|
|
64
|
+
? Number(status)
|
|
65
|
+
: undefined;
|
|
66
|
+
const normalized = typeof status === "string" ? status.trim().toLowerCase() : undefined;
|
|
67
|
+
const accepted = (numeric !== undefined && numeric >= 200 && numeric < 300) ||
|
|
68
|
+
normalized === "ok" ||
|
|
69
|
+
normalized === "success" ||
|
|
70
|
+
normalized === "accepted" ||
|
|
71
|
+
normalized === "joined";
|
|
72
|
+
const rejected = (numeric !== undefined && numeric >= 400) ||
|
|
73
|
+
normalized === "error" ||
|
|
74
|
+
normalized === "failed" ||
|
|
75
|
+
normalized === "forbidden" ||
|
|
76
|
+
normalized === "unauthorized" ||
|
|
77
|
+
normalized === "rejected";
|
|
78
|
+
return { accepted, normalized, numeric, rejected };
|
|
79
|
+
});
|
|
80
|
+
const hasUnknownStatus = statuses.some((status) => !status.accepted && !status.rejected);
|
|
81
|
+
const explicitlyAccepted = record["ok"] === true ||
|
|
82
|
+
record["accepted"] === true ||
|
|
83
|
+
record["success"] === true ||
|
|
84
|
+
statuses.some((status) => status.accepted);
|
|
85
|
+
const cursorAliases = ["cursor", "recoveryCursor"];
|
|
86
|
+
const cursorValues = cursorAliases
|
|
87
|
+
.filter((alias) => Object.hasOwn(record, alias))
|
|
88
|
+
.map((alias) => record[alias]);
|
|
89
|
+
const cursorInvalid = cursorValues.some((cursorValue) => typeof cursorValue !== "string");
|
|
90
|
+
const cursorConflict = cursorValues.length > 1 &&
|
|
91
|
+
cursorValues.some((cursorValue) => cursorValue !== cursorValues[0]);
|
|
92
|
+
const recoveryAliases = ["recovered", "recoveryComplete"];
|
|
93
|
+
const recoveryDeclarations = recoveryAliases
|
|
94
|
+
.filter((alias) => Object.hasOwn(record, alias))
|
|
95
|
+
.map((alias) => record[alias]);
|
|
96
|
+
const recoveryInvalid = recoveryDeclarations.some((declaration) => typeof declaration !== "boolean");
|
|
97
|
+
const recoveryConflict = recoveryDeclarations.includes(true) && recoveryDeclarations.includes(false);
|
|
98
|
+
const normalizedError = typeof record["error"] === "string"
|
|
99
|
+
? record["error"].trim().toLowerCase()
|
|
100
|
+
: undefined;
|
|
101
|
+
const explicitlyRejected = hasInvalidBooleanAlias ||
|
|
102
|
+
hasInvalidStatusAlias ||
|
|
103
|
+
record["ok"] === false ||
|
|
104
|
+
record["accepted"] === false ||
|
|
105
|
+
record["success"] === false ||
|
|
106
|
+
record["error"] !== undefined ||
|
|
107
|
+
cursorInvalid ||
|
|
108
|
+
cursorConflict ||
|
|
109
|
+
recoveryInvalid ||
|
|
110
|
+
recoveryConflict ||
|
|
111
|
+
(!reconnect && recoveryDeclarations.includes(false)) ||
|
|
112
|
+
hasUnknownStatus ||
|
|
113
|
+
statuses.some((status) => status.rejected);
|
|
114
|
+
const accepted = explicitlyAccepted && !explicitlyRejected;
|
|
115
|
+
const cursor = cursorValues[0];
|
|
116
|
+
const recoveryComplete = !reconnect ||
|
|
117
|
+
(recoveryDeclarations.length > 0 &&
|
|
118
|
+
recoveryDeclarations.every((declaration) => declaration === true));
|
|
119
|
+
const authenticationRejected = statuses.some(({ normalized, numeric }) => numeric === 401 || normalized === "unauthorized") || normalizedError === "unauthorized";
|
|
120
|
+
const authorizationRejected = statuses.some(({ normalized, numeric }) => numeric === 403 || normalized === "forbidden") || normalizedError === "forbidden";
|
|
121
|
+
return {
|
|
122
|
+
accepted,
|
|
123
|
+
...(authenticationRejected
|
|
124
|
+
? { rejectionCode: "authentication" }
|
|
125
|
+
: authorizationRejected
|
|
126
|
+
? { rejectionCode: "authorization" }
|
|
127
|
+
: accepted
|
|
128
|
+
? {}
|
|
129
|
+
: { rejectionCode: "validation" }),
|
|
130
|
+
recoveryComplete,
|
|
131
|
+
...(typeof cursor === "string" ? { cursor } : {}),
|
|
132
|
+
...(typeof record["message"] === "string"
|
|
133
|
+
? { message: record["message"] }
|
|
134
|
+
: {}),
|
|
135
|
+
};
|
|
136
|
+
};
|
|
20
137
|
const sameRun = (first, second) => first === second ||
|
|
21
138
|
(first != null &&
|
|
22
139
|
second != null &&
|
|
@@ -102,9 +219,9 @@ export const createSocketIoFactoryWith = (connect) => ({ getAuth, namespaceUrl,
|
|
|
102
219
|
});
|
|
103
220
|
export const createSocketIoFactory = createSocketIoFactoryWith(io);
|
|
104
221
|
export class TFRobotSocketClient {
|
|
105
|
-
#active;
|
|
106
222
|
#disposed = false;
|
|
107
223
|
#establishmentAbort;
|
|
224
|
+
#establishing;
|
|
108
225
|
#factory;
|
|
109
226
|
#namespaceUrl;
|
|
110
227
|
#now;
|
|
@@ -112,6 +229,8 @@ export class TFRobotSocketClient {
|
|
|
112
229
|
#path;
|
|
113
230
|
#reconnectStatusLoader;
|
|
114
231
|
#runs = new Map();
|
|
232
|
+
#subscriptions = new Set();
|
|
233
|
+
#subscriptionGeneration = 0;
|
|
115
234
|
constructor(options, reconnectStatusLoader) {
|
|
116
235
|
this.#options = options;
|
|
117
236
|
this.#factory = options.socketFactory ?? createSocketIoFactory;
|
|
@@ -134,7 +253,9 @@ export class TFRobotSocketClient {
|
|
|
134
253
|
error: createGatewayDeadlineExceededError(),
|
|
135
254
|
};
|
|
136
255
|
}
|
|
137
|
-
this.#
|
|
256
|
+
const generation = ++this.#subscriptionGeneration;
|
|
257
|
+
const subscriptionId = `tfrobot-subscription-${generation}`;
|
|
258
|
+
this.#cancelEstablishment();
|
|
138
259
|
const establishmentAbort = new AbortController();
|
|
139
260
|
this.#establishmentAbort = establishmentAbort;
|
|
140
261
|
const authOutcome = await awaitBounded(() => this.#getSocketAuth(conversationId, "connect"), {
|
|
@@ -142,9 +263,6 @@ export class TFRobotSocketClient {
|
|
|
142
263
|
now: this.#now,
|
|
143
264
|
signal: establishmentAbort.signal,
|
|
144
265
|
});
|
|
145
|
-
if (this.#establishmentAbort === establishmentAbort) {
|
|
146
|
-
this.#establishmentAbort = undefined;
|
|
147
|
-
}
|
|
148
266
|
switch (authOutcome.kind) {
|
|
149
267
|
case "aborted": {
|
|
150
268
|
return {
|
|
@@ -224,6 +342,20 @@ export class TFRobotSocketClient {
|
|
|
224
342
|
};
|
|
225
343
|
}
|
|
226
344
|
const listeners = new Map();
|
|
345
|
+
let joinTimeout;
|
|
346
|
+
const activeErrorIds = new Map();
|
|
347
|
+
const replaceableErrorSources = new Set([
|
|
348
|
+
"authentication",
|
|
349
|
+
"connection",
|
|
350
|
+
"recovery",
|
|
351
|
+
"subscription",
|
|
352
|
+
]);
|
|
353
|
+
let errorSequence = 0;
|
|
354
|
+
let publishUpdate = () => undefined;
|
|
355
|
+
const pendingNotifications = [];
|
|
356
|
+
const pendingRealtimeActions = [];
|
|
357
|
+
let joinPending = false;
|
|
358
|
+
let subscriptionEstablished = false;
|
|
227
359
|
let setupFailure;
|
|
228
360
|
const add = (eventName, listener) => {
|
|
229
361
|
if (setupFailure !== undefined)
|
|
@@ -244,15 +376,52 @@ export class TFRobotSocketClient {
|
|
|
244
376
|
// Diagnostics are observational and cannot break the chat stream.
|
|
245
377
|
}
|
|
246
378
|
};
|
|
247
|
-
const
|
|
248
|
-
if (!active.active)
|
|
249
|
-
return;
|
|
379
|
+
const notifyError = (error) => {
|
|
250
380
|
try {
|
|
251
381
|
observer.error?.(error);
|
|
252
382
|
}
|
|
253
383
|
catch {
|
|
254
384
|
// Host observers are isolated from the transport listener.
|
|
255
385
|
}
|
|
386
|
+
};
|
|
387
|
+
const resolveError = (source) => {
|
|
388
|
+
const errorId = activeErrorIds.get(source);
|
|
389
|
+
if (errorId === undefined || !active.active)
|
|
390
|
+
return;
|
|
391
|
+
activeErrorIds.delete(source);
|
|
392
|
+
publishUpdate(chatUpdateSchema.parse({
|
|
393
|
+
kind: "error.resolved",
|
|
394
|
+
conversationId: errorConversationId(),
|
|
395
|
+
errorId,
|
|
396
|
+
}));
|
|
397
|
+
};
|
|
398
|
+
const report = (error, source = "protocol") => {
|
|
399
|
+
if (!active.active)
|
|
400
|
+
return;
|
|
401
|
+
if (replaceableErrorSources.has(source))
|
|
402
|
+
resolveError(source);
|
|
403
|
+
const errorId = `${subscriptionId}:error:${++errorSequence}`;
|
|
404
|
+
if (replaceableErrorSources.has(source)) {
|
|
405
|
+
activeErrorIds.set(source, errorId);
|
|
406
|
+
}
|
|
407
|
+
const occurrenceError = error.conversationId === undefined
|
|
408
|
+
? error
|
|
409
|
+
: { ...error, conversationId: errorConversationId() };
|
|
410
|
+
publishUpdate(chatUpdateSchema.parse({
|
|
411
|
+
kind: "error.reported",
|
|
412
|
+
conversationId: errorConversationId(),
|
|
413
|
+
error: occurrenceError,
|
|
414
|
+
errorId,
|
|
415
|
+
source,
|
|
416
|
+
scope: source === "domain"
|
|
417
|
+
? { kind: "conversation", id: errorConversationId() }
|
|
418
|
+
: { kind: "subscription", id: subscriptionId },
|
|
419
|
+
generation,
|
|
420
|
+
}));
|
|
421
|
+
if (subscriptionEstablished)
|
|
422
|
+
notifyError(error);
|
|
423
|
+
else
|
|
424
|
+
pendingNotifications.push({ kind: "error", error });
|
|
256
425
|
diagnose(error);
|
|
257
426
|
};
|
|
258
427
|
const sanitizePayload = (payload) => {
|
|
@@ -276,6 +445,10 @@ export class TFRobotSocketClient {
|
|
|
276
445
|
updateConversationId !== errorConversationId()) {
|
|
277
446
|
return;
|
|
278
447
|
}
|
|
448
|
+
if (!subscriptionEstablished) {
|
|
449
|
+
pendingNotifications.push({ kind: "update", update });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
279
452
|
try {
|
|
280
453
|
observer.next(update);
|
|
281
454
|
}
|
|
@@ -283,6 +456,74 @@ export class TFRobotSocketClient {
|
|
|
283
456
|
diagnose(this.#error("unknown", "TFRobot Gateway observer rejected an update", false, errorConversationId()));
|
|
284
457
|
}
|
|
285
458
|
};
|
|
459
|
+
publishUpdate = next;
|
|
460
|
+
const dispatchRealtime = (action) => {
|
|
461
|
+
if (active.acceptingEvents)
|
|
462
|
+
action();
|
|
463
|
+
else if (joinPending)
|
|
464
|
+
pendingRealtimeActions.push(action);
|
|
465
|
+
};
|
|
466
|
+
const flushRealtime = () => {
|
|
467
|
+
for (const action of pendingRealtimeActions.splice(0))
|
|
468
|
+
action();
|
|
469
|
+
};
|
|
470
|
+
const lifecycle = (status, recovery, joinLatencyMs) => {
|
|
471
|
+
if (!active.active)
|
|
472
|
+
return;
|
|
473
|
+
const value = {
|
|
474
|
+
status,
|
|
475
|
+
generation,
|
|
476
|
+
reconnectAttempt: active.reconnectAttempt,
|
|
477
|
+
subscriptionId,
|
|
478
|
+
...(recovery === undefined ? {} : { recovery }),
|
|
479
|
+
};
|
|
480
|
+
const update = chatUpdateSchema.parse({
|
|
481
|
+
kind: "lifecycle.changed",
|
|
482
|
+
conversationId: errorConversationId(),
|
|
483
|
+
lifecycle: value,
|
|
484
|
+
});
|
|
485
|
+
next(update);
|
|
486
|
+
try {
|
|
487
|
+
void Promise.resolve(this.#options.onLifecycleDiagnostic?.({
|
|
488
|
+
kind: "socket.lifecycle",
|
|
489
|
+
conversationId: errorConversationId(),
|
|
490
|
+
subscriptionId,
|
|
491
|
+
generation,
|
|
492
|
+
status,
|
|
493
|
+
reconnectAttempt: active.reconnectAttempt,
|
|
494
|
+
...(joinLatencyMs === undefined ? {} : { joinLatencyMs }),
|
|
495
|
+
...(recovery === undefined
|
|
496
|
+
? {}
|
|
497
|
+
: {
|
|
498
|
+
recoveryComplete: recovery.complete,
|
|
499
|
+
...(recovery.cursor === undefined
|
|
500
|
+
? {}
|
|
501
|
+
: { recoveryCursor: recovery.cursor }),
|
|
502
|
+
...(recovery.reason === undefined
|
|
503
|
+
? {}
|
|
504
|
+
: { recoveryReason: recovery.reason }),
|
|
505
|
+
}),
|
|
506
|
+
})).catch(() => undefined);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
// Lifecycle diagnostics are observational.
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
const enterAuthRequired = () => {
|
|
513
|
+
active.acceptingEvents = false;
|
|
514
|
+
active.authRequired = true;
|
|
515
|
+
active.terminal = true;
|
|
516
|
+
joinPending = false;
|
|
517
|
+
pendingRealtimeActions.length = 0;
|
|
518
|
+
active.recoveryRevision += 1;
|
|
519
|
+
lifecycle("auth-required");
|
|
520
|
+
try {
|
|
521
|
+
active.socket.disconnect();
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// Authentication is already terminal for this subscription episode.
|
|
525
|
+
}
|
|
526
|
+
};
|
|
286
527
|
const mapAndNext = (invalidMessage, map) => {
|
|
287
528
|
let update;
|
|
288
529
|
try {
|
|
@@ -295,10 +536,18 @@ export class TFRobotSocketClient {
|
|
|
295
536
|
next(update);
|
|
296
537
|
};
|
|
297
538
|
let settleEstablishment;
|
|
298
|
-
let connectedOnce = false;
|
|
299
539
|
add("connect", () => {
|
|
300
540
|
if (!active.active)
|
|
301
541
|
return;
|
|
542
|
+
if (active.terminal) {
|
|
543
|
+
try {
|
|
544
|
+
active.socket.disconnect();
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
// Terminal subscriptions never rejoin.
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
302
551
|
if (settleEstablishment !== undefined &&
|
|
303
552
|
isGatewayDeadlineExceeded(options, this.#now())) {
|
|
304
553
|
settleEstablishment({
|
|
@@ -307,13 +556,206 @@ export class TFRobotSocketClient {
|
|
|
307
556
|
});
|
|
308
557
|
return;
|
|
309
558
|
}
|
|
310
|
-
|
|
311
|
-
|
|
559
|
+
if (joinTimeout !== undefined) {
|
|
560
|
+
clearTimeout(joinTimeout);
|
|
561
|
+
joinTimeout = undefined;
|
|
562
|
+
}
|
|
563
|
+
const reconnect = subscriptionEstablished;
|
|
564
|
+
if (reconnect)
|
|
565
|
+
active.reconnectAttempt += 1;
|
|
312
566
|
const recoveryRevision = ++active.recoveryRevision;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
567
|
+
const runRevision = active.runRevision;
|
|
568
|
+
const joinStartedAt = this.#now();
|
|
569
|
+
active.acceptingEvents = false;
|
|
570
|
+
joinPending = true;
|
|
571
|
+
pendingRealtimeActions.length = 0;
|
|
572
|
+
lifecycle("joining");
|
|
573
|
+
let acknowledgementSettled = false;
|
|
574
|
+
const settleJoin = (...acknowledgementArguments) => {
|
|
575
|
+
if (!active.active ||
|
|
576
|
+
!this.#subscriptions.has(active) ||
|
|
577
|
+
active.recoveryRevision !== recoveryRevision) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (acknowledgementSettled)
|
|
581
|
+
return;
|
|
582
|
+
acknowledgementSettled = true;
|
|
583
|
+
if (joinTimeout !== undefined) {
|
|
584
|
+
clearTimeout(joinTimeout);
|
|
585
|
+
joinTimeout = undefined;
|
|
586
|
+
}
|
|
587
|
+
let acknowledgement;
|
|
588
|
+
try {
|
|
589
|
+
const rawAcknowledgement = acknowledgementArguments[0];
|
|
590
|
+
acknowledgement = parseJoinAcknowledgement(rawAcknowledgement === undefined || rawAcknowledgement === true
|
|
591
|
+
? rawAcknowledgement
|
|
592
|
+
: sanitizeCredentialRaw(rawAcknowledgement, credentialValues), reconnect);
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
acknowledgement = {
|
|
596
|
+
accepted: false,
|
|
597
|
+
recoveryComplete: false,
|
|
598
|
+
rejectionCode: "validation",
|
|
599
|
+
message: "Invalid TFRobot conversation subscription acknowledgement",
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
if (!acknowledgement.accepted) {
|
|
603
|
+
joinPending = false;
|
|
604
|
+
pendingRealtimeActions.length = 0;
|
|
605
|
+
const rejectionCode = acknowledgement.rejectionCode ?? "validation";
|
|
606
|
+
const error = this.#error(rejectionCode, acknowledgement.message ??
|
|
607
|
+
"TFRobot conversation subscription was rejected", false, errorConversationId());
|
|
608
|
+
if (settleEstablishment !== undefined) {
|
|
609
|
+
lifecycle("subscription-failed");
|
|
610
|
+
if (rejectionCode === "authentication" ||
|
|
611
|
+
rejectionCode === "authorization") {
|
|
612
|
+
void this.#invalidateSession(error, "rejected");
|
|
613
|
+
}
|
|
614
|
+
settleEstablishment({ ok: false, error });
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
const authenticationRejected = rejectionCode === "authentication" ||
|
|
618
|
+
rejectionCode === "authorization";
|
|
619
|
+
if (authenticationRejected) {
|
|
620
|
+
enterAuthRequired();
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
active.acceptingEvents = false;
|
|
624
|
+
active.terminal = true;
|
|
625
|
+
active.recoveryRevision += 1;
|
|
626
|
+
try {
|
|
627
|
+
active.socket.disconnect();
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
// Subscription failure is already terminal for this episode.
|
|
631
|
+
}
|
|
632
|
+
lifecycle("subscription-failed");
|
|
633
|
+
}
|
|
634
|
+
report(error, authenticationRejected ? "authentication" : "subscription");
|
|
635
|
+
if (authenticationRejected) {
|
|
636
|
+
void this.#invalidateSession(error, "rejected");
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (!reconnect) {
|
|
642
|
+
joinPending = false;
|
|
643
|
+
subscriptionEstablished = true;
|
|
644
|
+
for (const notification of pendingNotifications.splice(0)) {
|
|
645
|
+
if (notification.kind === "update")
|
|
646
|
+
next(notification.update);
|
|
647
|
+
else
|
|
648
|
+
notifyError(notification.error);
|
|
649
|
+
}
|
|
650
|
+
settleEstablishment?.({
|
|
651
|
+
ok: true,
|
|
652
|
+
value: {
|
|
653
|
+
dispose: () => {
|
|
654
|
+
if (!active.active)
|
|
655
|
+
return;
|
|
656
|
+
active.active = false;
|
|
657
|
+
active.cleanup();
|
|
658
|
+
this.#subscriptions.delete(active);
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
resolveError("connection");
|
|
663
|
+
resolveError("authentication");
|
|
664
|
+
lifecycle("active", {
|
|
665
|
+
complete: true,
|
|
666
|
+
...(acknowledgement.cursor === undefined
|
|
667
|
+
? {}
|
|
668
|
+
: { cursor: acknowledgement.cursor }),
|
|
669
|
+
}, this.#now() - joinStartedAt);
|
|
670
|
+
active.acceptingEvents = true;
|
|
671
|
+
flushRealtime();
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
joinPending = false;
|
|
675
|
+
const joinLatencyMs = this.#now() - joinStartedAt;
|
|
676
|
+
lifecycle("recovering", {
|
|
677
|
+
complete: false,
|
|
678
|
+
...(acknowledgement.cursor === undefined
|
|
679
|
+
? {}
|
|
680
|
+
: { cursor: acknowledgement.cursor }),
|
|
681
|
+
...(acknowledgement.recoveryComplete
|
|
682
|
+
? {}
|
|
683
|
+
: { reason: "server-replay-contract-unavailable" }),
|
|
684
|
+
}, joinLatencyMs);
|
|
685
|
+
active.acceptingEvents = true;
|
|
686
|
+
flushRealtime();
|
|
687
|
+
void this.#reconcileRunAfterReconnect(active, conversationId, credentialValues, recoveryRevision, runRevision, next, report)
|
|
688
|
+
.then((outcome) => {
|
|
689
|
+
if (outcome === "auth-required") {
|
|
690
|
+
enterAuthRequired();
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if ((outcome !== "success" && outcome !== "superseded-by-realtime") ||
|
|
694
|
+
!acknowledgement.recoveryComplete) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
resolveError("connection");
|
|
698
|
+
resolveError("recovery");
|
|
699
|
+
resolveError("authentication");
|
|
700
|
+
active.manualReconnectAttempted = false;
|
|
701
|
+
active.manualReconnectPending = false;
|
|
702
|
+
active.authRequired = false;
|
|
703
|
+
active.terminal = false;
|
|
704
|
+
lifecycle("active", {
|
|
705
|
+
complete: true,
|
|
706
|
+
...(acknowledgement.cursor === undefined
|
|
707
|
+
? {}
|
|
708
|
+
: { cursor: acknowledgement.cursor }),
|
|
709
|
+
}, joinLatencyMs);
|
|
710
|
+
})
|
|
711
|
+
.catch(() => {
|
|
712
|
+
active.acceptingEvents = false;
|
|
713
|
+
active.terminal = true;
|
|
714
|
+
try {
|
|
715
|
+
active.socket.disconnect();
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
// Offline is already terminal for this subscription episode.
|
|
719
|
+
}
|
|
720
|
+
lifecycle("offline");
|
|
721
|
+
report(this.#error("unknown", "TFRobot reconnect reconciliation failed", true, errorConversationId()), "recovery");
|
|
316
722
|
});
|
|
723
|
+
};
|
|
724
|
+
const joinDeadlineAt = reconnect
|
|
725
|
+
? this.#now() + RECONNECT_JOIN_TIMEOUT_MS
|
|
726
|
+
: options.deadlineAt;
|
|
727
|
+
joinTimeout = setTimeout(() => {
|
|
728
|
+
joinTimeout = undefined;
|
|
729
|
+
if (!active.active || active.recoveryRevision !== recoveryRevision) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
active.recoveryRevision += 1;
|
|
733
|
+
joinPending = false;
|
|
734
|
+
pendingRealtimeActions.length = 0;
|
|
735
|
+
const error = createGatewayDeadlineExceededError(errorConversationId());
|
|
736
|
+
if (settleEstablishment !== undefined) {
|
|
737
|
+
lifecycle("subscription-failed");
|
|
738
|
+
settleEstablishment({ ok: false, error });
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
if (active.manualReconnectPending)
|
|
742
|
+
enterAuthRequired();
|
|
743
|
+
else {
|
|
744
|
+
active.acceptingEvents = false;
|
|
745
|
+
active.terminal = true;
|
|
746
|
+
lifecycle("subscription-failed");
|
|
747
|
+
}
|
|
748
|
+
report(error, active.manualReconnectPending ? "connection" : "subscription");
|
|
749
|
+
try {
|
|
750
|
+
active.socket.disconnect();
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
// The failed join attempt is already invalidated.
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}, Math.min(MAX_TIMER_DELAY, Math.max(0, joinDeadlineAt - this.#now())));
|
|
757
|
+
try {
|
|
758
|
+
socket.emit("join_conversation", { conversation_id: conversationId }, settleJoin);
|
|
317
759
|
}
|
|
318
760
|
catch (reason) {
|
|
319
761
|
const error = this.#transportError(reason, "Unable to join the TFRobot conversation", conversationId, credentialValues);
|
|
@@ -321,112 +763,97 @@ export class TFRobotSocketClient {
|
|
|
321
763
|
settleEstablishment({ ok: false, error });
|
|
322
764
|
}
|
|
323
765
|
else {
|
|
324
|
-
report(error);
|
|
766
|
+
report(error, "subscription");
|
|
325
767
|
active.active = false;
|
|
326
768
|
active.cleanup();
|
|
327
|
-
|
|
328
|
-
this.#active = undefined;
|
|
769
|
+
this.#subscriptions.delete(active);
|
|
329
770
|
}
|
|
330
771
|
return;
|
|
331
772
|
}
|
|
332
|
-
if (settleEstablishment !== undefined &&
|
|
333
|
-
isGatewayDeadlineExceeded(options, this.#now())) {
|
|
334
|
-
settleEstablishment({
|
|
335
|
-
ok: false,
|
|
336
|
-
error: createGatewayDeadlineExceededError(errorConversationId()),
|
|
337
|
-
});
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
settleEstablishment?.({
|
|
341
|
-
ok: true,
|
|
342
|
-
value: {
|
|
343
|
-
dispose: () => {
|
|
344
|
-
if (!active.active)
|
|
345
|
-
return;
|
|
346
|
-
active.active = false;
|
|
347
|
-
active.cleanup();
|
|
348
|
-
if (this.#active === active)
|
|
349
|
-
this.#active = undefined;
|
|
350
|
-
},
|
|
351
|
-
},
|
|
352
|
-
});
|
|
353
|
-
if (reconnect) {
|
|
354
|
-
void this.#reconcileRunAfterReconnect(active, conversationId, credentialValues, recoveryRevision, next, report).catch(() => {
|
|
355
|
-
report(this.#error("unknown", "TFRobot reconnect reconciliation failed", true, errorConversationId()));
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
773
|
});
|
|
359
774
|
add("chat_message", (payload) => {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
775
|
+
dispatchRealtime(() => {
|
|
776
|
+
if (belongsToForeignConversation(payload, conversationId))
|
|
777
|
+
return;
|
|
778
|
+
const parsed = messageDtoSchema.safeParse(sanitizePayload(payload));
|
|
779
|
+
if (!parsed.success) {
|
|
780
|
+
report(this.#error("validation", "Invalid TFRobot chat_message payload", false, errorConversationId()));
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
mapAndNext("TFRobot chat_message could not be normalized", () => mapMessageUpdate(parsed.data));
|
|
784
|
+
});
|
|
368
785
|
});
|
|
369
786
|
add("chat_event", (payload) => {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
787
|
+
dispatchRealtime(() => {
|
|
788
|
+
if (belongsToForeignConversation(payload, conversationId))
|
|
789
|
+
return;
|
|
790
|
+
const parsed = eventDtoSchema.safeParse(sanitizePayload(payload));
|
|
791
|
+
if (!parsed.success) {
|
|
792
|
+
report(this.#error("validation", "Invalid TFRobot chat_event payload", false, errorConversationId()));
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
mapAndNext("TFRobot chat_event could not be normalized", () => mapEventUpdate(parsed.data));
|
|
796
|
+
});
|
|
378
797
|
});
|
|
379
798
|
add("conversation_state_changed", (payload) => {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
targetConversationId !==
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
799
|
+
dispatchRealtime(() => {
|
|
800
|
+
if (belongsToForeignConversation(payload, conversationId))
|
|
801
|
+
return;
|
|
802
|
+
const parsed = stateChangedDtoSchema.safeParse(sanitizePayload(payload));
|
|
803
|
+
if (!parsed.success) {
|
|
804
|
+
report(this.#error("validation", "Invalid TFRobot conversation_state_changed payload", false, errorConversationId()));
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const targetConversationId = String(parsed.data.conversationId);
|
|
808
|
+
if (targetConversationId !== conversationId &&
|
|
809
|
+
targetConversationId !== errorConversationId()) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
active.runRevision += 1;
|
|
813
|
+
mapAndNext("TFRobot run state could not be normalized", () => {
|
|
814
|
+
const run = mapRun(targetConversationId, {
|
|
815
|
+
working: parsed.data.state === "working",
|
|
816
|
+
taskId: parsed.data.taskId,
|
|
817
|
+
});
|
|
818
|
+
this.#runs.set(targetConversationId, run);
|
|
819
|
+
return chatUpdateSchema.parse({
|
|
820
|
+
kind: "run.replace",
|
|
821
|
+
conversationId: targetConversationId,
|
|
822
|
+
run,
|
|
823
|
+
});
|
|
403
824
|
});
|
|
404
825
|
});
|
|
405
826
|
});
|
|
406
827
|
add("chat_error", (payload) => {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
targetConversationId !==
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
828
|
+
dispatchRealtime(() => {
|
|
829
|
+
if (belongsToForeignConversation(payload, conversationId))
|
|
830
|
+
return;
|
|
831
|
+
const parsed = chatErrorEventDtoSchema.safeParse(sanitizePayload(payload));
|
|
832
|
+
if (!parsed.success) {
|
|
833
|
+
report(this.#error("validation", "Invalid TFRobot chat_error payload", false, errorConversationId()));
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const targetConversationId = String(parsed.data.conversationId);
|
|
837
|
+
if (targetConversationId !== conversationId &&
|
|
838
|
+
targetConversationId !== errorConversationId()) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
report(this.#error("server", typeof parsed.data.error === "string"
|
|
842
|
+
? parsed.data.error
|
|
843
|
+
: "TFRobot run failed", false, targetConversationId, parsed.data, credentialValues), "domain");
|
|
844
|
+
});
|
|
422
845
|
});
|
|
423
846
|
add("error", (payload) => {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
847
|
+
dispatchRealtime(() => {
|
|
848
|
+
const parsed = socketProtocolErrorDtoSchema.safeParse(sanitizePayload(payload));
|
|
849
|
+
report(this.#error("validation", parsed.success && typeof parsed.data.message === "string"
|
|
850
|
+
? parsed.data.message
|
|
851
|
+
: "TFRobot Socket protocol error", false, conversationId, undefined, credentialValues));
|
|
852
|
+
});
|
|
428
853
|
});
|
|
429
854
|
add("connect_error", (reason) => {
|
|
855
|
+
if (!active.active || active.terminal)
|
|
856
|
+
return;
|
|
430
857
|
const injectedError = chatErrorSchema.safeParse(reason);
|
|
431
858
|
const rejectionCode = socketAuthRejectionCode(reason);
|
|
432
859
|
const error = authenticationFailure !== undefined
|
|
@@ -448,23 +875,82 @@ export class TFRobotSocketClient {
|
|
|
448
875
|
settleEstablishment({ ok: false, error });
|
|
449
876
|
return;
|
|
450
877
|
}
|
|
451
|
-
|
|
878
|
+
if (active.manualReconnectPending) {
|
|
879
|
+
enterAuthRequired();
|
|
880
|
+
report(error, error.code === "authentication" || error.code === "authorization"
|
|
881
|
+
? "authentication"
|
|
882
|
+
: "connection");
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (error.code === "authentication" || error.code === "authorization") {
|
|
886
|
+
enterAuthRequired();
|
|
887
|
+
}
|
|
888
|
+
report(error, error.code === "authentication" || error.code === "authorization"
|
|
889
|
+
? "authentication"
|
|
890
|
+
: "connection");
|
|
452
891
|
});
|
|
453
892
|
add("disconnect", (reason) => {
|
|
454
893
|
if (reason === "io client disconnect")
|
|
455
894
|
return;
|
|
895
|
+
if (active.terminal)
|
|
896
|
+
return;
|
|
897
|
+
active.acceptingEvents = false;
|
|
898
|
+
joinPending = false;
|
|
899
|
+
pendingRealtimeActions.length = 0;
|
|
900
|
+
if (joinTimeout !== undefined) {
|
|
901
|
+
clearTimeout(joinTimeout);
|
|
902
|
+
joinTimeout = undefined;
|
|
903
|
+
}
|
|
456
904
|
active.recoveryRevision += 1;
|
|
905
|
+
lifecycle("reconnecting");
|
|
457
906
|
const injectedError = chatErrorSchema.safeParse(reason);
|
|
458
907
|
report(injectedError.success
|
|
459
908
|
? sanitizeCredentialError(injectedError.data, credentialValues)
|
|
460
|
-
: this.#error("network", `TFRobot Socket disconnected: ${String(reason)}`, true, conversationId, undefined, credentialValues));
|
|
461
|
-
|
|
462
|
-
const anyListener = (eventName, payload) => {
|
|
463
|
-
if (KNOWN_EVENTS.has(eventName))
|
|
909
|
+
: this.#error("network", `TFRobot Socket disconnected: ${String(reason)}`, true, conversationId, undefined, credentialValues), "connection");
|
|
910
|
+
if (reason !== "io server disconnect")
|
|
464
911
|
return;
|
|
465
|
-
if (
|
|
912
|
+
if (active.manualReconnectAttempted) {
|
|
913
|
+
enterAuthRequired();
|
|
466
914
|
return;
|
|
467
|
-
|
|
915
|
+
}
|
|
916
|
+
active.manualReconnectAttempted = true;
|
|
917
|
+
active.manualReconnectPending = true;
|
|
918
|
+
const invalidationRevision = active.recoveryRevision;
|
|
919
|
+
const invalidationError = this.#error("authentication", "TFRobot Socket session was invalidated by the server", true, errorConversationId());
|
|
920
|
+
void awaitBounded(() => this.#invalidateSession(invalidationError, "unknown"), {
|
|
921
|
+
deadlineAt: this.#now() + RECONNECT_AUTH_TIMEOUT_MS,
|
|
922
|
+
now: this.#now,
|
|
923
|
+
signal: connectionAbort.signal,
|
|
924
|
+
}).then((outcome) => {
|
|
925
|
+
if (!active.active ||
|
|
926
|
+
active.socket.connected ||
|
|
927
|
+
active.authRequired ||
|
|
928
|
+
!active.manualReconnectPending ||
|
|
929
|
+
active.recoveryRevision !== invalidationRevision) {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (outcome.kind !== "value" || !outcome.value) {
|
|
933
|
+
report(this.#error("authentication", "Unable to invalidate the TFRobot Socket session", false, errorConversationId()), "authentication");
|
|
934
|
+
enterAuthRequired();
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
try {
|
|
938
|
+
active.socket.connect();
|
|
939
|
+
}
|
|
940
|
+
catch (reconnectError) {
|
|
941
|
+
report(this.#transportError(reconnectError, "Unable to reconnect the TFRobot Socket transport", conversationId, credentialValues), "authentication");
|
|
942
|
+
enterAuthRequired();
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
const anyListener = (eventName, payload) => {
|
|
947
|
+
dispatchRealtime(() => {
|
|
948
|
+
if (KNOWN_EVENTS.has(eventName))
|
|
949
|
+
return;
|
|
950
|
+
if (belongsToForeignConversation(payload, conversationId))
|
|
951
|
+
return;
|
|
952
|
+
mapAndNext("Unknown TFRobot Socket event could not be normalized", () => mapUnknownSocketEvent(sanitizeCredentialText(eventName, credentialValues), sanitizePayload(payload), errorConversationId(), this.#now()));
|
|
953
|
+
});
|
|
468
954
|
};
|
|
469
955
|
if (setupFailure === undefined) {
|
|
470
956
|
try {
|
|
@@ -476,6 +962,10 @@ export class TFRobotSocketClient {
|
|
|
476
962
|
}
|
|
477
963
|
const cleanup = () => {
|
|
478
964
|
connectionAbort.abort();
|
|
965
|
+
if (joinTimeout !== undefined) {
|
|
966
|
+
clearTimeout(joinTimeout);
|
|
967
|
+
joinTimeout = undefined;
|
|
968
|
+
}
|
|
479
969
|
for (const [eventName, listener] of listeners) {
|
|
480
970
|
try {
|
|
481
971
|
socket.off(eventName, listener);
|
|
@@ -498,6 +988,8 @@ export class TFRobotSocketClient {
|
|
|
498
988
|
}
|
|
499
989
|
firstAuth = undefined;
|
|
500
990
|
credentialValues.clear();
|
|
991
|
+
pendingNotifications.length = 0;
|
|
992
|
+
pendingRealtimeActions.length = 0;
|
|
501
993
|
};
|
|
502
994
|
if (setupFailure !== undefined) {
|
|
503
995
|
const error = this.#transportError(setupFailure.reason, "Unable to configure the TFRobot Socket transport", conversationId, credentialValues);
|
|
@@ -508,7 +1000,9 @@ export class TFRobotSocketClient {
|
|
|
508
1000
|
};
|
|
509
1001
|
}
|
|
510
1002
|
const active = {
|
|
1003
|
+
acceptingEvents: true,
|
|
511
1004
|
active: true,
|
|
1005
|
+
authRequired: false,
|
|
512
1006
|
cancelEstablishment: () => {
|
|
513
1007
|
settleEstablishment?.({
|
|
514
1008
|
ok: false,
|
|
@@ -517,11 +1011,19 @@ export class TFRobotSocketClient {
|
|
|
517
1011
|
},
|
|
518
1012
|
cleanup,
|
|
519
1013
|
conversationId,
|
|
1014
|
+
generation,
|
|
1015
|
+
manualReconnectAttempted: false,
|
|
1016
|
+
manualReconnectPending: false,
|
|
520
1017
|
observer,
|
|
1018
|
+
reconnectAttempt: 0,
|
|
521
1019
|
recoveryRevision: 0,
|
|
1020
|
+
runRevision: 0,
|
|
522
1021
|
socket,
|
|
1022
|
+
subscriptionId,
|
|
1023
|
+
terminal: false,
|
|
523
1024
|
};
|
|
524
|
-
this.#active
|
|
1025
|
+
this.#subscriptions.add(active);
|
|
1026
|
+
this.#establishing = active;
|
|
525
1027
|
return new Promise((resolve) => {
|
|
526
1028
|
let settled = false;
|
|
527
1029
|
const settle = (result) => {
|
|
@@ -529,12 +1031,16 @@ export class TFRobotSocketClient {
|
|
|
529
1031
|
return;
|
|
530
1032
|
settled = true;
|
|
531
1033
|
settleEstablishment = undefined;
|
|
1034
|
+
if (this.#establishmentAbort === establishmentAbort) {
|
|
1035
|
+
this.#establishmentAbort = undefined;
|
|
1036
|
+
}
|
|
1037
|
+
if (this.#establishing === active)
|
|
1038
|
+
this.#establishing = undefined;
|
|
532
1039
|
clearTimeout(timeout);
|
|
533
1040
|
if (!result.ok) {
|
|
534
1041
|
active.active = false;
|
|
535
1042
|
active.cleanup();
|
|
536
|
-
|
|
537
|
-
this.#active = undefined;
|
|
1043
|
+
this.#subscriptions.delete(active);
|
|
538
1044
|
}
|
|
539
1045
|
resolve(result);
|
|
540
1046
|
};
|
|
@@ -553,6 +1059,7 @@ export class TFRobotSocketClient {
|
|
|
553
1059
|
return;
|
|
554
1060
|
}
|
|
555
1061
|
try {
|
|
1062
|
+
lifecycle("connecting");
|
|
556
1063
|
socket.connect();
|
|
557
1064
|
}
|
|
558
1065
|
catch (reason) {
|
|
@@ -574,7 +1081,12 @@ export class TFRobotSocketClient {
|
|
|
574
1081
|
if (this.#disposed)
|
|
575
1082
|
return;
|
|
576
1083
|
this.#disposed = true;
|
|
577
|
-
this.#
|
|
1084
|
+
this.#cancelEstablishment();
|
|
1085
|
+
for (const subscription of this.#subscriptions) {
|
|
1086
|
+
subscription.active = false;
|
|
1087
|
+
subscription.cleanup();
|
|
1088
|
+
}
|
|
1089
|
+
this.#subscriptions.clear();
|
|
578
1090
|
this.#runs.clear();
|
|
579
1091
|
}
|
|
580
1092
|
rememberRun(conversationId, run) {
|
|
@@ -582,16 +1094,11 @@ export class TFRobotSocketClient {
|
|
|
582
1094
|
return;
|
|
583
1095
|
this.#runs.set(conversationId, run);
|
|
584
1096
|
}
|
|
585
|
-
#
|
|
1097
|
+
#cancelEstablishment() {
|
|
586
1098
|
this.#establishmentAbort?.abort();
|
|
587
1099
|
this.#establishmentAbort = undefined;
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
return;
|
|
591
|
-
current.cancelEstablishment();
|
|
592
|
-
current.active = false;
|
|
593
|
-
current.cleanup();
|
|
594
|
-
this.#active = undefined;
|
|
1100
|
+
this.#establishing?.cancelEstablishment();
|
|
1101
|
+
this.#establishing = undefined;
|
|
595
1102
|
}
|
|
596
1103
|
#defaultNamespaceUrl(baseUrl) {
|
|
597
1104
|
const url = new URL(baseUrl);
|
|
@@ -611,17 +1118,28 @@ export class TFRobotSocketClient {
|
|
|
611
1118
|
}
|
|
612
1119
|
return authOf(session);
|
|
613
1120
|
}
|
|
614
|
-
async #reconcileRunAfterReconnect(active, conversationId, credentialValues, recoveryRevision, next, report) {
|
|
1121
|
+
async #reconcileRunAfterReconnect(active, conversationId, credentialValues, recoveryRevision, runRevision, next, report) {
|
|
615
1122
|
const result = await this.#reconnectStatusLoader(conversationId);
|
|
616
1123
|
if (!active.active ||
|
|
617
|
-
this.#active
|
|
1124
|
+
!this.#subscriptions.has(active) ||
|
|
618
1125
|
!active.socket.connected ||
|
|
619
1126
|
active.recoveryRevision !== recoveryRevision) {
|
|
620
|
-
return;
|
|
1127
|
+
return "failure";
|
|
621
1128
|
}
|
|
622
1129
|
if (!result.ok) {
|
|
623
|
-
|
|
624
|
-
|
|
1130
|
+
const error = sanitizeCredentialError(result.error, credentialValues);
|
|
1131
|
+
if (error.code === "authentication" || error.code === "authorization") {
|
|
1132
|
+
report(error, "authentication");
|
|
1133
|
+
return "auth-required";
|
|
1134
|
+
}
|
|
1135
|
+
if (active.runRevision !== runRevision) {
|
|
1136
|
+
return "superseded-by-realtime";
|
|
1137
|
+
}
|
|
1138
|
+
report(error, "recovery");
|
|
1139
|
+
return "failure";
|
|
1140
|
+
}
|
|
1141
|
+
if (active.runRevision !== runRevision) {
|
|
1142
|
+
return "superseded-by-realtime";
|
|
625
1143
|
}
|
|
626
1144
|
try {
|
|
627
1145
|
const updateConversationId = sanitizeCredentialText(conversationId, credentialValues);
|
|
@@ -629,16 +1147,18 @@ export class TFRobotSocketClient {
|
|
|
629
1147
|
const run = mapRun(updateConversationId, safeStatus);
|
|
630
1148
|
const previous = this.#runs.get(conversationId);
|
|
631
1149
|
this.#runs.set(conversationId, run);
|
|
632
|
-
if (sameRun(previous, run))
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
1150
|
+
if (!sameRun(previous, run)) {
|
|
1151
|
+
next(chatUpdateSchema.parse({
|
|
1152
|
+
kind: "run.replace",
|
|
1153
|
+
conversationId: updateConversationId,
|
|
1154
|
+
run,
|
|
1155
|
+
}));
|
|
1156
|
+
}
|
|
1157
|
+
return "success";
|
|
639
1158
|
}
|
|
640
1159
|
catch {
|
|
641
|
-
report(this.#error("validation", "Reconnected TFRobot run status could not be normalized", false, conversationId, undefined, credentialValues));
|
|
1160
|
+
report(this.#error("validation", "Reconnected TFRobot run status could not be normalized", false, conversationId, undefined, credentialValues), "recovery");
|
|
1161
|
+
return "failure";
|
|
642
1162
|
}
|
|
643
1163
|
}
|
|
644
1164
|
async #invalidateSession(error, reason) {
|
|
@@ -647,9 +1167,10 @@ export class TFRobotSocketClient {
|
|
|
647
1167
|
reason: reason ?? (error.code === "authorization" ? "forbidden" : "expired"),
|
|
648
1168
|
error,
|
|
649
1169
|
});
|
|
1170
|
+
return true;
|
|
650
1171
|
}
|
|
651
1172
|
catch {
|
|
652
|
-
|
|
1173
|
+
return false;
|
|
653
1174
|
}
|
|
654
1175
|
}
|
|
655
1176
|
#transportError(reason, fallback, conversationId, credentialValues = []) {
|