stream-chat 9.50.3 → 10.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.browser.js +19798 -14472
- package/dist/cjs/index.browser.js.map +4 -4
- package/dist/cjs/index.node.js +19817 -14482
- package/dist/cjs/index.node.js.map +4 -4
- package/dist/esm/index.mjs +19795 -14469
- package/dist/esm/index.mjs.map +4 -4
- package/dist/types/ChannelManager.d.ts +221 -0
- package/dist/types/EventHandlerPipeline.d.ts +104 -0
- package/dist/types/api-client.d.ts +26 -0
- package/dist/types/campaign.d.ts +0 -44
- package/dist/types/channel.d.ts +454 -479
- package/dist/types/channel_batch_updater.d.ts +0 -93
- package/dist/types/channel_state.d.ts +53 -201
- package/dist/types/client.d.ts +451 -1920
- package/dist/types/client_state.d.ts +4 -4
- package/dist/types/configuration/InstanceConfigurationService.d.ts +26 -0
- package/dist/types/configuration/index.d.ts +1 -0
- package/dist/types/configuration/types.d.ts +53 -0
- package/dist/types/connection.d.ts +49 -41
- package/dist/types/connection_fallback.d.ts +6 -4
- package/dist/types/constants.d.ts +0 -4
- package/dist/types/custom_types.d.ts +2 -0
- package/dist/types/entityStore/EntityStore.d.ts +111 -0
- package/dist/types/entityStore/StoreBackedItemIndex.d.ts +60 -0
- package/dist/types/entityStore/applyReactionLocally.d.ts +26 -0
- package/dist/types/entityStore/index.d.ts +2 -0
- package/dist/types/errors.d.ts +10 -2
- package/dist/types/gen/chat/ChannelApi.d.ts +45 -0
- package/dist/types/gen/chat/ChatApi.d.ts +334 -0
- package/dist/types/gen/model-decoders/decoders.d.ts +3 -0
- package/dist/types/gen/model-decoders/event-decoder-mapping.d.ts +6 -0
- package/dist/types/gen/models/index.d.ts +8836 -0
- package/dist/types/gen/moderation/ModerationApi.d.ts +50 -0
- package/dist/types/gen-imports.d.ts +3 -0
- package/dist/types/index.d.ts +8 -7
- package/dist/types/insights.d.ts +9 -8
- package/dist/types/logger.d.ts +9 -0
- package/dist/types/messageComposer/LocationComposer.d.ts +7 -4
- package/dist/types/messageComposer/attachmentIdentity.d.ts +2 -2
- package/dist/types/messageComposer/attachmentManager.d.ts +8 -4
- package/dist/types/messageComposer/configuration/types.d.ts +11 -11
- package/dist/types/messageComposer/linkPreviewsManager.d.ts +25 -13
- package/dist/types/messageComposer/messageComposer.d.ts +10 -10
- package/dist/types/messageComposer/middleware/messageComposer/types.d.ts +3 -3
- package/dist/types/messageComposer/middleware/pollComposer/types.d.ts +4 -7
- package/dist/types/messageComposer/middleware/textComposer/commandUtils.d.ts +3 -3
- package/dist/types/messageComposer/middleware/textComposer/commands.d.ts +7 -7
- package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +218 -4
- package/dist/types/messageComposer/middleware/textComposer/types.d.ts +9 -4
- package/dist/types/messageComposer/pollComposer.d.ts +5 -4
- package/dist/types/messageComposer/textComposer.d.ts +8 -3
- package/dist/types/messageComposer/types.d.ts +6 -28
- package/dist/types/messageDelivery/MessageDeliveryReporter.d.ts +40 -25
- package/dist/types/messageDelivery/MessageReceiptsTracker.d.ts +53 -12
- package/dist/types/messageOperations/MessageOperationStatePolicy.d.ts +19 -0
- package/dist/types/messageOperations/MessageOperations.d.ts +17 -0
- package/dist/types/messageOperations/index.d.ts +3 -0
- package/dist/types/messageOperations/types.d.ts +49 -0
- package/dist/types/moderation.d.ts +21 -203
- package/dist/types/notifications/types.d.ts +8 -7
- package/dist/types/offline-support/offline_support_api.d.ts +111 -87
- package/dist/types/offline-support/offline_sync_manager.d.ts +0 -7
- package/dist/types/offline-support/types.d.ts +19 -23
- package/dist/types/offline-support/util.d.ts +4 -2
- package/dist/types/pagination/FilterBuilder.d.ts +1 -1
- package/dist/types/pagination/ItemIndex.d.ts +25 -0
- package/dist/types/pagination/cursorDerivation/createdAtAroundPaginationFlags.d.ts +9 -0
- package/dist/types/pagination/cursorDerivation/idAroundPaginationFlags.d.ts +6 -0
- package/dist/types/pagination/cursorDerivation/index.d.ts +1 -0
- package/dist/types/pagination/cursorDerivation/linearPaginationFlags.d.ts +6 -0
- package/dist/types/pagination/filterCompiler.d.ts +7 -0
- package/dist/types/pagination/index.d.ts +1 -3
- package/dist/types/pagination/paginators/BasePaginator.d.ts +848 -0
- package/dist/types/pagination/paginators/ChannelPaginator.d.ts +212 -0
- package/dist/types/pagination/paginators/MessageIntervalPaginator.d.ts +282 -0
- package/dist/types/pagination/paginators/MessagePaginator.d.ts +208 -0
- package/dist/types/pagination/paginators/PinnedMessagePaginator.d.ts +39 -0
- package/dist/types/pagination/paginators/ReminderPaginator.d.ts +19 -0
- package/dist/types/pagination/paginators/UserGroupPaginator.d.ts +24 -0
- package/dist/types/pagination/paginators/index.d.ts +7 -0
- package/dist/types/pagination/paginators/stateThrottling.d.ts +2 -0
- package/dist/types/pagination/sortCompiler.d.ts +49 -0
- package/dist/types/pagination/types.normalization.d.ts +11 -0
- package/dist/types/pagination/utility.normalization.d.ts +18 -0
- package/dist/types/pagination/utility.queryChannel.d.ts +26 -0
- package/dist/types/pagination/utility.search.d.ts +13 -0
- package/dist/types/permissions.d.ts +1 -1
- package/dist/types/poll.d.ts +29 -32
- package/dist/types/poll_manager.d.ts +2 -2
- package/dist/types/reminders/Reminder.d.ts +2 -5
- package/dist/types/reminders/ReminderManager.d.ts +2 -9
- package/dist/types/search/MessageSearchSource.d.ts +4 -4
- package/dist/types/search/UserSearchSource.d.ts +1 -1
- package/dist/types/search/types.d.ts +3 -3
- package/dist/types/segment.d.ts +0 -34
- package/dist/types/signing.d.ts +105 -65
- package/dist/types/store.d.ts +1 -0
- package/dist/types/thread.d.ts +70 -34
- package/dist/types/thread_manager.d.ts +3 -3
- package/dist/types/token_manager.d.ts +15 -10
- package/dist/types/types.d.ts +197 -3404
- package/dist/types/utils/FixedSizeQueueCache.d.ts +12 -6
- package/dist/types/utils/WithSubscriptions.d.ts +2 -1
- package/dist/types/utils/concurrency.d.ts +4 -4
- package/dist/types/utils/mergeWith/mergeWith.d.ts +3 -3
- package/dist/types/utils/mergeWith/mergeWithCore.d.ts +8 -3
- package/dist/types/utils/retryable.d.ts +34 -0
- package/dist/types/utils/throttling/throttle.d.ts +36 -0
- package/dist/types/utils.d.ts +100 -194
- package/package.json +7 -3
- package/src/ChannelManager.ts +724 -0
- package/src/CooldownTimer.ts +2 -2
- package/src/EventHandlerPipeline.ts +228 -0
- package/src/LiveLocationManager.ts +12 -11
- package/src/api-client.ts +275 -0
- package/src/campaign.ts +1 -77
- package/src/channel.ts +1344 -1282
- package/src/channel_batch_updater.ts +1 -211
- package/src/channel_state.ts +144 -1013
- package/src/client.ts +936 -3995
- package/src/client_state.ts +4 -4
- package/src/configuration/InstanceConfigurationService.ts +73 -0
- package/src/configuration/index.ts +1 -0
- package/src/configuration/types.ts +81 -0
- package/src/connection.ts +145 -94
- package/src/connection_fallback.ts +29 -27
- package/src/constants.ts +1 -5
- package/src/custom_types.ts +4 -1
- package/src/entityStore/EntityStore.ts +208 -0
- package/src/entityStore/StoreBackedItemIndex.ts +115 -0
- package/src/entityStore/applyReactionLocally.ts +113 -0
- package/src/entityStore/index.ts +2 -0
- package/src/errors.ts +14 -2
- package/src/gen/chat/ChannelApi.ts +275 -0
- package/src/gen/chat/ChatApi.ts +2554 -0
- package/src/gen/model-decoders/decoders.ts +2680 -0
- package/src/gen/model-decoders/event-decoder-mapping.ts +198 -0
- package/src/gen/models/index.ts +12673 -0
- package/src/gen/moderation/ModerationApi.ts +607 -0
- package/src/gen-imports.ts +3 -0
- package/src/index.ts +11 -12
- package/src/insights.ts +6 -5
- package/src/logger.ts +28 -0
- package/src/messageComposer/LocationComposer.ts +9 -5
- package/src/messageComposer/MessageComposerEffectHandlers.ts +1 -0
- package/src/messageComposer/attachmentIdentity.ts +16 -9
- package/src/messageComposer/attachmentManager.ts +13 -8
- package/src/messageComposer/configuration/types.ts +11 -11
- package/src/messageComposer/linkPreviewsManager.ts +9 -10
- package/src/messageComposer/messageComposer.ts +57 -53
- package/src/messageComposer/middleware/messageComposer/attachments.ts +1 -2
- package/src/messageComposer/middleware/messageComposer/cleanData.ts +1 -1
- package/src/messageComposer/middleware/messageComposer/compositionValidation.ts +2 -2
- package/src/messageComposer/middleware/messageComposer/messageComposerState.ts +2 -2
- package/src/messageComposer/middleware/messageComposer/sharedLocation.ts +4 -3
- package/src/messageComposer/middleware/messageComposer/textComposer.ts +2 -2
- package/src/messageComposer/middleware/messageComposer/types.ts +3 -4
- package/src/messageComposer/middleware/messageComposer/userDataInjection.ts +9 -5
- package/src/messageComposer/middleware/pollComposer/state.ts +0 -2
- package/src/messageComposer/middleware/pollComposer/types.ts +4 -7
- package/src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts +10 -1
- package/src/messageComposer/middleware/textComposer/commandEffects.ts +4 -4
- package/src/messageComposer/middleware/textComposer/commandUtils.ts +3 -6
- package/src/messageComposer/middleware/textComposer/commands.ts +3 -3
- package/src/messageComposer/middleware/textComposer/mentionUtils.ts +1 -1
- package/src/messageComposer/middleware/textComposer/mentions.ts +31 -15
- package/src/messageComposer/middleware/textComposer/types.ts +9 -4
- package/src/messageComposer/pollComposer.ts +6 -8
- package/src/messageComposer/textComposer.ts +27 -2
- package/src/messageComposer/types.ts +6 -28
- package/src/messageDelivery/MessageDeliveryReporter.ts +101 -45
- package/src/messageDelivery/MessageReceiptsTracker.ts +294 -21
- package/src/messageOperations/MessageOperationStatePolicy.ts +77 -0
- package/src/messageOperations/MessageOperations.ts +212 -0
- package/src/messageOperations/index.ts +10 -0
- package/src/messageOperations/types.ts +64 -0
- package/src/moderation.ts +38 -431
- package/src/notifications/types.ts +8 -7
- package/src/offline-support/offline_support_api.ts +200 -133
- package/src/offline-support/offline_sync_manager.ts +23 -44
- package/src/offline-support/types.ts +23 -26
- package/src/offline-support/util.ts +5 -4
- package/src/pagination/FilterBuilder.ts +4 -1
- package/src/pagination/ItemIndex.ts +25 -0
- package/src/pagination/cursorDerivation/createdAtAroundPaginationFlags.ts +73 -0
- package/src/pagination/cursorDerivation/idAroundPaginationFlags.ts +53 -0
- package/src/pagination/cursorDerivation/index.ts +1 -0
- package/src/pagination/cursorDerivation/linearPaginationFlags.ts +86 -0
- package/src/pagination/filterCompiler.ts +196 -0
- package/src/pagination/index.ts +1 -3
- package/src/pagination/paginators/BasePaginator.ts +2859 -0
- package/src/pagination/paginators/ChannelPaginator.ts +729 -0
- package/src/pagination/paginators/MessageIntervalPaginator.ts +1116 -0
- package/src/pagination/paginators/MessagePaginator.ts +508 -0
- package/src/pagination/paginators/PinnedMessagePaginator.ts +108 -0
- package/src/pagination/paginators/ReminderPaginator.ts +107 -0
- package/src/pagination/paginators/UserGroupPaginator.ts +127 -0
- package/src/pagination/paginators/index.ts +7 -0
- package/src/pagination/paginators/stateThrottling.ts +31 -0
- package/src/pagination/sortCompiler.ts +193 -0
- package/src/pagination/types.normalization.ts +14 -0
- package/src/pagination/utility.normalization.ts +106 -0
- package/src/pagination/utility.queryChannel.ts +84 -0
- package/src/pagination/utility.search.ts +80 -0
- package/src/permissions.ts +7 -8
- package/src/poll.ts +101 -110
- package/src/poll_manager.ts +14 -8
- package/src/reminders/Reminder.ts +2 -5
- package/src/reminders/ReminderManager.ts +19 -21
- package/src/search/BaseSearchSource.ts +0 -3
- package/src/search/ChannelMemberSearchSource.ts +7 -1
- package/src/search/ChannelSearchSource.ts +10 -3
- package/src/search/MessageSearchSource.ts +29 -30
- package/src/search/UserSearchSource.ts +12 -8
- package/src/search/types.ts +3 -4
- package/src/segment.ts +1 -95
- package/src/signing.ts +107 -67
- package/src/store.ts +2 -1
- package/src/thread.ts +562 -238
- package/src/thread_manager.ts +37 -16
- package/src/token_manager.ts +21 -10
- package/src/types.ts +364 -4533
- package/src/uploadManager.ts +8 -0
- package/src/utils/FixedSizeQueueCache.ts +12 -6
- package/src/utils/WithSubscriptions.ts +2 -1
- package/src/utils/concurrency.ts +4 -4
- package/src/utils/mergeWith/mergeWith.ts +3 -3
- package/src/utils/mergeWith/mergeWithCore.ts +140 -164
- package/src/utils/mergeWith/mergeWithDiff.ts +3 -3
- package/src/utils/retryable.ts +117 -0
- package/src/utils/throttling/throttle.ts +130 -0
- package/src/utils.ts +208 -754
- package/dist/types/channel_manager.d.ts +0 -122
- package/dist/types/events.d.ts +0 -72
- package/dist/types/pagination/BasePaginator.d.ts +0 -69
- package/dist/types/pagination/ReminderPaginator.d.ts +0 -16
- package/dist/types/pagination/UserGroupPaginator.d.ts +0 -21
- package/src/channel_manager.ts +0 -830
- package/src/events.ts +0 -75
- package/src/pagination/BasePaginator.ts +0 -184
- package/src/pagination/ReminderPaginator.ts +0 -56
- package/src/pagination/UserGroupPaginator.ts +0 -93
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ReadStateResponse, UserResponse } from '../types';
|
|
2
|
+
import { StateStore } from '../store';
|
|
3
|
+
import type { Channel } from '../channel';
|
|
4
|
+
import { WithSubscriptions } from '../utils/WithSubscriptions';
|
|
2
5
|
|
|
3
6
|
type UserId = string;
|
|
4
7
|
type MessageId = string;
|
|
@@ -11,11 +14,39 @@ export type UserProgress = {
|
|
|
11
14
|
lastReadRef: MsgRef; // MIN_REF if none
|
|
12
15
|
lastDeliveredRef: MsgRef; // MIN_REF if none; always >= readRef
|
|
13
16
|
};
|
|
17
|
+
export type MessageReceiptsSnapshot = {
|
|
18
|
+
revision: number;
|
|
19
|
+
readersByMessageId: Record<MessageId, UserResponse[]>;
|
|
20
|
+
deliveredByMessageId: Record<MessageId, UserResponse[]>;
|
|
21
|
+
};
|
|
22
|
+
export type ReadStoreReconcileMeta = {
|
|
23
|
+
changedUserIds?: string[];
|
|
24
|
+
removedUserIds?: string[];
|
|
25
|
+
};
|
|
26
|
+
type ReadStoreUserState = {
|
|
27
|
+
last_read?: Date | string;
|
|
28
|
+
unread_messages?: number;
|
|
29
|
+
user?: UserResponse;
|
|
30
|
+
first_unread_message_id?: string;
|
|
31
|
+
last_read_message_id?: string;
|
|
32
|
+
last_delivered_at?: Date | string;
|
|
33
|
+
last_delivered_message_id?: string;
|
|
34
|
+
};
|
|
14
35
|
|
|
15
36
|
// ---------- ordering utilities ----------
|
|
16
37
|
|
|
17
38
|
const MIN_REF: MsgRef = { timestampMs: Number.NEGATIVE_INFINITY, msgId: '' } as const;
|
|
18
39
|
|
|
40
|
+
const toTimestampMs = (value: Date | string) =>
|
|
41
|
+
value instanceof Date ? value.getTime() : new Date(value).getTime();
|
|
42
|
+
|
|
43
|
+
const isValidReadState = (
|
|
44
|
+
readState: ReadStoreUserState | undefined,
|
|
45
|
+
): readState is ReadStoreUserState & {
|
|
46
|
+
last_read: Date | string;
|
|
47
|
+
user: UserResponse;
|
|
48
|
+
} => !!readState?.user && !!readState.last_read;
|
|
49
|
+
|
|
19
50
|
const compareRefsAsc = (a: MsgRef, b: MsgRef) =>
|
|
20
51
|
a.timestampMs !== b.timestampMs ? a.timestampMs - b.timestampMs : 0;
|
|
21
52
|
|
|
@@ -31,10 +62,13 @@ const findIndex = <T>(arr: T[], target: MsgRef, keyOf: (x: T) => MsgRef): number
|
|
|
31
62
|
};
|
|
32
63
|
|
|
33
64
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* @param
|
|
65
|
+
* Finds the insertion index after the last equal item. E.g. when array `[a]` exists and `b` is
|
|
66
|
+
* being inserted we want `[a, b]`, not `[b, a]`.
|
|
67
|
+
*
|
|
68
|
+
* @param arr - The sorted array to search.
|
|
69
|
+
* @param target - The reference value to compare against.
|
|
70
|
+
* @param keyOf - Accessor that maps an item to its comparable reference.
|
|
71
|
+
* @returns The insertion index in `arr`.
|
|
38
72
|
*/
|
|
39
73
|
const findUpperIndex = <T>(arr: T[], target: MsgRef, keyOf: (x: T) => MsgRef): number => {
|
|
40
74
|
let lo = 0,
|
|
@@ -71,7 +105,8 @@ const removeByOldKey = (
|
|
|
71
105
|
};
|
|
72
106
|
|
|
73
107
|
export type OwnMessageReceiptsTrackerOptions = {
|
|
74
|
-
|
|
108
|
+
channel: Channel;
|
|
109
|
+
locateMessage?: OwnMessageReceiptsTrackerMessageLocator;
|
|
75
110
|
};
|
|
76
111
|
|
|
77
112
|
/**
|
|
@@ -92,13 +127,14 @@ export type OwnMessageReceiptsTrackerOptions = {
|
|
|
92
127
|
*
|
|
93
128
|
* Construction
|
|
94
129
|
* ------------
|
|
95
|
-
* `new MessageReceiptsTracker({locateMessage})`
|
|
96
|
-
* -
|
|
97
|
-
*
|
|
130
|
+
* `new MessageReceiptsTracker({ channel, locateMessage? })`
|
|
131
|
+
* - By default, message references are read through `channel.state.findMessageByTimestamp`.
|
|
132
|
+
* - `locateMessage` can override this lookup strategy.
|
|
133
|
+
* If a message cannot be resolved locally, the event is ignored.
|
|
98
134
|
*
|
|
99
135
|
* Event ingestion
|
|
100
136
|
* ---------------
|
|
101
|
-
* - `ingestInitial(rows:
|
|
137
|
+
* - `ingestInitial(rows: ReadStateResponse[])`: Builds initial state from server snapshot.
|
|
102
138
|
* If a user’s `last_read` is ahead of `last_delivered_at`, the tracker enforces
|
|
103
139
|
* the invariant `lastDeliveredRef >= lastReadRef`.
|
|
104
140
|
* - `onMessageRead(user, readAtISO)`:
|
|
@@ -131,18 +167,104 @@ export type OwnMessageReceiptsTrackerOptions = {
|
|
|
131
167
|
* equal-timestamp plateau (upper-bound insertion), preserving intuitive arrival order.
|
|
132
168
|
* - This tracker models **others’ progress toward own messages**;
|
|
133
169
|
*/
|
|
134
|
-
export class MessageReceiptsTracker {
|
|
170
|
+
export class MessageReceiptsTracker extends WithSubscriptions {
|
|
135
171
|
private byUser = new Map<UserId, UserProgress>();
|
|
136
172
|
private readSorted: UserProgress[] = []; // asc by lastReadRef
|
|
137
173
|
private deliveredSorted: UserProgress[] = []; // asc by lastDeliveredRef
|
|
174
|
+
private channel: Channel;
|
|
138
175
|
private locateMessage: OwnMessageReceiptsTrackerMessageLocator;
|
|
176
|
+
private pendingReadStoreReconcileMeta?: ReadStoreReconcileMeta;
|
|
177
|
+
readonly snapshotStore = new StateStore<MessageReceiptsSnapshot>({
|
|
178
|
+
revision: 0,
|
|
179
|
+
readersByMessageId: {},
|
|
180
|
+
deliveredByMessageId: {},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
constructor({ channel, locateMessage }: OwnMessageReceiptsTrackerOptions) {
|
|
184
|
+
super();
|
|
185
|
+
this.channel = channel;
|
|
186
|
+
this.locateMessage =
|
|
187
|
+
locateMessage ??
|
|
188
|
+
((timestampMs: number) => {
|
|
189
|
+
const message = this.channel.messagePaginator.findItemByTimestamp(timestampMs);
|
|
190
|
+
return message ? { timestampMs, msgId: message.id } : null;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public registerSubscriptions = () => {
|
|
195
|
+
this.incrementRefCount();
|
|
196
|
+
if (this.hasSubscriptions) return;
|
|
197
|
+
|
|
198
|
+
this.addUnsubscribeFunction(
|
|
199
|
+
this.channel.state.readStore.subscribe((next, prev) => {
|
|
200
|
+
this.reconcileFromReadStore({
|
|
201
|
+
previousReadState: prev?.read,
|
|
202
|
+
nextReadState: next.read,
|
|
203
|
+
meta: this.pendingReadStoreReconcileMeta,
|
|
204
|
+
});
|
|
205
|
+
this.pendingReadStoreReconcileMeta = undefined;
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
public unregisterSubscriptions = () => {
|
|
211
|
+
this.pendingReadStoreReconcileMeta = undefined;
|
|
212
|
+
return super.unregisterSubscriptions();
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
public setPendingReadStoreReconcileMeta(meta?: ReadStoreReconcileMeta) {
|
|
216
|
+
this.pendingReadStoreReconcileMeta = meta;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
reconcileFromReadStore({
|
|
220
|
+
previousReadState,
|
|
221
|
+
nextReadState,
|
|
222
|
+
meta,
|
|
223
|
+
}: {
|
|
224
|
+
previousReadState?: Record<string, { user: UserResponse }>;
|
|
225
|
+
nextReadState: Record<string, ReadStoreUserState>;
|
|
226
|
+
meta?: ReadStoreReconcileMeta;
|
|
227
|
+
}) {
|
|
228
|
+
if (!previousReadState) {
|
|
229
|
+
this.ingestInitial(this.readStoreStateToResponses(nextReadState));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// For non-bootstrap updates, we require patch metadata from channel read-store mutations.
|
|
234
|
+
if (!meta) return;
|
|
235
|
+
|
|
236
|
+
const removedUserIds = new Set(meta?.removedUserIds ?? []);
|
|
237
|
+
const changedUserIds = new Set(meta?.changedUserIds ?? []);
|
|
139
238
|
|
|
140
|
-
|
|
141
|
-
|
|
239
|
+
const changedOrRemovedUserIds = new Set<string>([
|
|
240
|
+
...changedUserIds,
|
|
241
|
+
...removedUserIds,
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
if (!changedOrRemovedUserIds.size) return;
|
|
245
|
+
|
|
246
|
+
let hasEffectiveChange = false;
|
|
247
|
+
|
|
248
|
+
for (const userId of changedOrRemovedUserIds) {
|
|
249
|
+
if (removedUserIds.has(userId) || !nextReadState[userId]) {
|
|
250
|
+
hasEffectiveChange = this.removeUserProgress(userId) || hasEffectiveChange;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const nextUserReadState = nextReadState[userId];
|
|
255
|
+
if (!isValidReadState(nextUserReadState)) continue;
|
|
256
|
+
const resolvedProgress = this.readStateToUserProgress(nextUserReadState);
|
|
257
|
+
hasEffectiveChange =
|
|
258
|
+
this.upsertUserProgress(resolvedProgress) || hasEffectiveChange;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (hasEffectiveChange) {
|
|
262
|
+
this.emitSnapshot();
|
|
263
|
+
}
|
|
142
264
|
}
|
|
143
265
|
|
|
144
266
|
/** Build initial state from server snapshots (single pass + sort). */
|
|
145
|
-
ingestInitial(responses:
|
|
267
|
+
ingestInitial(responses: ReadStateResponse[]) {
|
|
146
268
|
this.byUser.clear();
|
|
147
269
|
this.readSorted = [];
|
|
148
270
|
this.deliveredSorted = [];
|
|
@@ -173,6 +295,8 @@ export class MessageReceiptsTracker {
|
|
|
173
295
|
userProgress,
|
|
174
296
|
);
|
|
175
297
|
}
|
|
298
|
+
|
|
299
|
+
this.emitSnapshot();
|
|
176
300
|
}
|
|
177
301
|
|
|
178
302
|
/** message.delivered — user device confirmed delivery up to and including messageId. */
|
|
@@ -182,13 +306,13 @@ export class MessageReceiptsTracker {
|
|
|
182
306
|
lastDeliveredMessageId,
|
|
183
307
|
}: {
|
|
184
308
|
user: UserResponse;
|
|
185
|
-
deliveredAt:
|
|
309
|
+
deliveredAt: Date;
|
|
186
310
|
lastDeliveredMessageId?: string;
|
|
187
311
|
}) {
|
|
188
|
-
const timestampMs =
|
|
312
|
+
const timestampMs = deliveredAt.getTime();
|
|
189
313
|
const msgRef = lastDeliveredMessageId
|
|
190
314
|
? { timestampMs, msgId: lastDeliveredMessageId }
|
|
191
|
-
: this.locateMessage(
|
|
315
|
+
: this.locateMessage(deliveredAt.getTime());
|
|
192
316
|
if (!msgRef) return;
|
|
193
317
|
const userProgress = this.ensureUser(user);
|
|
194
318
|
|
|
@@ -207,6 +331,7 @@ export class MessageReceiptsTracker {
|
|
|
207
331
|
);
|
|
208
332
|
userProgress.lastDeliveredRef = newDelivered;
|
|
209
333
|
insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
|
|
334
|
+
this.emitSnapshot();
|
|
210
335
|
}
|
|
211
336
|
|
|
212
337
|
/** message.read — user read up to and including messageId. */
|
|
@@ -216,10 +341,10 @@ export class MessageReceiptsTracker {
|
|
|
216
341
|
lastReadMessageId,
|
|
217
342
|
}: {
|
|
218
343
|
user: UserResponse;
|
|
219
|
-
readAt:
|
|
344
|
+
readAt: Date;
|
|
220
345
|
lastReadMessageId?: string;
|
|
221
346
|
}) {
|
|
222
|
-
const timestampMs =
|
|
347
|
+
const timestampMs = readAt.getTime();
|
|
223
348
|
const msgRef = lastReadMessageId
|
|
224
349
|
? { timestampMs, msgId: lastReadMessageId }
|
|
225
350
|
: this.locateMessage(timestampMs);
|
|
@@ -249,6 +374,8 @@ export class MessageReceiptsTracker {
|
|
|
249
374
|
userProgress.lastDeliveredRef = userProgress.lastReadRef;
|
|
250
375
|
insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
|
|
251
376
|
}
|
|
377
|
+
|
|
378
|
+
this.emitSnapshot();
|
|
252
379
|
}
|
|
253
380
|
|
|
254
381
|
/** notification.mark_unread — user marked messages unread starting at `first_unread_message_id`.
|
|
@@ -263,13 +390,13 @@ export class MessageReceiptsTracker {
|
|
|
263
390
|
lastReadMessageId,
|
|
264
391
|
}: {
|
|
265
392
|
user: UserResponse;
|
|
266
|
-
lastReadAt?:
|
|
393
|
+
lastReadAt?: Date;
|
|
267
394
|
lastReadMessageId?: string;
|
|
268
395
|
}) {
|
|
269
396
|
const userProgress = this.ensureUser(user);
|
|
270
397
|
|
|
271
398
|
const newReadRef: MsgRef = lastReadAt
|
|
272
|
-
? { timestampMs:
|
|
399
|
+
? { timestampMs: lastReadAt.getTime(), msgId: lastReadMessageId ?? '' }
|
|
273
400
|
: { ...MIN_REF };
|
|
274
401
|
|
|
275
402
|
// If no change, exit early.
|
|
@@ -300,6 +427,8 @@ export class MessageReceiptsTracker {
|
|
|
300
427
|
userProgress.lastDeliveredRef = userProgress.lastReadRef;
|
|
301
428
|
insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
|
|
302
429
|
}
|
|
430
|
+
|
|
431
|
+
this.emitSnapshot();
|
|
303
432
|
}
|
|
304
433
|
|
|
305
434
|
/** All users who READ this message. */
|
|
@@ -414,4 +543,148 @@ export class MessageReceiptsTracker {
|
|
|
414
543
|
}
|
|
415
544
|
return up;
|
|
416
545
|
}
|
|
546
|
+
|
|
547
|
+
private removeUserProgress(userId: string) {
|
|
548
|
+
const userProgress = this.byUser.get(userId);
|
|
549
|
+
if (!userProgress) return false;
|
|
550
|
+
|
|
551
|
+
removeByOldKey(
|
|
552
|
+
this.readSorted,
|
|
553
|
+
userProgress,
|
|
554
|
+
userProgress.lastReadRef,
|
|
555
|
+
(x) => x.lastReadRef,
|
|
556
|
+
);
|
|
557
|
+
removeByOldKey(
|
|
558
|
+
this.deliveredSorted,
|
|
559
|
+
userProgress,
|
|
560
|
+
userProgress.lastDeliveredRef,
|
|
561
|
+
(x) => x.lastDeliveredRef,
|
|
562
|
+
);
|
|
563
|
+
this.byUser.delete(userId);
|
|
564
|
+
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private upsertUserProgress(nextUserProgress: UserProgress) {
|
|
569
|
+
const existingUserProgress = this.byUser.get(nextUserProgress.user.id);
|
|
570
|
+
if (!existingUserProgress) {
|
|
571
|
+
this.byUser.set(nextUserProgress.user.id, nextUserProgress);
|
|
572
|
+
insertByKey(this.readSorted, nextUserProgress, (x) => x.lastReadRef);
|
|
573
|
+
insertByKey(this.deliveredSorted, nextUserProgress, (x) => x.lastDeliveredRef);
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const hasSameReadRef =
|
|
578
|
+
compareRefsAsc(existingUserProgress.lastReadRef, nextUserProgress.lastReadRef) ===
|
|
579
|
+
0 &&
|
|
580
|
+
existingUserProgress.lastReadRef.msgId === nextUserProgress.lastReadRef.msgId;
|
|
581
|
+
const hasSameDeliveredRef =
|
|
582
|
+
compareRefsAsc(
|
|
583
|
+
existingUserProgress.lastDeliveredRef,
|
|
584
|
+
nextUserProgress.lastDeliveredRef,
|
|
585
|
+
) === 0 &&
|
|
586
|
+
existingUserProgress.lastDeliveredRef.msgId ===
|
|
587
|
+
nextUserProgress.lastDeliveredRef.msgId;
|
|
588
|
+
const hasSameUser = existingUserProgress.user.id === nextUserProgress.user.id;
|
|
589
|
+
|
|
590
|
+
if (hasSameReadRef && hasSameDeliveredRef && hasSameUser) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
removeByOldKey(
|
|
595
|
+
this.readSorted,
|
|
596
|
+
existingUserProgress,
|
|
597
|
+
existingUserProgress.lastReadRef,
|
|
598
|
+
(x) => x.lastReadRef,
|
|
599
|
+
);
|
|
600
|
+
removeByOldKey(
|
|
601
|
+
this.deliveredSorted,
|
|
602
|
+
existingUserProgress,
|
|
603
|
+
existingUserProgress.lastDeliveredRef,
|
|
604
|
+
(x) => x.lastDeliveredRef,
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
existingUserProgress.user = nextUserProgress.user;
|
|
608
|
+
existingUserProgress.lastReadRef = nextUserProgress.lastReadRef;
|
|
609
|
+
existingUserProgress.lastDeliveredRef = nextUserProgress.lastDeliveredRef;
|
|
610
|
+
|
|
611
|
+
insertByKey(this.readSorted, existingUserProgress, (x) => x.lastReadRef);
|
|
612
|
+
insertByKey(this.deliveredSorted, existingUserProgress, (x) => x.lastDeliveredRef);
|
|
613
|
+
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private readStateToUserProgress(readState: {
|
|
618
|
+
last_read: Date | string;
|
|
619
|
+
unread_messages?: number;
|
|
620
|
+
user: UserResponse;
|
|
621
|
+
first_unread_message_id?: string;
|
|
622
|
+
last_read_message_id?: string;
|
|
623
|
+
last_delivered_at?: Date | string;
|
|
624
|
+
last_delivered_message_id?: string;
|
|
625
|
+
}): UserProgress {
|
|
626
|
+
const lastReadTimestamp = toTimestampMs(readState.last_read);
|
|
627
|
+
const lastDeliveredTimestamp = readState.last_delivered_at
|
|
628
|
+
? toTimestampMs(readState.last_delivered_at)
|
|
629
|
+
: null;
|
|
630
|
+
const lastReadRef = readState.last_read_message_id
|
|
631
|
+
? { timestampMs: lastReadTimestamp, msgId: readState.last_read_message_id }
|
|
632
|
+
: (this.locateMessage(lastReadTimestamp) ?? MIN_REF);
|
|
633
|
+
let lastDeliveredRef = readState.last_delivered_message_id
|
|
634
|
+
? {
|
|
635
|
+
timestampMs: lastDeliveredTimestamp ?? lastReadTimestamp,
|
|
636
|
+
msgId: readState.last_delivered_message_id,
|
|
637
|
+
}
|
|
638
|
+
: lastDeliveredTimestamp
|
|
639
|
+
? (this.locateMessage(lastDeliveredTimestamp) ?? MIN_REF)
|
|
640
|
+
: MIN_REF;
|
|
641
|
+
|
|
642
|
+
if (compareRefsAsc(lastDeliveredRef, lastReadRef) < 0) {
|
|
643
|
+
lastDeliveredRef = lastReadRef;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
user: readState.user,
|
|
648
|
+
lastReadRef,
|
|
649
|
+
lastDeliveredRef,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private readStoreStateToResponses(
|
|
654
|
+
readState: Record<string, ReadStoreUserState>,
|
|
655
|
+
): ReadStateResponse[] {
|
|
656
|
+
return Object.values(readState).reduce<ReadStateResponse[]>(
|
|
657
|
+
(responses, userReadState) => {
|
|
658
|
+
if (!isValidReadState(userReadState)) return responses;
|
|
659
|
+
const lastReadDate = new Date(userReadState.last_read);
|
|
660
|
+
if (Number.isNaN(lastReadDate.getTime())) return responses;
|
|
661
|
+
|
|
662
|
+
responses.push({
|
|
663
|
+
last_read: lastReadDate,
|
|
664
|
+
user: userReadState.user,
|
|
665
|
+
last_read_message_id: userReadState.last_read_message_id,
|
|
666
|
+
unread_messages: userReadState.unread_messages ?? 0,
|
|
667
|
+
last_delivered_at: userReadState.last_delivered_at
|
|
668
|
+
? new Date(userReadState.last_delivered_at)
|
|
669
|
+
: undefined,
|
|
670
|
+
last_delivered_message_id: userReadState.last_delivered_message_id,
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
return responses;
|
|
674
|
+
},
|
|
675
|
+
[],
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
private emitSnapshot() {
|
|
680
|
+
const readersByMessageId = this.groupUsersByLastReadMessage();
|
|
681
|
+
const deliveredByMessageId = this.groupUsersByLastDeliveredMessage();
|
|
682
|
+
const currentSnapshot = this.snapshotStore.getLatestValue();
|
|
683
|
+
|
|
684
|
+
this.snapshotStore.next({
|
|
685
|
+
revision: currentSnapshot.revision + 1,
|
|
686
|
+
readersByMessageId,
|
|
687
|
+
deliveredByMessageId,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
417
690
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { LocalMessage, MessageResponse, StreamAPIError } from '../types';
|
|
2
|
+
import { formatMessage } from '../utils';
|
|
3
|
+
|
|
4
|
+
export type MessageOperationStatePolicyContext = {
|
|
5
|
+
ingest: (m: LocalMessage) => void;
|
|
6
|
+
get: (id: string) => LocalMessage | undefined;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const parseError = (error: unknown): StreamAPIError => {
|
|
10
|
+
const stringError = JSON.stringify(error);
|
|
11
|
+
return (stringError ? JSON.parse(stringError) : {}) as StreamAPIError;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const isAlreadyExistsError = (error: unknown, parsed: StreamAPIError) =>
|
|
15
|
+
parsed.code === 4 && error instanceof Error && error.message.includes('already exists');
|
|
16
|
+
|
|
17
|
+
export class MessageOperationStatePolicy {
|
|
18
|
+
private ctx: MessageOperationStatePolicyContext;
|
|
19
|
+
|
|
20
|
+
constructor(ctx: MessageOperationStatePolicyContext) {
|
|
21
|
+
this.ctx = ctx;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
optimistic(localMessage: LocalMessage) {
|
|
25
|
+
this.ctx.ingest({
|
|
26
|
+
...localMessage,
|
|
27
|
+
error: undefined,
|
|
28
|
+
status:
|
|
29
|
+
!localMessage.status || localMessage.status === 'failed'
|
|
30
|
+
? 'sending'
|
|
31
|
+
: localMessage.status,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
success({
|
|
36
|
+
messageFromResponse,
|
|
37
|
+
messageId,
|
|
38
|
+
}: {
|
|
39
|
+
messageFromResponse: MessageResponse;
|
|
40
|
+
messageId: string;
|
|
41
|
+
}) {
|
|
42
|
+
const formatted = formatMessage({ ...messageFromResponse, status: 'received' });
|
|
43
|
+
const existing = this.ctx.get(messageId);
|
|
44
|
+
|
|
45
|
+
const serverNewer =
|
|
46
|
+
!existing || formatted.updated_at.getTime() > existing.updated_at.getTime();
|
|
47
|
+
const serverSameOrNewer =
|
|
48
|
+
!existing || formatted.updated_at.getTime() >= existing.updated_at.getTime();
|
|
49
|
+
const existingIsOurOptimisticSend = existing?.status === 'sending';
|
|
50
|
+
|
|
51
|
+
if (serverNewer || (existingIsOurOptimisticSend && serverSameOrNewer)) {
|
|
52
|
+
this.ctx.ingest(formatted);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
failure({
|
|
57
|
+
error,
|
|
58
|
+
localMessage,
|
|
59
|
+
messageId,
|
|
60
|
+
}: {
|
|
61
|
+
error: unknown;
|
|
62
|
+
localMessage: LocalMessage;
|
|
63
|
+
messageId: string;
|
|
64
|
+
}) {
|
|
65
|
+
const parsed = parseError(error);
|
|
66
|
+
|
|
67
|
+
if (isAlreadyExistsError(error, parsed)) {
|
|
68
|
+
const existing = this.ctx.get(messageId);
|
|
69
|
+
if (existing?.status === 'sending') {
|
|
70
|
+
this.ctx.ingest({ ...localMessage, status: 'received' });
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.ctx.ingest({ ...localMessage, status: 'failed', error: parsed });
|
|
76
|
+
}
|
|
77
|
+
}
|