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
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
sleep,
|
|
9
9
|
} from './utils';
|
|
10
10
|
import { isAPIError, isConnectionIDError, isErrorRetryable } from './errors';
|
|
11
|
-
import
|
|
11
|
+
import { chatLoggerSystem } from './logger';
|
|
12
|
+
import type { ConnectionOpen, UR } from './types';
|
|
13
|
+
import type { WSEvent } from './gen/models';
|
|
14
|
+
|
|
15
|
+
const logger = chatLoggerSystem.getLogger('connection-fallback');
|
|
12
16
|
|
|
13
17
|
export enum ConnectionState {
|
|
14
18
|
Closed = 'CLOSED',
|
|
@@ -33,15 +37,8 @@ export class WSConnectionFallback {
|
|
|
33
37
|
addConnectionEventListeners(this._onlineStatusChanged);
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
_log(msg: string, extra: UR = {}, level: LogLevel = 'info') {
|
|
37
|
-
this.client.logger(level, 'WSConnectionFallback:' + msg, {
|
|
38
|
-
tags: ['connection_fallback', 'connection'],
|
|
39
|
-
...extra,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
40
|
_setState(state: ConnectionState) {
|
|
44
|
-
|
|
41
|
+
logger.withExtraTags('_setState').debug(`Transitioning to state: ${state}.`);
|
|
45
42
|
|
|
46
43
|
// transition from connecting => connected
|
|
47
44
|
if (
|
|
@@ -60,7 +57,9 @@ export class WSConnectionFallback {
|
|
|
60
57
|
|
|
61
58
|
/** @private */
|
|
62
59
|
_onlineStatusChanged = (event: { type: string }) => {
|
|
63
|
-
|
|
60
|
+
logger
|
|
61
|
+
.withExtraTags('_onlineStatusChanged')
|
|
62
|
+
.info(`Network status changed to ${event.type}.`);
|
|
64
63
|
|
|
65
64
|
if (event.type === 'offline') {
|
|
66
65
|
this._setState(ConnectionState.Closed);
|
|
@@ -85,24 +84,22 @@ export class WSConnectionFallback {
|
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
try {
|
|
88
|
-
const res = await this.client.doAxiosRequest<T>(
|
|
87
|
+
const res = await this.client.api.doAxiosRequest<T>(
|
|
89
88
|
'get',
|
|
90
89
|
(this.client.baseURL as string).replace(':3030', ':8900') + '/longpoll', // replace port if present for testing with local API
|
|
91
90
|
undefined,
|
|
92
|
-
{
|
|
93
|
-
config: { ...config, cancelToken: this.cancelToken?.token },
|
|
94
|
-
params,
|
|
95
|
-
},
|
|
91
|
+
{ ...config, cancelToken: this.cancelToken?.token, params },
|
|
96
92
|
);
|
|
97
93
|
|
|
98
94
|
this.consecutiveFailures = 0; // always reset in case of no error
|
|
99
95
|
return res;
|
|
100
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
96
|
} catch (error: any) {
|
|
102
97
|
this.consecutiveFailures += 1;
|
|
103
98
|
|
|
104
99
|
if (retry && isErrorRetryable(error)) {
|
|
105
|
-
|
|
100
|
+
logger
|
|
101
|
+
.withExtraTags('_req')
|
|
102
|
+
.debug('Encountered a retryable error. Retrying the request.');
|
|
106
103
|
await sleep(retryInterval(this.consecutiveFailures));
|
|
107
104
|
return this._req<T>(params, config, retry);
|
|
108
105
|
}
|
|
@@ -116,7 +113,7 @@ export class WSConnectionFallback {
|
|
|
116
113
|
while (this.state === ConnectionState.Connected) {
|
|
117
114
|
try {
|
|
118
115
|
const data = await this._req<{
|
|
119
|
-
events:
|
|
116
|
+
events: WSEvent[];
|
|
120
117
|
}>({}, { timeout: 30000 }, true); // 30s => API responds in 20s if there is no event
|
|
121
118
|
|
|
122
119
|
if (data.events?.length) {
|
|
@@ -124,17 +121,18 @@ export class WSConnectionFallback {
|
|
|
124
121
|
this.client.dispatchEvent(data.events[i]);
|
|
125
122
|
}
|
|
126
123
|
}
|
|
127
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
128
124
|
} catch (error: any) {
|
|
129
125
|
if (axios.isCancel(error)) {
|
|
130
|
-
|
|
126
|
+
logger.withExtraTags('_poll').debug('Axios canceled the request.');
|
|
131
127
|
return;
|
|
132
128
|
}
|
|
133
129
|
|
|
134
130
|
/** client.doAxiosRequest will take care of TOKEN_EXPIRED error */
|
|
135
131
|
|
|
136
132
|
if (isConnectionIDError(error)) {
|
|
137
|
-
|
|
133
|
+
logger
|
|
134
|
+
.withExtraTags('_poll')
|
|
135
|
+
.warn('Received a connection ID error. Reconnecting without an ID.');
|
|
138
136
|
this._setState(ConnectionState.Disconnected);
|
|
139
137
|
this.connect(true);
|
|
140
138
|
return;
|
|
@@ -152,15 +150,20 @@ export class WSConnectionFallback {
|
|
|
152
150
|
|
|
153
151
|
/**
|
|
154
152
|
* connect try to open a longpoll request
|
|
155
|
-
*
|
|
153
|
+
*
|
|
154
|
+
* @param reconnect - should be false for first call and true for subsequent calls to keep the connection alive and call recoverState
|
|
156
155
|
*/
|
|
157
156
|
connect = async (reconnect = false) => {
|
|
158
157
|
if (this.state === ConnectionState.Connecting) {
|
|
159
|
-
|
|
158
|
+
logger
|
|
159
|
+
.withExtraTags('connect')
|
|
160
|
+
.warn('A connection attempt is already in progress.', { reconnect });
|
|
160
161
|
return;
|
|
161
162
|
}
|
|
162
163
|
if (this.state === ConnectionState.Connected) {
|
|
163
|
-
|
|
164
|
+
logger
|
|
165
|
+
.withExtraTags('connect')
|
|
166
|
+
.warn('Already connected and polling.', { reconnect });
|
|
164
167
|
return;
|
|
165
168
|
}
|
|
166
169
|
|
|
@@ -175,7 +178,6 @@ export class WSConnectionFallback {
|
|
|
175
178
|
|
|
176
179
|
this._setState(ConnectionState.Connected);
|
|
177
180
|
this.connectionID = event.connection_id;
|
|
178
|
-
// @ts-expect-error type mismatch
|
|
179
181
|
this.client.dispatchEvent(event);
|
|
180
182
|
this._poll();
|
|
181
183
|
if (reconnect) {
|
|
@@ -205,9 +207,9 @@ export class WSConnectionFallback {
|
|
|
205
207
|
|
|
206
208
|
try {
|
|
207
209
|
await this._req({ close: true, connection_id }, { timeout }, false);
|
|
208
|
-
|
|
210
|
+
logger.withExtraTags('disconnect').info('Closed the connection ID.');
|
|
209
211
|
} catch (err) {
|
|
210
|
-
|
|
212
|
+
logger.withExtraTags('disconnect').error('Disconnect failed.', { error: err });
|
|
211
213
|
}
|
|
212
214
|
};
|
|
213
215
|
}
|
package/src/constants.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
export const DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE = 25;
|
|
2
2
|
export const DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE = 100;
|
|
3
|
-
export const DEFAULT_MESSAGE_SET_PAGINATION = Object.freeze({
|
|
4
|
-
hasNext: false,
|
|
5
|
-
hasPrev: false,
|
|
6
|
-
});
|
|
7
3
|
export const DEFAULT_UPLOAD_SIZE_LIMIT_BYTES = 100 * 1024 * 1024; // 100 MB
|
|
8
4
|
export const API_MAX_FILES_ALLOWED_PER_MESSAGE = 10;
|
|
9
5
|
export const MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY = 100;
|
|
@@ -23,7 +19,7 @@ export const RESERVED_UPDATED_MESSAGE_FIELDS = Object.freeze({
|
|
|
23
19
|
own_reactions: true,
|
|
24
20
|
reaction_counts: true,
|
|
25
21
|
reply_count: true,
|
|
26
|
-
//
|
|
22
|
+
// MessageRequest text related fields that shouldn't be in update
|
|
27
23
|
i18n: true,
|
|
28
24
|
type: true,
|
|
29
25
|
html: true,
|
package/src/custom_types.ts
CHANGED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { Unsubscribe } from '../store';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A batch of entity-store changes delivered to a subscriber in a single notification.
|
|
5
|
+
*
|
|
6
|
+
* `changedIds` are the ids the subscriber watches whose canonical object changed reference this
|
|
7
|
+
* flush (an upsert, or a removal — a removal makes `store.get(id)` return `undefined`).
|
|
8
|
+
*/
|
|
9
|
+
export type EntityStoreChangeBatch = {
|
|
10
|
+
changedIds: ReadonlySet<string>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Anything that observes entities held in an {@link EntityStore}.
|
|
15
|
+
*
|
|
16
|
+
* A subscriber watches a *set* of entity ids (a paginator watches all ids in its
|
|
17
|
+
* intervals; a thread watches its single parent id). It is notified at most once
|
|
18
|
+
* per store transaction with the subset of its watched ids that changed.
|
|
19
|
+
*/
|
|
20
|
+
export type EntityStoreSubscriber = {
|
|
21
|
+
onEntitiesChanged: (batch: EntityStoreChangeBatch) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Optional: emit any throttled/pending state notification immediately. Called by
|
|
24
|
+
* {@link EntityStore.flushSubscribers} after an optimistic (local-user) write so the change
|
|
25
|
+
* renders without throttle delay. A paginator implements this by flushing its throttled window
|
|
26
|
+
* publish.
|
|
27
|
+
*/
|
|
28
|
+
flushState?: () => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type EntityStoreOptions<T> = {
|
|
32
|
+
/** Extracts the canonical id an entity is stored and addressed under. */
|
|
33
|
+
getEntityId: (entity: T) => string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A client-global, normalized store for domain entities addressed by id.
|
|
38
|
+
*
|
|
39
|
+
* The store holds exactly **one** canonical `T` per id and lets any number of entities
|
|
40
|
+
* (paginators, threads, ad-hoc consumers) subscribe to individual ids. Every mutation of a
|
|
41
|
+
* stored entity becomes a single `upsert`, and the store fans the change out to exactly the
|
|
42
|
+
* subscribers holding that id — replacing the manual copy-to-copy fan-out that keeping N
|
|
43
|
+
* per-consumer copies in sync required.
|
|
44
|
+
*
|
|
45
|
+
* The store is entity-agnostic: it learns how to extract an id from a `T` via the `getEntityId`
|
|
46
|
+
* function passed to its constructor. The client holds one as `client.messageStore`, an
|
|
47
|
+
* `EntityStore<LocalMessage>`, for message content.
|
|
48
|
+
*
|
|
49
|
+
* ## Design
|
|
50
|
+
*
|
|
51
|
+
* - **Content:** `byId` (`Map<id, T>`) is the single source of truth. Objects are immutable
|
|
52
|
+
* snapshots: `upsert` *replaces*, never mutates in place, so the reference-equality
|
|
53
|
+
* short-circuit (mirroring {@link StateStore.next}) and every downstream selector keep working.
|
|
54
|
+
* - **Subscription registry / refcount:** `subscribers` (`Map<id, Set<subscriber>>`) is both the
|
|
55
|
+
* per-id notification list and the reference count — when the last subscriber of an id unlinks,
|
|
56
|
+
* the canonical copy is garbage-collected.
|
|
57
|
+
* - **Batching:** `transaction` coalesces a bulk write (e.g. a page of N entities) so each affected
|
|
58
|
+
* subscriber is notified **once** with the full changed-id set, instead of N times.
|
|
59
|
+
* - **Signal-then-pull:** subscribers receive the set of changed ids and pull the new content via
|
|
60
|
+
* `get`; the store never pushes objects.
|
|
61
|
+
*
|
|
62
|
+
* This is deliberately a hand-rolled per-id registry rather than a {@link StateStore}: a single
|
|
63
|
+
* `StateStore` would run every subscriber's selector on every change (O(all subscribers) per emit),
|
|
64
|
+
* and a store-per-entity would allocate a full `StateStore` per id with no cross-id batching. The
|
|
65
|
+
* `Map<id, Set>` gives O(subscribers-of-changed-id) fan-out — the same idiom as `PollManager`'s
|
|
66
|
+
* cache.
|
|
67
|
+
*
|
|
68
|
+
* @template T The domain entity type held by the store.
|
|
69
|
+
*/
|
|
70
|
+
export class EntityStore<T> {
|
|
71
|
+
private byId = new Map<string, T>();
|
|
72
|
+
private subscribers = new Map<string, Set<EntityStoreSubscriber>>();
|
|
73
|
+
private readonly getEntityId: (entity: T) => string;
|
|
74
|
+
|
|
75
|
+
private transactionDepth = 0;
|
|
76
|
+
private pendingChanged = new Map<EntityStoreSubscriber, Set<string>>();
|
|
77
|
+
|
|
78
|
+
constructor({ getEntityId }: EntityStoreOptions<T>) {
|
|
79
|
+
this.getEntityId = getEntityId;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---- reads ----
|
|
83
|
+
|
|
84
|
+
get(id: string | undefined): T | undefined {
|
|
85
|
+
return typeof id === 'string' ? this.byId.get(id) : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
has(id: string): boolean {
|
|
89
|
+
return this.byId.has(id);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---- writes ----
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Replaces the canonical copy of `entity` and notifies every subscriber watching its id —
|
|
96
|
+
* except `subscriber`, the one that performed the write, which is expected to re-render itself
|
|
97
|
+
* (the paginator that performed the write already emits its own window inline, so it must not be
|
|
98
|
+
* notified a second time through the subscription).
|
|
99
|
+
*/
|
|
100
|
+
upsert(entity: T, subscriber?: EntityStoreSubscriber): void {
|
|
101
|
+
const id = this.getEntityId(entity);
|
|
102
|
+
const previous = this.byId.get(id);
|
|
103
|
+
// do not notify if the value hasn't changed (mirrors StateStore.next)
|
|
104
|
+
if (previous === entity) return;
|
|
105
|
+
this.byId.set(id, entity);
|
|
106
|
+
this.markDirty(id, subscriber);
|
|
107
|
+
this.autoFlush();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---- subscription registry / refcount ----
|
|
111
|
+
|
|
112
|
+
/** Registers `subscriber` for `id` (notification target + refcount). */
|
|
113
|
+
link(id: string, subscriber: EntityStoreSubscriber): void {
|
|
114
|
+
let subscribers = this.subscribers.get(id);
|
|
115
|
+
if (!subscribers) {
|
|
116
|
+
subscribers = new Set();
|
|
117
|
+
this.subscribers.set(id, subscribers);
|
|
118
|
+
}
|
|
119
|
+
subscribers.add(subscriber);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Drops `subscriber` from `id`; GCs the canonical copy when none remain. */
|
|
123
|
+
unlink(id: string, subscriber: EntityStoreSubscriber): void {
|
|
124
|
+
const subscribers = this.subscribers.get(id);
|
|
125
|
+
if (!subscribers) return;
|
|
126
|
+
subscribers.delete(subscriber);
|
|
127
|
+
if (subscribers.size === 0) {
|
|
128
|
+
this.subscribers.delete(id);
|
|
129
|
+
// refcount GC: nobody holds this entity any longer.
|
|
130
|
+
this.byId.delete(id);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Sugar for atomic / ad-hoc consumers that watch a single id (e.g. a thread watching its parent
|
|
136
|
+
* message). Fires `handler` immediately with the current value (mirroring
|
|
137
|
+
* {@link StateStore.subscribe}) and on every subsequent change.
|
|
138
|
+
*/
|
|
139
|
+
subscribe(id: string, handler: (entity: T | undefined) => void): Unsubscribe {
|
|
140
|
+
const subscriber: EntityStoreSubscriber = {
|
|
141
|
+
onEntitiesChanged: () => handler(this.byId.get(id)),
|
|
142
|
+
};
|
|
143
|
+
this.link(id, subscriber);
|
|
144
|
+
handler(this.byId.get(id));
|
|
145
|
+
return () => this.unlink(id, subscriber);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---- batching ----
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Runs `fn`, coalescing all notifications produced by writes inside it into a single flush on
|
|
152
|
+
* exit. Re-entrant: nested transactions flush only when the outermost exits.
|
|
153
|
+
*/
|
|
154
|
+
transaction<R>(fn: () => R): R {
|
|
155
|
+
this.transactionDepth += 1;
|
|
156
|
+
try {
|
|
157
|
+
return fn();
|
|
158
|
+
} finally {
|
|
159
|
+
this.transactionDepth -= 1;
|
|
160
|
+
if (this.transactionDepth === 0) this.flush();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Immediately flushes any throttled/pending state publish on the subscribers of `id` (via
|
|
166
|
+
* {@link EntityStoreSubscriber.flushState}). Called after an optimistic (local-user) write to
|
|
167
|
+
* `id` so it renders without the throttle delay. Only that id's own subscribers are flushed — the
|
|
168
|
+
* write touched no other id — and flushing a subscriber with nothing pending is a no-op.
|
|
169
|
+
*/
|
|
170
|
+
flushSubscribers(id: string): void {
|
|
171
|
+
const subscribers = this.subscribers.get(id);
|
|
172
|
+
if (!subscribers) return;
|
|
173
|
+
for (const subscriber of subscribers) subscriber.flushState?.();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Records `id` as changed for every subscriber watching it, except `subscriber` — the one that
|
|
178
|
+
* wrote it and is resposible for updating itself (see {@link EntityStore.upsert}).
|
|
179
|
+
*/
|
|
180
|
+
private markDirty(id: string, subscriber?: EntityStoreSubscriber): void {
|
|
181
|
+
const subscribers = this.subscribers.get(id);
|
|
182
|
+
if (!subscribers) return;
|
|
183
|
+
for (const other of subscribers) {
|
|
184
|
+
if (other === subscriber) continue;
|
|
185
|
+
let changed = this.pendingChanged.get(other);
|
|
186
|
+
if (!changed) {
|
|
187
|
+
changed = new Set();
|
|
188
|
+
this.pendingChanged.set(other, changed);
|
|
189
|
+
}
|
|
190
|
+
changed.add(id);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private autoFlush(): void {
|
|
195
|
+
if (this.transactionDepth === 0) this.flush();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private flush(): void {
|
|
199
|
+
if (this.pendingChanged.size === 0) return;
|
|
200
|
+
// swap out the pending map before notifying so writes made from within a
|
|
201
|
+
// subscriber accumulate into the next flush rather than mutating this one.
|
|
202
|
+
const changedBySubscriber = this.pendingChanged;
|
|
203
|
+
this.pendingChanged = new Map();
|
|
204
|
+
for (const [subscriber, changedIds] of changedBySubscriber) {
|
|
205
|
+
subscriber.onEntitiesChanged({ changedIds });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { ItemIndexApi } from '../pagination/ItemIndex';
|
|
2
|
+
import { EntityStore, type EntityStoreSubscriber } from './EntityStore';
|
|
3
|
+
|
|
4
|
+
export type StoreBackedItemIndexOptions<T> = {
|
|
5
|
+
getEntityId: (item: T) => string;
|
|
6
|
+
/**
|
|
7
|
+
* The entity that owns this index; used as the store subscriber, which also holds its refcount.
|
|
8
|
+
* Optional: an index over a private store (see `store`) has no one to fan out to, so an
|
|
9
|
+
* owner-less index just uses an internal no-op subscriber to hold its refcount. Message paginators pass themselves so
|
|
10
|
+
* cross-collection updates reach them; single-home collections (channels, reminders, …) omit it.
|
|
11
|
+
*/
|
|
12
|
+
owner?: EntityStoreSubscriber;
|
|
13
|
+
/**
|
|
14
|
+
* The shared, client-global store that holds the canonical content. Optional: when omitted the
|
|
15
|
+
* index provisions a **private** {@link EntityStore} of its own, so it behaves exactly like a
|
|
16
|
+
* plain, per-instance index — same code path, no shared content, no fan-out. This is the mode used
|
|
17
|
+
* by single-home collections and by detached paginators (e.g. built without a client, in tests).
|
|
18
|
+
*/
|
|
19
|
+
store?: EntityStore<T>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** No-op subscriber that holds the refcount for an owner-less (private-store) index. */
|
|
23
|
+
const NOOP_OWNER: EntityStoreSubscriber = { onEntitiesChanged: () => undefined };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* An {@link ItemIndexApi} implementation that keeps entity **content** in an {@link EntityStore}
|
|
27
|
+
* while keeping **membership** local.
|
|
28
|
+
*
|
|
29
|
+
* A consumer sees the same minimal CRUD surface ({@link ItemIndexApi}), but:
|
|
30
|
+
*
|
|
31
|
+
* - `get`/`has`/`values`/`entries` are scoped to *this* index's membership (`memberIds`), so
|
|
32
|
+
* `getItem(id)` still means "does THIS index hold the id" — even though the canonical object
|
|
33
|
+
* lives in the (possibly shared) store. This is what keeps e.g. reaction routing
|
|
34
|
+
* (`threadPaginator.getItem(id) ? thread : channel`) correct and keeps the `.values()` scans
|
|
35
|
+
* from ever walking other channels' entities.
|
|
36
|
+
* - `setOne` writes content once into the store and links the owner as a subscriber (drives both
|
|
37
|
+
* notification fan-out and refcount GC). The write passes the owner as the `subscriber` to skip, so
|
|
38
|
+
* the owner is not notified of its own write (it re-emits its window inline); other subscribers of
|
|
39
|
+
* the same id ARE notified and re-project.
|
|
40
|
+
* - `remove`/`clear` unlink the owner rather than hard-deleting content, so an entity still held by
|
|
41
|
+
* another index (e.g. a `show_in_channel` reply in both the channel list and its thread) survives;
|
|
42
|
+
* the store GCs it only when the last subscriber unlinks.
|
|
43
|
+
*
|
|
44
|
+
* When no shared store is supplied the index holds a private store — every id it links has exactly
|
|
45
|
+
* one subscriber (its own owner), so no fan-out ever fires and removal GCs immediately, matching a plain
|
|
46
|
+
* per-instance index.
|
|
47
|
+
*
|
|
48
|
+
* @template T The domain item type held by the index; collocated with the {@link EntityStore}'s.
|
|
49
|
+
*/
|
|
50
|
+
export class StoreBackedItemIndex<T> implements ItemIndexApi<T> {
|
|
51
|
+
private memberIds = new Set<string>();
|
|
52
|
+
private readonly store: EntityStore<T>;
|
|
53
|
+
private readonly owner: EntityStoreSubscriber;
|
|
54
|
+
private readonly getEntityId: (item: T) => string;
|
|
55
|
+
|
|
56
|
+
constructor({ store, owner, getEntityId }: StoreBackedItemIndexOptions<T>) {
|
|
57
|
+
this.store = store ?? new EntityStore<T>({ getEntityId });
|
|
58
|
+
this.owner = owner ?? NOOP_OWNER;
|
|
59
|
+
this.getEntityId = getEntityId;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setMany(items: T[]) {
|
|
63
|
+
this.store.transaction(() => {
|
|
64
|
+
for (const item of items) this.setOne(item);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setOne(item: T) {
|
|
69
|
+
const id = this.getEntityId(item);
|
|
70
|
+
this.store.link(id, this.owner);
|
|
71
|
+
this.memberIds.add(id);
|
|
72
|
+
this.store.upsert(item, this.owner);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get(id: string): T | undefined {
|
|
76
|
+
return this.memberIds.has(id) ? this.store.get(id) : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
has(id: string): boolean {
|
|
80
|
+
return this.memberIds.has(id);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
remove(id: string) {
|
|
84
|
+
if (!this.memberIds.has(id)) return;
|
|
85
|
+
this.memberIds.delete(id);
|
|
86
|
+
this.store.unlink(id, this.owner);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
clear() {
|
|
90
|
+
for (const id of this.memberIds) this.store.unlink(id, this.owner);
|
|
91
|
+
this.memberIds.clear();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
entries(): [string, T][] {
|
|
95
|
+
const result: [string, T][] = [];
|
|
96
|
+
for (const id of this.memberIds) {
|
|
97
|
+
const item = this.store.get(id);
|
|
98
|
+
if (item) result.push([id, item]);
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
values(): T[] {
|
|
104
|
+
const result: T[] = [];
|
|
105
|
+
for (const id of this.memberIds) {
|
|
106
|
+
const item = this.store.get(id);
|
|
107
|
+
if (item) result.push(item);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
batch<R>(fn: () => R): R {
|
|
113
|
+
return this.store.transaction(fn);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { StreamChat } from '../client';
|
|
2
|
+
import type { ReactionRequest, ReactionResponse, UserResponse } from '../types';
|
|
3
|
+
import {
|
|
4
|
+
computeOwnReactions,
|
|
5
|
+
messageWithReactionAdded,
|
|
6
|
+
messageWithReactionRemoved,
|
|
7
|
+
} from '../utils';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Applies a reaction to the single canonical copy of a message in the client-global
|
|
11
|
+
* {@link EntityStore} (`client.messageStore`), addressed purely by id — NOT through any paginator's
|
|
12
|
+
* membership. It reads the
|
|
13
|
+
* current message POJO (`store.get`), produces a new POJO with the reaction folded into its
|
|
14
|
+
* `reaction_groups` / `latest_reactions` (shared count helpers) and `own_reactions`
|
|
15
|
+
* ({@link computeOwnReactions}), and writes it back (`store.upsert`). The store then notifies every
|
|
16
|
+
* collection currently holding that id — the main-list / thread-reply paginators, a thread's
|
|
17
|
+
* subscribed parent, any future consumer — and each re-projects. The message stays a plain object;
|
|
18
|
+
* the store's per-id registry is the reactivity.
|
|
19
|
+
*
|
|
20
|
+
* Because it is keyed by id rather than by a paginator's `getItem`, it reaches a message held by ANY
|
|
21
|
+
* collection, including the thread parent that lives in no paginator — with no per-home wiring.
|
|
22
|
+
*
|
|
23
|
+
* Writes memory (synchronous) + the offline DB (fire-and-forget), and returns an `undo()` reversing
|
|
24
|
+
* both on the current state (inverse deltas, concurrency-safe), or `undefined` when the user isn't
|
|
25
|
+
* connected or the message isn't in the store.
|
|
26
|
+
*/
|
|
27
|
+
export const applyReactionLocally = (
|
|
28
|
+
client: StreamChat,
|
|
29
|
+
{
|
|
30
|
+
enforceUnique = false,
|
|
31
|
+
messageId,
|
|
32
|
+
reaction,
|
|
33
|
+
removed = false,
|
|
34
|
+
}: {
|
|
35
|
+
messageId: string;
|
|
36
|
+
reaction: ReactionRequest;
|
|
37
|
+
enforceUnique?: boolean;
|
|
38
|
+
removed?: boolean;
|
|
39
|
+
},
|
|
40
|
+
): (() => void) | undefined => {
|
|
41
|
+
const store = client.messageStore;
|
|
42
|
+
const user = client.user;
|
|
43
|
+
const existing = store.get(messageId);
|
|
44
|
+
if (!user || !existing) return;
|
|
45
|
+
|
|
46
|
+
const now = new Date();
|
|
47
|
+
// Spread `reaction` first so the authoritative fields below win, while still preserving any values
|
|
48
|
+
// the caller already carried (e.g. the original `created_at` when undo re-applies a captured
|
|
49
|
+
// reaction) via `?? now`. `message_id`/`user`/`user_id` are always derived from this message and
|
|
50
|
+
// the connected user, so they never need to come off `reaction`.
|
|
51
|
+
const reactionResponse: ReactionResponse = {
|
|
52
|
+
...reaction,
|
|
53
|
+
created_at: reaction.created_at ?? now,
|
|
54
|
+
custom: reaction.custom ?? {},
|
|
55
|
+
message_id: messageId,
|
|
56
|
+
score: reaction.score ?? 1,
|
|
57
|
+
type: reaction.type,
|
|
58
|
+
updated_at: reaction.updated_at ?? now,
|
|
59
|
+
user: user as UserResponse,
|
|
60
|
+
user_id: user.id,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Capture what this op removes so undo() can restore it faithfully (reaction spread last): the
|
|
64
|
+
// deleted reaction for a removal, or the user's displaced reactions for an enforce_unique add.
|
|
65
|
+
const removedReactions: ReactionResponse[] = removed
|
|
66
|
+
? (existing.own_reactions?.filter((r) => r.type === reactionResponse.type) ?? [])
|
|
67
|
+
: enforceUnique
|
|
68
|
+
? (existing.own_reactions ?? [])
|
|
69
|
+
: [];
|
|
70
|
+
|
|
71
|
+
const withCounts = removed
|
|
72
|
+
? messageWithReactionRemoved(existing, reactionResponse)
|
|
73
|
+
: messageWithReactionAdded(existing, reactionResponse, enforceUnique);
|
|
74
|
+
const own_reactions = computeOwnReactions({
|
|
75
|
+
current: existing.own_reactions ?? [],
|
|
76
|
+
enforceUnique,
|
|
77
|
+
reaction: reactionResponse,
|
|
78
|
+
removed,
|
|
79
|
+
userId: user.id,
|
|
80
|
+
});
|
|
81
|
+
store.upsert({ ...withCounts, own_reactions });
|
|
82
|
+
store.flushSubscribers(messageId);
|
|
83
|
+
|
|
84
|
+
const persisted = store.get(messageId);
|
|
85
|
+
if (persisted) {
|
|
86
|
+
client.offlineDb?.executeQuerySafely(
|
|
87
|
+
(db) =>
|
|
88
|
+
removed
|
|
89
|
+
? db.deleteReaction({ message: persisted, reaction: reactionResponse })
|
|
90
|
+
: enforceUnique
|
|
91
|
+
? db.updateReaction({ message: persisted, reaction: reactionResponse })
|
|
92
|
+
: db.insertReaction({ message: persisted, reaction: reactionResponse }),
|
|
93
|
+
{ method: 'applyReactionLocally' },
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return () => {
|
|
98
|
+
if (!removed) {
|
|
99
|
+
applyReactionLocally(client, {
|
|
100
|
+
messageId,
|
|
101
|
+
reaction: reactionResponse,
|
|
102
|
+
removed: true,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
for (const removedReaction of removedReactions) {
|
|
106
|
+
applyReactionLocally(client, {
|
|
107
|
+
messageId,
|
|
108
|
+
reaction: removedReaction,
|
|
109
|
+
removed: false,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
};
|
package/src/errors.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AxiosResponse } from 'axios';
|
|
2
|
-
import type {
|
|
2
|
+
import type { APIError as Gen_APIError } from './types';
|
|
3
3
|
|
|
4
4
|
export const APIErrorCodes: Record<string, { name: string; retryable: boolean }> = {
|
|
5
5
|
'-1': { name: 'InternalSystemError', retryable: true },
|
|
@@ -48,6 +48,18 @@ export function isErrorRetryable(error: APIError) {
|
|
|
48
48
|
return err.retryable;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Whether an error is EPHEMERAL — a transient failure worth queueing/retrying rather than a
|
|
53
|
+
* definitive rejection. True when the server never responded (connection/network/offline error - no
|
|
54
|
+
* `response`, i.e an axios network error or an `OfflineError`) and when the server responded with a
|
|
55
|
+
* retryable code (see {@link APIErrorCodes}); false only when the server responded with a
|
|
56
|
+
* non-retryable code (InputError 4, DoesNotExist 16, NotAllowed 17, …).
|
|
57
|
+
*/
|
|
58
|
+
export function isEphemeral(error: Error): boolean {
|
|
59
|
+
if (!(error as { response?: unknown }).response) return true;
|
|
60
|
+
return isErrorRetryable(error as APIError);
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
export function isConnectionIDError(error: APIError) {
|
|
52
64
|
return error.code === 46; // ConnectionIDNotFoundError
|
|
53
65
|
}
|
|
@@ -66,6 +78,6 @@ export function isWSFailure(err: APIError): boolean {
|
|
|
66
78
|
|
|
67
79
|
export function isErrorResponse(
|
|
68
80
|
res: AxiosResponse<unknown>,
|
|
69
|
-
): res is AxiosResponse<
|
|
81
|
+
): res is AxiosResponse<Gen_APIError> {
|
|
70
82
|
return !res.status || res.status < 200 || 300 <= res.status;
|
|
71
83
|
}
|