stream-chat 10.0.0-rc.7 → 10.0.0-rc.8
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/cjs/index.browser.js +1326 -671
- package/dist/cjs/index.browser.js.map +4 -4
- package/dist/cjs/index.node.js +1328 -671
- package/dist/cjs/index.node.js.map +4 -4
- package/dist/esm/index.mjs +1326 -671
- package/dist/esm/index.mjs.map +4 -4
- package/dist/types/ChannelManager.d.ts +31 -0
- package/dist/types/ConnectionRecoveryManager.d.ts +156 -0
- package/dist/types/EventHandlerPipeline.d.ts +11 -15
- package/dist/types/channel.d.ts +31 -17
- package/dist/types/client.d.ts +28 -13
- package/dist/types/configuration/types.d.ts +2 -0
- package/dist/types/errors.d.ts +10 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/messageComposer/linkPreviewsManager.d.ts +1 -1
- package/dist/types/messageOperations/MessageOperationStatePolicy.d.ts +51 -5
- package/dist/types/messageOperations/MessageOperations.d.ts +5 -0
- package/dist/types/messageOperations/index.d.ts +4 -0
- package/dist/types/messageOperations/optimistic.d.ts +118 -0
- package/dist/types/messageOperations/persistence.d.ts +21 -0
- package/dist/types/messageOperations/types.d.ts +21 -0
- package/dist/types/offline-support/offline_support_api.d.ts +66 -5
- package/dist/types/offline-support/queueableOperations.d.ts +86 -0
- package/dist/types/offline-support/types.d.ts +53 -22
- package/dist/types/pagination/filterCompiler.d.ts +5 -0
- package/dist/types/pagination/paginators/BasePaginator.d.ts +5 -0
- package/dist/types/pagination/paginators/MessageIntervalPaginator.d.ts +1 -1
- package/dist/types/thread.d.ts +36 -6
- package/dist/types/types.d.ts +0 -10
- package/dist/types/types.utility.d.ts +8 -0
- package/package.json +1 -1
- package/src/ChannelManager.ts +129 -0
- package/src/ConnectionRecoveryManager.ts +293 -0
- package/src/EventHandlerPipeline.ts +12 -22
- package/src/channel.ts +162 -179
- package/src/client.ts +51 -89
- package/src/configuration/shape.ts +19 -0
- package/src/configuration/types.ts +2 -0
- package/src/connection.ts +1 -3
- package/src/errors.ts +15 -0
- package/src/index.ts +1 -0
- package/src/messageOperations/MessageOperationStatePolicy.ts +234 -12
- package/src/messageOperations/MessageOperations.ts +38 -9
- package/src/messageOperations/index.ts +15 -0
- package/src/messageOperations/optimistic.ts +249 -0
- package/src/messageOperations/persistence.ts +40 -0
- package/src/messageOperations/types.ts +21 -0
- package/src/offline-support/offline_support_api.ts +233 -96
- package/src/offline-support/offline_sync_manager.ts +10 -0
- package/src/offline-support/queueableOperations.ts +218 -0
- package/src/offline-support/types.ts +57 -31
- package/src/pagination/filterCompiler.ts +17 -0
- package/src/pagination/paginators/BasePaginator.ts +19 -0
- package/src/pagination/paginators/ChannelPaginator.ts +2 -22
- package/src/pagination/paginators/MessageIntervalPaginator.ts +49 -3
- package/src/thread.ts +118 -61
- package/src/types.ts +0 -10
- package/src/types.utility.ts +14 -0
|
@@ -51,11 +51,13 @@ __export(src_exports, {
|
|
|
51
51
|
ChannelWatchStatus: () => ChannelWatchStatus,
|
|
52
52
|
ClientState: () => ClientState,
|
|
53
53
|
CommandSearchSource: () => CommandSearchSource,
|
|
54
|
+
ConnectionRecoveryManager: () => ConnectionRecoveryManager,
|
|
54
55
|
CustomDataManager: () => CustomDataManager,
|
|
55
56
|
DEFAULT_ATTACHMENT_MANAGER_CONFIG: () => DEFAULT_ATTACHMENT_MANAGER_CONFIG,
|
|
56
57
|
DEFAULT_CHANNEL_CONFIG: () => DEFAULT_CHANNEL_CONFIG,
|
|
57
58
|
DEFAULT_COMMANDS_CONFIG: () => DEFAULT_COMMANDS_CONFIG,
|
|
58
59
|
DEFAULT_COMPOSER_CONFIG: () => DEFAULT_COMPOSER_CONFIG,
|
|
60
|
+
DEFAULT_CONNECTION_RECOVERY_MANAGER_CONFIG: () => DEFAULT_CONNECTION_RECOVERY_MANAGER_CONFIG,
|
|
59
61
|
DEFAULT_LINK_PREVIEW_MANAGER_CONFIG: () => DEFAULT_LINK_PREVIEW_MANAGER_CONFIG,
|
|
60
62
|
DEFAULT_LIVE_LOCATION_MANAGER_CONFIG: () => DEFAULT_LIVE_LOCATION_MANAGER_CONFIG,
|
|
61
63
|
DEFAULT_LOCATION_COMPOSER_CONFIG: () => DEFAULT_LOCATION_COMPOSER_CONFIG,
|
|
@@ -1394,228 +1396,100 @@ var CooldownTimer = class extends WithSubscriptions {
|
|
|
1394
1396
|
}
|
|
1395
1397
|
};
|
|
1396
1398
|
|
|
1397
|
-
// src/
|
|
1398
|
-
var
|
|
1399
|
-
var
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
"9": { name: "RateLimitError", retryable: true },
|
|
1406
|
-
"16": { name: "DoesNotExistError", retryable: false },
|
|
1407
|
-
"17": { name: "NotAllowedError", retryable: false },
|
|
1408
|
-
"18": { name: "EventNotSupportedError", retryable: false },
|
|
1409
|
-
"19": { name: "ChannelFeatureNotSupportedError", retryable: false },
|
|
1410
|
-
"20": { name: "MessageTooLongError", retryable: false },
|
|
1411
|
-
"21": { name: "MultipleNestingLevelError", retryable: false },
|
|
1412
|
-
"22": { name: "PayloadTooBigError", retryable: false },
|
|
1413
|
-
"23": { name: "RequestTimeoutError", retryable: true },
|
|
1414
|
-
"24": { name: "MaxHeaderSizeExceededError", retryable: false },
|
|
1415
|
-
"40": { name: "AuthErrorTokenExpired", retryable: false },
|
|
1416
|
-
"41": { name: "AuthErrorTokenNotValidYet", retryable: false },
|
|
1417
|
-
"42": { name: "AuthErrorTokenUsedBeforeIssuedAt", retryable: false },
|
|
1418
|
-
"43": { name: "AuthErrorTokenSignatureInvalid", retryable: false },
|
|
1419
|
-
"44": { name: "CustomCommandEndpointMissingError", retryable: false },
|
|
1420
|
-
"45": { name: "CustomCommandEndpointCallError", retryable: true },
|
|
1421
|
-
"46": { name: "ConnectionIDNotFoundError", retryable: false },
|
|
1422
|
-
"60": { name: "CoolDownError", retryable: true },
|
|
1423
|
-
"69": { name: "ErrWrongRegion", retryable: false },
|
|
1424
|
-
"70": { name: "ErrQueryChannelPermissions", retryable: false },
|
|
1425
|
-
"71": { name: "ErrTooManyConnections", retryable: true },
|
|
1426
|
-
"99": { name: "AppSuspendedError", retryable: false }
|
|
1427
|
-
};
|
|
1428
|
-
function isAPIError(error) {
|
|
1429
|
-
return error.code !== void 0;
|
|
1430
|
-
}
|
|
1431
|
-
function isErrorRetryable(error) {
|
|
1432
|
-
if (!error.code) return false;
|
|
1433
|
-
const err = APIErrorCodes[`${error.code}`];
|
|
1434
|
-
if (!err) return false;
|
|
1435
|
-
return err.retryable;
|
|
1436
|
-
}
|
|
1437
|
-
function isEphemeral(error) {
|
|
1438
|
-
if ((0, import_axios.isCancel)(error)) return false;
|
|
1439
|
-
if (!error.response) return true;
|
|
1440
|
-
return isErrorRetryable(error);
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
// src/entityStore/applyReactionLocally.ts
|
|
1444
|
-
var applyReactionLocally = (client, {
|
|
1445
|
-
enforceUnique = false,
|
|
1446
|
-
messageId,
|
|
1447
|
-
reaction,
|
|
1448
|
-
removed = false
|
|
1449
|
-
}) => {
|
|
1450
|
-
const store = client.messageStore;
|
|
1451
|
-
const user = client.user;
|
|
1452
|
-
const existing = store.get(messageId);
|
|
1453
|
-
if (!user || !existing) return;
|
|
1454
|
-
const now = /* @__PURE__ */ new Date();
|
|
1455
|
-
const reactionResponse = {
|
|
1456
|
-
...reaction,
|
|
1457
|
-
created_at: reaction.created_at ?? now,
|
|
1458
|
-
custom: reaction.custom ?? {},
|
|
1459
|
-
message_id: messageId,
|
|
1460
|
-
score: reaction.score ?? 1,
|
|
1461
|
-
type: reaction.type,
|
|
1462
|
-
updated_at: reaction.updated_at ?? now,
|
|
1463
|
-
user,
|
|
1464
|
-
user_id: user.id
|
|
1465
|
-
};
|
|
1466
|
-
const removedReactions = removed ? existing.own_reactions?.filter((r) => r.type === reactionResponse.type) ?? [] : enforceUnique ? existing.own_reactions ?? [] : [];
|
|
1467
|
-
const withCounts = removed ? messageWithReactionRemoved(existing, reactionResponse) : messageWithReactionAdded(existing, reactionResponse, enforceUnique);
|
|
1468
|
-
const own_reactions = computeOwnReactions({
|
|
1469
|
-
current: existing.own_reactions ?? [],
|
|
1470
|
-
enforceUnique,
|
|
1471
|
-
reaction: reactionResponse,
|
|
1472
|
-
removed,
|
|
1473
|
-
userId: user.id
|
|
1474
|
-
});
|
|
1475
|
-
store.upsert({ ...withCounts, own_reactions });
|
|
1476
|
-
store.flushSubscribers(messageId);
|
|
1477
|
-
const persisted = store.get(messageId);
|
|
1478
|
-
if (persisted) {
|
|
1479
|
-
client.offlineDb?.executeQuerySafely(
|
|
1480
|
-
(db) => removed ? db.deleteReaction({ message: persisted, reaction: reactionResponse }) : enforceUnique ? db.updateReaction({ message: persisted, reaction: reactionResponse }) : db.insertReaction({ message: persisted, reaction: reactionResponse }),
|
|
1481
|
-
{ method: "applyReactionLocally" }
|
|
1399
|
+
// src/offline-support/queueableOperations.ts
|
|
1400
|
+
var logger2 = chatLoggerSystem.getLogger("offline-db");
|
|
1401
|
+
var channelOf = (client, task, channel) => {
|
|
1402
|
+
if (channel) return channel;
|
|
1403
|
+
const { channelId, channelType } = task;
|
|
1404
|
+
if (!channelType || !channelId) {
|
|
1405
|
+
throw new Error(
|
|
1406
|
+
`Cannot replay a "${task.type}" task without a channel type and id (message: ${task.messageId}).`
|
|
1482
1407
|
);
|
|
1483
1408
|
}
|
|
1484
|
-
return ()
|
|
1485
|
-
if (!removed) {
|
|
1486
|
-
applyReactionLocally(client, {
|
|
1487
|
-
messageId,
|
|
1488
|
-
reaction: reactionResponse,
|
|
1489
|
-
removed: true
|
|
1490
|
-
});
|
|
1491
|
-
}
|
|
1492
|
-
for (const removedReaction of removedReactions) {
|
|
1493
|
-
applyReactionLocally(client, {
|
|
1494
|
-
messageId,
|
|
1495
|
-
reaction: removedReaction,
|
|
1496
|
-
removed: false
|
|
1497
|
-
});
|
|
1498
|
-
}
|
|
1499
|
-
};
|
|
1409
|
+
return client.channel(channelType, channelId);
|
|
1500
1410
|
};
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
/**
|
|
1554
|
-
* Sugar for atomic / ad-hoc consumers that watch a single id (e.g. a thread watching its parent
|
|
1555
|
-
* message). Fires `handler` immediately with the current value (mirroring
|
|
1556
|
-
* {@link StateStore.subscribe}) and on every subsequent change.
|
|
1557
|
-
*/
|
|
1558
|
-
subscribe(id, handler) {
|
|
1559
|
-
const subscriber = {
|
|
1560
|
-
onEntitiesChanged: () => handler(this.byId.get(id))
|
|
1561
|
-
};
|
|
1562
|
-
this.link(id, subscriber);
|
|
1563
|
-
handler(this.byId.get(id));
|
|
1564
|
-
return () => this.unlink(id, subscriber);
|
|
1411
|
+
var QUEUEABLE_OPERATIONS = {
|
|
1412
|
+
"create-draft": {
|
|
1413
|
+
logFailureAs: {
|
|
1414
|
+
message: "Creating the draft in the offline database failed.",
|
|
1415
|
+
method: "createDraft"
|
|
1416
|
+
},
|
|
1417
|
+
run: ({ channel, client, task }) => channelOf(client, task, channel)._createDraft(...task.payload)
|
|
1418
|
+
},
|
|
1419
|
+
"delete-draft": {
|
|
1420
|
+
logFailureAs: {
|
|
1421
|
+
message: "Deleting the draft from the offline database failed.",
|
|
1422
|
+
method: "deleteDraft"
|
|
1423
|
+
},
|
|
1424
|
+
run: ({ channel, client, task }) => channelOf(client, task, channel)._deleteDraft(...task.payload)
|
|
1425
|
+
},
|
|
1426
|
+
"delete-message": {
|
|
1427
|
+
logFailureAs: { message: "Deleting the message failed.", method: "deleteMessage" },
|
|
1428
|
+
run: ({ client, task }) => client._deleteMessage(...task.payload)
|
|
1429
|
+
},
|
|
1430
|
+
"delete-reaction": {
|
|
1431
|
+
logFailureAs: { message: "Deleting the reaction failed.", method: "deleteReaction" },
|
|
1432
|
+
run: ({ channel, client, task }) => channelOf(client, task, channel)._deleteReaction(...task.payload)
|
|
1433
|
+
},
|
|
1434
|
+
"send-message": {
|
|
1435
|
+
logFailureAs: { message: "Sending the message failed.", method: "sendMessage" },
|
|
1436
|
+
/**
|
|
1437
|
+
* A replayed send is the first time local state learns the message exists server-side: the
|
|
1438
|
+
* optimistic layer that would normally reconcile it belongs to a `MessageOperations` call that
|
|
1439
|
+
* finished (and failed) long ago.
|
|
1440
|
+
*/
|
|
1441
|
+
onReplay: ({ client, result, task }) => {
|
|
1442
|
+
const message = result?.message;
|
|
1443
|
+
if (!message) return;
|
|
1444
|
+
if (message.parent_id) {
|
|
1445
|
+
client.threads.threadsById[message.parent_id]?.upsertReplyLocally({
|
|
1446
|
+
message,
|
|
1447
|
+
timestampChanged: true
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
channelOf(client, task).messagePaginator.trackLastMessage(formatMessage(message));
|
|
1451
|
+
},
|
|
1452
|
+
run: ({ channel, client, task }) => channelOf(client, task, channel)._sendMessage(...task.payload)
|
|
1453
|
+
},
|
|
1454
|
+
"send-reaction": {
|
|
1455
|
+
logFailureAs: { message: "Sending the reaction failed.", method: "sendReaction" },
|
|
1456
|
+
run: ({ channel, client, task }) => channelOf(client, task, channel)._sendReaction(...task.payload)
|
|
1457
|
+
},
|
|
1458
|
+
"update-message": {
|
|
1459
|
+
logFailureAs: { message: "Updating the message failed.", method: "updateMessage" },
|
|
1460
|
+
run: ({ client, task }) => client._updateMessage(...task.payload)
|
|
1565
1461
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1462
|
+
};
|
|
1463
|
+
var queueOrRun = async ({
|
|
1464
|
+
channel,
|
|
1465
|
+
client,
|
|
1466
|
+
queue = true,
|
|
1467
|
+
task
|
|
1468
|
+
}) => {
|
|
1469
|
+
const { offlineDb } = client;
|
|
1470
|
+
if (offlineDb && queue) {
|
|
1573
1471
|
try {
|
|
1574
|
-
return
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
}
|
|
1580
|
-
/**
|
|
1581
|
-
* Immediately flushes any throttled/pending state publish on the subscribers of `id` (via
|
|
1582
|
-
* {@link EntityStoreSubscriber.flushState}). Called after an optimistic (local-user) write to
|
|
1583
|
-
* `id` so it renders without the throttle delay. Only that id's own subscribers are flushed — the
|
|
1584
|
-
* write touched no other id — and flushing a subscriber with nothing pending is a no-op.
|
|
1585
|
-
*/
|
|
1586
|
-
flushSubscribers(id) {
|
|
1587
|
-
const subscribers = this.subscribers.get(id);
|
|
1588
|
-
if (!subscribers) return;
|
|
1589
|
-
for (const subscriber of subscribers) subscriber.flushState?.();
|
|
1590
|
-
}
|
|
1591
|
-
/**
|
|
1592
|
-
* Records `id` as changed for every subscriber watching it, except `subscriber` — the one that
|
|
1593
|
-
* wrote it and is resposible for updating itself (see {@link EntityStore.upsert}).
|
|
1594
|
-
*/
|
|
1595
|
-
markDirty(id, subscriber) {
|
|
1596
|
-
const subscribers = this.subscribers.get(id);
|
|
1597
|
-
if (!subscribers) return;
|
|
1598
|
-
for (const other of subscribers) {
|
|
1599
|
-
if (other === subscriber) continue;
|
|
1600
|
-
let changed = this.pendingChanged.get(other);
|
|
1601
|
-
if (!changed) {
|
|
1602
|
-
changed = /* @__PURE__ */ new Set();
|
|
1603
|
-
this.pendingChanged.set(other, changed);
|
|
1604
|
-
}
|
|
1605
|
-
changed.add(id);
|
|
1472
|
+
return await offlineDb.queueTask({ task });
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
const { message, method } = QUEUEABLE_OPERATIONS[task.type].logFailureAs;
|
|
1475
|
+
const cid = task.channelType && task.channelId ? [`${task.channelType}:${task.channelId}`] : [];
|
|
1476
|
+
logger2.withExtraTags(method, ...cid).error(message, { error });
|
|
1606
1477
|
}
|
|
1607
1478
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1479
|
+
return await runQueueableOperation({ channel, client, task });
|
|
1480
|
+
};
|
|
1481
|
+
var runQueueableOperation = async ({
|
|
1482
|
+
channel,
|
|
1483
|
+
client,
|
|
1484
|
+
task
|
|
1485
|
+
}) => {
|
|
1486
|
+
const operation = QUEUEABLE_OPERATIONS[task.type];
|
|
1487
|
+
if (!operation) {
|
|
1488
|
+
throw new Error(
|
|
1489
|
+
`Tried to execute invalid pending task type (${task.type}) while synchronizing the database.`
|
|
1490
|
+
);
|
|
1618
1491
|
}
|
|
1492
|
+
return await operation.run({ channel, client, task });
|
|
1619
1493
|
};
|
|
1620
1494
|
|
|
1621
1495
|
// src/messageComposer/attachmentIdentity.ts
|
|
@@ -3802,6 +3676,55 @@ var TextComposer = class {
|
|
|
3802
3676
|
// --- END TEXT PROCESSING ----
|
|
3803
3677
|
};
|
|
3804
3678
|
|
|
3679
|
+
// src/errors.ts
|
|
3680
|
+
var import_axios = require("axios");
|
|
3681
|
+
var APIErrorCodes = {
|
|
3682
|
+
"-1": { name: "InternalSystemError", retryable: true },
|
|
3683
|
+
"2": { name: "AccessKeyError", retryable: false },
|
|
3684
|
+
"3": { name: "AuthenticationFailedError", retryable: true },
|
|
3685
|
+
"4": { name: "InputError", retryable: false },
|
|
3686
|
+
"6": { name: "DuplicateUsernameError", retryable: false },
|
|
3687
|
+
"9": { name: "RateLimitError", retryable: true },
|
|
3688
|
+
"16": { name: "DoesNotExistError", retryable: false },
|
|
3689
|
+
"17": { name: "NotAllowedError", retryable: false },
|
|
3690
|
+
"18": { name: "EventNotSupportedError", retryable: false },
|
|
3691
|
+
"19": { name: "ChannelFeatureNotSupportedError", retryable: false },
|
|
3692
|
+
"20": { name: "MessageTooLongError", retryable: false },
|
|
3693
|
+
"21": { name: "MultipleNestingLevelError", retryable: false },
|
|
3694
|
+
"22": { name: "PayloadTooBigError", retryable: false },
|
|
3695
|
+
"23": { name: "RequestTimeoutError", retryable: true },
|
|
3696
|
+
"24": { name: "MaxHeaderSizeExceededError", retryable: false },
|
|
3697
|
+
"40": { name: "AuthErrorTokenExpired", retryable: false },
|
|
3698
|
+
"41": { name: "AuthErrorTokenNotValidYet", retryable: false },
|
|
3699
|
+
"42": { name: "AuthErrorTokenUsedBeforeIssuedAt", retryable: false },
|
|
3700
|
+
"43": { name: "AuthErrorTokenSignatureInvalid", retryable: false },
|
|
3701
|
+
"44": { name: "CustomCommandEndpointMissingError", retryable: false },
|
|
3702
|
+
"45": { name: "CustomCommandEndpointCallError", retryable: true },
|
|
3703
|
+
"46": { name: "ConnectionIDNotFoundError", retryable: false },
|
|
3704
|
+
"60": { name: "CoolDownError", retryable: true },
|
|
3705
|
+
"69": { name: "ErrWrongRegion", retryable: false },
|
|
3706
|
+
"70": { name: "ErrQueryChannelPermissions", retryable: false },
|
|
3707
|
+
"71": { name: "ErrTooManyConnections", retryable: true },
|
|
3708
|
+
"99": { name: "AppSuspendedError", retryable: false }
|
|
3709
|
+
};
|
|
3710
|
+
function isAPIError(error) {
|
|
3711
|
+
return error.code !== void 0;
|
|
3712
|
+
}
|
|
3713
|
+
function isDoesNotExistError(error) {
|
|
3714
|
+
return isAPIError(error) && APIErrorCodes[`${error.code}`]?.name === "DoesNotExistError";
|
|
3715
|
+
}
|
|
3716
|
+
function isErrorRetryable(error) {
|
|
3717
|
+
if (!error.code) return false;
|
|
3718
|
+
const err = APIErrorCodes[`${error.code}`];
|
|
3719
|
+
if (!err) return false;
|
|
3720
|
+
return err.retryable;
|
|
3721
|
+
}
|
|
3722
|
+
function isEphemeral(error) {
|
|
3723
|
+
if ((0, import_axios.isCancel)(error)) return false;
|
|
3724
|
+
if (!error.response) return true;
|
|
3725
|
+
return isErrorRetryable(error);
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3805
3728
|
// src/search/BaseSearchSource.ts
|
|
3806
3729
|
var DEFAULT_SEARCH_SOURCE_OPTIONS = {
|
|
3807
3730
|
pageSize: 10,
|
|
@@ -4325,6 +4248,17 @@ function autoCompleteOp(value, query) {
|
|
|
4325
4248
|
return value.some((v) => typeof v === "string" && matchOneString(v));
|
|
4326
4249
|
return false;
|
|
4327
4250
|
}
|
|
4251
|
+
var LOGICAL_FILTER_OPERATORS = ["$and", "$or", "$nor"];
|
|
4252
|
+
var filterConstrainsField = (filters, field) => {
|
|
4253
|
+
if (!filters || typeof filters !== "object") return false;
|
|
4254
|
+
if (Array.isArray(filters))
|
|
4255
|
+
return filters.some((entry) => filterConstrainsField(entry, field));
|
|
4256
|
+
const node = filters;
|
|
4257
|
+
if (field in node) return true;
|
|
4258
|
+
return LOGICAL_FILTER_OPERATORS.some(
|
|
4259
|
+
(operator) => filterConstrainsField(node[operator], field)
|
|
4260
|
+
);
|
|
4261
|
+
};
|
|
4328
4262
|
|
|
4329
4263
|
// src/pagination/paginators/stateThrottling.ts
|
|
4330
4264
|
var isTestRunner = () => {
|
|
@@ -4334,8 +4268,127 @@ var isTestRunner = () => {
|
|
|
4334
4268
|
return false;
|
|
4335
4269
|
}
|
|
4336
4270
|
};
|
|
4337
|
-
var enabled = !isTestRunner();
|
|
4338
|
-
var isStateThrottlingEnabled = () => enabled;
|
|
4271
|
+
var enabled = !isTestRunner();
|
|
4272
|
+
var isStateThrottlingEnabled = () => enabled;
|
|
4273
|
+
|
|
4274
|
+
// src/entityStore/EntityStore.ts
|
|
4275
|
+
var EntityStore = class {
|
|
4276
|
+
constructor({ getEntityId }) {
|
|
4277
|
+
this.byId = /* @__PURE__ */ new Map();
|
|
4278
|
+
this.subscribers = /* @__PURE__ */ new Map();
|
|
4279
|
+
this.transactionDepth = 0;
|
|
4280
|
+
this.pendingChanged = /* @__PURE__ */ new Map();
|
|
4281
|
+
this.getEntityId = getEntityId;
|
|
4282
|
+
}
|
|
4283
|
+
// ---- reads ----
|
|
4284
|
+
get(id) {
|
|
4285
|
+
return typeof id === "string" ? this.byId.get(id) : void 0;
|
|
4286
|
+
}
|
|
4287
|
+
has(id) {
|
|
4288
|
+
return this.byId.has(id);
|
|
4289
|
+
}
|
|
4290
|
+
// ---- writes ----
|
|
4291
|
+
/**
|
|
4292
|
+
* Replaces the canonical copy of `entity` and notifies every subscriber watching its id —
|
|
4293
|
+
* except `subscriber`, the one that performed the write, which is expected to re-render itself
|
|
4294
|
+
* (the paginator that performed the write already emits its own window inline, so it must not be
|
|
4295
|
+
* notified a second time through the subscription).
|
|
4296
|
+
*/
|
|
4297
|
+
upsert(entity, subscriber) {
|
|
4298
|
+
const id = this.getEntityId(entity);
|
|
4299
|
+
const previous = this.byId.get(id);
|
|
4300
|
+
if (previous === entity) return;
|
|
4301
|
+
this.byId.set(id, entity);
|
|
4302
|
+
this.markDirty(id, subscriber);
|
|
4303
|
+
this.autoFlush();
|
|
4304
|
+
}
|
|
4305
|
+
// ---- subscription registry / refcount ----
|
|
4306
|
+
/** Registers `subscriber` for `id` (notification target + refcount). */
|
|
4307
|
+
link(id, subscriber) {
|
|
4308
|
+
let subscribers = this.subscribers.get(id);
|
|
4309
|
+
if (!subscribers) {
|
|
4310
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
4311
|
+
this.subscribers.set(id, subscribers);
|
|
4312
|
+
}
|
|
4313
|
+
subscribers.add(subscriber);
|
|
4314
|
+
}
|
|
4315
|
+
/** Drops `subscriber` from `id`; GCs the canonical copy when none remain. */
|
|
4316
|
+
unlink(id, subscriber) {
|
|
4317
|
+
const subscribers = this.subscribers.get(id);
|
|
4318
|
+
if (!subscribers) return;
|
|
4319
|
+
subscribers.delete(subscriber);
|
|
4320
|
+
if (subscribers.size === 0) {
|
|
4321
|
+
this.subscribers.delete(id);
|
|
4322
|
+
this.byId.delete(id);
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
/**
|
|
4326
|
+
* Sugar for atomic / ad-hoc consumers that watch a single id (e.g. a thread watching its parent
|
|
4327
|
+
* message). Fires `handler` immediately with the current value (mirroring
|
|
4328
|
+
* {@link StateStore.subscribe}) and on every subsequent change.
|
|
4329
|
+
*/
|
|
4330
|
+
subscribe(id, handler) {
|
|
4331
|
+
const subscriber = {
|
|
4332
|
+
onEntitiesChanged: () => handler(this.byId.get(id))
|
|
4333
|
+
};
|
|
4334
|
+
this.link(id, subscriber);
|
|
4335
|
+
handler(this.byId.get(id));
|
|
4336
|
+
return () => this.unlink(id, subscriber);
|
|
4337
|
+
}
|
|
4338
|
+
// ---- batching ----
|
|
4339
|
+
/**
|
|
4340
|
+
* Runs `fn`, coalescing all notifications produced by writes inside it into a single flush on
|
|
4341
|
+
* exit. Re-entrant: nested transactions flush only when the outermost exits.
|
|
4342
|
+
*/
|
|
4343
|
+
transaction(fn) {
|
|
4344
|
+
this.transactionDepth += 1;
|
|
4345
|
+
try {
|
|
4346
|
+
return fn();
|
|
4347
|
+
} finally {
|
|
4348
|
+
this.transactionDepth -= 1;
|
|
4349
|
+
if (this.transactionDepth === 0) this.flush();
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
/**
|
|
4353
|
+
* Immediately flushes any throttled/pending state publish on the subscribers of `id` (via
|
|
4354
|
+
* {@link EntityStoreSubscriber.flushState}). Called after an optimistic (local-user) write to
|
|
4355
|
+
* `id` so it renders without the throttle delay. Only that id's own subscribers are flushed — the
|
|
4356
|
+
* write touched no other id — and flushing a subscriber with nothing pending is a no-op.
|
|
4357
|
+
*/
|
|
4358
|
+
flushSubscribers(id) {
|
|
4359
|
+
const subscribers = this.subscribers.get(id);
|
|
4360
|
+
if (!subscribers) return;
|
|
4361
|
+
for (const subscriber of subscribers) subscriber.flushState?.();
|
|
4362
|
+
}
|
|
4363
|
+
/**
|
|
4364
|
+
* Records `id` as changed for every subscriber watching it, except `subscriber` — the one that
|
|
4365
|
+
* wrote it and is resposible for updating itself (see {@link EntityStore.upsert}).
|
|
4366
|
+
*/
|
|
4367
|
+
markDirty(id, subscriber) {
|
|
4368
|
+
const subscribers = this.subscribers.get(id);
|
|
4369
|
+
if (!subscribers) return;
|
|
4370
|
+
for (const other of subscribers) {
|
|
4371
|
+
if (other === subscriber) continue;
|
|
4372
|
+
let changed = this.pendingChanged.get(other);
|
|
4373
|
+
if (!changed) {
|
|
4374
|
+
changed = /* @__PURE__ */ new Set();
|
|
4375
|
+
this.pendingChanged.set(other, changed);
|
|
4376
|
+
}
|
|
4377
|
+
changed.add(id);
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
autoFlush() {
|
|
4381
|
+
if (this.transactionDepth === 0) this.flush();
|
|
4382
|
+
}
|
|
4383
|
+
flush() {
|
|
4384
|
+
if (this.pendingChanged.size === 0) return;
|
|
4385
|
+
const changedBySubscriber = this.pendingChanged;
|
|
4386
|
+
this.pendingChanged = /* @__PURE__ */ new Map();
|
|
4387
|
+
for (const [subscriber, changedIds] of changedBySubscriber) {
|
|
4388
|
+
subscriber.onEntitiesChanged({ changedIds });
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
};
|
|
4339
4392
|
|
|
4340
4393
|
// src/entityStore/StoreBackedItemIndex.ts
|
|
4341
4394
|
var NOOP_OWNER = { onEntitiesChanged: () => void 0 };
|
|
@@ -5750,6 +5803,7 @@ var BasePaginator = class {
|
|
|
5750
5803
|
const itemHasBeenRemoved = !!removedItemCoordinates?.state && removedItemCoordinates.state.currentIndex > -1;
|
|
5751
5804
|
this._itemIndex.setOne(ingestedItem);
|
|
5752
5805
|
if (!this.matchesFilter(ingestedItem)) {
|
|
5806
|
+
this.shrinkOffsetAfterRemoval(removedItemCoordinates);
|
|
5753
5807
|
if (!this.isWindowPublishSuspended && this.isStateThrottled && itemHasBeenRemoved)
|
|
5754
5808
|
this.scheduleWindowPublish();
|
|
5755
5809
|
return itemHasBeenRemoved;
|
|
@@ -5905,6 +5959,17 @@ var BasePaginator = class {
|
|
|
5905
5959
|
}
|
|
5906
5960
|
return result;
|
|
5907
5961
|
}
|
|
5962
|
+
/**
|
|
5963
|
+
* Keeps `offset` addressing the same position in the *server's* result set after an item left the
|
|
5964
|
+
* loaded window.
|
|
5965
|
+
*/
|
|
5966
|
+
shrinkOffsetAfterRemoval(coordinates) {
|
|
5967
|
+
if (this.isCursorPagination) return;
|
|
5968
|
+
if ((coordinates?.state?.currentIndex ?? -1) < 0) return;
|
|
5969
|
+
const { offset } = this.state.getLatestValue();
|
|
5970
|
+
if (typeof offset !== "number") return;
|
|
5971
|
+
this.state.partialNext({ offset: Math.max(0, offset - 1) });
|
|
5972
|
+
}
|
|
5908
5973
|
/**
|
|
5909
5974
|
* Meaning of location values
|
|
5910
5975
|
* - currentIndex === -1 could not be found
|
|
@@ -5923,6 +5988,7 @@ var BasePaginator = class {
|
|
|
5923
5988
|
if (!coords.state && !coords.interval) return noAction;
|
|
5924
5989
|
const result = this.removeItemAtCoordinates(coords);
|
|
5925
5990
|
this._itemIndex.remove(this.getItemId(item));
|
|
5991
|
+
this.shrinkOffsetAfterRemoval(result);
|
|
5926
5992
|
if (!this.isWindowPublishSuspended && this.isStateThrottled)
|
|
5927
5993
|
this.scheduleWindowPublish();
|
|
5928
5994
|
return result;
|
|
@@ -6651,17 +6717,7 @@ var ChannelPaginator = class extends BasePaginator {
|
|
|
6651
6717
|
}
|
|
6652
6718
|
matchesFilter(channel) {
|
|
6653
6719
|
const filters = this.buildMatchFilters();
|
|
6654
|
-
|
|
6655
|
-
const filterConstrainsHidden = (filters2) => {
|
|
6656
|
-
if (!filters2 || typeof filters2 !== "object") return false;
|
|
6657
|
-
if (Array.isArray(filters2)) return filters2.some(filterConstrainsHidden);
|
|
6658
|
-
const node = filters2;
|
|
6659
|
-
if ("hidden" in node) return true;
|
|
6660
|
-
return LOGICAL_FILTER_OPERATORS.some(
|
|
6661
|
-
(operator) => filterConstrainsHidden(node[operator])
|
|
6662
|
-
);
|
|
6663
|
-
};
|
|
6664
|
-
if (channel.data?.hidden && !filterConstrainsHidden(filters)) return false;
|
|
6720
|
+
if (channel.data?.hidden && !filterConstrainsField(filters, "hidden")) return false;
|
|
6665
6721
|
return itemMatchesFilter(channel, filters, {
|
|
6666
6722
|
resolvers: this._filterFieldToDataResolvers
|
|
6667
6723
|
});
|
|
@@ -7200,7 +7256,30 @@ var MessageIntervalPaginator = class extends BasePaginator {
|
|
|
7200
7256
|
*/
|
|
7201
7257
|
this.mergeNewestPage = (page, options) => {
|
|
7202
7258
|
const headInterval = this.itemIntervals[0];
|
|
7203
|
-
if (!headInterval?.isHead)
|
|
7259
|
+
if (!headInterval?.isHead) {
|
|
7260
|
+
const logicalHead = this.liveHeadLogical;
|
|
7261
|
+
const hasAnchoredHead = this.itemIntervals.some(
|
|
7262
|
+
(interval2) => !isLogicalInterval(interval2) && interval2.isHead
|
|
7263
|
+
);
|
|
7264
|
+
if (!logicalHead || hasAnchoredHead || !page?.length) return;
|
|
7265
|
+
const takeOverView = this.isActiveInterval(logicalHead);
|
|
7266
|
+
const anchored = this.ingestPage({ page, isHead: true, setActive: false });
|
|
7267
|
+
if (!anchored || !takeOverView) return;
|
|
7268
|
+
const hasMoreTail2 = !this.pageReachedChannelStart(page, options);
|
|
7269
|
+
anchored.hasMoreTail = hasMoreTail2;
|
|
7270
|
+
anchored.isTail = !hasMoreTail2;
|
|
7271
|
+
this.setActiveInterval(anchored, { updateState: false });
|
|
7272
|
+
this.state.partialNext({
|
|
7273
|
+
items: this.intervalToItems(anchored),
|
|
7274
|
+
hasMoreHead: false,
|
|
7275
|
+
hasMoreTail: hasMoreTail2,
|
|
7276
|
+
cursor: {
|
|
7277
|
+
headward: null,
|
|
7278
|
+
tailward: hasMoreTail2 ? anchored.itemIds[0] ?? null : null
|
|
7279
|
+
}
|
|
7280
|
+
});
|
|
7281
|
+
return;
|
|
7282
|
+
}
|
|
7204
7283
|
const wasAtChannelStart = !this.state.getLatestValue().hasMoreTail;
|
|
7205
7284
|
const preMergeLoadedCount = headInterval.itemIds.length;
|
|
7206
7285
|
if (!this.isActiveInterval(headInterval)) {
|
|
@@ -7529,7 +7608,7 @@ var MessageIntervalPaginator = class extends BasePaginator {
|
|
|
7529
7608
|
enforceUnique,
|
|
7530
7609
|
reaction,
|
|
7531
7610
|
removed,
|
|
7532
|
-
userId: this.channel.getClient().
|
|
7611
|
+
userId: this.channel.getClient().userId
|
|
7533
7612
|
});
|
|
7534
7613
|
this.ingestItem({ ...formatted, own_reactions });
|
|
7535
7614
|
};
|
|
@@ -7687,6 +7766,9 @@ var MessageIntervalPaginator = class extends BasePaginator {
|
|
|
7687
7766
|
};
|
|
7688
7767
|
const isJump = this.isJumpQueryShape(queryShape);
|
|
7689
7768
|
if (options?.reconcile && !isJump && typeof this.items !== "undefined") {
|
|
7769
|
+
if (this.state.getLatestValue().lastQueryError) {
|
|
7770
|
+
this.state.partialNext({ lastQueryError: void 0 });
|
|
7771
|
+
}
|
|
7690
7772
|
this.mergeNewestPage(messages, {
|
|
7691
7773
|
candidateIds: options.candidateIds,
|
|
7692
7774
|
requestedLimit: requestedPageSize
|
|
@@ -8525,7 +8607,7 @@ var ChannelMemberSearchSource = class extends BaseSearchSource {
|
|
|
8525
8607
|
};
|
|
8526
8608
|
|
|
8527
8609
|
// src/configuration/utils/applyInstanceConfiguration.ts
|
|
8528
|
-
var
|
|
8610
|
+
var logger3 = chatLoggerSystem.getLogger("instance-configuration");
|
|
8529
8611
|
var applyInstanceConfiguration = ({
|
|
8530
8612
|
alsoWatch,
|
|
8531
8613
|
applyConfig,
|
|
@@ -8534,7 +8616,7 @@ var applyInstanceConfiguration = ({
|
|
|
8534
8616
|
key,
|
|
8535
8617
|
reinitializeConfig
|
|
8536
8618
|
}) => {
|
|
8537
|
-
const scopedLogger2 =
|
|
8619
|
+
const scopedLogger2 = logger3.withExtraTags(key);
|
|
8538
8620
|
let tearDown = null;
|
|
8539
8621
|
const runTearDown = () => {
|
|
8540
8622
|
if (!tearDown) return;
|
|
@@ -10749,7 +10831,7 @@ var createTextComposerPreValidationMiddleware = (composer) => ({
|
|
|
10749
10831
|
});
|
|
10750
10832
|
|
|
10751
10833
|
// src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts
|
|
10752
|
-
var
|
|
10834
|
+
var logger4 = chatLoggerSystem.getLogger("text-composer");
|
|
10753
10835
|
var TextComposerMiddlewareExecutor = class extends MiddlewareExecutor {
|
|
10754
10836
|
constructor({ composer }) {
|
|
10755
10837
|
super();
|
|
@@ -10772,7 +10854,7 @@ var TextComposerMiddlewareExecutor = class extends MiddlewareExecutor {
|
|
|
10772
10854
|
});
|
|
10773
10855
|
const { query, searchSource } = result.state.suggestions ?? {};
|
|
10774
10856
|
searchSource?.search(query)?.catch(
|
|
10775
|
-
(error) =>
|
|
10857
|
+
(error) => logger4.withExtraTags("execute").error("Searching for suggestions failed.", { error })
|
|
10776
10858
|
);
|
|
10777
10859
|
return result;
|
|
10778
10860
|
}
|
|
@@ -11381,44 +11463,142 @@ var parseError = (error) => {
|
|
|
11381
11463
|
return stringError ? JSON.parse(stringError) : {};
|
|
11382
11464
|
};
|
|
11383
11465
|
var isAlreadyExistsError = (error, parsed) => parsed.code === 4 && error instanceof Error && error.message.includes("already exists");
|
|
11466
|
+
var isHardDelete = (options) => !!options?.hard;
|
|
11384
11467
|
var MessageOperationStatePolicy = class {
|
|
11385
11468
|
constructor(ctx) {
|
|
11386
11469
|
this.ctx = ctx;
|
|
11387
11470
|
}
|
|
11388
|
-
optimistic(
|
|
11389
|
-
|
|
11471
|
+
optimistic(kind, params) {
|
|
11472
|
+
const { localMessage } = params;
|
|
11473
|
+
const previous = this.ctx.get(localMessage.id);
|
|
11474
|
+
if (kind === "delete") {
|
|
11475
|
+
const deleteForMe = params.options?.delete_for_me;
|
|
11476
|
+
if (isHardDelete(params.options)) {
|
|
11477
|
+
this.ctx.remove(localMessage.id);
|
|
11478
|
+
this.ctx.purge(localMessage.id);
|
|
11479
|
+
return { previous };
|
|
11480
|
+
}
|
|
11481
|
+
if (!previous) return {};
|
|
11482
|
+
const applied2 = {
|
|
11483
|
+
...localMessage,
|
|
11484
|
+
deleted_at: /* @__PURE__ */ new Date(),
|
|
11485
|
+
type: "deleted",
|
|
11486
|
+
...deleteForMe ? { deleted_for_me: true } : {}
|
|
11487
|
+
};
|
|
11488
|
+
this.ctx.ingest(applied2);
|
|
11489
|
+
this.ctx.persist(applied2);
|
|
11490
|
+
return { applied: applied2, previous };
|
|
11491
|
+
}
|
|
11492
|
+
if (kind === "update") {
|
|
11493
|
+
const isFailed = localMessage.status === "failed";
|
|
11494
|
+
const editedAt = /* @__PURE__ */ new Date();
|
|
11495
|
+
const applied2 = {
|
|
11496
|
+
...localMessage,
|
|
11497
|
+
error: isFailed ? localMessage.error : void 0,
|
|
11498
|
+
updated_at: editedAt,
|
|
11499
|
+
// `message_text_updated_at` is what drives the "edited" indicator, so it has to appear
|
|
11500
|
+
// immediately — but only for a message the server has actually seen. A failed message never
|
|
11501
|
+
// received a server-confirmed text update and must not appear to have had one.
|
|
11502
|
+
...isFailed ? {} : { message_text_updated_at: editedAt }
|
|
11503
|
+
};
|
|
11504
|
+
this.ctx.ingest(applied2);
|
|
11505
|
+
this.ctx.persist(applied2);
|
|
11506
|
+
return { applied: applied2, previous };
|
|
11507
|
+
}
|
|
11508
|
+
const applied = {
|
|
11390
11509
|
...localMessage,
|
|
11391
11510
|
error: void 0,
|
|
11392
11511
|
status: !localMessage.status || localMessage.status === "failed" ? "sending" : localMessage.status
|
|
11393
|
-
}
|
|
11512
|
+
};
|
|
11513
|
+
this.ctx.ingest(applied);
|
|
11514
|
+
this.ctx.persist({ ...applied, status: "failed" });
|
|
11515
|
+
return { applied, previous };
|
|
11394
11516
|
}
|
|
11395
11517
|
success({
|
|
11518
|
+
kind,
|
|
11396
11519
|
messageFromResponse,
|
|
11397
|
-
messageId
|
|
11520
|
+
messageId,
|
|
11521
|
+
optimistic,
|
|
11522
|
+
options
|
|
11398
11523
|
}) {
|
|
11524
|
+
if (!messageFromResponse) return;
|
|
11399
11525
|
const formatted = formatMessage({ ...messageFromResponse, status: "received" });
|
|
11526
|
+
if (kind === "delete") {
|
|
11527
|
+
if (isHardDelete(options)) {
|
|
11528
|
+
this.ctx.remove(messageId);
|
|
11529
|
+
this.ctx.purge(messageId);
|
|
11530
|
+
return;
|
|
11531
|
+
}
|
|
11532
|
+
if (!this.ctx.get(messageId)) return;
|
|
11533
|
+
this.ctx.ingest(formatted);
|
|
11534
|
+
this.ctx.persist(formatted);
|
|
11535
|
+
return;
|
|
11536
|
+
}
|
|
11400
11537
|
const existing = this.ctx.get(messageId);
|
|
11538
|
+
const nothingWroteSinceOptimistic = !!optimistic?.applied && existing === optimistic.applied;
|
|
11401
11539
|
const serverNewer = !existing || formatted.updated_at.getTime() > existing.updated_at.getTime();
|
|
11402
11540
|
const serverSameOrNewer = !existing || formatted.updated_at.getTime() >= existing.updated_at.getTime();
|
|
11403
11541
|
const existingIsOurOptimisticSend = existing?.status === "sending";
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
|
|
11542
|
+
const applyServerCopy = nothingWroteSinceOptimistic || serverNewer || existingIsOurOptimisticSend && serverSameOrNewer;
|
|
11543
|
+
if (!applyServerCopy) return;
|
|
11544
|
+
this.ctx.ingest(formatted);
|
|
11545
|
+
this.ctx.persist(formatted);
|
|
11407
11546
|
}
|
|
11408
|
-
failure({
|
|
11547
|
+
async failure({
|
|
11409
11548
|
error,
|
|
11549
|
+
kind,
|
|
11410
11550
|
localMessage,
|
|
11411
|
-
messageId
|
|
11551
|
+
messageId,
|
|
11552
|
+
optimistic,
|
|
11553
|
+
options
|
|
11412
11554
|
}) {
|
|
11413
11555
|
const parsed = parseError(error);
|
|
11414
11556
|
if (isAlreadyExistsError(error, parsed)) {
|
|
11415
11557
|
const existing = this.ctx.get(messageId);
|
|
11416
11558
|
if (existing?.status === "sending") {
|
|
11417
|
-
|
|
11559
|
+
const received = { ...localMessage, status: "received" };
|
|
11560
|
+
this.ctx.ingest(received);
|
|
11561
|
+
this.ctx.persist(received);
|
|
11418
11562
|
}
|
|
11419
11563
|
return;
|
|
11420
11564
|
}
|
|
11421
|
-
|
|
11565
|
+
const queued = kind !== "send" && kind !== "retry" && await this.ctx.isQueued(messageId, [
|
|
11566
|
+
kind === "delete" ? "delete-message" : "update-message"
|
|
11567
|
+
]);
|
|
11568
|
+
if (kind === "delete") {
|
|
11569
|
+
if (!queued) this.revertDelete({ messageId, optimistic, options });
|
|
11570
|
+
return;
|
|
11571
|
+
}
|
|
11572
|
+
if (kind === "update") {
|
|
11573
|
+
if (queued) return;
|
|
11574
|
+
const failed2 = {
|
|
11575
|
+
...this.ctx.get(messageId) ?? localMessage,
|
|
11576
|
+
error: parsed,
|
|
11577
|
+
status: "failed"
|
|
11578
|
+
};
|
|
11579
|
+
this.ctx.ingest(failed2);
|
|
11580
|
+
this.ctx.persist(failed2);
|
|
11581
|
+
return;
|
|
11582
|
+
}
|
|
11583
|
+
const failed = { ...localMessage, error: parsed, status: "failed" };
|
|
11584
|
+
this.ctx.ingest(failed);
|
|
11585
|
+
this.ctx.persist(failed);
|
|
11586
|
+
}
|
|
11587
|
+
revertDelete({
|
|
11588
|
+
messageId,
|
|
11589
|
+
optimistic,
|
|
11590
|
+
options
|
|
11591
|
+
}) {
|
|
11592
|
+
const previous = optimistic?.previous;
|
|
11593
|
+
if (!previous) return;
|
|
11594
|
+
if (isHardDelete(options)) {
|
|
11595
|
+
this.ctx.ingest(previous);
|
|
11596
|
+
this.ctx.persist(previous);
|
|
11597
|
+
return;
|
|
11598
|
+
}
|
|
11599
|
+
if (optimistic?.applied && this.ctx.get(messageId) !== optimistic.applied) return;
|
|
11600
|
+
this.ctx.ingest(previous);
|
|
11601
|
+
this.ctx.persist(previous);
|
|
11422
11602
|
}
|
|
11423
11603
|
};
|
|
11424
11604
|
|
|
@@ -11431,7 +11611,14 @@ var MessageOperations = class {
|
|
|
11431
11611
|
constructor(ctx) {
|
|
11432
11612
|
this.failedSendCache = /* @__PURE__ */ new Map();
|
|
11433
11613
|
this.ctx = ctx;
|
|
11434
|
-
this.policy = new MessageOperationStatePolicy({
|
|
11614
|
+
this.policy = new MessageOperationStatePolicy({
|
|
11615
|
+
get: ctx.get,
|
|
11616
|
+
ingest: ctx.ingest,
|
|
11617
|
+
isQueued: ctx.isQueued,
|
|
11618
|
+
persist: ctx.persist,
|
|
11619
|
+
purge: ctx.purge,
|
|
11620
|
+
remove: ctx.remove
|
|
11621
|
+
});
|
|
11435
11622
|
this.configController = new ConfigController({
|
|
11436
11623
|
defaults: DEFAULT_MESSAGE_OPERATIONS_CONFIG
|
|
11437
11624
|
});
|
|
@@ -11505,14 +11692,32 @@ var MessageOperations = class {
|
|
|
11505
11692
|
clearCachedFailedSend(messageId) {
|
|
11506
11693
|
this.failedSendCache.delete(messageId);
|
|
11507
11694
|
}
|
|
11508
|
-
|
|
11695
|
+
/**
|
|
11696
|
+
* The shared lifecycle: apply the optimistic state, fire the request, then reconcile or record the
|
|
11697
|
+
* failure. `kind` is threaded through because the three operations want materially different state
|
|
11698
|
+
* transitions — see {@link MessageOperationStatePolicy}.
|
|
11699
|
+
*/
|
|
11700
|
+
async run(kind, params, doRequest) {
|
|
11509
11701
|
const messageId = params.localMessage.id;
|
|
11510
|
-
this.policy.optimistic(params
|
|
11702
|
+
const optimistic = this.policy.optimistic(kind, params);
|
|
11511
11703
|
try {
|
|
11512
|
-
const
|
|
11513
|
-
this.policy.success({
|
|
11704
|
+
const response = await doRequest(params);
|
|
11705
|
+
this.policy.success({
|
|
11706
|
+
kind,
|
|
11707
|
+
messageFromResponse: response?.message,
|
|
11708
|
+
messageId,
|
|
11709
|
+
optimistic,
|
|
11710
|
+
options: params.options
|
|
11711
|
+
});
|
|
11514
11712
|
} catch (e) {
|
|
11515
|
-
this.policy.failure({
|
|
11713
|
+
await this.policy.failure({
|
|
11714
|
+
error: e,
|
|
11715
|
+
kind,
|
|
11716
|
+
localMessage: params.localMessage,
|
|
11717
|
+
messageId,
|
|
11718
|
+
optimistic,
|
|
11719
|
+
options: params.options
|
|
11720
|
+
});
|
|
11516
11721
|
throw e;
|
|
11517
11722
|
}
|
|
11518
11723
|
}
|
|
@@ -11523,6 +11728,7 @@ var MessageOperations = class {
|
|
|
11523
11728
|
);
|
|
11524
11729
|
try {
|
|
11525
11730
|
await this.run(
|
|
11731
|
+
"send",
|
|
11526
11732
|
{ ...params, message: messageToSend },
|
|
11527
11733
|
requestFn ?? handlers.send ?? (async (p) => await this.ctx.defaults.send(p.message ?? messageToSend, p.options))
|
|
11528
11734
|
);
|
|
@@ -11547,6 +11753,7 @@ var MessageOperations = class {
|
|
|
11547
11753
|
const sendAsRetry = send ? (p) => send({ ...p }) : void 0;
|
|
11548
11754
|
try {
|
|
11549
11755
|
await this.run(
|
|
11756
|
+
"retry",
|
|
11550
11757
|
{
|
|
11551
11758
|
...params,
|
|
11552
11759
|
message: messageToSend,
|
|
@@ -11575,6 +11782,7 @@ var MessageOperations = class {
|
|
|
11575
11782
|
updateOptions.skip_push = params.options.skip_push;
|
|
11576
11783
|
}
|
|
11577
11784
|
return await this.run(
|
|
11785
|
+
"update",
|
|
11578
11786
|
params,
|
|
11579
11787
|
requestFn ?? handlers.update ?? (async (p) => await this.ctx.defaults.update(p.localMessage, updateOptions))
|
|
11580
11788
|
);
|
|
@@ -11582,11 +11790,140 @@ var MessageOperations = class {
|
|
|
11582
11790
|
async delete(params, requestFn) {
|
|
11583
11791
|
const handlers = this.ctx.handlers();
|
|
11584
11792
|
const doRequest = requestFn ?? handlers.delete ?? (async (p) => await this.ctx.defaults.delete(p.localMessage.id, p.options));
|
|
11585
|
-
|
|
11586
|
-
|
|
11793
|
+
return await this.run("delete", params, doRequest);
|
|
11794
|
+
}
|
|
11795
|
+
};
|
|
11796
|
+
|
|
11797
|
+
// src/entityStore/applyReactionLocally.ts
|
|
11798
|
+
var applyReactionLocally = (client, {
|
|
11799
|
+
enforceUnique = false,
|
|
11800
|
+
messageId,
|
|
11801
|
+
reaction,
|
|
11802
|
+
removed = false
|
|
11803
|
+
}) => {
|
|
11804
|
+
const store = client.messageStore;
|
|
11805
|
+
const user = client.user;
|
|
11806
|
+
const existing = store.get(messageId);
|
|
11807
|
+
if (!user || !existing) return;
|
|
11808
|
+
const now = /* @__PURE__ */ new Date();
|
|
11809
|
+
const reactionResponse = {
|
|
11810
|
+
...reaction,
|
|
11811
|
+
created_at: reaction.created_at ?? now,
|
|
11812
|
+
custom: reaction.custom ?? {},
|
|
11813
|
+
message_id: messageId,
|
|
11814
|
+
score: reaction.score ?? 1,
|
|
11815
|
+
type: reaction.type,
|
|
11816
|
+
updated_at: reaction.updated_at ?? now,
|
|
11817
|
+
user,
|
|
11818
|
+
user_id: user.id
|
|
11819
|
+
};
|
|
11820
|
+
const removedReactions = removed ? existing.own_reactions?.filter((r) => r.type === reactionResponse.type) ?? [] : enforceUnique ? existing.own_reactions ?? [] : [];
|
|
11821
|
+
const withCounts = removed ? messageWithReactionRemoved(existing, reactionResponse) : messageWithReactionAdded(existing, reactionResponse, enforceUnique);
|
|
11822
|
+
const own_reactions = computeOwnReactions({
|
|
11823
|
+
current: existing.own_reactions ?? [],
|
|
11824
|
+
enforceUnique,
|
|
11825
|
+
reaction: reactionResponse,
|
|
11826
|
+
removed,
|
|
11827
|
+
userId: user.id
|
|
11828
|
+
});
|
|
11829
|
+
store.upsert({ ...withCounts, own_reactions });
|
|
11830
|
+
store.flushSubscribers(messageId);
|
|
11831
|
+
const persisted = store.get(messageId);
|
|
11832
|
+
if (persisted) {
|
|
11833
|
+
client.offlineDb?.executeQuerySafely(
|
|
11834
|
+
(db) => removed ? db.deleteReaction({ message: persisted, reaction: reactionResponse }) : enforceUnique ? db.updateReaction({ message: persisted, reaction: reactionResponse }) : db.insertReaction({ message: persisted, reaction: reactionResponse }),
|
|
11835
|
+
{ method: "applyReactionLocally" }
|
|
11836
|
+
);
|
|
11837
|
+
}
|
|
11838
|
+
return () => {
|
|
11839
|
+
if (!removed) {
|
|
11840
|
+
applyReactionLocally(client, {
|
|
11841
|
+
messageId,
|
|
11842
|
+
reaction: reactionResponse,
|
|
11843
|
+
removed: true
|
|
11844
|
+
});
|
|
11845
|
+
}
|
|
11846
|
+
for (const removedReaction of removedReactions) {
|
|
11847
|
+
applyReactionLocally(client, {
|
|
11848
|
+
messageId,
|
|
11849
|
+
reaction: removedReaction,
|
|
11850
|
+
removed: false
|
|
11851
|
+
});
|
|
11852
|
+
}
|
|
11853
|
+
};
|
|
11854
|
+
};
|
|
11855
|
+
|
|
11856
|
+
// src/messageOperations/optimistic.ts
|
|
11857
|
+
var isQueuedForReplay = async (client, messageId, types) => {
|
|
11858
|
+
const { offlineDb } = client;
|
|
11859
|
+
if (!offlineDb?.state.getLatestValue().initialized) return false;
|
|
11860
|
+
const pending = await offlineDb.getPendingTasks({ messageId });
|
|
11861
|
+
return !!pending?.some((task) => types.includes(task.type));
|
|
11862
|
+
};
|
|
11863
|
+
var reconcileHeldMessage = (channel, message) => {
|
|
11864
|
+
if (!message) return;
|
|
11865
|
+
const { messageStore } = channel.getClient();
|
|
11866
|
+
if (!messageStore.has(message.id)) return;
|
|
11867
|
+
messageStore.upsert(formatMessage(message));
|
|
11868
|
+
};
|
|
11869
|
+
var addReactionOptimistically = async ({
|
|
11870
|
+
channel,
|
|
11871
|
+
messageId,
|
|
11872
|
+
options,
|
|
11873
|
+
reaction
|
|
11874
|
+
}) => {
|
|
11875
|
+
const client = channel.getClient();
|
|
11876
|
+
const undo = applyReactionLocally(client, {
|
|
11877
|
+
enforceUnique: options?.enforce_unique ?? false,
|
|
11878
|
+
messageId,
|
|
11879
|
+
reaction
|
|
11880
|
+
});
|
|
11881
|
+
try {
|
|
11882
|
+
const response = await channel.sendReaction({ id: messageId, reaction, ...options });
|
|
11883
|
+
reconcileHeldMessage(channel, response?.message);
|
|
11884
|
+
} catch (error) {
|
|
11885
|
+
if (!await isQueuedForReplay(client, messageId, ["send-reaction"])) undo?.();
|
|
11886
|
+
throw error;
|
|
11887
|
+
}
|
|
11888
|
+
};
|
|
11889
|
+
var deleteReactionOptimistically = async ({
|
|
11890
|
+
channel,
|
|
11891
|
+
messageId,
|
|
11892
|
+
type
|
|
11893
|
+
}) => {
|
|
11894
|
+
const client = channel.getClient();
|
|
11895
|
+
const undo = applyReactionLocally(client, {
|
|
11896
|
+
messageId,
|
|
11897
|
+
reaction: { type },
|
|
11898
|
+
removed: true
|
|
11899
|
+
});
|
|
11900
|
+
try {
|
|
11901
|
+
const response = await channel.deleteReaction({ id: messageId, type });
|
|
11902
|
+
reconcileHeldMessage(channel, response?.message);
|
|
11903
|
+
} catch (error) {
|
|
11904
|
+
if (!await isQueuedForReplay(client, messageId, ["delete-reaction"])) undo?.();
|
|
11905
|
+
throw error;
|
|
11587
11906
|
}
|
|
11588
11907
|
};
|
|
11589
11908
|
|
|
11909
|
+
// src/messageOperations/persistence.ts
|
|
11910
|
+
var createMessageOperationsPersistence = ({
|
|
11911
|
+
channel
|
|
11912
|
+
}) => ({
|
|
11913
|
+
persist: (message) => {
|
|
11914
|
+
channel.getClient().offlineDb?.executeQuerySafely(
|
|
11915
|
+
(db) => db.upsertMessageWithChannelGuard({ message: { ...message, cid: channel.cid } }),
|
|
11916
|
+
{ method: "messageOperations:persist" }
|
|
11917
|
+
);
|
|
11918
|
+
},
|
|
11919
|
+
purge: (id) => {
|
|
11920
|
+
channel.getClient().offlineDb?.executeQuerySafely((db) => db.hardDeleteMessage({ id }), {
|
|
11921
|
+
method: "messageOperations:purge"
|
|
11922
|
+
});
|
|
11923
|
+
},
|
|
11924
|
+
isQueued: (messageId, types) => isQueuedForReplay(channel.getClient(), messageId, types)
|
|
11925
|
+
});
|
|
11926
|
+
|
|
11590
11927
|
// src/configuration/utils/declarativeSlices.ts
|
|
11591
11928
|
var mergeDeclarativeSlice = (general, specific) => {
|
|
11592
11929
|
if (!general) return specific;
|
|
@@ -11625,23 +11962,60 @@ var Thread = class extends WithSubscriptions {
|
|
|
11625
11962
|
defaults: DEFAULT_THREAD_CONFIG
|
|
11626
11963
|
});
|
|
11627
11964
|
this.failedRepliesMap = /* @__PURE__ */ new Map();
|
|
11965
|
+
/** How many consumers have called `activate()` without a matching `deactivate()`. */
|
|
11966
|
+
this._activeRefCount = 0;
|
|
11967
|
+
/**
|
|
11968
|
+
* Declares that a consumer is displaying this thread (mirrors `channel.activate()`).
|
|
11969
|
+
*
|
|
11970
|
+
* `active` is also what {@link ConnectionRecoveryManager} filters on to decide which threads to
|
|
11971
|
+
* reload on reconnect, so an unbalanced `deactivate()` now costs more than a missed auto-read —
|
|
11972
|
+
* hence the refcount, matching `channel.activate()`. A thread held by more than one mount stays
|
|
11973
|
+
* active until the last holder releases it.
|
|
11974
|
+
*/
|
|
11628
11975
|
this.activate = () => {
|
|
11629
|
-
this.
|
|
11976
|
+
this._activeRefCount += 1;
|
|
11977
|
+
if (this._activeRefCount === 1) {
|
|
11978
|
+
this.state.partialNext({ active: true });
|
|
11979
|
+
}
|
|
11630
11980
|
};
|
|
11981
|
+
/**
|
|
11982
|
+
* Declares that a consumer has stopped displaying this thread (mirrors `channel.deactivate()`).
|
|
11983
|
+
* Only flips `active` back to `false` once the last holder deactivates.
|
|
11984
|
+
*/
|
|
11631
11985
|
this.deactivate = () => {
|
|
11632
|
-
this.
|
|
11986
|
+
if (this._activeRefCount === 0) return;
|
|
11987
|
+
this._activeRefCount -= 1;
|
|
11988
|
+
if (this._activeRefCount === 0) {
|
|
11989
|
+
this.state.partialNext({ active: false });
|
|
11990
|
+
}
|
|
11633
11991
|
};
|
|
11992
|
+
/**
|
|
11993
|
+
* Re-queries this thread's replies, sized to the loaded window, and reconciles them in place — the
|
|
11994
|
+
* thread analogue of {@link Channel.reload}, and what connection recovery calls for every active
|
|
11995
|
+
* thread.
|
|
11996
|
+
*
|
|
11997
|
+
* Preserves failed (unsent) replies. They are read out of the reply paginator rather than out of
|
|
11998
|
+
* `failedRepliesMap`, because that map is only written by `upsertReplyLocally`, whose callers are
|
|
11999
|
+
* this thread's own subscriptions and the offline-DB path keyed on `ThreadManager.threadsById` —
|
|
12000
|
+
* neither of which covers a thread constructed directly and never registered (the common path in
|
|
12001
|
+
* the React Native SDK, which resolves `threadsById[id] ?? new Thread(...)`). Reading the paginator
|
|
12002
|
+
* is true for managed and unmanaged threads alike. An overlap merge keeps them anyway (the
|
|
12003
|
+
* reconcile's provenance guard never prunes a non-server message); only a disjoint rebuild can drop
|
|
12004
|
+
* them, so any that actually fell out are re-ingested below.
|
|
12005
|
+
*
|
|
12006
|
+
* Rethrows, except for a not-found answer on a thread that never had replies (see the catch).
|
|
12007
|
+
*/
|
|
11634
12008
|
this.reload = async () => {
|
|
11635
12009
|
if (this.state.getLatestValue().isLoading) {
|
|
11636
12010
|
return;
|
|
11637
12011
|
}
|
|
11638
12012
|
this.state.partialNext({ isLoading: true });
|
|
11639
12013
|
try {
|
|
11640
|
-
const
|
|
11641
|
-
const
|
|
11642
|
-
const
|
|
11643
|
-
|
|
11644
|
-
);
|
|
12014
|
+
const loadedReplies = this.messagePaginator.items ?? [];
|
|
12015
|
+
const loadedReplyCount = loadedReplies.length;
|
|
12016
|
+
const requestedReplyLimit = Math.max(loadedReplyCount, this.messagePaginator.pageSize ?? 0) || void 0;
|
|
12017
|
+
const reconcileCandidateIds = new Set(loadedReplies.map((reply) => reply.id));
|
|
12018
|
+
const failedBefore = loadedReplies.filter((reply) => reply.status === "failed");
|
|
11645
12019
|
const thread = await this.client.getThreadAndHydrate(this.id, {
|
|
11646
12020
|
watch: true,
|
|
11647
12021
|
reply_limit: requestedReplyLimit
|
|
@@ -11652,6 +12026,22 @@ var Thread = class extends WithSubscriptions {
|
|
|
11652
12026
|
candidateIds: reconcileCandidateIds
|
|
11653
12027
|
}
|
|
11654
12028
|
});
|
|
12029
|
+
if (failedBefore.length) {
|
|
12030
|
+
const visible = new Set((this.messagePaginator.items ?? []).map((r) => r.id));
|
|
12031
|
+
this.messagePaginator.batch(
|
|
12032
|
+
() => {
|
|
12033
|
+
for (const failed of failedBefore) {
|
|
12034
|
+
if (!visible.has(failed.id)) this.messagePaginator.ingestItem(failed);
|
|
12035
|
+
}
|
|
12036
|
+
},
|
|
12037
|
+
{ coalesce: true }
|
|
12038
|
+
);
|
|
12039
|
+
}
|
|
12040
|
+
} catch (error) {
|
|
12041
|
+
const notFound = isDoesNotExistError(error) || error?.status === 404;
|
|
12042
|
+
const neverExisted = notFound && this.state.getLatestValue().replyCount === 0;
|
|
12043
|
+
if (neverExisted) return;
|
|
12044
|
+
throw error;
|
|
11655
12045
|
} finally {
|
|
11656
12046
|
this.state.partialNext({ isLoading: false });
|
|
11657
12047
|
}
|
|
@@ -11918,7 +12308,7 @@ var Thread = class extends WithSubscriptions {
|
|
|
11918
12308
|
enforceUnique: eventType === "reaction.updated",
|
|
11919
12309
|
reaction,
|
|
11920
12310
|
removed: eventType === "reaction.deleted",
|
|
11921
|
-
userId: this.client.
|
|
12311
|
+
userId: this.client.userId
|
|
11922
12312
|
});
|
|
11923
12313
|
this.updateParentMessageLocally({ message: { ...message, own_reactions } });
|
|
11924
12314
|
}
|
|
@@ -12127,11 +12517,26 @@ var Thread = class extends WithSubscriptions {
|
|
|
12127
12517
|
compositionContext: this
|
|
12128
12518
|
});
|
|
12129
12519
|
this.messageOperations = new MessageOperations({
|
|
12520
|
+
...createMessageOperationsPersistence({ channel: this.channel }),
|
|
12130
12521
|
ingest: (m) => {
|
|
12131
|
-
this.
|
|
12132
|
-
this.
|
|
12522
|
+
const store = this.channel.getClient().messageStore;
|
|
12523
|
+
if (this.messagePaginator.matchesFilter(m)) {
|
|
12524
|
+
this.messagePaginator.ingestItem(m);
|
|
12525
|
+
} else if (store.has(m.id)) {
|
|
12526
|
+
store.upsert(m);
|
|
12527
|
+
}
|
|
12528
|
+
store.flushSubscribers(m.id);
|
|
12529
|
+
},
|
|
12530
|
+
// Mirrors `ingest`'s routing: a message this paginator does not hold can still be held by the
|
|
12531
|
+
// client-global store (a thread parent, a message displayed by another collection), and the
|
|
12532
|
+
// policy uses this both for its freshness comparison and to decide whether there is anything to
|
|
12533
|
+
// update optimistically at all. Reading only the paginator would make those two disagree.
|
|
12534
|
+
get: (id) => this.messagePaginator.getItem(id) ?? this.channel.getClient().messageStore.get(id),
|
|
12535
|
+
remove: (id) => {
|
|
12536
|
+
this.messagePaginator.removeItem({ id });
|
|
12537
|
+
this.channel.messagePaginator.removeItem({ id });
|
|
12538
|
+
this.channel.pinnedMessagesPaginator.removeItem({ id });
|
|
12133
12539
|
},
|
|
12134
|
-
get: (id) => this.messagePaginator.getItem(id),
|
|
12135
12540
|
normalizeOutgoingMessage: (m) => ({
|
|
12136
12541
|
...m,
|
|
12137
12542
|
parent_id: this.id
|
|
@@ -12293,63 +12698,32 @@ var Thread = class extends WithSubscriptions {
|
|
|
12293
12698
|
);
|
|
12294
12699
|
}
|
|
12295
12700
|
/**
|
|
12296
|
-
* Adds a reaction to a reply with an optimistic local state update
|
|
12297
|
-
* {@link Channel.
|
|
12298
|
-
*
|
|
12299
|
-
*
|
|
12701
|
+
* Adds a reaction to a reply with an optimistic local state update — see
|
|
12702
|
+
* {@link addReactionOptimistically}, which `Channel` shares. The request routes through the parent
|
|
12703
|
+
* channel because reactions are channel-level, while the local write is addressed by message id and
|
|
12704
|
+
* so reaches a pure reply no channel collection holds.
|
|
12300
12705
|
*/
|
|
12301
12706
|
async addReactionWithLocalUpdate({
|
|
12302
12707
|
messageId,
|
|
12303
12708
|
reaction,
|
|
12304
12709
|
options
|
|
12305
12710
|
}) {
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
enforceUnique: options?.enforce_unique ?? false,
|
|
12711
|
+
await addReactionOptimistically({
|
|
12712
|
+
channel: this.channel,
|
|
12309
12713
|
messageId,
|
|
12714
|
+
options,
|
|
12310
12715
|
reaction
|
|
12311
12716
|
});
|
|
12312
|
-
try {
|
|
12313
|
-
const response = await this.channel.sendReaction({
|
|
12314
|
-
id: messageId,
|
|
12315
|
-
reaction,
|
|
12316
|
-
...options
|
|
12317
|
-
});
|
|
12318
|
-
if (response?.message && client.messageStore.has(response.message.id)) {
|
|
12319
|
-
client.messageStore.upsert(formatMessage(response.message));
|
|
12320
|
-
}
|
|
12321
|
-
} catch (error) {
|
|
12322
|
-
if (undo && (!client.offlineDb || !isEphemeral(error))) {
|
|
12323
|
-
undo();
|
|
12324
|
-
}
|
|
12325
|
-
throw error;
|
|
12326
|
-
}
|
|
12327
12717
|
}
|
|
12328
12718
|
/**
|
|
12329
|
-
* Removes the current user's reaction from a reply with an optimistic local state update
|
|
12330
|
-
*
|
|
12719
|
+
* Removes the current user's reaction from a reply with an optimistic local state update — see
|
|
12720
|
+
* {@link deleteReactionOptimistically}, which `Channel` shares.
|
|
12331
12721
|
*/
|
|
12332
12722
|
async deleteReactionWithLocalUpdate({
|
|
12333
12723
|
messageId,
|
|
12334
12724
|
type
|
|
12335
12725
|
}) {
|
|
12336
|
-
|
|
12337
|
-
const undo = applyReactionLocally(client, {
|
|
12338
|
-
messageId,
|
|
12339
|
-
reaction: { type },
|
|
12340
|
-
removed: true
|
|
12341
|
-
});
|
|
12342
|
-
try {
|
|
12343
|
-
const response = await this.channel.deleteReaction({ id: messageId, type });
|
|
12344
|
-
if (response?.message && client.messageStore.has(response.message.id)) {
|
|
12345
|
-
client.messageStore.upsert(formatMessage(response.message));
|
|
12346
|
-
}
|
|
12347
|
-
} catch (error) {
|
|
12348
|
-
if (undo && (!client.offlineDb || !isEphemeral(error))) {
|
|
12349
|
-
undo();
|
|
12350
|
-
}
|
|
12351
|
-
throw error;
|
|
12352
|
-
}
|
|
12726
|
+
await deleteReactionOptimistically({ channel: this.channel, messageId, type });
|
|
12353
12727
|
}
|
|
12354
12728
|
};
|
|
12355
12729
|
var normalizeThreadParticipants = (participants, channelCid) => {
|
|
@@ -12432,7 +12806,7 @@ var initState6 = (composition) => {
|
|
|
12432
12806
|
editedMessage
|
|
12433
12807
|
};
|
|
12434
12808
|
};
|
|
12435
|
-
var
|
|
12809
|
+
var logger5 = chatLoggerSystem.getLogger("message-composer");
|
|
12436
12810
|
var offlineDbLogger = chatLoggerSystem.getLogger("offline-db");
|
|
12437
12811
|
var _MessageComposer = class _MessageComposer extends WithSubscriptions {
|
|
12438
12812
|
// todo: mediaRecorder: MediaRecorderController;
|
|
@@ -12915,7 +13289,7 @@ var _MessageComposer = class _MessageComposer extends WithSubscriptions {
|
|
|
12915
13289
|
);
|
|
12916
13290
|
this.initState({ composition: draft });
|
|
12917
13291
|
} catch (error) {
|
|
12918
|
-
|
|
13292
|
+
logger5.withExtraTags("getDraft", this.channel.cid).error("Retrieving the draft from the server failed.", { error });
|
|
12919
13293
|
}
|
|
12920
13294
|
};
|
|
12921
13295
|
this.createPoll = async () => {
|
|
@@ -14249,8 +14623,7 @@ var ChannelApi = class {
|
|
|
14249
14623
|
};
|
|
14250
14624
|
|
|
14251
14625
|
// src/channel.ts
|
|
14252
|
-
var
|
|
14253
|
-
var offlineDbLogger2 = chatLoggerSystem.getLogger("offline-db");
|
|
14626
|
+
var logger6 = chatLoggerSystem.getLogger("channel");
|
|
14254
14627
|
var ownDeclarativeConfig = (slice) => {
|
|
14255
14628
|
if (!slice) return void 0;
|
|
14256
14629
|
const {
|
|
@@ -14390,11 +14763,31 @@ var Channel = class extends ChannelApi {
|
|
|
14390
14763
|
this.cooldownTimer = new CooldownTimer({ channel: this });
|
|
14391
14764
|
this.cooldownTimer.registerSubscriptions();
|
|
14392
14765
|
this.messageOperations = new MessageOperations({
|
|
14766
|
+
...createMessageOperationsPersistence({ channel: this }),
|
|
14393
14767
|
ingest: (m) => {
|
|
14394
|
-
this.
|
|
14395
|
-
this.
|
|
14768
|
+
const store = this.getClient().messageStore;
|
|
14769
|
+
if (this.messagePaginator.matchesFilter(m)) {
|
|
14770
|
+
this.messagePaginator.ingestItem(m);
|
|
14771
|
+
} else if (store.has(m.id)) {
|
|
14772
|
+
store.upsert(m);
|
|
14773
|
+
}
|
|
14774
|
+
store.flushSubscribers(m.id);
|
|
14775
|
+
},
|
|
14776
|
+
// Mirrors `ingest`'s routing: a message this paginator does not hold can still be held by the
|
|
14777
|
+
// client-global store (a thread parent, a message displayed by another collection), and the
|
|
14778
|
+
// policy uses this both for its freshness comparison and to decide whether there is anything to
|
|
14779
|
+
// update optimistically at all. Reading only the paginator would make those two disagree.
|
|
14780
|
+
get: (id2) => this.messagePaginator.getItem(id2) ?? this.getClient().messageStore.get(id2),
|
|
14781
|
+
remove: (id2) => {
|
|
14782
|
+
const parentId = this.messagePaginator.getItem(id2)?.parent_id ?? this.getClient().messageStore.get(id2)?.parent_id;
|
|
14783
|
+
this.messagePaginator.removeItem({ id: id2 });
|
|
14784
|
+
this.pinnedMessagesPaginator.removeItem({ id: id2 });
|
|
14785
|
+
if (parentId) {
|
|
14786
|
+
this.getClient().threads.threadsById[parentId]?.messagePaginator.removeItem({
|
|
14787
|
+
id: id2
|
|
14788
|
+
});
|
|
14789
|
+
}
|
|
14396
14790
|
},
|
|
14397
|
-
get: (id2) => this.messagePaginator.getItem(id2),
|
|
14398
14791
|
handlers: () => {
|
|
14399
14792
|
const { requestHandlers } = this.configState.getLatestValue();
|
|
14400
14793
|
const deleteMessageRequest = requestHandlers?.deleteMessageRequest;
|
|
@@ -14600,24 +14993,20 @@ var Channel = class extends ChannelApi {
|
|
|
14600
14993
|
*/
|
|
14601
14994
|
async sendMessage(...args) {
|
|
14602
14995
|
const [request] = args;
|
|
14603
|
-
|
|
14604
|
-
|
|
14605
|
-
|
|
14606
|
-
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
});
|
|
14996
|
+
const messageId = request.message?.id;
|
|
14997
|
+
return await queueOrRun({
|
|
14998
|
+
channel: this,
|
|
14999
|
+
client: this.getClient(),
|
|
15000
|
+
// Nothing to key a queue entry on without a message id, so it runs but is not queued.
|
|
15001
|
+
queue: !!messageId,
|
|
15002
|
+
task: {
|
|
15003
|
+
channelId: this.id,
|
|
15004
|
+
channelType: this.type,
|
|
15005
|
+
messageId,
|
|
15006
|
+
payload: args,
|
|
15007
|
+
type: "send-message"
|
|
14616
15008
|
}
|
|
14617
|
-
}
|
|
14618
|
-
offlineDbLogger2.withExtraTags("sendMessage", this.cid).error("Sending the message failed.", { error });
|
|
14619
|
-
}
|
|
14620
|
-
return await this._sendMessage(...args);
|
|
15009
|
+
});
|
|
14621
15010
|
}
|
|
14622
15011
|
/**
|
|
14623
15012
|
* Sends a message with optimistic local state update.
|
|
@@ -14670,59 +15059,24 @@ var Channel = class extends ChannelApi {
|
|
|
14670
15059
|
);
|
|
14671
15060
|
}
|
|
14672
15061
|
/**
|
|
14673
|
-
* Adds a reaction with an optimistic local state update
|
|
14674
|
-
* message immediately ({@link applyReactionLocally}), then the request is
|
|
14675
|
-
* fired via {@link Channel.sendReaction} (which owns the offline-DB write + queue). The
|
|
14676
|
-
* server-authoritative counts reconcile on the response; the message is rolled back on failure.
|
|
15062
|
+
* Adds a reaction with an optimistic local state update - see {@link addReactionOptimistically}.
|
|
14677
15063
|
*/
|
|
14678
15064
|
async addReactionWithLocalUpdate({
|
|
14679
15065
|
messageId,
|
|
14680
15066
|
reaction,
|
|
14681
15067
|
options
|
|
14682
15068
|
}) {
|
|
14683
|
-
|
|
14684
|
-
const undo = applyReactionLocally(client, {
|
|
14685
|
-
enforceUnique: options?.enforce_unique ?? false,
|
|
14686
|
-
messageId,
|
|
14687
|
-
reaction
|
|
14688
|
-
});
|
|
14689
|
-
try {
|
|
14690
|
-
const response = await this.sendReaction({ id: messageId, reaction, ...options });
|
|
14691
|
-
if (response?.message && client.messageStore.has(response.message.id)) {
|
|
14692
|
-
client.messageStore.upsert(formatMessage(response.message));
|
|
14693
|
-
}
|
|
14694
|
-
} catch (error) {
|
|
14695
|
-
if (undo && (!client.offlineDb || !isEphemeral(error))) {
|
|
14696
|
-
undo();
|
|
14697
|
-
}
|
|
14698
|
-
throw error;
|
|
14699
|
-
}
|
|
15069
|
+
await addReactionOptimistically({ channel: this, messageId, options, reaction });
|
|
14700
15070
|
}
|
|
14701
15071
|
/**
|
|
14702
|
-
* Removes the current user's reaction with an optimistic local state update
|
|
14703
|
-
* {@link
|
|
15072
|
+
* Removes the current user's reaction with an optimistic local state update - see
|
|
15073
|
+
* {@link deleteReactionOptimistically}.
|
|
14704
15074
|
*/
|
|
14705
15075
|
async deleteReactionWithLocalUpdate({
|
|
14706
15076
|
messageId,
|
|
14707
15077
|
type
|
|
14708
15078
|
}) {
|
|
14709
|
-
|
|
14710
|
-
const undo = applyReactionLocally(client, {
|
|
14711
|
-
messageId,
|
|
14712
|
-
reaction: { type },
|
|
14713
|
-
removed: true
|
|
14714
|
-
});
|
|
14715
|
-
try {
|
|
14716
|
-
const response = await this.deleteReaction({ id: messageId, type });
|
|
14717
|
-
if (response?.message && client.messageStore.has(response.message.id)) {
|
|
14718
|
-
client.messageStore.upsert(formatMessage(response.message));
|
|
14719
|
-
}
|
|
14720
|
-
} catch (error) {
|
|
14721
|
-
if (undo && (!client.offlineDb || !isEphemeral(error))) {
|
|
14722
|
-
undo();
|
|
14723
|
-
}
|
|
14724
|
-
throw error;
|
|
14725
|
-
}
|
|
15079
|
+
await deleteReactionOptimistically({ channel: this, messageId, type });
|
|
14726
15080
|
}
|
|
14727
15081
|
/**
|
|
14728
15082
|
* Upload a file to this channel's file endpoint (multipart).
|
|
@@ -14817,23 +15171,17 @@ var Channel = class extends ChannelApi {
|
|
|
14817
15171
|
*/
|
|
14818
15172
|
async sendReaction(...args) {
|
|
14819
15173
|
const [{ id: messageId }] = args;
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
type: "send-reaction"
|
|
14830
|
-
}
|
|
14831
|
-
});
|
|
15174
|
+
return await queueOrRun({
|
|
15175
|
+
channel: this,
|
|
15176
|
+
client: this.getClient(),
|
|
15177
|
+
task: {
|
|
15178
|
+
channelId: this.id,
|
|
15179
|
+
channelType: this.type,
|
|
15180
|
+
messageId,
|
|
15181
|
+
payload: args,
|
|
15182
|
+
type: "send-reaction"
|
|
14832
15183
|
}
|
|
14833
|
-
}
|
|
14834
|
-
offlineDbLogger2.withExtraTags("sendReaction", this.cid).error("Sending the reaction failed.", { error });
|
|
14835
|
-
}
|
|
14836
|
-
return this._sendReaction(...args);
|
|
15184
|
+
});
|
|
14837
15185
|
}
|
|
14838
15186
|
_sendReaction(...args) {
|
|
14839
15187
|
return this.getClient().sendReaction(...args);
|
|
@@ -14841,23 +15189,17 @@ var Channel = class extends ChannelApi {
|
|
|
14841
15189
|
async deleteReaction(...args) {
|
|
14842
15190
|
this._checkInitialized();
|
|
14843
15191
|
const [request] = args;
|
|
14844
|
-
|
|
14845
|
-
|
|
14846
|
-
|
|
14847
|
-
|
|
14848
|
-
|
|
14849
|
-
|
|
14850
|
-
|
|
14851
|
-
|
|
14852
|
-
|
|
14853
|
-
type: "delete-reaction"
|
|
14854
|
-
}
|
|
14855
|
-
});
|
|
15192
|
+
return await queueOrRun({
|
|
15193
|
+
channel: this,
|
|
15194
|
+
client: this.getClient(),
|
|
15195
|
+
task: {
|
|
15196
|
+
channelId: this.id,
|
|
15197
|
+
channelType: this.type,
|
|
15198
|
+
messageId: request.id,
|
|
15199
|
+
payload: args,
|
|
15200
|
+
type: "delete-reaction"
|
|
14856
15201
|
}
|
|
14857
|
-
}
|
|
14858
|
-
offlineDbLogger2.withExtraTags("deleteReaction", this.cid).error("Deleting the reaction failed.", { error });
|
|
14859
|
-
}
|
|
14860
|
-
return await this._deleteReaction(...args);
|
|
15202
|
+
});
|
|
14861
15203
|
}
|
|
14862
15204
|
/**
|
|
14863
15205
|
* Deletes a reaction by user and type.
|
|
@@ -15547,7 +15889,7 @@ var Channel = class extends ChannelApi {
|
|
|
15547
15889
|
const previousData = this.data;
|
|
15548
15890
|
this.data = state.channel;
|
|
15549
15891
|
this.state.syncStateFromChannelData(this.data, previousData);
|
|
15550
|
-
|
|
15892
|
+
logger6.withExtraTags("watch", this.cid).info("Started watching the channel.");
|
|
15551
15893
|
return state;
|
|
15552
15894
|
}
|
|
15553
15895
|
/**
|
|
@@ -15559,13 +15901,17 @@ var Channel = class extends ChannelApi {
|
|
|
15559
15901
|
* This is intentionally thin: it only re-issues `watch()` with a limit sized to the loaded window
|
|
15560
15902
|
* (`items.length`, so the whole loaded window is refreshed — not the smaller channel-list page). The
|
|
15561
15903
|
* actual fold + destructive reconciliation happens inside `query()` → `seedFirstPageSync`
|
|
15562
|
-
* (the same path the channel-list re-hydrate and
|
|
15904
|
+
* (the same path the channel-list re-hydrate and connection recovery use), driven by the loaded-id
|
|
15563
15905
|
* snapshot `query()` captures before its await. Owning that single path is what lets the SDK stop
|
|
15564
15906
|
* passing the reconciliation window/snapshot itself.
|
|
15565
15907
|
*
|
|
15566
15908
|
* Preserves failed (unsent) messages: an overlap merge keeps them (the reconcile's provenance guard
|
|
15567
15909
|
* never prunes a non-server message); only a disjoint rebuild can drop them, so any that actually
|
|
15568
|
-
* fell out are re-ingested below.
|
|
15910
|
+
* fell out are re-ingested below. The offline DB is consulted alongside the in-memory window, because
|
|
15911
|
+
* a failed message can be absent from memory and still be the user's unsent work — a cold boot starts
|
|
15912
|
+
* with an empty paginator, and an eviction can drop it mid-session. v9 got this from a second, lagging
|
|
15913
|
+
* copy of the list (the React SDK's own state); with one reactive source of truth the persisted row is
|
|
15914
|
+
* that buffer.
|
|
15569
15915
|
*/
|
|
15570
15916
|
async reload() {
|
|
15571
15917
|
if (this._reloading || !this.initialized && !this.offlineMode) return;
|
|
@@ -15573,18 +15919,28 @@ var Channel = class extends ChannelApi {
|
|
|
15573
15919
|
try {
|
|
15574
15920
|
const paginator = this.messagePaginator;
|
|
15575
15921
|
const headItems = paginator.headItems;
|
|
15576
|
-
const requestedLimit = headItems.length
|
|
15577
|
-
const
|
|
15922
|
+
const requestedLimit = Math.max(headItems.length, paginator.pageSize ?? 0) || void 0;
|
|
15923
|
+
const offlineDb = this.getClient().offlineDb;
|
|
15924
|
+
const failedInMemory = headItems.filter((message) => message.status === "failed");
|
|
15925
|
+
const failedInDb = offlineDb ? await offlineDb.getFailedMessages({ cid: this.cid }) : [];
|
|
15926
|
+
const failedById = /* @__PURE__ */ new Map();
|
|
15927
|
+
for (const message of [...failedInDb, ...failedInMemory]) {
|
|
15928
|
+
failedById.set(message.id, message);
|
|
15929
|
+
}
|
|
15930
|
+
const failedBefore = [...failedById.values()];
|
|
15578
15931
|
await this.watch({ messages: { limit: requestedLimit } });
|
|
15579
15932
|
this.offlineMode = false;
|
|
15580
|
-
|
|
15581
|
-
() =>
|
|
15582
|
-
|
|
15583
|
-
|
|
15584
|
-
|
|
15585
|
-
|
|
15586
|
-
|
|
15587
|
-
|
|
15933
|
+
if (failedBefore.length) {
|
|
15934
|
+
const visible = new Set((paginator.items ?? []).map((message) => message.id));
|
|
15935
|
+
paginator.batch(
|
|
15936
|
+
() => {
|
|
15937
|
+
for (const failed of failedBefore) {
|
|
15938
|
+
if (!visible.has(failed.id)) paginator.ingestItem(failed);
|
|
15939
|
+
}
|
|
15940
|
+
},
|
|
15941
|
+
{ coalesce: true }
|
|
15942
|
+
);
|
|
15943
|
+
}
|
|
15588
15944
|
} finally {
|
|
15589
15945
|
this._reloading = false;
|
|
15590
15946
|
}
|
|
@@ -15600,7 +15956,7 @@ var Channel = class extends ChannelApi {
|
|
|
15600
15956
|
async stopWatching(...args) {
|
|
15601
15957
|
const response = await super.stopWatching(...args);
|
|
15602
15958
|
this.watchStatus = ChannelWatchStatus.NotWatching;
|
|
15603
|
-
|
|
15959
|
+
logger6.withExtraTags("stopWatching", this.cid).info("Stopped watching the channel.");
|
|
15604
15960
|
return response;
|
|
15605
15961
|
}
|
|
15606
15962
|
/**
|
|
@@ -15848,23 +16204,17 @@ var Channel = class extends ChannelApi {
|
|
|
15848
16204
|
*/
|
|
15849
16205
|
async createDraft(...args) {
|
|
15850
16206
|
const [request] = args;
|
|
15851
|
-
|
|
15852
|
-
|
|
15853
|
-
|
|
15854
|
-
|
|
15855
|
-
|
|
15856
|
-
|
|
15857
|
-
|
|
15858
|
-
|
|
15859
|
-
|
|
15860
|
-
type: "create-draft"
|
|
15861
|
-
}
|
|
15862
|
-
});
|
|
16207
|
+
return await queueOrRun({
|
|
16208
|
+
channel: this,
|
|
16209
|
+
client: this.getClient(),
|
|
16210
|
+
task: {
|
|
16211
|
+
channelId: this.id,
|
|
16212
|
+
channelType: this.type,
|
|
16213
|
+
threadId: request.message?.parent_id,
|
|
16214
|
+
payload: args,
|
|
16215
|
+
type: "create-draft"
|
|
15863
16216
|
}
|
|
15864
|
-
}
|
|
15865
|
-
offlineDbLogger2.withExtraTags("createDraft", this.cid).error("Creating the draft in the offline database failed.", { error });
|
|
15866
|
-
}
|
|
15867
|
-
return this._createDraft(...args);
|
|
16217
|
+
});
|
|
15868
16218
|
}
|
|
15869
16219
|
async _deleteDraft(...args) {
|
|
15870
16220
|
return await super.deleteDraft(...args);
|
|
@@ -15875,38 +16225,30 @@ var Channel = class extends ChannelApi {
|
|
|
15875
16225
|
*/
|
|
15876
16226
|
async deleteDraft(...args) {
|
|
15877
16227
|
const [request] = args;
|
|
15878
|
-
|
|
15879
|
-
|
|
15880
|
-
|
|
15881
|
-
|
|
15882
|
-
|
|
15883
|
-
|
|
15884
|
-
|
|
15885
|
-
|
|
15886
|
-
|
|
15887
|
-
payload: args,
|
|
15888
|
-
type: "delete-draft"
|
|
15889
|
-
}
|
|
15890
|
-
}
|
|
15891
|
-
);
|
|
16228
|
+
return await queueOrRun({
|
|
16229
|
+
channel: this,
|
|
16230
|
+
client: this.getClient(),
|
|
16231
|
+
task: {
|
|
16232
|
+
channelId: this.id,
|
|
16233
|
+
channelType: this.type,
|
|
16234
|
+
threadId: request?.parent_id,
|
|
16235
|
+
payload: args,
|
|
16236
|
+
type: "delete-draft"
|
|
15892
16237
|
}
|
|
15893
|
-
}
|
|
15894
|
-
offlineDbLogger2.withExtraTags("deleteDraft", this.cid).error("Deleting the draft from the offline database failed.", { error });
|
|
15895
|
-
}
|
|
15896
|
-
return this._deleteDraft(...args);
|
|
16238
|
+
});
|
|
15897
16239
|
}
|
|
15898
16240
|
on(callbackOrString, callbackOrNothing) {
|
|
15899
16241
|
const key = callbackOrNothing ? callbackOrString : "all";
|
|
15900
16242
|
const callback = callbackOrNothing ? callbackOrNothing : callbackOrString;
|
|
15901
16243
|
const set = this.listeners.get(key) ?? /* @__PURE__ */ new Set();
|
|
15902
|
-
|
|
16244
|
+
logger6.withExtraTags("on", this.cid).debug(`Attaching a listener for the "${key}" event.`);
|
|
15903
16245
|
set.add(callback);
|
|
15904
16246
|
if (!this.listeners.has(key)) {
|
|
15905
16247
|
this.listeners.set(key, set);
|
|
15906
16248
|
}
|
|
15907
16249
|
return {
|
|
15908
16250
|
unsubscribe: () => {
|
|
15909
|
-
|
|
16251
|
+
logger6.withExtraTags("on", this.cid).debug(`Removing the listener for the "${key}" event.`);
|
|
15910
16252
|
set.delete(callback);
|
|
15911
16253
|
if (!set.size) {
|
|
15912
16254
|
this.listeners.delete(key);
|
|
@@ -15917,7 +16259,7 @@ var Channel = class extends ChannelApi {
|
|
|
15917
16259
|
off(callbackOrString, callbackOrNothing) {
|
|
15918
16260
|
const key = callbackOrNothing ? callbackOrString : "all";
|
|
15919
16261
|
const callback = callbackOrNothing ? callbackOrNothing : callbackOrString;
|
|
15920
|
-
|
|
16262
|
+
logger6.withExtraTags("off", this.cid).debug(`Removing the listener for the "${key}" event.`);
|
|
15921
16263
|
const set = this.listeners.get(key);
|
|
15922
16264
|
set?.delete(callback);
|
|
15923
16265
|
if (!set?.size) {
|
|
@@ -15961,23 +16303,25 @@ var Channel = class extends ChannelApi {
|
|
|
15961
16303
|
* `state.unreadCount` derives from — so channel-wide resets (`channel.truncated`, "all channels
|
|
15962
16304
|
* read") must route through here rather than writing a count of their own.
|
|
15963
16305
|
*/
|
|
16306
|
+
/** @returns whether the count actually changed, so callers can react only to the channels it did. */
|
|
15964
16307
|
_setOwnUnreadCount(unreadCount) {
|
|
15965
|
-
if (this.pendingDisposal) return;
|
|
16308
|
+
if (this.pendingDisposal) return false;
|
|
15966
16309
|
const userId = this.getClient().userId;
|
|
15967
|
-
if (!userId) return;
|
|
16310
|
+
if (!userId) return false;
|
|
15968
16311
|
const currentUserReadState = this.state.read[userId];
|
|
15969
16312
|
if (!currentUserReadState || currentUserReadState.unread_messages === unreadCount) {
|
|
15970
|
-
return;
|
|
16313
|
+
return false;
|
|
15971
16314
|
}
|
|
15972
16315
|
this._upsertReadState(
|
|
15973
16316
|
userId,
|
|
15974
16317
|
() => ({ ...currentUserReadState, unread_messages: unreadCount }),
|
|
15975
16318
|
{ changedUserIds: [userId] }
|
|
15976
16319
|
);
|
|
16320
|
+
return true;
|
|
15977
16321
|
}
|
|
15978
16322
|
_handleChannelEvent(event) {
|
|
15979
16323
|
const channel = this;
|
|
15980
|
-
|
|
16324
|
+
logger6.withExtraTags("_handleChannelEvent", this.cid).debug(`Received an event of type "${event.type}".`, { event });
|
|
15981
16325
|
const channelState = channel.state;
|
|
15982
16326
|
switch (event.type) {
|
|
15983
16327
|
case "typing.start":
|
|
@@ -16001,13 +16345,14 @@ var Channel = class extends ChannelApi {
|
|
|
16001
16345
|
case "ai_indicator.stop":
|
|
16002
16346
|
channelState.partialNext({ aiState: AIStates.Stop });
|
|
16003
16347
|
break;
|
|
16004
|
-
// `
|
|
16005
|
-
//
|
|
16006
|
-
//
|
|
16007
|
-
|
|
16348
|
+
// `notification.mark_read` is the only read signal for a channel the user is not watching, since
|
|
16349
|
+
// `message.read` goes to watchers only. `message.read_locally` is the local stand-in when read
|
|
16350
|
+
// events are disabled (livestreams). Both reuse the `message.read` logic, minus its delivery sync.
|
|
16351
|
+
case "notification.mark_read":
|
|
16008
16352
|
case "message.read_locally":
|
|
16009
16353
|
case "message.read":
|
|
16010
|
-
if (event.user?.id && event.created_at
|
|
16354
|
+
if (event.user?.id && event.created_at && // the same event announces a thread read, which says nothing about the channel
|
|
16355
|
+
!(event.type === "notification.mark_read" && event.thread_id)) {
|
|
16011
16356
|
const eventUser = event.user;
|
|
16012
16357
|
const readAtDate = new Date(event.created_at);
|
|
16013
16358
|
const toDate = (value) => value ? value instanceof Date ? value : new Date(value) : void 0;
|
|
@@ -16495,7 +16840,7 @@ var Channel = class extends ChannelApi {
|
|
|
16495
16840
|
}
|
|
16496
16841
|
}
|
|
16497
16842
|
_disconnect() {
|
|
16498
|
-
|
|
16843
|
+
logger6.withExtraTags("_disconnect", this.cid).info("Disconnecting the channel.");
|
|
16499
16844
|
this.unsubscribeConfiguration?.();
|
|
16500
16845
|
this.unsubscribeConfiguration = void 0;
|
|
16501
16846
|
this.unsubscribeServerConfig?.();
|
|
@@ -18614,7 +18959,7 @@ var decodeWSEvent = (data) => {
|
|
|
18614
18959
|
};
|
|
18615
18960
|
|
|
18616
18961
|
// src/connection.ts
|
|
18617
|
-
var
|
|
18962
|
+
var logger7 = chatLoggerSystem.getLogger("connection");
|
|
18618
18963
|
var decodeConnectionEvent = (data) => data.type === "connection.ok" ? decoders.HealthCheckEvent(data) : decodeWSEvent(data);
|
|
18619
18964
|
var isCloseEvent = (res) => res.code !== void 0;
|
|
18620
18965
|
var isErrorEvent = (res) => res.error !== void 0;
|
|
@@ -18645,10 +18990,10 @@ var StableWSConnection = class {
|
|
|
18645
18990
|
*/
|
|
18646
18991
|
this.onlineStatusChanged = (event) => {
|
|
18647
18992
|
if (event.type === "offline") {
|
|
18648
|
-
|
|
18993
|
+
logger7.withExtraTags("onlineStatusChanged").info("Network status changed to offline.");
|
|
18649
18994
|
this._setHealth(false);
|
|
18650
18995
|
} else if (event.type === "online") {
|
|
18651
|
-
|
|
18996
|
+
logger7.withExtraTags("onlineStatusChanged").info(`Network status changed to online. isHealthy: ${this.isHealthy}.`);
|
|
18652
18997
|
if (!this.isHealthy) {
|
|
18653
18998
|
this._reconnect({ interval: 10 });
|
|
18654
18999
|
}
|
|
@@ -18656,7 +19001,7 @@ var StableWSConnection = class {
|
|
|
18656
19001
|
};
|
|
18657
19002
|
this.onopen = (wsId, authMessage) => {
|
|
18658
19003
|
if (this.wsID !== wsId) return;
|
|
18659
|
-
|
|
19004
|
+
logger7.withExtraTags("onopen").debug("WebSocket onopen callback fired.", {
|
|
18660
19005
|
wsID: wsId
|
|
18661
19006
|
});
|
|
18662
19007
|
const { ws } = this;
|
|
@@ -18664,14 +19009,14 @@ var StableWSConnection = class {
|
|
|
18664
19009
|
try {
|
|
18665
19010
|
ws.send(authMessage);
|
|
18666
19011
|
} catch (error) {
|
|
18667
|
-
|
|
19012
|
+
logger7.withExtraTags("onopen").error("Failed to send the WebSocket auth message.", { error });
|
|
18668
19013
|
this.rejectPromise?.(this._errorFromWSEvent(error));
|
|
18669
19014
|
this._destroyCurrentWSConnection();
|
|
18670
19015
|
}
|
|
18671
19016
|
};
|
|
18672
19017
|
this.onmessage = (wsId, event) => {
|
|
18673
19018
|
if (this.wsID !== wsId) return;
|
|
18674
|
-
|
|
19019
|
+
logger7.withExtraTags("onmessage").trace("WebSocket onmessage callback fired.", {
|
|
18675
19020
|
event,
|
|
18676
19021
|
wsID: wsId
|
|
18677
19022
|
});
|
|
@@ -18696,7 +19041,7 @@ var StableWSConnection = class {
|
|
|
18696
19041
|
};
|
|
18697
19042
|
this.onclose = (wsId, event) => {
|
|
18698
19043
|
if (this.wsID !== wsId) return;
|
|
18699
|
-
|
|
19044
|
+
logger7.withExtraTags("onclose").debug(`WebSocket onclose callback fired with code ${event.code}.`, {
|
|
18700
19045
|
event,
|
|
18701
19046
|
wsID: wsId
|
|
18702
19047
|
});
|
|
@@ -18707,14 +19052,14 @@ var StableWSConnection = class {
|
|
|
18707
19052
|
error.wasClean = event.wasClean;
|
|
18708
19053
|
error.target = event.target;
|
|
18709
19054
|
this.rejectPromise?.(error);
|
|
18710
|
-
|
|
19055
|
+
logger7.withExtraTags("onclose").warn(`The WebSocket connection was rejected: ${event.reason}.`, { event });
|
|
18711
19056
|
} else {
|
|
18712
19057
|
this.consecutiveFailures += 1;
|
|
18713
19058
|
this.totalFailures += 1;
|
|
18714
19059
|
this._setHealth(false);
|
|
18715
19060
|
this.isConnecting = false;
|
|
18716
19061
|
this.rejectPromise?.(this._errorFromWSEvent(event));
|
|
18717
|
-
|
|
19062
|
+
logger7.withExtraTags("onclose").warn("The WebSocket connection was closed. Attempting to reconnect.", {
|
|
18718
19063
|
event
|
|
18719
19064
|
});
|
|
18720
19065
|
this._reconnect();
|
|
@@ -18727,7 +19072,7 @@ var StableWSConnection = class {
|
|
|
18727
19072
|
this._setHealth(false);
|
|
18728
19073
|
this.isConnecting = false;
|
|
18729
19074
|
this.rejectPromise?.(this._errorFromWSEvent(event));
|
|
18730
|
-
|
|
19075
|
+
logger7.withExtraTags("onerror").warn("The WebSocket connection raised an error.", { event });
|
|
18731
19076
|
this._reconnect();
|
|
18732
19077
|
};
|
|
18733
19078
|
/**
|
|
@@ -18769,7 +19114,7 @@ var StableWSConnection = class {
|
|
|
18769
19114
|
statusCode = event.error.StatusCode;
|
|
18770
19115
|
message = event.error.message;
|
|
18771
19116
|
}
|
|
18772
|
-
|
|
19117
|
+
logger7.withExtraTags("_errorFromWSEvent").warn(`The WebSocket failed with code ${code}.`, { event });
|
|
18773
19118
|
const error = new Error(
|
|
18774
19119
|
`WS failed with code ${code} and reason - ${message}`
|
|
18775
19120
|
);
|
|
@@ -18815,7 +19160,7 @@ var StableWSConnection = class {
|
|
|
18815
19160
|
this.connectionCheckTimeoutRef = setTimeout(() => {
|
|
18816
19161
|
const now = /* @__PURE__ */ new Date();
|
|
18817
19162
|
if (this.lastEvent && now.getTime() - this.lastEvent.getTime() > this.connectionCheckTimeout) {
|
|
18818
|
-
|
|
19163
|
+
logger7.withExtraTags("scheduleConnectionCheck").warn("No events received within the health-check window. Reconnecting.");
|
|
18819
19164
|
this._setHealth(false);
|
|
18820
19165
|
this._reconnect();
|
|
18821
19166
|
}
|
|
@@ -18853,13 +19198,13 @@ var StableWSConnection = class {
|
|
|
18853
19198
|
try {
|
|
18854
19199
|
const healthCheck = await this._connect();
|
|
18855
19200
|
this.consecutiveFailures = 0;
|
|
18856
|
-
|
|
19201
|
+
logger7.withExtraTags("connect").info(`Established a WebSocket connection. Health check: ${healthCheck}.`);
|
|
18857
19202
|
} catch (error) {
|
|
18858
19203
|
this.isHealthy = false;
|
|
18859
19204
|
this.consecutiveFailures += 1;
|
|
18860
19205
|
const e = error;
|
|
18861
19206
|
if (e.code === chatCodes.TOKEN_EXPIRED && !this.client.tokenManager.isStatic()) {
|
|
18862
|
-
|
|
19207
|
+
logger7.withExtraTags("connect").warn(
|
|
18863
19208
|
"WebSocket connection failed due to an expired token. Reloading the token and reconnecting."
|
|
18864
19209
|
);
|
|
18865
19210
|
this._reconnect({ refreshToken: true });
|
|
@@ -18924,7 +19269,7 @@ var StableWSConnection = class {
|
|
|
18924
19269
|
* @param timeout - Optional timeout in milliseconds to wait for the close frame from the server.
|
|
18925
19270
|
*/
|
|
18926
19271
|
disconnect(timeout) {
|
|
18927
|
-
|
|
19272
|
+
logger7.withExtraTags("disconnect").info(`Closing the WebSocket connection for wsID ${this.wsID}.`);
|
|
18928
19273
|
this.wsID += 1;
|
|
18929
19274
|
this.isConnecting = false;
|
|
18930
19275
|
this.isDisconnected = true;
|
|
@@ -18941,7 +19286,7 @@ var StableWSConnection = class {
|
|
|
18941
19286
|
if (ws && ws.close && ws.readyState === ws.OPEN) {
|
|
18942
19287
|
isClosedPromise = new Promise((resolve) => {
|
|
18943
19288
|
const onclose = (event) => {
|
|
18944
|
-
|
|
19289
|
+
logger7.withExtraTags("disconnect").debug(
|
|
18945
19290
|
`Resolving the close promise ${event ? "with" : "without"} a close frame.`,
|
|
18946
19291
|
{ event }
|
|
18947
19292
|
);
|
|
@@ -18950,13 +19295,13 @@ var StableWSConnection = class {
|
|
|
18950
19295
|
ws.onclose = onclose;
|
|
18951
19296
|
setTimeout(onclose, timeout != null ? timeout : 1e3);
|
|
18952
19297
|
});
|
|
18953
|
-
|
|
19298
|
+
logger7.withExtraTags("disconnect").debug("Manually closing the connection via client.disconnect().");
|
|
18954
19299
|
ws.close(
|
|
18955
19300
|
chatCodes.WS_CLOSED_SUCCESS,
|
|
18956
19301
|
"Manually closed connection by calling client.disconnect()"
|
|
18957
19302
|
);
|
|
18958
19303
|
} else {
|
|
18959
|
-
|
|
19304
|
+
logger7.withExtraTags("disconnect").debug("The WebSocket connection does not exist or is already closed.");
|
|
18960
19305
|
isClosedPromise = Promise.resolve();
|
|
18961
19306
|
}
|
|
18962
19307
|
delete this.ws;
|
|
@@ -18974,20 +19319,20 @@ var StableWSConnection = class {
|
|
|
18974
19319
|
this.client.insightMetrics.connectionStartTimestamp = (/* @__PURE__ */ new Date()).getTime();
|
|
18975
19320
|
let isTokenReady = false;
|
|
18976
19321
|
try {
|
|
18977
|
-
|
|
19322
|
+
logger7.withExtraTags("_connect").debug("Waiting for the auth token.");
|
|
18978
19323
|
await this.client.tokenManager.tokenReady();
|
|
18979
19324
|
isTokenReady = true;
|
|
18980
19325
|
} catch (e) {
|
|
18981
19326
|
}
|
|
18982
19327
|
try {
|
|
18983
19328
|
if (!isTokenReady) {
|
|
18984
|
-
|
|
19329
|
+
logger7.withExtraTags("_connect").warn("The token provider failed previously. Retrying.");
|
|
18985
19330
|
await this.client.tokenManager.loadToken();
|
|
18986
19331
|
}
|
|
18987
19332
|
this._setupConnectionPromise();
|
|
18988
19333
|
const wsURL = this._buildUrl();
|
|
18989
19334
|
const authMessage = this.client._buildWSAuthMessage();
|
|
18990
|
-
|
|
19335
|
+
logger7.withExtraTags("_connect").info(`Connecting to ${wsURL}.`, {
|
|
18991
19336
|
wsURL,
|
|
18992
19337
|
requestID: this.requestID
|
|
18993
19338
|
});
|
|
@@ -19012,7 +19357,7 @@ var StableWSConnection = class {
|
|
|
19012
19357
|
}
|
|
19013
19358
|
} catch (error) {
|
|
19014
19359
|
this.isConnecting = false;
|
|
19015
|
-
|
|
19360
|
+
logger7.withExtraTags("_connect").warn("An error occurred while connecting.", { error });
|
|
19016
19361
|
if (this.client.options.enableInsights) {
|
|
19017
19362
|
this.client.insightMetrics.wsConsecutiveFailures++;
|
|
19018
19363
|
this.client.insightMetrics.wsTotalFailures++;
|
|
@@ -19033,9 +19378,9 @@ var StableWSConnection = class {
|
|
|
19033
19378
|
* @param options.refreshToken - Reload/refresh the user token before attempting to reconnect.
|
|
19034
19379
|
*/
|
|
19035
19380
|
async _reconnect(options = {}) {
|
|
19036
|
-
|
|
19381
|
+
logger7.withExtraTags("_reconnect").info("Initiating a reconnect.");
|
|
19037
19382
|
if (this.isConnecting || this.isHealthy) {
|
|
19038
|
-
|
|
19383
|
+
logger7.withExtraTags("_reconnect").debug("Aborting reconnect: already connecting or healthy (check 1).");
|
|
19039
19384
|
return;
|
|
19040
19385
|
}
|
|
19041
19386
|
let interval = options.interval;
|
|
@@ -19044,39 +19389,37 @@ var StableWSConnection = class {
|
|
|
19044
19389
|
}
|
|
19045
19390
|
await sleep(interval);
|
|
19046
19391
|
if (this.isConnecting || this.isHealthy) {
|
|
19047
|
-
|
|
19392
|
+
logger7.withExtraTags("_reconnect").debug("Aborting reconnect: already connecting or healthy (check 2).");
|
|
19048
19393
|
return;
|
|
19049
19394
|
}
|
|
19050
19395
|
if (this.isDisconnected) {
|
|
19051
|
-
|
|
19396
|
+
logger7.withExtraTags("_reconnect").debug("Aborting reconnect: disconnect() was called.");
|
|
19052
19397
|
return;
|
|
19053
19398
|
}
|
|
19054
|
-
|
|
19399
|
+
logger7.withExtraTags("_reconnect").info("Destroying the current WebSocket connection.");
|
|
19055
19400
|
this._destroyCurrentWSConnection();
|
|
19056
19401
|
if (options.refreshToken) {
|
|
19057
19402
|
await this.client.tokenManager.loadToken();
|
|
19058
19403
|
}
|
|
19059
19404
|
try {
|
|
19060
19405
|
await this._connect();
|
|
19061
|
-
|
|
19062
|
-
await this.client.recoverState();
|
|
19063
|
-
logger6.withExtraTags("_reconnect").debug("Finished the recover callback.");
|
|
19406
|
+
this.client._settleConnectPromises();
|
|
19064
19407
|
this.consecutiveFailures = 0;
|
|
19065
19408
|
} catch (error) {
|
|
19066
19409
|
this.isHealthy = false;
|
|
19067
19410
|
this.consecutiveFailures += 1;
|
|
19068
19411
|
if (error.code === chatCodes.TOKEN_EXPIRED && !this.client.tokenManager.isStatic()) {
|
|
19069
|
-
|
|
19412
|
+
logger7.withExtraTags("_reconnect").warn(
|
|
19070
19413
|
"WebSocket connection failed due to an expired token. Reloading the token and reconnecting."
|
|
19071
19414
|
);
|
|
19072
19415
|
return this._reconnect({ refreshToken: true });
|
|
19073
19416
|
}
|
|
19074
19417
|
if (error.isWSFailure) {
|
|
19075
|
-
|
|
19418
|
+
logger7.withExtraTags("_reconnect").warn("WebSocket connection failed. Retrying the reconnect.");
|
|
19076
19419
|
this._reconnect();
|
|
19077
19420
|
}
|
|
19078
19421
|
}
|
|
19079
|
-
|
|
19422
|
+
logger7.withExtraTags("_reconnect").debug("Reconnect attempt finished.");
|
|
19080
19423
|
}
|
|
19081
19424
|
/**
|
|
19082
19425
|
* Removes the current WS connection.
|
|
@@ -19091,7 +19434,7 @@ var StableWSConnection = class {
|
|
|
19091
19434
|
};
|
|
19092
19435
|
|
|
19093
19436
|
// src/uploadManager.ts
|
|
19094
|
-
var
|
|
19437
|
+
var logger8 = chatLoggerSystem.getLogger("upload-manager");
|
|
19095
19438
|
var initState7 = () => ({ uploads: {} });
|
|
19096
19439
|
var upsertById = (uploads, record) => ({
|
|
19097
19440
|
...uploads,
|
|
@@ -19181,7 +19524,7 @@ var UploadManager = class {
|
|
|
19181
19524
|
resolvePromise(response);
|
|
19182
19525
|
} catch (error) {
|
|
19183
19526
|
if (!abortController.signal.aborted) {
|
|
19184
|
-
|
|
19527
|
+
logger8.withExtraTags("upload", channelCid).error(`Upload "${id}" failed.`, { error });
|
|
19185
19528
|
}
|
|
19186
19529
|
rejectPromise(error);
|
|
19187
19530
|
} finally {
|
|
@@ -19232,7 +19575,7 @@ function UserFromToken(token) {
|
|
|
19232
19575
|
}
|
|
19233
19576
|
|
|
19234
19577
|
// src/token_manager.ts
|
|
19235
|
-
var
|
|
19578
|
+
var logger9 = chatLoggerSystem.getLogger("token-manager");
|
|
19236
19579
|
var TokenManager = class {
|
|
19237
19580
|
/**
|
|
19238
19581
|
* Initializes the token manager.
|
|
@@ -19302,7 +19645,7 @@ var TokenManager = class {
|
|
|
19302
19645
|
try {
|
|
19303
19646
|
this.token = await this.tokenProvider();
|
|
19304
19647
|
} catch (e) {
|
|
19305
|
-
|
|
19648
|
+
logger9.withExtraTags("loadToken").error("The token provider threw an error.", { error: e });
|
|
19306
19649
|
return reject(
|
|
19307
19650
|
new Error(`Call to tokenProvider failed with message: ${e}`, { cause: e })
|
|
19308
19651
|
);
|
|
@@ -19382,7 +19725,7 @@ var runWithRetry = (f, {
|
|
|
19382
19725
|
};
|
|
19383
19726
|
|
|
19384
19727
|
// src/api-client.ts
|
|
19385
|
-
var
|
|
19728
|
+
var logger10 = chatLoggerSystem.getLogger("api-client");
|
|
19386
19729
|
var MULTIPART_CONTENT_TYPE = "multipart/form-data";
|
|
19387
19730
|
var UPLOAD_REQUEST_DEFAULTS = {
|
|
19388
19731
|
timeout: 0,
|
|
@@ -19501,7 +19844,7 @@ var ApiClient = class {
|
|
|
19501
19844
|
requestResponse = await this.client.axiosInstance.request(config);
|
|
19502
19845
|
} catch (error) {
|
|
19503
19846
|
if (isTokenExpiredError(error)) {
|
|
19504
|
-
|
|
19847
|
+
logger10.withExtraTags("_doRequest").debug(
|
|
19505
19848
|
`The token expired on a ${type.toUpperCase()} request. Reloading the token before retrying.`,
|
|
19506
19849
|
{ url, config }
|
|
19507
19850
|
);
|
|
@@ -20144,7 +20487,7 @@ var THREAD_MANAGER_INITIAL_STATE = {
|
|
|
20144
20487
|
},
|
|
20145
20488
|
ready: false
|
|
20146
20489
|
};
|
|
20147
|
-
var
|
|
20490
|
+
var logger11 = chatLoggerSystem.getLogger("thread-manager");
|
|
20148
20491
|
var ThreadManager = class extends WithSubscriptions {
|
|
20149
20492
|
constructor({ client }) {
|
|
20150
20493
|
super();
|
|
@@ -20294,7 +20637,7 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
20294
20637
|
ready: true
|
|
20295
20638
|
}));
|
|
20296
20639
|
} catch (error) {
|
|
20297
|
-
|
|
20640
|
+
logger11.withExtraTags("reload").error("Failed to reload the thread list.", { error });
|
|
20298
20641
|
this.state.next((current) => ({
|
|
20299
20642
|
...current,
|
|
20300
20643
|
pagination: {
|
|
@@ -20330,7 +20673,7 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
20330
20673
|
}
|
|
20331
20674
|
}));
|
|
20332
20675
|
} catch (error) {
|
|
20333
|
-
|
|
20676
|
+
logger11.withExtraTags("loadNextPage").error("Failed to load the next page of threads.", { error });
|
|
20334
20677
|
this.state.next((current) => ({
|
|
20335
20678
|
...current,
|
|
20336
20679
|
pagination: {
|
|
@@ -21070,6 +21413,13 @@ var ignoreEventsForUnknownChannels = ({ event, ctx: { channelManager } }) => {
|
|
|
21070
21413
|
);
|
|
21071
21414
|
if (!channel) return { action: "stop" };
|
|
21072
21415
|
};
|
|
21416
|
+
var restoreInterruptedWatch = (channel, client) => {
|
|
21417
|
+
if (channel.pendingDisposal) return;
|
|
21418
|
+
if (channel.watchStatus !== ChannelWatchStatus.WasWatching) return;
|
|
21419
|
+
runDetached(getChannel({ channel, client }), {
|
|
21420
|
+
context: `restoreInterruptedWatch(${channel.cid})`
|
|
21421
|
+
});
|
|
21422
|
+
};
|
|
21073
21423
|
var updateLists = async ({
|
|
21074
21424
|
event,
|
|
21075
21425
|
ctx: { channelManager }
|
|
@@ -21104,6 +21454,9 @@ var updateLists = async ({
|
|
|
21104
21454
|
}
|
|
21105
21455
|
paginator.ingestItem(channel);
|
|
21106
21456
|
});
|
|
21457
|
+
if (event.type !== "channel.hidden") {
|
|
21458
|
+
restoreInterruptedWatch(channel, channelManager.client);
|
|
21459
|
+
}
|
|
21107
21460
|
};
|
|
21108
21461
|
var channelDeletedHandler = {
|
|
21109
21462
|
handle: removeItem,
|
|
@@ -21133,6 +21486,22 @@ var messageNewHandler = {
|
|
|
21133
21486
|
handle: updateLists,
|
|
21134
21487
|
id: "ChannelManager:default-handler:message.new"
|
|
21135
21488
|
};
|
|
21489
|
+
var READ_STATE_SORT_FIELDS = ["has_unread", "unread_count"];
|
|
21490
|
+
var dependsOnReadState = (paginator) => paginator.effectiveSort.some(
|
|
21491
|
+
({ field }) => !!field && READ_STATE_SORT_FIELDS.includes(field)
|
|
21492
|
+
) || filterConstrainsField(paginator.buildMatchFilters(), "has_unread");
|
|
21493
|
+
var readStateChangedHandler = {
|
|
21494
|
+
handle: ({ event, ctx: { channelManager } }) => {
|
|
21495
|
+
const { client } = channelManager;
|
|
21496
|
+
if (event.type === "message.read" && event.user?.id !== client.userId) return;
|
|
21497
|
+
if (event.thread_id) return;
|
|
21498
|
+
const channel = getCachedChannelFromEvent(event, client.activeChannels);
|
|
21499
|
+
if (!channel) return;
|
|
21500
|
+
if (!channelManager.paginators.some(dependsOnReadState)) return;
|
|
21501
|
+
channelManager.ingestChannel(channel);
|
|
21502
|
+
},
|
|
21503
|
+
id: "ChannelManager:default-handler:read-state-changed"
|
|
21504
|
+
};
|
|
21136
21505
|
var notificationAddedToChannelHandler = {
|
|
21137
21506
|
handle: updateLists,
|
|
21138
21507
|
id: "ChannelManager:default-handler:notification.added_to_channel"
|
|
@@ -21210,6 +21579,33 @@ var _ChannelManager = class _ChannelManager extends WithSubscriptions {
|
|
|
21210
21579
|
await paginator.reload();
|
|
21211
21580
|
})
|
|
21212
21581
|
);
|
|
21582
|
+
/**
|
|
21583
|
+
* Re-run every loaded list's own first-page query — the channel-list half of connection recovery,
|
|
21584
|
+
* driven by {@link ConnectionRecoveryManager}.
|
|
21585
|
+
*
|
|
21586
|
+
* Each list re-asserts its *own* `filters` / `sort` / `pageSize` rather than some substitute query,
|
|
21587
|
+
* and because `client.queryChannels` watches by default, that page's channels are re-watched as a
|
|
21588
|
+
* side effect. Channels below the first page are deliberately not touched — see
|
|
21589
|
+
* `restoreInterruptedWatch` for how they come back.
|
|
21590
|
+
*
|
|
21591
|
+
* `{ keepPreviousItems: true, reset: 'yes' }` is the non-destructive refresh: `reset` restarts the
|
|
21592
|
+
* window at page 1, while `keepPreviousItems` merges the fetched page into the loaded list instead
|
|
21593
|
+
* of replacing it and keeps the item index populated — the latter is what stops a concurrent
|
|
21594
|
+
* offline-replay ingest from rebuilding the list out of a cleared index. Notably NOT `silent`: the
|
|
21595
|
+
* `isLoading` transition it would suppress is the in-flight guard that keeps a second overlapping
|
|
21596
|
+
* query from being issued.
|
|
21597
|
+
*
|
|
21598
|
+
* Distinct from {@link ChannelManager.reload}, which is a destructive `reset` that blanks each list
|
|
21599
|
+
* to its loading state.
|
|
21600
|
+
*
|
|
21601
|
+
* Paginators that were never queried are skipped: they run their own first query when they mount,
|
|
21602
|
+
* and querying them here would race it.
|
|
21603
|
+
*/
|
|
21604
|
+
this.recover = async () => await Promise.allSettled(
|
|
21605
|
+
this.paginators.filter((paginator) => paginator.isInitialized).map(async (paginator) => {
|
|
21606
|
+
await paginator.toTail({ keepPreviousItems: true, reset: "yes" });
|
|
21607
|
+
})
|
|
21608
|
+
);
|
|
21213
21609
|
this.client = client;
|
|
21214
21610
|
this.state = new StateStore({ paginators: paginators ?? [] });
|
|
21215
21611
|
this.setOwnershipResolver(ownershipResolver);
|
|
@@ -21292,6 +21688,19 @@ var _ChannelManager = class _ChannelManager extends WithSubscriptions {
|
|
|
21292
21688
|
}
|
|
21293
21689
|
});
|
|
21294
21690
|
}
|
|
21691
|
+
/**
|
|
21692
|
+
* Drops a channel from the lists whose filters it no longer matches, adding it nowhere.
|
|
21693
|
+
*
|
|
21694
|
+
* For state the client changes without an event naming the channel — zeroing every unread count on a
|
|
21695
|
+
* global read. Removal-only: the backend rejects `has_unread: false`, so becoming read can never make
|
|
21696
|
+
* a channel belong to a list, and `ingestChannel` would pull unloaded channels into matching ones.
|
|
21697
|
+
*/
|
|
21698
|
+
dropFromUnmatchedLists(channel) {
|
|
21699
|
+
this.paginators.forEach((paginator) => {
|
|
21700
|
+
if (paginator.matchesFilter(channel)) return;
|
|
21701
|
+
paginator.removeItem({ item: channel });
|
|
21702
|
+
});
|
|
21703
|
+
}
|
|
21295
21704
|
/**
|
|
21296
21705
|
* Filter a page of query results for a specific paginator according to ownership rules.
|
|
21297
21706
|
* If no owners are specified by the resolver, all matching paginators keep the item.
|
|
@@ -21508,14 +21917,191 @@ _ChannelManager.defaultEventHandlers = {
|
|
|
21508
21917
|
"channel.visible": [channelVisibleHandler],
|
|
21509
21918
|
"member.updated": [memberUpdatedHandler],
|
|
21510
21919
|
"message.new": [messageNewHandler],
|
|
21920
|
+
"message.read": [readStateChangedHandler],
|
|
21921
|
+
"message.read_locally": [readStateChangedHandler],
|
|
21511
21922
|
"notification.added_to_channel": [notificationAddedToChannelHandler],
|
|
21512
21923
|
"notification.channel_mutes_updated": [notificationChannelMutesUpdatedHandler],
|
|
21924
|
+
"notification.mark_read": [readStateChangedHandler],
|
|
21925
|
+
"notification.mark_unread": [readStateChangedHandler],
|
|
21513
21926
|
"notification.message_new": [notificationMessageNewHandler],
|
|
21514
21927
|
"notification.removed_from_channel": [notificationRemovedFromChannelHandler],
|
|
21515
21928
|
"user.presence.changed": [userPresenceChangedHandler]
|
|
21516
21929
|
};
|
|
21517
21930
|
var ChannelManager = _ChannelManager;
|
|
21518
21931
|
|
|
21932
|
+
// src/ConnectionRecoveryManager.ts
|
|
21933
|
+
var logger12 = chatLoggerSystem.getLogger("client");
|
|
21934
|
+
var DEFAULT_CONNECTION_RECOVERY_MANAGER_CONFIG = deepFreezeConfig({ enabled: true });
|
|
21935
|
+
var ConnectionRecoveryManager = class extends WithSubscriptions {
|
|
21936
|
+
constructor({ client }) {
|
|
21937
|
+
super();
|
|
21938
|
+
/** Set while a recovery is in flight; only used to keep the logs readable. */
|
|
21939
|
+
this.isRecovering = false;
|
|
21940
|
+
this.registerSubscriptions = () => {
|
|
21941
|
+
if (!this.hasSubscriptions) {
|
|
21942
|
+
this.addUnsubscribeFunction(
|
|
21943
|
+
this.client.on("connection.changed", (event) => {
|
|
21944
|
+
if (!event.online) return;
|
|
21945
|
+
runDetached(this.recoverChannelLists(), { context: "recoverChannelLists" });
|
|
21946
|
+
const syncEdgeDrivesActiveChannels = this.ensureSyncStatusSubscription();
|
|
21947
|
+
if (!syncEdgeDrivesActiveChannels) {
|
|
21948
|
+
runDetached(this.recoverActiveChannels(), {
|
|
21949
|
+
context: "recoverActiveChannels"
|
|
21950
|
+
});
|
|
21951
|
+
}
|
|
21952
|
+
}).unsubscribe
|
|
21953
|
+
);
|
|
21954
|
+
this.addUnsubscribeFunction(() => {
|
|
21955
|
+
this.unsubscribeSyncStatus?.();
|
|
21956
|
+
this.unsubscribeSyncStatus = void 0;
|
|
21957
|
+
});
|
|
21958
|
+
}
|
|
21959
|
+
this.incrementRefCount();
|
|
21960
|
+
return () => this.unregisterSubscriptions();
|
|
21961
|
+
};
|
|
21962
|
+
/**
|
|
21963
|
+
* Bind to the offline DB's sync-status edge, if there is one to bind to yet.
|
|
21964
|
+
*
|
|
21965
|
+
* Resolved off `this.client` rather than injected: the DB is already reachable there, it is simply
|
|
21966
|
+
* attached after this manager is constructed (and initialized later still), so the binding has to be
|
|
21967
|
+
* deferred rather than handed in. Doing it from the `connection.changed` handler is safe even when
|
|
21968
|
+
* this manager's listener runs before the sync manager's: `OfflineDBSyncManager` awaits
|
|
21969
|
+
* `syncAndExecutePendingTasks()` before publishing, so the edge cannot land in the same synchronous
|
|
21970
|
+
* dispatch that registers us.
|
|
21971
|
+
*
|
|
21972
|
+
* An offline DB whose `init()` never succeeded publishes no edge, so it must not be treated as the
|
|
21973
|
+
* trigger — hence the `initialized` check, which also means a later successful init is picked up on
|
|
21974
|
+
* the next reconnect.
|
|
21975
|
+
*
|
|
21976
|
+
* @returns whether the sync-status edge is now driving active-channel recovery.
|
|
21977
|
+
*/
|
|
21978
|
+
this.ensureSyncStatusSubscription = () => {
|
|
21979
|
+
if (this.unsubscribeSyncStatus) return true;
|
|
21980
|
+
const { offlineDb } = this.client;
|
|
21981
|
+
if (!offlineDb?.state.getLatestValue().initialized) return false;
|
|
21982
|
+
const { unsubscribe } = offlineDb.syncManager.onSyncStatusChange((status) => {
|
|
21983
|
+
if (!status) return;
|
|
21984
|
+
runDetached(this.recoverActiveChannels(), { context: "recoverActiveChannels" });
|
|
21985
|
+
});
|
|
21986
|
+
this.unsubscribeSyncStatus = unsubscribe;
|
|
21987
|
+
return true;
|
|
21988
|
+
};
|
|
21989
|
+
this.recoverChannelLists = async () => {
|
|
21990
|
+
if (!this.config.enabled) return;
|
|
21991
|
+
await this.client.channelManager.recover();
|
|
21992
|
+
};
|
|
21993
|
+
this.recoverActiveChannels = async () => {
|
|
21994
|
+
if (!this.config.enabled) return;
|
|
21995
|
+
const channels = this.recoverableActiveChannels;
|
|
21996
|
+
const threads = this.recoverableActiveThreads;
|
|
21997
|
+
this.isRecovering = true;
|
|
21998
|
+
logger12.withExtraTags("connectionRecovery").info(
|
|
21999
|
+
`Recovering ${channels.length} active channel(s) and ${threads.length} active thread(s).`
|
|
22000
|
+
);
|
|
22001
|
+
await Promise.allSettled([
|
|
22002
|
+
...channels.map((channel) => channel.reload()),
|
|
22003
|
+
...threads.map((thread) => thread.reload())
|
|
22004
|
+
]);
|
|
22005
|
+
this.isRecovering = false;
|
|
22006
|
+
this.client.dispatchEvent({ type: "connection.recovered" });
|
|
22007
|
+
};
|
|
22008
|
+
/**
|
|
22009
|
+
* Run a full recovery now, without waiting for a connection event.
|
|
22010
|
+
*/
|
|
22011
|
+
this.recover = async () => {
|
|
22012
|
+
if (this.isRecovering) {
|
|
22013
|
+
logger12.withExtraTags("connectionRecovery").debug("A recovery is already in flight.");
|
|
22014
|
+
}
|
|
22015
|
+
await Promise.allSettled([this.recoverChannelLists(), this.recoverActiveChannels()]);
|
|
22016
|
+
};
|
|
22017
|
+
this.configController = new ConfigController({
|
|
22018
|
+
defaults: DEFAULT_CONNECTION_RECOVERY_MANAGER_CONFIG
|
|
22019
|
+
});
|
|
22020
|
+
this.client = client;
|
|
22021
|
+
}
|
|
22022
|
+
/**
|
|
22023
|
+
* Resolved configuration, as a store — the shape every configurable class exposes
|
|
22024
|
+
* (`configState` / `config` / `updateConfig`).
|
|
22025
|
+
*/
|
|
22026
|
+
get configState() {
|
|
22027
|
+
return this.configController.state;
|
|
22028
|
+
}
|
|
22029
|
+
/** The current resolved configuration. `Readonly` — change it through {@link updateConfig}. */
|
|
22030
|
+
get config() {
|
|
22031
|
+
return this.configState.getLatestValue();
|
|
22032
|
+
}
|
|
22033
|
+
/** Merges a partial configuration into the resolved config and notifies subscribers. */
|
|
22034
|
+
updateConfig(config) {
|
|
22035
|
+
this.configController.patch(config);
|
|
22036
|
+
}
|
|
22037
|
+
/**
|
|
22038
|
+
* Rebuilds the resolved configuration from package defaults plus the declarative slice — the
|
|
22039
|
+
* derivation entry point every configurable entity exposes, so the client routes a slice here and
|
|
22040
|
+
* knows nothing about this manager's defaults or merge semantics.
|
|
22041
|
+
*/
|
|
22042
|
+
initializeConfig(config) {
|
|
22043
|
+
this.configController.initialize(config);
|
|
22044
|
+
}
|
|
22045
|
+
/**
|
|
22046
|
+
* The channels this recovery reloads: the ones a consumer has declared it is currently reading.
|
|
22047
|
+
*
|
|
22048
|
+
* Deliberately NOT filtered on `watchStatus`. A channel opened while offline never reached
|
|
22049
|
+
* `Watching` at all (its `watch()` threw, leaving `offlineMode`), so a `WasWatching` filter here
|
|
22050
|
+
* would permanently strand the single most important offline case. `Channel.reload()`'s own
|
|
22051
|
+
* `initialized || offlineMode` guard is the correct gate, and this is not a "restore a lost watch"
|
|
22052
|
+
* decision anyway — it is "the channel being read must show the truth".
|
|
22053
|
+
*/
|
|
22054
|
+
get recoverableActiveChannels() {
|
|
22055
|
+
const channels = [];
|
|
22056
|
+
for (const cid in this.client.activeChannels) {
|
|
22057
|
+
const channel = this.client.activeChannels[cid];
|
|
22058
|
+
if (channel?.active && !channel.pendingDisposal) channels.push(channel);
|
|
22059
|
+
}
|
|
22060
|
+
return channels;
|
|
22061
|
+
}
|
|
22062
|
+
/**
|
|
22063
|
+
* The threads this recovery reloads: the ones a consumer has declared it is displaying.
|
|
22064
|
+
*
|
|
22065
|
+
* Threads need their own pass because nothing else brings an open thread's replies back — a
|
|
22066
|
+
* channel reload refreshes the main message list, `ChannelManager.recover()` refreshes the channel
|
|
22067
|
+
* lists*, and `ThreadManager`'s own recovery refreshes the thread *list*, reusing existing
|
|
22068
|
+
* instances without rehydrating them unless they were separately marked stale (which only
|
|
22069
|
+
* `user.watching.stop` does, never a reconnect).
|
|
22070
|
+
*
|
|
22071
|
+
* `active` is the filter that matters: `threadsById` holds every thread the list has paged in,
|
|
22072
|
+
* which is not what should be re-fetched on a reconnect — only what someone is actually reading.
|
|
22073
|
+
* Guarded on the owning channel the same way active channels are: a thread whose channel is being
|
|
22074
|
+
* torn down has nothing to recover into.
|
|
22075
|
+
*
|
|
22076
|
+
* NOTE: `threadsById` is the thread LIST, not a thread cache. A thread opened from a message list
|
|
22077
|
+
* is constructed directly (`threadsById[id] ?? new Thread(...)`) and only reaches the list because
|
|
22078
|
+
* the UI SDKs adopt it there — prepended, once its replies have loaded — which is a workaround, not
|
|
22079
|
+
* a contract. Two consequences, both accepted for now and both narrow:
|
|
22080
|
+
*
|
|
22081
|
+
* - A reconnect landing before that adoption misses the thread. Self-limiting: the UI SDKs re-issue
|
|
22082
|
+
* the load while the reply list is still unloaded, so a thread that failed to load gets adopted on
|
|
22083
|
+
* the retry.
|
|
22084
|
+
* - `ThreadManager.reload()` rebuilds `state.threads` purely from the query response, so an adopted
|
|
22085
|
+
* thread absent from that response is evicted while still active. It cannot bite within the
|
|
22086
|
+
* reconnect that caused it: this getter is read before `connection.recovered` is dispatched, and
|
|
22087
|
+
* that event is what triggers the UI SDKs' list reload. It would take a LATER reconnect, after an
|
|
22088
|
+
* eviction, to miss the thread.
|
|
22089
|
+
*
|
|
22090
|
+
* The fix is a real off-list registry — see the commented-out `threadCache` in `ThreadManager` — at
|
|
22091
|
+
* which point this getter reads from that instead, with no change to the `active` filter.
|
|
22092
|
+
*/
|
|
22093
|
+
get recoverableActiveThreads() {
|
|
22094
|
+
const threads = [];
|
|
22095
|
+
for (const thread of Object.values(this.client.threads.threadsById)) {
|
|
22096
|
+
if (!thread) continue;
|
|
22097
|
+
const { active } = thread.state.getLatestValue();
|
|
22098
|
+
if (!active || thread.channel.pendingDisposal) continue;
|
|
22099
|
+
threads.push(thread);
|
|
22100
|
+
}
|
|
22101
|
+
return threads;
|
|
22102
|
+
}
|
|
22103
|
+
};
|
|
22104
|
+
|
|
21519
22105
|
// src/reminders/ReminderTimer.ts
|
|
21520
22106
|
var oneMinute = 60 * 1e3;
|
|
21521
22107
|
var oneHour = 60 * oneMinute;
|
|
@@ -22003,7 +22589,7 @@ var CONSTRUCTION_ONLY_CONFIG_PATHS = {
|
|
|
22003
22589
|
};
|
|
22004
22590
|
|
|
22005
22591
|
// src/configuration/InstanceConfigurationRegistry.ts
|
|
22006
|
-
var
|
|
22592
|
+
var logger13 = chatLoggerSystem.getLogger("instance-configuration");
|
|
22007
22593
|
var InstanceConfigurationRegistry = class {
|
|
22008
22594
|
constructor() {
|
|
22009
22595
|
/**
|
|
@@ -22201,7 +22787,7 @@ var InstanceConfigurationRegistry = class {
|
|
|
22201
22787
|
try {
|
|
22202
22788
|
instance.reinitializeConfig?.();
|
|
22203
22789
|
} catch (error) {
|
|
22204
|
-
|
|
22790
|
+
logger13.error("reinitializeConfig threw during reset", error);
|
|
22205
22791
|
}
|
|
22206
22792
|
}
|
|
22207
22793
|
}
|
|
@@ -22259,7 +22845,7 @@ var InstanceConfigurationRegistry = class {
|
|
|
22259
22845
|
warnIfKeyLooksUnknown(key) {
|
|
22260
22846
|
if (INSTANCE_CONFIG_TREE_KEYS.includes(key)) return;
|
|
22261
22847
|
if (this.hasLiveInstances(key)) return;
|
|
22262
|
-
|
|
22848
|
+
logger13.withExtraTags(key).warn(
|
|
22263
22849
|
"Configuration registered for a key this package does not define, with nothing subscribed to it. Check the spelling; a key declared by a downstream SDK is expected here only if its owner subscribes later."
|
|
22264
22850
|
);
|
|
22265
22851
|
}
|
|
@@ -22286,7 +22872,7 @@ var InstanceConfigurationRegistry = class {
|
|
|
22286
22872
|
return !isEqual(getPath(registered, path), getPath(config, path));
|
|
22287
22873
|
});
|
|
22288
22874
|
if (late.length === 0) return;
|
|
22289
|
-
|
|
22875
|
+
logger13.withExtraTags(key).warn(
|
|
22290
22876
|
`These paths are read once during construction, so they will not affect the ${key} instance(s) that already exist: ${late.join(", ")}. Register configuration before the instances are created \u2014 typically alongside StreamChat.getInstance().`
|
|
22291
22877
|
);
|
|
22292
22878
|
}
|
|
@@ -24352,8 +24938,8 @@ var ChatApi = class {
|
|
|
24352
24938
|
function isString(value) {
|
|
24353
24939
|
return typeof value === "string" || value instanceof String;
|
|
24354
24940
|
}
|
|
24355
|
-
var
|
|
24356
|
-
var
|
|
24941
|
+
var logger14 = chatLoggerSystem.getLogger("client");
|
|
24942
|
+
var offlineDbLogger2 = chatLoggerSystem.getLogger("offline-db");
|
|
24357
24943
|
var StreamChat = class _StreamChat extends ChatApi {
|
|
24358
24944
|
/**
|
|
24359
24945
|
* Initializes a client.
|
|
@@ -24411,7 +24997,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24411
24997
|
throw new Error('The "id" field on the user is missing');
|
|
24412
24998
|
}
|
|
24413
24999
|
if (this.userId === user.id && this.setUserPromise) {
|
|
24414
|
-
|
|
25000
|
+
logger14.withExtraTags("connectUser").warn(
|
|
24415
25001
|
"Detected consecutive calls to connectUser. Ideally, this function should only be called once."
|
|
24416
25002
|
);
|
|
24417
25003
|
return this.setUserPromise;
|
|
@@ -24422,7 +25008,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24422
25008
|
);
|
|
24423
25009
|
}
|
|
24424
25010
|
if (this.node && !this.options.allowServerSideConnect) {
|
|
24425
|
-
|
|
25011
|
+
logger14.withExtraTags("connectUser").warn(
|
|
24426
25012
|
'Do not use connectUser server-side. connectUser impacts MAU and concurrent connection usage, and therefore your bill. If you have a valid use case, set "allowServerSideConnect: true" in the client options to disable this warning.'
|
|
24427
25013
|
);
|
|
24428
25014
|
}
|
|
@@ -24501,11 +25087,11 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24501
25087
|
);
|
|
24502
25088
|
}
|
|
24503
25089
|
if (this.wsConnection?.isConnecting && this.wsPromise) {
|
|
24504
|
-
|
|
25090
|
+
logger14.withExtraTags("openConnection").debug("A connection attempt is already in progress.");
|
|
24505
25091
|
return this.wsPromise;
|
|
24506
25092
|
}
|
|
24507
25093
|
if (this.wsConnection?.isHealthy && this._hasConnectionID()) {
|
|
24508
|
-
|
|
25094
|
+
logger14.withExtraTags("openConnection").debug("openConnection was called twice; a healthy connection already exists.");
|
|
24509
25095
|
return;
|
|
24510
25096
|
}
|
|
24511
25097
|
this.clientId = `${this.userId}--${randomId()}`;
|
|
@@ -24521,7 +25107,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24521
25107
|
* @returns The close-connection promise.
|
|
24522
25108
|
*/
|
|
24523
25109
|
this.disconnectUser = (timeout) => {
|
|
24524
|
-
|
|
25110
|
+
logger14.withExtraTags("disconnectUser").info("Disconnecting the client.");
|
|
24525
25111
|
delete this.user;
|
|
24526
25112
|
delete this._user;
|
|
24527
25113
|
const closePromise = this.closeConnection(timeout);
|
|
@@ -24540,7 +25126,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24540
25126
|
closePromise.finally(() => {
|
|
24541
25127
|
this.tokenManager.reset();
|
|
24542
25128
|
}).catch(
|
|
24543
|
-
(err) =>
|
|
25129
|
+
(err) => logger14.withExtraTags("disconnectUser").error("The close promise rejected during disconnect.", { error: err })
|
|
24544
25130
|
);
|
|
24545
25131
|
return closePromise;
|
|
24546
25132
|
};
|
|
@@ -24558,7 +25144,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24558
25144
|
*/
|
|
24559
25145
|
this.connectAnonymousUser = () => {
|
|
24560
25146
|
if (this.node && !this.options.allowServerSideConnect) {
|
|
24561
|
-
|
|
25147
|
+
logger14.withExtraTags("connectAnonymousUser").warn(
|
|
24562
25148
|
'Do not use connectUser server-side. connectUser impacts MAU and concurrent connection usage, and therefore your bill. If you have a valid use case, set "allowServerSideConnect: true" in the client options to disable this warning.'
|
|
24563
25149
|
);
|
|
24564
25150
|
}
|
|
@@ -24712,27 +25298,19 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24712
25298
|
(set) => set?.forEach((handleEvent) => handleEvent(event))
|
|
24713
25299
|
);
|
|
24714
25300
|
};
|
|
24715
|
-
|
|
24716
|
-
|
|
24717
|
-
|
|
24718
|
-
|
|
24719
|
-
|
|
24720
|
-
|
|
24721
|
-
|
|
24722
|
-
|
|
24723
|
-
|
|
24724
|
-
|
|
24725
|
-
|
|
24726
|
-
|
|
24727
|
-
|
|
24728
|
-
this.dispatchEvent({
|
|
24729
|
-
type: "connection.recovered"
|
|
24730
|
-
});
|
|
24731
|
-
} else {
|
|
24732
|
-
this.dispatchEvent({
|
|
24733
|
-
type: "connection.recovered"
|
|
24734
|
-
});
|
|
24735
|
-
}
|
|
25301
|
+
/**
|
|
25302
|
+
* Settles the connect promises after a successful reconnect, so the `await this.wsPromise` gates
|
|
25303
|
+
* spread across the client stop resolving against a superseded (possibly rejected) attempt.
|
|
25304
|
+
*
|
|
25305
|
+
* Called by `StableWSConnection._reconnect()`. Recovery itself is owned by
|
|
25306
|
+
* {@link ConnectionRecoveryManager}, which subscribes to the connection lifecycle and so covers
|
|
25307
|
+
* every reconnect path — including `closeConnection()` → `openConnection()` (mobile backgrounding),
|
|
25308
|
+
* which never reaches `_reconnect()` at all.
|
|
25309
|
+
*
|
|
25310
|
+
* @internal
|
|
25311
|
+
*/
|
|
25312
|
+
this._settleConnectPromises = () => {
|
|
25313
|
+
logger14.withExtraTags("_settleConnectPromises").info(`Connection re-established with connection ID ${this._getConnectionID()}.`);
|
|
24736
25314
|
this.wsPromise = Promise.resolve();
|
|
24737
25315
|
this.setUserPromise = Promise.resolve();
|
|
24738
25316
|
};
|
|
@@ -24822,7 +25400,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24822
25400
|
return this.userAgent;
|
|
24823
25401
|
}
|
|
24824
25402
|
if (!this.cachedUserAgent) {
|
|
24825
|
-
const version = "10.0.0-rc.
|
|
25403
|
+
const version = "10.0.0-rc.8";
|
|
24826
25404
|
const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
|
|
24827
25405
|
const { name: appName, version: appVersion } = this.appIdentifier ?? {};
|
|
24828
25406
|
const { os, model: deviceModel } = this.deviceIdentifier ?? {};
|
|
@@ -24881,7 +25459,6 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24881
25459
|
this.node = !this.browser;
|
|
24882
25460
|
this.options = {
|
|
24883
25461
|
warmUp: false,
|
|
24884
|
-
recoverStateOnReconnect: true,
|
|
24885
25462
|
disableCache: false,
|
|
24886
25463
|
isLocalUnreadCountEnabled: false,
|
|
24887
25464
|
wsUrlParams: new URLSearchParams({}),
|
|
@@ -24910,13 +25487,14 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24910
25487
|
this.tokenManager = new TokenManager();
|
|
24911
25488
|
this.insightMetrics = new InsightMetrics();
|
|
24912
25489
|
this.defaultWSTimeout = 15 * 1e3;
|
|
24913
|
-
this.recoverStateOnReconnect = this.options.recoverStateOnReconnect;
|
|
24914
25490
|
this.messageStore = new EntityStore({
|
|
24915
25491
|
getEntityId: (message) => message.id
|
|
24916
25492
|
});
|
|
24917
25493
|
this.threads = new ThreadManager({ client: this });
|
|
24918
25494
|
this.polls = new PollManager({ client: this });
|
|
24919
25495
|
this.channelManager = new ChannelManager({ client: this });
|
|
25496
|
+
this.connectionRecovery = new ConnectionRecoveryManager({ client: this });
|
|
25497
|
+
this.connectionRecovery.registerSubscriptions();
|
|
24920
25498
|
this.reminders = new ReminderManager({ client: this });
|
|
24921
25499
|
this.messageDeliveryReporter = new MessageDeliveryReporter({ client: this });
|
|
24922
25500
|
this.messageComposerCache = new FixedSizeQueueCache(64);
|
|
@@ -24983,6 +25561,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24983
25561
|
*/
|
|
24984
25562
|
initializeManagerConfig() {
|
|
24985
25563
|
const config = this.config.getConfig("client");
|
|
25564
|
+
this.connectionRecovery.initializeConfig(config?.connectionRecovery);
|
|
24986
25565
|
this.reminders.initializeConfig(config?.reminders);
|
|
24987
25566
|
this.threads.initializeConfig(config?.threads);
|
|
24988
25567
|
this.messageDeliveryReporter.initializeConfig(config?.messageDelivery);
|
|
@@ -25144,14 +25723,14 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
25144
25723
|
const key = callbackOrNothing ? callbackOrString : "all";
|
|
25145
25724
|
const callback = callbackOrNothing ? callbackOrNothing : callbackOrString;
|
|
25146
25725
|
const set = this.listeners.get(key) ?? /* @__PURE__ */ new Set();
|
|
25147
|
-
|
|
25726
|
+
logger14.withExtraTags("on").debug(`Attaching a listener for the "${key}" event.`);
|
|
25148
25727
|
set.add(callback);
|
|
25149
25728
|
if (!this.listeners.has(key)) {
|
|
25150
25729
|
this.listeners.set(key, set);
|
|
25151
25730
|
}
|
|
25152
25731
|
return {
|
|
25153
25732
|
unsubscribe: () => {
|
|
25154
|
-
|
|
25733
|
+
logger14.withExtraTags("on").debug(`Removing the listener for the "${key}" event.`);
|
|
25155
25734
|
set.delete(callback);
|
|
25156
25735
|
if (!set.size) {
|
|
25157
25736
|
this.listeners.delete(key);
|
|
@@ -25162,7 +25741,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
25162
25741
|
off(callbackOrString, callbackOrNothing) {
|
|
25163
25742
|
const key = callbackOrNothing ? callbackOrString : "all";
|
|
25164
25743
|
const callback = callbackOrNothing ? callbackOrNothing : callbackOrString;
|
|
25165
|
-
|
|
25744
|
+
logger14.withExtraTags("off").debug(`Removing the listener for the "${key}" event.`);
|
|
25166
25745
|
const set = this.listeners.get(key);
|
|
25167
25746
|
set?.delete(callback);
|
|
25168
25747
|
if (!set?.size) {
|
|
@@ -25172,7 +25751,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
25172
25751
|
_handleClientEvent(event) {
|
|
25173
25752
|
const client = this;
|
|
25174
25753
|
const postListenerCallbacks = [];
|
|
25175
|
-
|
|
25754
|
+
logger14.withExtraTags("_handleClientEvent").debug(`Received an event of type "${event.type}".`, { event });
|
|
25176
25755
|
if (event.type === "user.presence.changed" || event.type === "user.updated" || event.type === "user.deleted") {
|
|
25177
25756
|
this._handleUserEvent(event);
|
|
25178
25757
|
}
|
|
@@ -25199,14 +25778,10 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
25199
25778
|
this.mutedUsers = event.me.mutes;
|
|
25200
25779
|
}
|
|
25201
25780
|
if (event.type === "notification.mark_read" && event.unread_channels === 0) {
|
|
25202
|
-
|
|
25203
|
-
|
|
25204
|
-
(
|
|
25205
|
-
|
|
25206
|
-
// not stay stale.
|
|
25207
|
-
this.activeChannels[activeChannelKey]._setOwnUnreadCount(0)
|
|
25208
|
-
)
|
|
25209
|
-
);
|
|
25781
|
+
Object.values(this.activeChannels).forEach((channel) => {
|
|
25782
|
+
if (!channel._setOwnUnreadCount(0)) return;
|
|
25783
|
+
this.channelManager.dropFromUnmatchedLists(channel);
|
|
25784
|
+
});
|
|
25210
25785
|
}
|
|
25211
25786
|
if ((event.type === "channel.deleted" || event.type === "notification.channel_deleted") && event.cid) {
|
|
25212
25787
|
const { cid } = event;
|
|
@@ -25461,7 +26036,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
25461
26036
|
});
|
|
25462
26037
|
}
|
|
25463
26038
|
} catch (e) {
|
|
25464
|
-
|
|
26039
|
+
offlineDbLogger2.withExtraTags("queryReactionsAndHydrate").warn("An error occurred while querying offline reactions.", { error: e });
|
|
25465
26040
|
}
|
|
25466
26041
|
}
|
|
25467
26042
|
await this.wsPromise;
|
|
@@ -25729,21 +26304,15 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
25729
26304
|
*/
|
|
25730
26305
|
async updateMessage(...args) {
|
|
25731
26306
|
const [request] = args;
|
|
25732
|
-
|
|
25733
|
-
|
|
25734
|
-
|
|
25735
|
-
|
|
25736
|
-
|
|
25737
|
-
|
|
25738
|
-
|
|
25739
|
-
type: "update-message"
|
|
25740
|
-
}
|
|
25741
|
-
});
|
|
26307
|
+
return await queueOrRun({
|
|
26308
|
+
client: this,
|
|
26309
|
+
task: {
|
|
26310
|
+
...getPendingTaskChannelData(request.message?.cid),
|
|
26311
|
+
messageId: request.id,
|
|
26312
|
+
payload: args,
|
|
26313
|
+
type: "update-message"
|
|
25742
26314
|
}
|
|
25743
|
-
}
|
|
25744
|
-
offlineDbLogger3.withExtraTags("updateMessage").error("Updating the message failed.", { error });
|
|
25745
|
-
}
|
|
25746
|
-
return await this._updateMessage(...args);
|
|
26315
|
+
});
|
|
25747
26316
|
}
|
|
25748
26317
|
async _updateMessage(...args) {
|
|
25749
26318
|
return await super.updateMessage(...args);
|
|
@@ -25754,28 +26323,14 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
25754
26323
|
*/
|
|
25755
26324
|
async deleteMessage(...args) {
|
|
25756
26325
|
const [request] = args;
|
|
25757
|
-
|
|
25758
|
-
|
|
25759
|
-
|
|
25760
|
-
|
|
25761
|
-
|
|
25762
|
-
|
|
25763
|
-
id: request.id,
|
|
25764
|
-
deleteForMe: request.delete_for_me
|
|
25765
|
-
});
|
|
25766
|
-
}
|
|
25767
|
-
return await this.offlineDb.queueTask({
|
|
25768
|
-
task: {
|
|
25769
|
-
messageId: request.id,
|
|
25770
|
-
payload: args,
|
|
25771
|
-
type: "delete-message"
|
|
25772
|
-
}
|
|
25773
|
-
});
|
|
26326
|
+
return await queueOrRun({
|
|
26327
|
+
client: this,
|
|
26328
|
+
task: {
|
|
26329
|
+
messageId: request.id,
|
|
26330
|
+
payload: args,
|
|
26331
|
+
type: "delete-message"
|
|
25774
26332
|
}
|
|
25775
|
-
}
|
|
25776
|
-
offlineDbLogger3.withExtraTags("deleteMessage").error("Deleting the message failed.", { error });
|
|
25777
|
-
}
|
|
25778
|
-
return this._deleteMessage(...args);
|
|
26333
|
+
});
|
|
25779
26334
|
}
|
|
25780
26335
|
async _deleteMessage(...args) {
|
|
25781
26336
|
const [request] = args;
|
|
@@ -26253,7 +26808,19 @@ var REMINDER_FIELDS = {
|
|
|
26253
26808
|
type: "number"
|
|
26254
26809
|
}
|
|
26255
26810
|
};
|
|
26811
|
+
var CONNECTION_RECOVERY_FIELDS = {
|
|
26812
|
+
enabled: {
|
|
26813
|
+
description: "Whether the client recovers its own state on reconnect. Turn it off only if the application re-queries and re-watches everything on screen itself.",
|
|
26814
|
+
kind: "value",
|
|
26815
|
+
type: "boolean"
|
|
26816
|
+
}
|
|
26817
|
+
};
|
|
26256
26818
|
var CLIENT_FIELDS = {
|
|
26819
|
+
connectionRecovery: {
|
|
26820
|
+
description: "Bringing the channels, lists and threads you are reading back in line after a reconnect.",
|
|
26821
|
+
fields: CONNECTION_RECOVERY_FIELDS,
|
|
26822
|
+
kind: "group"
|
|
26823
|
+
},
|
|
26257
26824
|
messageDelivery: {
|
|
26258
26825
|
description: "Delivery and read receipt reporting.",
|
|
26259
26826
|
fields: MESSAGE_DELIVERY_FIELDS,
|
|
@@ -26426,7 +26993,7 @@ var OfflineError = class extends Error {
|
|
|
26426
26993
|
|
|
26427
26994
|
// src/offline-support/offline_sync_manager.ts
|
|
26428
26995
|
var import_axios5 = require("axios");
|
|
26429
|
-
var
|
|
26996
|
+
var logger15 = chatLoggerSystem.getLogger("offline-db");
|
|
26430
26997
|
var OfflineDBSyncManager = class {
|
|
26431
26998
|
constructor({
|
|
26432
26999
|
client,
|
|
@@ -26464,7 +27031,7 @@ var OfflineDBSyncManager = class {
|
|
|
26464
27031
|
}
|
|
26465
27032
|
);
|
|
26466
27033
|
} catch (error) {
|
|
26467
|
-
|
|
27034
|
+
logger15.withExtraTags("init").error("Failed to initialize the offline DB sync manager.", { error });
|
|
26468
27035
|
}
|
|
26469
27036
|
};
|
|
26470
27037
|
/**
|
|
@@ -26551,10 +27118,11 @@ var OfflineDBSyncManager = class {
|
|
|
26551
27118
|
channel_cids: cids,
|
|
26552
27119
|
last_sync_at: lastSyncedAtDate
|
|
26553
27120
|
});
|
|
27121
|
+
result.events = result.events?.map((event) => decodeWSEvent(event));
|
|
26554
27122
|
const { syncMaxEventCount } = this;
|
|
26555
27123
|
const exceedsLimit = typeof syncMaxEventCount === "number" && syncMaxEventCount > 0 && result.events.length > syncMaxEventCount;
|
|
26556
27124
|
if (exceedsLimit) {
|
|
26557
|
-
|
|
27125
|
+
logger15.withExtraTags("sync").warn(
|
|
26558
27126
|
`Skipping sync event replay: received ${result.events.length} events, which exceeds the configured limit of ${syncMaxEventCount}. Visible channels are refreshed on reconnect instead.`
|
|
26559
27127
|
);
|
|
26560
27128
|
} else {
|
|
@@ -26574,7 +27142,7 @@ var OfflineDBSyncManager = class {
|
|
|
26574
27142
|
lastSyncedAt: (/* @__PURE__ */ new Date()).toString()
|
|
26575
27143
|
});
|
|
26576
27144
|
} catch (e) {
|
|
26577
|
-
|
|
27145
|
+
logger15.withExtraTags("syncAndExecutePendingTasks").error("An error occurred while syncing the database.", { error: e });
|
|
26578
27146
|
if ((0, import_axios5.isAxiosError)(e) && e.code === "ECONNABORTED") {
|
|
26579
27147
|
return;
|
|
26580
27148
|
}
|
|
@@ -26614,7 +27182,7 @@ var OfflineDBSyncManager = class {
|
|
|
26614
27182
|
};
|
|
26615
27183
|
|
|
26616
27184
|
// src/offline-support/offline_support_api.ts
|
|
26617
|
-
var
|
|
27185
|
+
var logger16 = chatLoggerSystem.getLogger("offline-db");
|
|
26618
27186
|
var AbstractOfflineDB = class {
|
|
26619
27187
|
constructor({
|
|
26620
27188
|
client,
|
|
@@ -26639,7 +27207,7 @@ var AbstractOfflineDB = class {
|
|
|
26639
27207
|
}
|
|
26640
27208
|
} catch (error) {
|
|
26641
27209
|
this.state.partialNext({ initialized: false, userId: void 0 });
|
|
26642
|
-
|
|
27210
|
+
logger16.withExtraTags("init").error("Failed to initialize the offline database.", { error });
|
|
26643
27211
|
}
|
|
26644
27212
|
};
|
|
26645
27213
|
/**
|
|
@@ -26685,51 +27253,75 @@ var AbstractOfflineDB = class {
|
|
|
26685
27253
|
}, createQueries) => {
|
|
26686
27254
|
const channelFromEvent = event.channel;
|
|
26687
27255
|
const cid = event.cid || channelFromEvent?.cid;
|
|
26688
|
-
const type = event.type;
|
|
26689
27256
|
if (!cid) {
|
|
26690
27257
|
return await createQueries(execute);
|
|
26691
27258
|
}
|
|
26692
|
-
|
|
26693
|
-
|
|
26694
|
-
|
|
26695
|
-
|
|
26696
|
-
|
|
26697
|
-
const channelFromState = this.client.channel(
|
|
26698
|
-
event_.channel_type,
|
|
26699
|
-
event_.channel_id
|
|
26700
|
-
);
|
|
26701
|
-
if (channelFromState.initialized && !channelFromState.pendingDisposal) {
|
|
26702
|
-
channelData = channelFromState.data;
|
|
26703
|
-
}
|
|
27259
|
+
if (execute && !forceUpdate) {
|
|
27260
|
+
try {
|
|
27261
|
+
return await createQueries(true);
|
|
27262
|
+
} catch (error) {
|
|
27263
|
+
if (await this.channelExists({ cid })) throw error;
|
|
26704
27264
|
}
|
|
26705
|
-
|
|
26706
|
-
|
|
26707
|
-
|
|
26708
|
-
|
|
26709
|
-
|
|
26710
|
-
|
|
26711
|
-
|
|
26712
|
-
|
|
26713
|
-
|
|
26714
|
-
|
|
26715
|
-
|
|
26716
|
-
|
|
26717
|
-
|
|
26718
|
-
|
|
26719
|
-
|
|
26720
|
-
|
|
26721
|
-
|
|
26722
|
-
|
|
26723
|
-
|
|
26724
|
-
|
|
26725
|
-
|
|
26726
|
-
|
|
26727
|
-
|
|
26728
|
-
|
|
26729
|
-
|
|
27265
|
+
return await this.queriesBehindChannelGuard({
|
|
27266
|
+
createQueries,
|
|
27267
|
+
event,
|
|
27268
|
+
execute: true
|
|
27269
|
+
});
|
|
27270
|
+
}
|
|
27271
|
+
if (!forceUpdate && await this.channelExists({ cid })) {
|
|
27272
|
+
return await createQueries(execute);
|
|
27273
|
+
}
|
|
27274
|
+
return await this.queriesBehindChannelGuard({ createQueries, event, execute });
|
|
27275
|
+
};
|
|
27276
|
+
/**
|
|
27277
|
+
* Prepends the channel row to `createQueries`' output so the queries cannot fail on the `cid`
|
|
27278
|
+
* foreign key, sourcing the channel from the event and falling back to the in-memory channel.
|
|
27279
|
+
*
|
|
27280
|
+
* Skips the write entirely when neither can supply it: without a channel row the queries would fail
|
|
27281
|
+
* anyway, and dropping the event is what the caller expects.
|
|
27282
|
+
*/
|
|
27283
|
+
this.queriesBehindChannelGuard = async ({
|
|
27284
|
+
createQueries,
|
|
27285
|
+
event,
|
|
27286
|
+
execute
|
|
27287
|
+
}) => {
|
|
27288
|
+
const type = event.type;
|
|
27289
|
+
const channelFromEvent = event.channel;
|
|
27290
|
+
const event_ = event;
|
|
27291
|
+
let channelData = channelFromEvent;
|
|
27292
|
+
if (!channelData && event_.channel_type && event_.channel_id) {
|
|
27293
|
+
const channelFromState = this.client.channel(
|
|
27294
|
+
event_.channel_type,
|
|
27295
|
+
event_.channel_id
|
|
27296
|
+
);
|
|
27297
|
+
if (channelFromState.initialized && !channelFromState.pendingDisposal) {
|
|
27298
|
+
channelData = channelFromState.data;
|
|
26730
27299
|
}
|
|
26731
27300
|
}
|
|
26732
|
-
|
|
27301
|
+
if (!channelData) {
|
|
27302
|
+
logger16.withExtraTags("queriesWithChannelGuard").warn(
|
|
27303
|
+
`Received a "${type}" event for a non-initialized channel that is not in the database. Skipping the event.`,
|
|
27304
|
+
{ event }
|
|
27305
|
+
);
|
|
27306
|
+
return [];
|
|
27307
|
+
}
|
|
27308
|
+
const channelQuery = await this.upsertChannelData({
|
|
27309
|
+
channel: channelData,
|
|
27310
|
+
execute: false
|
|
27311
|
+
});
|
|
27312
|
+
if (!channelQuery) {
|
|
27313
|
+
logger16.withExtraTags("queriesWithChannelGuard").warn(
|
|
27314
|
+
`Could not create channel queries on a "${type}" event for an initialized channel that is not in the database. Skipping the event.`,
|
|
27315
|
+
{ event }
|
|
27316
|
+
);
|
|
27317
|
+
return [];
|
|
27318
|
+
}
|
|
27319
|
+
const createdQueries = await createQueries(false);
|
|
27320
|
+
const newQueries = [...channelQuery, ...createdQueries];
|
|
27321
|
+
if (execute) {
|
|
27322
|
+
await this.executeSqlBatch(newQueries);
|
|
27323
|
+
}
|
|
27324
|
+
return newQueries;
|
|
26733
27325
|
};
|
|
26734
27326
|
/**
|
|
26735
27327
|
* Handles a message.new event. Will always use a channel guard for the inner queries
|
|
@@ -26865,6 +27457,92 @@ var AbstractOfflineDB = class {
|
|
|
26865
27457
|
}
|
|
26866
27458
|
return queries;
|
|
26867
27459
|
};
|
|
27460
|
+
/**
|
|
27461
|
+
* Upsert a single message, first making sure its channel row exists so the insert cannot fail on the
|
|
27462
|
+
* foreign key.
|
|
27463
|
+
*
|
|
27464
|
+
* Concrete on purpose — it composes {@link channelExists}, {@link upsertChannelData} and
|
|
27465
|
+
* {@link upsertMessages}, so every implementation inherits it and no custom offline DB has to
|
|
27466
|
+
* implement anything new (same reasoning as {@link hardDeleteMessages}).
|
|
27467
|
+
*
|
|
27468
|
+
* The guard matters because the message-operations engine writes optimistically, and the very first
|
|
27469
|
+
* message in a freshly created channel can reach here before that channel has ever been persisted.
|
|
27470
|
+
* `upsertMessages` alone is used rather than `updateMessage` because `updateMessage` is an
|
|
27471
|
+
* UPDATE — it no-ops when the row does not exist yet, which is exactly the write-ahead case.
|
|
27472
|
+
*
|
|
27473
|
+
* @param payload.message - The message to persist. Its `cid` decides which channel row is guarded.
|
|
27474
|
+
* @param payload.execute - Whether to immediately execute the operation (optional, defaults to `true`).
|
|
27475
|
+
*/
|
|
27476
|
+
this.upsertMessageWithChannelGuard = async ({
|
|
27477
|
+
message,
|
|
27478
|
+
execute = true
|
|
27479
|
+
}) => {
|
|
27480
|
+
const { cid } = message;
|
|
27481
|
+
if (!cid) return [];
|
|
27482
|
+
const messages = [message];
|
|
27483
|
+
if (execute) {
|
|
27484
|
+
try {
|
|
27485
|
+
return await this.upsertMessages({ messages, execute: true });
|
|
27486
|
+
} catch (error) {
|
|
27487
|
+
if (await this.channelExists({ cid })) throw error;
|
|
27488
|
+
}
|
|
27489
|
+
return await this.upsertMessageBehindChannelGuard({ cid, messages, execute: true });
|
|
27490
|
+
}
|
|
27491
|
+
if (await this.channelExists({ cid })) {
|
|
27492
|
+
return await this.upsertMessages({ messages, execute: false });
|
|
27493
|
+
}
|
|
27494
|
+
return await this.upsertMessageBehindChannelGuard({ cid, messages, execute });
|
|
27495
|
+
};
|
|
27496
|
+
/**
|
|
27497
|
+
* Writes the channel row ahead of the messages so the insert cannot fail on the `cid` foreign key.
|
|
27498
|
+
* Skips the write when there is nothing to build a channel row from — the message stays correct in
|
|
27499
|
+
* memory either way, and an insert that cannot resolve its foreign key is not worth attempting.
|
|
27500
|
+
*/
|
|
27501
|
+
this.upsertMessageBehindChannelGuard = async ({
|
|
27502
|
+
cid,
|
|
27503
|
+
execute,
|
|
27504
|
+
messages
|
|
27505
|
+
}) => {
|
|
27506
|
+
const channel = this.client.activeChannels[cid];
|
|
27507
|
+
if (!channel?.initialized || channel.pendingDisposal) {
|
|
27508
|
+
logger16.withExtraTags("upsertMessageWithChannelGuard", cid).warn("Skipping message persistence: the channel is not in the database.");
|
|
27509
|
+
return [];
|
|
27510
|
+
}
|
|
27511
|
+
const queries = [
|
|
27512
|
+
...await this.upsertChannelData({
|
|
27513
|
+
channel: channel.data,
|
|
27514
|
+
execute: false
|
|
27515
|
+
}),
|
|
27516
|
+
...await this.upsertMessages({ messages, execute: false })
|
|
27517
|
+
];
|
|
27518
|
+
if (execute) {
|
|
27519
|
+
await this.executeSqlBatch(queries);
|
|
27520
|
+
}
|
|
27521
|
+
return queries;
|
|
27522
|
+
};
|
|
27523
|
+
/**
|
|
27524
|
+
* The messages of a channel that are still in a local `failed` state, i.e. composed by this user but
|
|
27525
|
+
* never accepted by the server.
|
|
27526
|
+
*
|
|
27527
|
+
* Concrete, composed from the existing {@link getChannels} primitive, so it adds nothing to
|
|
27528
|
+
* {@link OfflineDBApi} that a custom offline DB would have to implement.
|
|
27529
|
+
*
|
|
27530
|
+
* This is the buffer that lets a failed message survive. v9 recovered such messages by diffing a
|
|
27531
|
+
* second, lagging copy of the message list (the React SDK state) against `channel.state`; with a
|
|
27532
|
+
* single reactive source of truth the persisted row is the only place that memory can be
|
|
27533
|
+
* re-populated from — see `Channel.reload`.
|
|
27534
|
+
*
|
|
27535
|
+
* @param payload.cid - The channel whose failed messages to read.
|
|
27536
|
+
*/
|
|
27537
|
+
this.getFailedMessages = async ({
|
|
27538
|
+
cid
|
|
27539
|
+
}) => {
|
|
27540
|
+
const { initialized, userId } = this.state.getLatestValue();
|
|
27541
|
+
if (!initialized || !userId) return [];
|
|
27542
|
+
const channels = await this.getChannels({ cids: [cid], userId });
|
|
27543
|
+
const messages = channels?.[0]?.messages ?? [];
|
|
27544
|
+
return messages.map((message) => formatMessage(message)).filter((message) => message.status === "failed");
|
|
27545
|
+
};
|
|
26868
27546
|
/**
|
|
26869
27547
|
* A utility method to handle read events. It will calculate the state of the reads if
|
|
26870
27548
|
* present in the event, or optionally rely on the hard override in unreadMessages.
|
|
@@ -27322,46 +28000,23 @@ var AbstractOfflineDB = class {
|
|
|
27322
28000
|
* @param isPendingTask - a control value telling us if it's an actual pending task being executed
|
|
27323
28001
|
* or delayed execution
|
|
27324
28002
|
*/
|
|
28003
|
+
/**
|
|
28004
|
+
* Replays one queued operation, through the {@link QUEUEABLE_OPERATIONS} registry rather than a
|
|
28005
|
+
* per-type branch here — the same registry a first attempt resolves through, so the two cannot
|
|
28006
|
+
* describe different requests.
|
|
28007
|
+
*
|
|
28008
|
+
* @param task - the task to run.
|
|
28009
|
+
* @param isPendingTask - whether this is a replay rather than a first attempt. A first attempt has an
|
|
28010
|
+
* optimistic layer above it that settles local state; a replay does not, so some operations have to
|
|
28011
|
+
* settle it themselves (see `onReplay`).
|
|
28012
|
+
*/
|
|
27325
28013
|
this.executeTask = async ({ task }, isPendingTask = false) => {
|
|
27326
|
-
|
|
27327
|
-
|
|
27328
|
-
|
|
27329
|
-
|
|
27330
|
-
return await this.client._deleteMessage(...task.payload);
|
|
27331
|
-
}
|
|
27332
|
-
const { channelType, channelId } = task;
|
|
27333
|
-
if (channelType && channelId) {
|
|
27334
|
-
const channel = this.client.channel(channelType, channelId);
|
|
27335
|
-
if (task.type === "send-reaction") {
|
|
27336
|
-
return await channel._sendReaction(...task.payload);
|
|
27337
|
-
}
|
|
27338
|
-
if (task.type === "delete-reaction") {
|
|
27339
|
-
return await channel._deleteReaction(...task.payload);
|
|
27340
|
-
}
|
|
27341
|
-
if (task.type === "create-draft") {
|
|
27342
|
-
return await channel._createDraft(...task.payload);
|
|
27343
|
-
}
|
|
27344
|
-
if (task.type === "delete-draft") {
|
|
27345
|
-
return await channel._deleteDraft(...task.payload);
|
|
27346
|
-
}
|
|
27347
|
-
if (task.type === "send-message") {
|
|
27348
|
-
const newMessageResponse = await channel._sendMessage(...task.payload);
|
|
27349
|
-
const newMessage = newMessageResponse?.message;
|
|
27350
|
-
if (isPendingTask && newMessage) {
|
|
27351
|
-
if (newMessage?.parent_id) {
|
|
27352
|
-
this.client.threads.threadsById[newMessage.parent_id]?.upsertReplyLocally({
|
|
27353
|
-
message: newMessage,
|
|
27354
|
-
timestampChanged: true
|
|
27355
|
-
});
|
|
27356
|
-
}
|
|
27357
|
-
channel.messagePaginator.trackLastMessage(formatMessage(newMessage));
|
|
27358
|
-
}
|
|
27359
|
-
return newMessageResponse;
|
|
27360
|
-
}
|
|
28014
|
+
const result = await runQueueableOperation({ client: this.client, task });
|
|
28015
|
+
if (isPendingTask) {
|
|
28016
|
+
const operation = QUEUEABLE_OPERATIONS[task.type];
|
|
28017
|
+
operation.onReplay?.({ client: this.client, result, task });
|
|
27361
28018
|
}
|
|
27362
|
-
|
|
27363
|
-
`Tried to execute invalid pending task type (${task.type}) while synchronizing the database.`
|
|
27364
|
-
);
|
|
28019
|
+
return result;
|
|
27365
28020
|
};
|
|
27366
28021
|
/**
|
|
27367
28022
|
* A utility method used to execute all pending tasks. As each task succeeds execution,
|