stream-chat 10.0.0-rc.15 → 10.0.0-rc.17
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 +855 -336
- package/dist/cjs/index.browser.js.map +4 -4
- package/dist/cjs/index.node.js +858 -336
- package/dist/cjs/index.node.js.map +4 -4
- package/dist/esm/index.mjs +853 -334
- package/dist/esm/index.mjs.map +4 -4
- package/dist/types/api-client.d.ts +6 -1
- package/dist/types/client.d.ts +46 -12
- package/dist/types/connection/ConnectionIdManager.d.ts +9 -5
- package/dist/types/connection/ConnectionRecoveryManager.d.ts +17 -24
- package/dist/types/connection/networkConnection/NetworkConnectionObserver.d.ts +8 -0
- package/dist/types/connection/networkConnection/reporters.d.ts +4 -4
- package/dist/types/connection/wsConnection/WSConnection.d.ts +34 -9
- package/dist/types/connection/wsConnection/WSConnectionFallback.d.ts +55 -0
- package/dist/types/connection/wsConnection/types.d.ts +17 -4
- package/dist/types/entityStore/EntityStore.d.ts +4 -0
- package/dist/types/errors.d.ts +1 -0
- package/dist/types/gen/chat/ChatApi.d.ts +1 -0
- package/dist/types/messageComposer/linkPreviewsManager.d.ts +3 -3
- package/dist/types/messageComposer/messageComposer.d.ts +2 -2
- package/dist/types/offline-support/offline_support_api.d.ts +5 -6
- package/dist/types/pagination/paginators/BasePaginator.d.ts +8 -1
- package/dist/types/pagination/paginators/ChannelPaginator.d.ts +5 -1
- package/dist/types/pagination/paginators/ThreadPaginator.d.ts +57 -0
- package/dist/types/pagination/paginators/index.d.ts +1 -0
- package/dist/types/thread.d.ts +30 -21
- package/dist/types/thread_manager.d.ts +74 -32
- package/dist/types/types.d.ts +8 -0
- package/package.json +1 -1
- package/src/api-client.ts +25 -24
- package/src/client.ts +97 -40
- package/src/configuration/shape.ts +7 -1
- package/src/connection/ConnectionIdManager.ts +9 -5
- package/src/connection/ConnectionRecoveryManager.ts +30 -30
- package/src/connection/networkConnection/NetworkConnectionObserver.ts +19 -1
- package/src/connection/networkConnection/reporters.ts +4 -4
- package/src/connection/wsConnection/WSConnection.ts +86 -14
- package/src/connection/wsConnection/WSConnectionFallback.ts +259 -0
- package/src/connection/wsConnection/config.ts +1 -0
- package/src/connection/wsConnection/types.ts +17 -4
- package/src/entityStore/EntityStore.ts +10 -0
- package/src/errors.ts +4 -0
- package/src/gen/chat/ChatApi.ts +2 -1
- package/src/messageComposer/messageComposer.ts +3 -3
- package/src/messageOperations/createMessageOperations.ts +3 -3
- package/src/offline-support/offline_support_api.ts +5 -6
- package/src/offline-support/queueableOperations.ts +1 -1
- package/src/pagination/paginators/BasePaginator.ts +45 -8
- package/src/pagination/paginators/ChannelPaginator.ts +17 -3
- package/src/pagination/paginators/ThreadPaginator.ts +180 -0
- package/src/pagination/paginators/index.ts +1 -0
- package/src/thread.ts +188 -220
- package/src/thread_manager.ts +257 -173
- package/src/types.ts +6 -0
|
@@ -71,6 +71,8 @@ __export(src_exports, {
|
|
|
71
71
|
DEFAULT_STOP_REFRESH_BOUNDARY_MS: () => DEFAULT_STOP_REFRESH_BOUNDARY_MS,
|
|
72
72
|
DEFAULT_TEXT_COMPOSER_CONFIG: () => DEFAULT_TEXT_COMPOSER_CONFIG,
|
|
73
73
|
DEFAULT_THREAD_MANAGER_CONFIG: () => DEFAULT_THREAD_MANAGER_CONFIG,
|
|
74
|
+
DEFAULT_THREAD_PAGE_SIZE: () => DEFAULT_THREAD_PAGE_SIZE,
|
|
75
|
+
DEFAULT_THREAD_QUERY: () => DEFAULT_THREAD_QUERY,
|
|
74
76
|
DEFAULT_WS_CONNECTION_CONFIG: () => DEFAULT_WS_CONNECTION_CONFIG,
|
|
75
77
|
EntityStore: () => EntityStore,
|
|
76
78
|
EventHandlerPipeline: () => EventHandlerPipeline,
|
|
@@ -125,6 +127,7 @@ __export(src_exports, {
|
|
|
125
127
|
TextComposerMiddlewareExecutor: () => TextComposerMiddlewareExecutor,
|
|
126
128
|
Thread: () => Thread,
|
|
127
129
|
ThreadManager: () => ThreadManager,
|
|
130
|
+
ThreadPaginator: () => ThreadPaginator,
|
|
128
131
|
TokenManager: () => TokenManager,
|
|
129
132
|
UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT: () => UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT,
|
|
130
133
|
UploadManager: () => UploadManager,
|
|
@@ -263,7 +266,7 @@ __export(src_exports, {
|
|
|
263
266
|
module.exports = __toCommonJS(src_exports);
|
|
264
267
|
|
|
265
268
|
// src/client.ts
|
|
266
|
-
var
|
|
269
|
+
var import_axios5 = __toESM(require("axios"));
|
|
267
270
|
|
|
268
271
|
// src/gen/models/index.ts
|
|
269
272
|
var ChannelOwnCapability = {
|
|
@@ -1446,6 +1449,14 @@ var EntityStore = class {
|
|
|
1446
1449
|
has(id) {
|
|
1447
1450
|
return this.byId.has(id);
|
|
1448
1451
|
}
|
|
1452
|
+
/** Whether `subscriber` is one of the holders of `id`. */
|
|
1453
|
+
isHeldBy(id, subscriber) {
|
|
1454
|
+
return this.subscribers.get(id)?.has(subscriber) ?? false;
|
|
1455
|
+
}
|
|
1456
|
+
/** Every entity currently in the store. */
|
|
1457
|
+
values() {
|
|
1458
|
+
return Array.from(this.byId.values());
|
|
1459
|
+
}
|
|
1449
1460
|
// ---- writes ----
|
|
1450
1461
|
/**
|
|
1451
1462
|
* Replaces the canonical copy of `entity` and notifies every subscriber watching its id —
|
|
@@ -2613,6 +2624,16 @@ var BasePaginator = class {
|
|
|
2613
2624
|
* {@link setIntervals}), which bypasses the per-interval {@link commitInterval}/{@link dropInterval}
|
|
2614
2625
|
* publishing. No-ops when the views are already empty so a reset does not emit needlessly.
|
|
2615
2626
|
*/
|
|
2627
|
+
/**
|
|
2628
|
+
* Drops every loaded interval and item without publishing anything, so the next ingest starts from
|
|
2629
|
+
* empty storage while `state.items` keeps showing the previous window until the caller publishes.
|
|
2630
|
+
*/
|
|
2631
|
+
clearItemStorage() {
|
|
2632
|
+
this.setIntervals([]);
|
|
2633
|
+
this.setActiveInterval(void 0);
|
|
2634
|
+
this._itemIndex.clear();
|
|
2635
|
+
this.clearIntervalViews();
|
|
2636
|
+
}
|
|
2616
2637
|
clearIntervalViews() {
|
|
2617
2638
|
this._pendingViewChangedIds.clear();
|
|
2618
2639
|
const { logicalHead, logicalTail, anchoredHead } = this.intervalViews.getLatestValue();
|
|
@@ -4080,10 +4101,7 @@ var BasePaginator = class {
|
|
|
4080
4101
|
if (!this.canExecuteQuery({ direction, reset })) return;
|
|
4081
4102
|
const isForcedReset = reset === "yes" && !keepPreviousItems;
|
|
4082
4103
|
if (isForcedReset) {
|
|
4083
|
-
this.
|
|
4084
|
-
this.setActiveInterval(void 0);
|
|
4085
|
-
this._itemIndex.clear();
|
|
4086
|
-
this.clearIntervalViews();
|
|
4104
|
+
this.clearItemStorage();
|
|
4087
4105
|
this.state.next(this.getStateBeforeFirstQuery());
|
|
4088
4106
|
} else if (reset === "yes" && !forcedQueryShape) {
|
|
4089
4107
|
this.state.partialNext({
|
|
@@ -4091,7 +4109,9 @@ var BasePaginator = class {
|
|
|
4091
4109
|
offset: this.config.initialOffset ?? 0
|
|
4092
4110
|
});
|
|
4093
4111
|
}
|
|
4094
|
-
const
|
|
4112
|
+
const isRefresh = reset === "yes" && keepPreviousItems && !forcedQueryShape;
|
|
4113
|
+
const pageSize = isRefresh ? Math.max(this.pageSize, this.items?.length ?? 0) : this.pageSize;
|
|
4114
|
+
const queryShape = forcedQueryShape ?? this.getNextQueryShape({ direction, pageSize });
|
|
4095
4115
|
const isFirstPage = this.isFirstPageQuery({ queryShape, reset });
|
|
4096
4116
|
if (isFirstPage && !keepPreviousItems) {
|
|
4097
4117
|
const state = this.getStateBeforeFirstQuery();
|
|
@@ -4155,6 +4175,7 @@ var BasePaginator = class {
|
|
|
4155
4175
|
const filteredItems = this.filterQueryResults(items);
|
|
4156
4176
|
stateUpdate.items = filteredItems;
|
|
4157
4177
|
const isJumpQuery = !!queryShape && this.isJumpQueryShape(queryShape);
|
|
4178
|
+
const keptItemIds = isFirstPage && keepPreviousItems && !this.isCursorPagination && this.items?.length ? new Set(this.items.map((item) => this.getItemId(item))) : void 0;
|
|
4158
4179
|
const interval = this.ingestPage({
|
|
4159
4180
|
page: stateUpdate.items,
|
|
4160
4181
|
policy: isJumpQuery ? "strict-overlap-only" : "auto",
|
|
@@ -4196,7 +4217,15 @@ var BasePaginator = class {
|
|
|
4196
4217
|
} else {
|
|
4197
4218
|
const startOffset = this.offset ?? 0;
|
|
4198
4219
|
stateUpdate.offset = startOffset + items.length;
|
|
4199
|
-
|
|
4220
|
+
const lastPageItem = items[items.length - 1];
|
|
4221
|
+
if (interval && keptItemIds && lastPageItem && keptItemIds.has(this.getItemId(lastPageItem))) {
|
|
4222
|
+
stateUpdate.offset = Math.max(
|
|
4223
|
+
stateUpdate.offset,
|
|
4224
|
+
startOffset + this.intervalToItems(interval).length
|
|
4225
|
+
);
|
|
4226
|
+
}
|
|
4227
|
+
stateUpdate.hasMoreTail = items.length >= this.pageSize;
|
|
4228
|
+
if (isFirstPage) stateUpdate.hasMoreHead = (this.config.initialOffset ?? 0) > 0;
|
|
4200
4229
|
}
|
|
4201
4230
|
if (interval) {
|
|
4202
4231
|
const current = this.state.getLatestValue();
|
|
@@ -4224,7 +4253,7 @@ var BasePaginator = class {
|
|
|
4224
4253
|
}
|
|
4225
4254
|
const state = this.getStateAfterQuery(stateUpdate, isFirstPage);
|
|
4226
4255
|
if (updateState) this.state.next(state);
|
|
4227
|
-
this.populateOfflineDbAfterQuery({ items: state.items, queryShape });
|
|
4256
|
+
if (isFirstPage) this.populateOfflineDbAfterQuery({ items: state.items, queryShape });
|
|
4228
4257
|
return {
|
|
4229
4258
|
stateCandidate: state,
|
|
4230
4259
|
targetInterval: interval
|
|
@@ -5854,7 +5883,7 @@ var QUEUEABLE_OPERATIONS = {
|
|
|
5854
5883
|
const message = result?.message;
|
|
5855
5884
|
if (!message) return;
|
|
5856
5885
|
if (message.parent_id) {
|
|
5857
|
-
client.threads.
|
|
5886
|
+
client.threads.get(message.parent_id)?.upsertReplyLocally({
|
|
5858
5887
|
message,
|
|
5859
5888
|
timestampChanged: true
|
|
5860
5889
|
});
|
|
@@ -7715,6 +7744,9 @@ function isErrorRetryable(error) {
|
|
|
7715
7744
|
if (!err) return false;
|
|
7716
7745
|
return err.retryable;
|
|
7717
7746
|
}
|
|
7747
|
+
function isConnectionIDError(error) {
|
|
7748
|
+
return error.code === 46;
|
|
7749
|
+
}
|
|
7718
7750
|
function isEphemeral(error) {
|
|
7719
7751
|
if ((0, import_axios2.isCancel)(error)) return false;
|
|
7720
7752
|
if (!error.response) return true;
|
|
@@ -8153,6 +8185,8 @@ var ChannelPaginator = class extends BasePaginator {
|
|
|
8153
8185
|
* continuation rather than a first page. See `executeQuery`.
|
|
8154
8186
|
*/
|
|
8155
8187
|
this._itemsLoadedFromOfflineDB = false;
|
|
8188
|
+
/** The query in flight refreshes a cache-only window, so its response replaces the window. */
|
|
8189
|
+
this._replaceOfflineSeedOnResponse = false;
|
|
8156
8190
|
/**
|
|
8157
8191
|
* Filters sent to the server with the next query (and used as the offline-db query key). Deliberately
|
|
8158
8192
|
* built from the locally configured filters only — feeding the backend-resolved filter back in would
|
|
@@ -8209,6 +8243,7 @@ var ChannelPaginator = class extends BasePaginator {
|
|
|
8209
8243
|
});
|
|
8210
8244
|
};
|
|
8211
8245
|
this.query = async () => {
|
|
8246
|
+
if (this._itemsLoadedFromOfflineDB) this._replaceOfflineSeedOnResponse = true;
|
|
8212
8247
|
this._itemsLoadedFromOfflineDB = false;
|
|
8213
8248
|
if (!this._nextQueryShape) {
|
|
8214
8249
|
this._nextQueryShape = this.getNextQueryShape();
|
|
@@ -8376,14 +8411,22 @@ var ChannelPaginator = class extends BasePaginator {
|
|
|
8376
8411
|
if (params.isFirstPage && params.results) {
|
|
8377
8412
|
this.applyPredefinedFilterResponse(pendingPredefinedFilter);
|
|
8378
8413
|
}
|
|
8414
|
+
const replaceOfflineSeed = this._replaceOfflineSeedOnResponse;
|
|
8415
|
+
this._replaceOfflineSeedOnResponse = false;
|
|
8416
|
+
if (replaceOfflineSeed && params.results) {
|
|
8417
|
+
this.clearItemStorage();
|
|
8418
|
+
return super.postQueryReconcile({ ...params, keepPreviousItems: false });
|
|
8419
|
+
}
|
|
8379
8420
|
return super.postQueryReconcile(params);
|
|
8380
8421
|
}
|
|
8381
8422
|
// invoked inside BasePaginator.executeQuery() to keep it as a query descriptor;
|
|
8382
|
-
getNextQueryShape(
|
|
8423
|
+
getNextQueryShape({
|
|
8424
|
+
pageSize
|
|
8425
|
+
} = {}) {
|
|
8383
8426
|
const shape = {
|
|
8384
8427
|
filter_conditions: this.buildQueryFilters(),
|
|
8385
8428
|
...this.options,
|
|
8386
|
-
limit: this.pageSize,
|
|
8429
|
+
limit: pageSize ?? this.pageSize,
|
|
8387
8430
|
offset: this.offset
|
|
8388
8431
|
};
|
|
8389
8432
|
if (this.sort) {
|
|
@@ -8420,7 +8463,7 @@ var ChannelPaginator = class extends BasePaginator {
|
|
|
8420
8463
|
}) {
|
|
8421
8464
|
this.client.offlineDb?.executeQuerySafely(
|
|
8422
8465
|
(db) => db.upsertCidsForQuery({
|
|
8423
|
-
cids,
|
|
8466
|
+
cids: cids.slice(0, this.pageSize),
|
|
8424
8467
|
filters: request.filter_conditions,
|
|
8425
8468
|
options: request,
|
|
8426
8469
|
predefinedFilter: this._predefinedFilter,
|
|
@@ -9022,6 +9065,130 @@ var UserGroupPaginator = class extends BasePaginator {
|
|
|
9022
9065
|
}
|
|
9023
9066
|
};
|
|
9024
9067
|
|
|
9068
|
+
// src/pagination/paginators/ThreadPaginator.ts
|
|
9069
|
+
var DEFAULT_THREAD_PAGE_SIZE = 25;
|
|
9070
|
+
var DEFAULT_THREAD_QUERY = {
|
|
9071
|
+
participant_limit: 10,
|
|
9072
|
+
reply_limit: 10,
|
|
9073
|
+
watch: true
|
|
9074
|
+
};
|
|
9075
|
+
var getQueryIdentity2 = (queryShape) => {
|
|
9076
|
+
if (!queryShape) return queryShape;
|
|
9077
|
+
const { limit: _, next: __, prev: ___, ...identity } = queryShape;
|
|
9078
|
+
return identity;
|
|
9079
|
+
};
|
|
9080
|
+
var hasPaginationQueryShapeChanged3 = (prevQueryShape, nextQueryShape) => !isEqual(getQueryIdentity2(prevQueryShape), getQueryIdentity2(nextQueryShape));
|
|
9081
|
+
var ThreadPaginator = class extends BasePaginator {
|
|
9082
|
+
constructor({
|
|
9083
|
+
client,
|
|
9084
|
+
store = new EntityStore({ getEntityId: (thread) => thread.id }),
|
|
9085
|
+
paginatorOptions
|
|
9086
|
+
}) {
|
|
9087
|
+
super({
|
|
9088
|
+
hasPaginationQueryShapeChanged: hasPaginationQueryShapeChanged3,
|
|
9089
|
+
initialCursor: ZERO_PAGE_CURSOR,
|
|
9090
|
+
itemIndex: new StoreBackedItemIndex({
|
|
9091
|
+
getEntityId: (thread) => thread.id,
|
|
9092
|
+
store
|
|
9093
|
+
}),
|
|
9094
|
+
pageSize: DEFAULT_THREAD_PAGE_SIZE,
|
|
9095
|
+
...paginatorOptions
|
|
9096
|
+
});
|
|
9097
|
+
this.query = async ({
|
|
9098
|
+
queryShape
|
|
9099
|
+
}) => {
|
|
9100
|
+
const { threads, next } = await this.client.queryThreadsAndHydrate(queryShape);
|
|
9101
|
+
return { items: threads.map(this.toStoredInstance), tailward: next };
|
|
9102
|
+
};
|
|
9103
|
+
/**
|
|
9104
|
+
* The stored instance for a queried thread, so there's one per id. The index upserts, so a second
|
|
9105
|
+
* instance would replace the stored one. A stale stored thread is refreshed from the query, unless
|
|
9106
|
+
* it's open (active) at which point it reloads itself and this would replace the replies on screen.
|
|
9107
|
+
*/
|
|
9108
|
+
this.toStoredInstance = (incoming) => {
|
|
9109
|
+
const stored = this.store.get(incoming.id);
|
|
9110
|
+
if (!stored) return incoming;
|
|
9111
|
+
if (stored.hasStaleState && !stored.state.getLatestValue().active) {
|
|
9112
|
+
stored.hydrateState(incoming);
|
|
9113
|
+
}
|
|
9114
|
+
return stored;
|
|
9115
|
+
};
|
|
9116
|
+
this.filterQueryResults = (threads) => threads;
|
|
9117
|
+
/**
|
|
9118
|
+
* Re-queries the first page and swaps the list in place, in server order; also the first load. The
|
|
9119
|
+
* current threads stay until the response lands, and a failed query leaves them untouched and
|
|
9120
|
+
* records `lastQueryError`; a successful one clears it. Like any paginator, the first load publishes
|
|
9121
|
+
* `isLoading`; a reload of a loaded list does not, since `isLoading` with items means the next page.
|
|
9122
|
+
*/
|
|
9123
|
+
this.reload = async ({ limit = this.pageSize } = {}) => {
|
|
9124
|
+
const isFirstLoad = !this.hasResults;
|
|
9125
|
+
if (isFirstLoad) this.state.partialNext({ isLoading: true });
|
|
9126
|
+
let replaced = false;
|
|
9127
|
+
try {
|
|
9128
|
+
const results = await this.runQueryRetryable({
|
|
9129
|
+
queryShape: { ...DEFAULT_THREAD_QUERY, limit },
|
|
9130
|
+
retryCount: this.config.retryCount
|
|
9131
|
+
});
|
|
9132
|
+
if (!results) return;
|
|
9133
|
+
const nextIds = new Set(results.items.map((thread) => thread.id));
|
|
9134
|
+
this.setIntervals([]);
|
|
9135
|
+
this.setActiveInterval(void 0);
|
|
9136
|
+
for (const [id] of this._itemIndex.entries()) {
|
|
9137
|
+
if (!nextIds.has(id)) this._itemIndex.remove(id);
|
|
9138
|
+
}
|
|
9139
|
+
this.setItems({
|
|
9140
|
+
cursor: { headward: void 0, tailward: results.tailward },
|
|
9141
|
+
isFirstPage: true,
|
|
9142
|
+
isLastPage: !results.tailward,
|
|
9143
|
+
valueOrFactory: results.items
|
|
9144
|
+
});
|
|
9145
|
+
replaced = true;
|
|
9146
|
+
} finally {
|
|
9147
|
+
const clearError = replaced && this.lastQueryError;
|
|
9148
|
+
if (isFirstLoad || clearError) {
|
|
9149
|
+
this.state.partialNext({
|
|
9150
|
+
...isFirstLoad ? { isLoading: false } : {},
|
|
9151
|
+
...clearError ? { lastQueryError: void 0 } : {}
|
|
9152
|
+
});
|
|
9153
|
+
}
|
|
9154
|
+
}
|
|
9155
|
+
};
|
|
9156
|
+
this.client = client;
|
|
9157
|
+
this.store = store;
|
|
9158
|
+
}
|
|
9159
|
+
/**
|
|
9160
|
+
* No next page until a load has landed (`master`'s `nextCursor: null`), so `toTail()` is a no-op
|
|
9161
|
+
* before the first successful reload, e.g. when an empty list fires `onEndReached` on mount.
|
|
9162
|
+
*/
|
|
9163
|
+
get initialState() {
|
|
9164
|
+
return {
|
|
9165
|
+
...super.initialState,
|
|
9166
|
+
hasMoreTail: false
|
|
9167
|
+
};
|
|
9168
|
+
}
|
|
9169
|
+
getItemId(thread) {
|
|
9170
|
+
return thread.id;
|
|
9171
|
+
}
|
|
9172
|
+
getNextQueryShape({
|
|
9173
|
+
direction
|
|
9174
|
+
}) {
|
|
9175
|
+
const cursor = this.cursor?.[direction] ?? void 0;
|
|
9176
|
+
return {
|
|
9177
|
+
...DEFAULT_THREAD_QUERY,
|
|
9178
|
+
limit: this.pageSize,
|
|
9179
|
+
...cursor ? { [direction === "tailward" ? "next" : "prev"]: cursor } : {}
|
|
9180
|
+
};
|
|
9181
|
+
}
|
|
9182
|
+
/**
|
|
9183
|
+
* Also releases the list's hold on its threads, which the base reset leaves in the index. Released
|
|
9184
|
+
* before the base publishes, so a subscriber sees the threads already gone from the registry.
|
|
9185
|
+
*/
|
|
9186
|
+
resetState() {
|
|
9187
|
+
this._itemIndex.clear();
|
|
9188
|
+
super.resetState();
|
|
9189
|
+
}
|
|
9190
|
+
};
|
|
9191
|
+
|
|
9025
9192
|
// src/search/ChannelMemberSearchSource.ts
|
|
9026
9193
|
var ChannelMemberSearchSource = class extends BaseSearchSource {
|
|
9027
9194
|
constructor(channel, options, filterBuilderOptions = {}) {
|
|
@@ -12621,7 +12788,7 @@ var createMessageOperations = (collection) => {
|
|
|
12621
12788
|
channel.messagePaginator.removeItem({ id });
|
|
12622
12789
|
channel.pinnedMessagesPaginator.removeItem({ id });
|
|
12623
12790
|
if (parentId) {
|
|
12624
|
-
client.threads.
|
|
12791
|
+
client.threads.get(parentId)?.messagePaginator.removeItem({ id });
|
|
12625
12792
|
}
|
|
12626
12793
|
},
|
|
12627
12794
|
...parentMessageId ? {
|
|
@@ -12666,7 +12833,7 @@ var createMessageOperations = (collection) => {
|
|
|
12666
12833
|
update: async (m, o) => {
|
|
12667
12834
|
const result = await channel.getClient().updateMessage({
|
|
12668
12835
|
id: m.id,
|
|
12669
|
-
message:
|
|
12836
|
+
message: toUpdatedMessagePayload(m),
|
|
12670
12837
|
...o
|
|
12671
12838
|
});
|
|
12672
12839
|
return { message: result.message };
|
|
@@ -12777,10 +12944,15 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12777
12944
|
* reload on reconnect, so an unbalanced `deactivate()` now costs more than a missed auto-read —
|
|
12778
12945
|
* hence the refcount, matching `channel.activate()`. A thread held by more than one mount stays
|
|
12779
12946
|
* active until the last holder releases it.
|
|
12947
|
+
*
|
|
12948
|
+
* The first activation also keeps the thread in `client.threads` for the rest of the session:
|
|
12949
|
+
* it stays subscribed and resolvable through `client.threads.get(id)` after it is deactivated,
|
|
12950
|
+
* whether or not the thread list holds it. Reopening it therefore needs no fetch unless it went stale.
|
|
12780
12951
|
*/
|
|
12781
12952
|
this.activate = () => {
|
|
12782
12953
|
this._activeRefCount += 1;
|
|
12783
12954
|
if (this._activeRefCount === 1) {
|
|
12955
|
+
this.client.threads.register(this);
|
|
12784
12956
|
this.state.partialNext({ active: true });
|
|
12785
12957
|
}
|
|
12786
12958
|
};
|
|
@@ -12802,10 +12974,9 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12802
12974
|
*
|
|
12803
12975
|
* Preserves failed (unsent) replies. They are read out of the reply paginator rather than out of
|
|
12804
12976
|
* `failedRepliesMap`, because that map is only written by `upsertReplyLocally`, whose callers are
|
|
12805
|
-
* this thread's own subscriptions and the offline-DB path
|
|
12806
|
-
* neither of which covers a thread
|
|
12807
|
-
*
|
|
12808
|
-
* is true for managed and unmanaged threads alike. An overlap merge keeps them anyway (the
|
|
12977
|
+
* this thread's own subscriptions and the offline-DB path resolved through `client.threads.get` —
|
|
12978
|
+
* neither of which covers a thread that was never activated or listed. Reading the paginator
|
|
12979
|
+
* is true for registered and unregistered threads alike. An overlap merge keeps them anyway (the
|
|
12809
12980
|
* reconcile's provenance guard never prunes a non-server message); only a disjoint rebuild can drop
|
|
12810
12981
|
* them, so any that actually fell out are re-ingested below.
|
|
12811
12982
|
*
|
|
@@ -12889,6 +13060,7 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12889
13060
|
this.client.messageStore.upsert(parentMessage);
|
|
12890
13061
|
}
|
|
12891
13062
|
const incomingReplies = thread.messagePaginator.state.getLatestValue().items ?? [];
|
|
13063
|
+
if (!options?.reconcile) this.messagePaginator.clearStateAndCache();
|
|
12892
13064
|
if (typeof this.messagePaginator.items === "undefined") {
|
|
12893
13065
|
this.messagePaginator.setItems({
|
|
12894
13066
|
valueOrFactory: incomingReplies,
|
|
@@ -12907,16 +13079,9 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12907
13079
|
}
|
|
12908
13080
|
this.addUnsubscribeFunction(this.subscribeThreadSetupStateChange());
|
|
12909
13081
|
this.addUnsubscribeFunction(this.subscribeParentMessageFromStore());
|
|
12910
|
-
this.addUnsubscribeFunction(this.subscribeThreadUpdated());
|
|
12911
13082
|
this.addUnsubscribeFunction(this.subscribeMarkActiveThreadRead());
|
|
12912
13083
|
this.addUnsubscribeFunction(this.subscribeReloadActiveStaleThread());
|
|
12913
|
-
this.addUnsubscribeFunction(this.
|
|
12914
|
-
this.addUnsubscribeFunction(this.subscribeNewReplies());
|
|
12915
|
-
this.addUnsubscribeFunction(this.subscribeRepliesRead());
|
|
12916
|
-
this.addUnsubscribeFunction(this.subscribeRepliesUnread());
|
|
12917
|
-
this.addUnsubscribeFunction(this.subscribeMessageDeleted());
|
|
12918
|
-
this.addUnsubscribeFunction(this.subscribeMessageUpdated());
|
|
12919
|
-
this.addUnsubscribeFunction(this.subscribeUserMessagesDeleted());
|
|
13084
|
+
this.addUnsubscribeFunction(this.subscribeMarkStaleOnStopWatching());
|
|
12920
13085
|
};
|
|
12921
13086
|
/**
|
|
12922
13087
|
* Subscribes this thread to the `'thread'` configuration key. Registered through
|
|
@@ -12933,10 +13098,10 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12933
13098
|
* `hasLiveInstances('thread')` does not count it when deciding whether to warn about a
|
|
12934
13099
|
* construction-only path registered too late.
|
|
12935
13100
|
*
|
|
12936
|
-
* So read "declarative configuration is unaffected" as *at construction only*. A thread
|
|
12937
|
-
* `ThreadManager` that has itself registered is covered —
|
|
12938
|
-
* `registerSubscriptions()` on every thread
|
|
12939
|
-
*
|
|
13101
|
+
* So read "declarative configuration is unaffected" as *at construction only*. A thread registered
|
|
13102
|
+
* with a `ThreadManager` that has itself registered is covered — the manager calls
|
|
13103
|
+
* `registerSubscriptions()` on every thread the list holds or that is active — so the common path
|
|
13104
|
+
* is fine. A thread that is neither, or held by an unregistered manager, is not.
|
|
12940
13105
|
*
|
|
12941
13106
|
* The alternative — applying the setup function at construction — would break the teardown symmetry
|
|
12942
13107
|
* `WithSubscriptions` provides, which is why the asymmetry stands.
|
|
@@ -12952,7 +13117,7 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12952
13117
|
// on a change there, so the setup function's overrides survive.
|
|
12953
13118
|
alsoWatch: ["messagePaginator", "messageOperations"]
|
|
12954
13119
|
});
|
|
12955
|
-
this.
|
|
13120
|
+
this.handleThreadUpdated = (event) => {
|
|
12956
13121
|
if (!event.thread || event.thread.parent_message_id !== this.id) {
|
|
12957
13122
|
return;
|
|
12958
13123
|
}
|
|
@@ -12963,7 +13128,7 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12963
13128
|
deletedAt: threadData.deleted_at ?? null,
|
|
12964
13129
|
custom: threadData.custom ?? {}
|
|
12965
13130
|
});
|
|
12966
|
-
}
|
|
13131
|
+
};
|
|
12967
13132
|
this.subscribeMarkActiveThreadRead = () => this.state.subscribeWithSelector(
|
|
12968
13133
|
(nextValue) => ({
|
|
12969
13134
|
active: nextValue.active,
|
|
@@ -12982,14 +13147,19 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12982
13147
|
}
|
|
12983
13148
|
}
|
|
12984
13149
|
);
|
|
12985
|
-
|
|
12986
|
-
|
|
12987
|
-
|
|
12988
|
-
|
|
13150
|
+
/**
|
|
13151
|
+
* A watch stopped while the connection stays up (`NotWatching`) ends the channel's events with no
|
|
13152
|
+
* reconnect to catch it. A dropped connection (`WasWatching`) is left to recovery.
|
|
13153
|
+
*/
|
|
13154
|
+
this.subscribeMarkStaleOnStopWatching = () => this.channel.state.subscribeWithSelector(
|
|
13155
|
+
({ watchStatus }) => ({ watchStatus }),
|
|
13156
|
+
({ watchStatus }, previous) => {
|
|
13157
|
+
if (previous?.watchStatus === ChannelWatchStatus.Watching && watchStatus === ChannelWatchStatus.NotWatching) {
|
|
13158
|
+
this.state.partialNext({ isStateStale: true });
|
|
13159
|
+
}
|
|
12989
13160
|
}
|
|
12990
|
-
|
|
12991
|
-
|
|
12992
|
-
this.subscribeRepliesUnread = () => this.client.on("notification.mark_unread", (event) => {
|
|
13161
|
+
);
|
|
13162
|
+
this.handleRepliesUnread = (event) => {
|
|
12993
13163
|
if (!event.user || event.created_at == null || !event.thread_id) return;
|
|
12994
13164
|
if (event.thread_id !== this.id) return;
|
|
12995
13165
|
const userId = event.user.id;
|
|
@@ -13008,8 +13178,8 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
13008
13178
|
}
|
|
13009
13179
|
}
|
|
13010
13180
|
}));
|
|
13011
|
-
}
|
|
13012
|
-
this.
|
|
13181
|
+
};
|
|
13182
|
+
this.handleNewReply = (event) => {
|
|
13013
13183
|
if (!this.client.userId || event.message?.parent_id !== this.id) {
|
|
13014
13184
|
return;
|
|
13015
13185
|
}
|
|
@@ -13047,8 +13217,8 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
13047
13217
|
}
|
|
13048
13218
|
}
|
|
13049
13219
|
this.state.partialNext({ read: nextRead });
|
|
13050
|
-
}
|
|
13051
|
-
this.
|
|
13220
|
+
};
|
|
13221
|
+
this.handleRepliesRead = (event) => {
|
|
13052
13222
|
if (!event.user || event.created_at == null || !event.thread) return;
|
|
13053
13223
|
if (event.thread.parent_message_id !== this.id) return;
|
|
13054
13224
|
const userId = event.user.id;
|
|
@@ -13066,8 +13236,8 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
13066
13236
|
}
|
|
13067
13237
|
}
|
|
13068
13238
|
}));
|
|
13069
|
-
}
|
|
13070
|
-
this.
|
|
13239
|
+
};
|
|
13240
|
+
this.handleMessageDeleted = (event) => {
|
|
13071
13241
|
if (!event.message) return;
|
|
13072
13242
|
const formattedMessage = formatMessage(event.message);
|
|
13073
13243
|
if (event.message.parent_id === this.id) {
|
|
@@ -13081,52 +13251,35 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
13081
13251
|
this.updateParentMessageLocally({ message: event.message });
|
|
13082
13252
|
}
|
|
13083
13253
|
this.messagePaginator.reflectQuotedMessageUpdate(formattedMessage);
|
|
13084
|
-
}).unsubscribe;
|
|
13085
|
-
this.subscribeMessageUpdated = () => {
|
|
13086
|
-
const messageUpdateTypes = ["message.updated", "message.undeleted"];
|
|
13087
|
-
const reactionTypes = [
|
|
13088
|
-
"reaction.new",
|
|
13089
|
-
"reaction.deleted",
|
|
13090
|
-
"reaction.updated"
|
|
13091
|
-
];
|
|
13092
|
-
const unsubscribeMessageUpdated = messageUpdateTypes.map(
|
|
13093
|
-
(eventType) => this.client.on(eventType, (event) => {
|
|
13094
|
-
if (!event.message) return;
|
|
13095
|
-
const message = event.message.parent_id === this.id ? {
|
|
13096
|
-
...event.message,
|
|
13097
|
-
own_reactions: this.messagePaginator.getItem(event.message.id)?.own_reactions ?? event.message.own_reactions
|
|
13098
|
-
} : !event.message.parent_id && event.message.id === this.id ? {
|
|
13099
|
-
...event.message,
|
|
13100
|
-
own_reactions: this.state.getLatestValue().parentMessage?.own_reactions ?? event.message.own_reactions
|
|
13101
|
-
} : event.message;
|
|
13102
|
-
this.updateParentMessageOrReplyLocally(message);
|
|
13103
|
-
this.messagePaginator.reflectQuotedMessageUpdate(formatMessage(event.message));
|
|
13104
|
-
}).unsubscribe
|
|
13105
|
-
);
|
|
13106
|
-
const unsubscribeReactions = reactionTypes.map(
|
|
13107
|
-
(eventType) => this.client.on(eventType, (event) => {
|
|
13108
|
-
if (!event.message || !event.reaction) return;
|
|
13109
|
-
const { message } = event;
|
|
13110
|
-
this.messagePaginator.reflectQuotedMessageUpdate(formatMessage(message));
|
|
13111
|
-
}).unsubscribe
|
|
13112
|
-
);
|
|
13113
|
-
const unsubscribeFunctions = [...unsubscribeMessageUpdated, ...unsubscribeReactions];
|
|
13114
|
-
return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
|
|
13115
13254
|
};
|
|
13116
|
-
this.
|
|
13117
|
-
|
|
13118
|
-
const
|
|
13119
|
-
|
|
13120
|
-
|
|
13121
|
-
|
|
13122
|
-
|
|
13123
|
-
|
|
13124
|
-
|
|
13125
|
-
|
|
13126
|
-
|
|
13127
|
-
|
|
13128
|
-
|
|
13129
|
-
|
|
13255
|
+
this.handleMessageUpdated = (event) => {
|
|
13256
|
+
if (!event.message) return;
|
|
13257
|
+
const message = event.message.parent_id === this.id ? {
|
|
13258
|
+
...event.message,
|
|
13259
|
+
own_reactions: this.messagePaginator.getItem(event.message.id)?.own_reactions ?? event.message.own_reactions
|
|
13260
|
+
} : !event.message.parent_id && event.message.id === this.id ? {
|
|
13261
|
+
...event.message,
|
|
13262
|
+
own_reactions: this.state.getLatestValue().parentMessage?.own_reactions ?? event.message.own_reactions
|
|
13263
|
+
} : event.message;
|
|
13264
|
+
this.updateParentMessageOrReplyLocally(message);
|
|
13265
|
+
this.messagePaginator.reflectQuotedMessageUpdate(formatMessage(event.message));
|
|
13266
|
+
};
|
|
13267
|
+
this.handleReactionChanged = (event) => {
|
|
13268
|
+
if (!event.message || !event.reaction) return;
|
|
13269
|
+
const { message } = event;
|
|
13270
|
+
this.messagePaginator.reflectQuotedMessageUpdate(formatMessage(message));
|
|
13271
|
+
};
|
|
13272
|
+
// Apply a user ban / deletion to this thread's own reply list. Previously
|
|
13273
|
+
// channel.state.deleteUserMessages marked banned-user replies deleted in the (now removed)
|
|
13274
|
+
// channel.state.threads shadow; the reply paginator is the thread's source of truth now.
|
|
13275
|
+
this.handleUserMessagesDeleted = (event) => {
|
|
13276
|
+
if (!event.user) return;
|
|
13277
|
+
const deletedAtSource = event.type === "user.deleted" ? event.user.deleted_at : event.created_at;
|
|
13278
|
+
this.messagePaginator.applyMessageDeletionForUser({
|
|
13279
|
+
userId: event.user.id,
|
|
13280
|
+
hardDelete: !!event.hard_delete,
|
|
13281
|
+
deletedAt: deletedAtSource ?? nowNs()
|
|
13282
|
+
});
|
|
13130
13283
|
};
|
|
13131
13284
|
// The parent message lives in the client-global message store (one canonical POJO per id);
|
|
13132
13285
|
// `state.parentMessage` and the fields derived from it are a projection of that copy. Every update
|
|
@@ -14221,13 +14374,13 @@ var _MessageComposer = class _MessageComposer extends WithSubscriptions {
|
|
|
14221
14374
|
*
|
|
14222
14375
|
* Thread replies live in `thread.messagePaginator`, which is independent of the channel's, so
|
|
14223
14376
|
* dispatching to the wrong one leaves the reply invisible where it was written. An edit composer
|
|
14224
|
-
* is built from the message rather than from its thread, so it
|
|
14225
|
-
*
|
|
14377
|
+
* is built from the message rather than from its thread, so it finds the thread only when
|
|
14378
|
+
* `client.threads` has it registered — listed, or active (open somewhere).
|
|
14226
14379
|
*/
|
|
14227
14380
|
get defaultSubmitTarget() {
|
|
14228
14381
|
if (this.compositionContext instanceof Thread) return this.compositionContext;
|
|
14229
14382
|
const { threadId } = this;
|
|
14230
|
-
return threadId && this.client.threads.
|
|
14383
|
+
return threadId && this.client.threads.get(threadId) || this.channel;
|
|
14231
14384
|
}
|
|
14232
14385
|
/**
|
|
14233
14386
|
* Whether submitting keeps the composer's contents rather than clearing them - see
|
|
@@ -17697,6 +17850,7 @@ var runWithRetry = (f, {
|
|
|
17697
17850
|
// src/api-client.ts
|
|
17698
17851
|
var logger9 = chatLoggerSystem.getLogger("api-client");
|
|
17699
17852
|
var MULTIPART_CONTENT_TYPE = "multipart/form-data";
|
|
17853
|
+
var LONG_POLL_PATH = "/api/v2/longpoll";
|
|
17700
17854
|
var UPLOAD_REQUEST_DEFAULTS = {
|
|
17701
17855
|
timeout: 0,
|
|
17702
17856
|
maxContentLength: Infinity,
|
|
@@ -17717,8 +17871,8 @@ var ApiClient = class {
|
|
|
17717
17871
|
params: queryParams,
|
|
17718
17872
|
headers: { "Content-Type": requestContentType },
|
|
17719
17873
|
...isMultipart ? UPLOAD_REQUEST_DEFAULTS : {},
|
|
17720
|
-
// Keep this last so a caller-supplied signal
|
|
17721
|
-
// it owns, so it can never clobber the upload defaults above.
|
|
17874
|
+
// Keep this last so a caller-supplied signal and timeout win - and keep it returning only
|
|
17875
|
+
// the keys it owns, so it can never clobber the upload defaults above.
|
|
17722
17876
|
...toAxiosRequestConfig(options)
|
|
17723
17877
|
});
|
|
17724
17878
|
}
|
|
@@ -17749,7 +17903,11 @@ var ApiClient = class {
|
|
|
17749
17903
|
}
|
|
17750
17904
|
}
|
|
17751
17905
|
if (resolved.startsWith("/")) {
|
|
17752
|
-
|
|
17906
|
+
const baseURL = resolved === LONG_POLL_PATH ? (
|
|
17907
|
+
// replace port if present for testing with local API
|
|
17908
|
+
this.client.baseURL?.replace(":3030", ":8900")
|
|
17909
|
+
) : this.client.baseURL;
|
|
17910
|
+
resolved = baseURL + resolved;
|
|
17753
17911
|
}
|
|
17754
17912
|
return resolved;
|
|
17755
17913
|
}
|
|
@@ -17803,6 +17961,7 @@ var ApiClient = class {
|
|
|
17803
17961
|
params: { ...additionalConfig.params, connection_id: connectionId }
|
|
17804
17962
|
};
|
|
17805
17963
|
}
|
|
17964
|
+
await this.client.tokenManager.tokenReady();
|
|
17806
17965
|
const initialRequestConfig = this.populateRequestConfigWithDefaults(additionalConfig);
|
|
17807
17966
|
const clientRequestId = initialRequestConfig.headers?.["x-client-request-id"];
|
|
17808
17967
|
try {
|
|
@@ -17864,25 +18023,23 @@ var ApiClient = class {
|
|
|
17864
18023
|
var requiresConnectionId = (params, body) => {
|
|
17865
18024
|
const payload = params?.payload;
|
|
17866
18025
|
const requestBody = typeof body === "object" && body !== null ? body : void 0;
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
17870
|
-
if ([params, payload, requestBody].some(
|
|
17871
|
-
(source) => source && ("watch" in source || "presence" in source)
|
|
17872
|
-
)) {
|
|
17873
|
-
return false;
|
|
17874
|
-
}
|
|
17875
|
-
return Boolean(params && "connection_id" in params);
|
|
18026
|
+
return Boolean(
|
|
18027
|
+
params?.watch || params?.presence || payload?.watch || payload?.presence || requestBody?.watch || requestBody?.presence
|
|
18028
|
+
);
|
|
17876
18029
|
};
|
|
17877
18030
|
var isUsableAbortSignal = (signal) => typeof signal?.addEventListener === "function";
|
|
17878
18031
|
var toAxiosRequestConfig = ({
|
|
17879
18032
|
signal,
|
|
17880
|
-
onUploadProgress
|
|
18033
|
+
onUploadProgress,
|
|
18034
|
+
timeout
|
|
17881
18035
|
} = {}) => ({
|
|
17882
18036
|
signal: isUsableAbortSignal(signal) ? signal : void 0,
|
|
17883
18037
|
// Same reasoning as `isUsableAbortSignal`: an options object revived from a persisted
|
|
17884
18038
|
// offline-db task payload has lost its functions.
|
|
17885
|
-
onUploadProgress: typeof onUploadProgress === "function" ? onUploadProgress : void 0
|
|
18039
|
+
onUploadProgress: typeof onUploadProgress === "function" ? onUploadProgress : void 0,
|
|
18040
|
+
// Only when set: an explicit `undefined` would clobber the upload defaults' `timeout: 0`, and
|
|
18041
|
+
// axios would fall back to the instance timeout.
|
|
18042
|
+
...typeof timeout === "number" ? { timeout } : {}
|
|
17886
18043
|
});
|
|
17887
18044
|
var errorIsApiError = (error) => {
|
|
17888
18045
|
if (!(error instanceof import_axios3.AxiosError)) return false;
|
|
@@ -18457,26 +18614,126 @@ var eventCarriesOwnUser = (event) => Object.hasOwn(event, "me");
|
|
|
18457
18614
|
var DEFAULT_THREAD_MANAGER_CONFIG = deepFreezeConfig({
|
|
18458
18615
|
connectionRecoveryThrottleMs: 1e3
|
|
18459
18616
|
});
|
|
18460
|
-
var MAX_QUERY_THREADS_LIMIT = 25;
|
|
18461
18617
|
var THREAD_MANAGER_INITIAL_STATE = {
|
|
18462
18618
|
active: false,
|
|
18463
|
-
wasActivatedAtLeastOnce: false,
|
|
18464
18619
|
isThreadOrderStale: false,
|
|
18465
|
-
threads: [],
|
|
18466
18620
|
unreadThreadCount: 0,
|
|
18467
18621
|
unseenThreadIds: [],
|
|
18468
|
-
|
|
18469
|
-
isLoading: false,
|
|
18470
|
-
isLoadingNext: false,
|
|
18471
|
-
nextCursor: null
|
|
18472
|
-
},
|
|
18473
|
-
ready: false
|
|
18622
|
+
wasActivatedAtLeastOnce: false
|
|
18474
18623
|
};
|
|
18475
18624
|
var logger10 = chatLoggerSystem.getLogger("thread-manager");
|
|
18625
|
+
var getThreadId = (thread) => thread.id;
|
|
18626
|
+
var REGISTERED_HOLDER = { onEntitiesChanged: () => void 0 };
|
|
18476
18627
|
var ThreadManager = class extends WithSubscriptions {
|
|
18477
18628
|
constructor({ client }) {
|
|
18478
18629
|
super();
|
|
18630
|
+
/**
|
|
18631
|
+
* Every live thread, held by the list (the paginator's own index over this store) and/or
|
|
18632
|
+
* `REGISTERED_HOLDER` (opened this session). A thread leaves once neither holds it.
|
|
18633
|
+
*/
|
|
18634
|
+
this.threadStore = new EntityStore({ getEntityId: getThreadId });
|
|
18635
|
+
/** A reload is in flight; a second one is dropped (`master`'s `pagination.isLoading` guard). */
|
|
18636
|
+
this.isReloadInFlight = false;
|
|
18637
|
+
/**
|
|
18638
|
+
* A local `pendingDisposal` listener (a state subscription, not a server watch) per channel instance
|
|
18639
|
+
* that has had a listed or opened thread; removed when that channel is disposed or on reset.
|
|
18640
|
+
*/
|
|
18641
|
+
this.disposalListeners = /* @__PURE__ */ new Map();
|
|
18642
|
+
/**
|
|
18643
|
+
* The live thread for `id`: one in the list, or one opened this session (e.g. from a message
|
|
18644
|
+
* list). Resolve threads through this, not `paginator.items`, which is the list only; list
|
|
18645
|
+
* membership is `paginator.getItem(id)`.
|
|
18646
|
+
*/
|
|
18647
|
+
this.get = (id) => this.threadStore.get(id);
|
|
18648
|
+
/**
|
|
18649
|
+
* Returns the thread for `parentMessage`, creating it if it doesn't exist yet. Use this to open a
|
|
18650
|
+
* thread instead of calling `new Thread()`.
|
|
18651
|
+
*
|
|
18652
|
+
* The returned thread is registered right away. If the thread list loads it before the UI
|
|
18653
|
+
* activates it, the list reuses this instance instead of creating a duplicate.
|
|
18654
|
+
*
|
|
18655
|
+
* A thread created here only has its parent message, so it starts stale and it loads its thread data
|
|
18656
|
+
* (participants, read state, replies) once, the first time it's opened.
|
|
18657
|
+
*/
|
|
18658
|
+
this.ensure = ({
|
|
18659
|
+
channel,
|
|
18660
|
+
parentMessage
|
|
18661
|
+
}) => {
|
|
18662
|
+
let thread = this.threadStore.get(parentMessage.id);
|
|
18663
|
+
if (!thread) {
|
|
18664
|
+
thread = new Thread({ channel, client: this.client, parentMessage });
|
|
18665
|
+
thread.state.partialNext({ isStateStale: true });
|
|
18666
|
+
}
|
|
18667
|
+
this.register(thread);
|
|
18668
|
+
return thread;
|
|
18669
|
+
};
|
|
18670
|
+
/**
|
|
18671
|
+
* Called by `thread.activate()`. Keeps `thread` for the session: it resolves through
|
|
18672
|
+
* {@link ThreadManager.get} and stays subscribed whether or not the list holds it, until its
|
|
18673
|
+
* channel is disposed or the user disconnects.
|
|
18674
|
+
*
|
|
18675
|
+
* @internal
|
|
18676
|
+
*/
|
|
18677
|
+
this.register = (thread) => {
|
|
18678
|
+
const { id } = thread;
|
|
18679
|
+
const registered = this.threadStore.get(id);
|
|
18680
|
+
if (registered && registered !== thread) {
|
|
18681
|
+
logger10.withExtraTags("register").warn(
|
|
18682
|
+
"Another instance of this thread is registered; this one stays unmanaged.",
|
|
18683
|
+
{
|
|
18684
|
+
threadId: id
|
|
18685
|
+
}
|
|
18686
|
+
);
|
|
18687
|
+
return;
|
|
18688
|
+
}
|
|
18689
|
+
this.threadStore.link(id, REGISTERED_HOLDER);
|
|
18690
|
+
this.threadStore.upsert(thread, REGISTERED_HOLDER);
|
|
18691
|
+
if (this.hasSubscriptions) thread.registerSubscriptions();
|
|
18692
|
+
this.listenForDisposal(thread);
|
|
18693
|
+
};
|
|
18694
|
+
/** Releases the `REGISTERED_HOLDER` hold on `thread`; the list's own diff unsubscribes it if it is still listed. */
|
|
18695
|
+
this.release = (thread) => {
|
|
18696
|
+
this.threadStore.unlink(thread.id, REGISTERED_HOLDER);
|
|
18697
|
+
if (this.hasSubscriptions && !this.paginator.items?.includes(thread)) {
|
|
18698
|
+
thread.unregisterSubscriptions();
|
|
18699
|
+
}
|
|
18700
|
+
};
|
|
18701
|
+
/** Listens for its channel's disposal the first time a thread on that channel is seen. */
|
|
18702
|
+
this.listenForDisposal = (thread) => {
|
|
18703
|
+
const { channel } = thread;
|
|
18704
|
+
if (this.disposalListeners.has(channel)) return;
|
|
18705
|
+
if (channel.pendingDisposal) {
|
|
18706
|
+
this.releaseChannel(channel);
|
|
18707
|
+
return;
|
|
18708
|
+
}
|
|
18709
|
+
this.disposalListeners.set(
|
|
18710
|
+
channel,
|
|
18711
|
+
channel.state.subscribeWithSelector(
|
|
18712
|
+
({ pendingDisposal }) => ({ pendingDisposal }),
|
|
18713
|
+
({ pendingDisposal }) => {
|
|
18714
|
+
if (pendingDisposal) this.releaseChannel(channel);
|
|
18715
|
+
}
|
|
18716
|
+
)
|
|
18717
|
+
);
|
|
18718
|
+
};
|
|
18719
|
+
/** The channel was disposed, so its threads are unusable: release every one of them. */
|
|
18720
|
+
this.releaseChannel = (channel) => {
|
|
18721
|
+
this.disposalListeners.get(channel)?.();
|
|
18722
|
+
this.disposalListeners.delete(channel);
|
|
18723
|
+
const onChannel = (thread) => thread.channel === channel;
|
|
18724
|
+
this.threadStore.values().filter(onChannel).forEach(this.release);
|
|
18725
|
+
const listed = (this.paginator.items ?? []).filter(onChannel);
|
|
18726
|
+
if (!listed.length) return;
|
|
18727
|
+
this.paginator.batch(
|
|
18728
|
+
() => listed.forEach(({ id }) => this.paginator.removeItem({ id })),
|
|
18729
|
+
{ coalesce: true }
|
|
18730
|
+
);
|
|
18731
|
+
};
|
|
18479
18732
|
this.resetState = () => {
|
|
18733
|
+
this.threadStore.values().forEach(this.release);
|
|
18734
|
+
this.paginator.resetState();
|
|
18735
|
+
this.disposalListeners.forEach((unsubscribe) => unsubscribe());
|
|
18736
|
+
this.disposalListeners.clear();
|
|
18480
18737
|
this.state.next(THREAD_MANAGER_INITIAL_STATE);
|
|
18481
18738
|
};
|
|
18482
18739
|
this.activate = () => {
|
|
@@ -18492,7 +18749,65 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18492
18749
|
this.addUnsubscribeFunction(this.subscribeReloadOnActivation());
|
|
18493
18750
|
this.addUnsubscribeFunction(this.subscribeNewReplies());
|
|
18494
18751
|
this.addUnsubscribeFunction(this.subscribeReloadOnConnectionRecovered());
|
|
18495
|
-
this.addUnsubscribeFunction(this.
|
|
18752
|
+
this.addUnsubscribeFunction(this.subscribeThreadEvents());
|
|
18753
|
+
this.threadStore.values().forEach((thread) => thread.registerSubscriptions());
|
|
18754
|
+
};
|
|
18755
|
+
/**
|
|
18756
|
+
* Hands client events to the threads in the store by id, as `PollManager` does for polls: a thread
|
|
18757
|
+
* doesn't listen to the client itself. A message event goes to the thread the message is a reply
|
|
18758
|
+
* in and the thread it starts; quoted-message updates stay within the reply's own thread, as on
|
|
18759
|
+
* `master`. A thread follows its channel's `watchStatus` itself, so watch events need no routing.
|
|
18760
|
+
*/
|
|
18761
|
+
this.subscribeThreadEvents = () => {
|
|
18762
|
+
const threadsOf = (message) => message ? [
|
|
18763
|
+
message.parent_id ? this.get(message.parent_id) : void 0,
|
|
18764
|
+
this.get(message.id)
|
|
18765
|
+
] : [];
|
|
18766
|
+
const unsubscribeFunctions = [
|
|
18767
|
+
this.client.on("message.new", (event) => {
|
|
18768
|
+
if (event.message?.parent_id)
|
|
18769
|
+
this.get(event.message.parent_id)?.handleNewReply(event);
|
|
18770
|
+
}),
|
|
18771
|
+
this.client.on("message.read", (event) => {
|
|
18772
|
+
if (event.thread)
|
|
18773
|
+
this.get(event.thread.parent_message_id)?.handleRepliesRead(event);
|
|
18774
|
+
}),
|
|
18775
|
+
this.client.on("notification.mark_unread", (event) => {
|
|
18776
|
+
if (event.thread_id) this.get(event.thread_id)?.handleRepliesUnread(event);
|
|
18777
|
+
}),
|
|
18778
|
+
this.client.on("thread.updated", (event) => {
|
|
18779
|
+
if (event.thread)
|
|
18780
|
+
this.get(event.thread.parent_message_id)?.handleThreadUpdated(event);
|
|
18781
|
+
}),
|
|
18782
|
+
this.client.on(
|
|
18783
|
+
"message.deleted",
|
|
18784
|
+
(event) => threadsOf(event.message).forEach((thread) => thread?.handleMessageDeleted(event))
|
|
18785
|
+
),
|
|
18786
|
+
...["message.updated", "message.undeleted"].map(
|
|
18787
|
+
(eventType) => this.client.on(
|
|
18788
|
+
eventType,
|
|
18789
|
+
(event) => threadsOf(event.message).forEach(
|
|
18790
|
+
(thread) => thread?.handleMessageUpdated(event)
|
|
18791
|
+
)
|
|
18792
|
+
)
|
|
18793
|
+
),
|
|
18794
|
+
...["reaction.new", "reaction.deleted", "reaction.updated"].map(
|
|
18795
|
+
(eventType) => this.client.on(
|
|
18796
|
+
eventType,
|
|
18797
|
+
(event) => threadsOf(event.message).forEach(
|
|
18798
|
+
(thread) => thread?.handleReactionChanged(event)
|
|
18799
|
+
)
|
|
18800
|
+
)
|
|
18801
|
+
),
|
|
18802
|
+
// A banned or deleted user's replies can be in any thread, so these two are the exception.
|
|
18803
|
+
...["user.messages.deleted", "user.deleted"].map(
|
|
18804
|
+
(eventType) => this.client.on(
|
|
18805
|
+
eventType,
|
|
18806
|
+
(event) => this.threadStore.values().forEach((thread) => thread.handleUserMessagesDeleted(event))
|
|
18807
|
+
)
|
|
18808
|
+
)
|
|
18809
|
+
].map(({ unsubscribe }) => unsubscribe);
|
|
18810
|
+
return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
|
|
18496
18811
|
};
|
|
18497
18812
|
this.subscribeUnreadThreadsCountChange = () => {
|
|
18498
18813
|
const { unread_threads: unreadThreadCount = 0 } = this.client.user ?? {};
|
|
@@ -18514,18 +18829,13 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18514
18829
|
);
|
|
18515
18830
|
return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
|
|
18516
18831
|
};
|
|
18517
|
-
this.
|
|
18518
|
-
|
|
18519
|
-
const { threads } = this.state.getLatestValue();
|
|
18520
|
-
const newThreads = threads.filter((thread) => thread.channel.cid !== cid);
|
|
18521
|
-
this.state.partialNext({ threads: newThreads });
|
|
18522
|
-
}).unsubscribe;
|
|
18523
|
-
this.subscribeManageThreadSubscriptions = () => this.state.subscribeWithSelector(
|
|
18524
|
-
(nextValue) => ({ threads: nextValue.threads }),
|
|
18832
|
+
this.subscribeManageThreadSubscriptions = () => this.paginator.state.subscribeWithSelector(
|
|
18833
|
+
(nextValue) => ({ threads: nextValue.items ?? [] }),
|
|
18525
18834
|
({ threads: nextThreads }, prev) => {
|
|
18526
18835
|
const { threads: prevThreads = [] } = prev ?? {};
|
|
18836
|
+
const listed = new Set(nextThreads);
|
|
18527
18837
|
const removedThreads = prevThreads.filter(
|
|
18528
|
-
(thread) => thread
|
|
18838
|
+
(thread) => !listed.has(thread) && !this.threadStore.isHeldBy(thread.id, REGISTERED_HOLDER)
|
|
18529
18839
|
);
|
|
18530
18840
|
nextThreads.forEach((thread) => thread.registerSubscriptions());
|
|
18531
18841
|
removedThreads.forEach((thread) => thread.unregisterSubscriptions());
|
|
@@ -18540,9 +18850,9 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18540
18850
|
this.subscribeNewReplies = () => this.client.on("notification.thread_message_new", (event) => {
|
|
18541
18851
|
const parentId = event.message?.parent_id;
|
|
18542
18852
|
if (!parentId) return;
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
if (this.
|
|
18853
|
+
if (!this.paginator.hasResults) return;
|
|
18854
|
+
const { unseenThreadIds } = this.state.getLatestValue();
|
|
18855
|
+
if (this.paginator.getItem(parentId)) {
|
|
18546
18856
|
this.state.partialNext({ isThreadOrderStale: true });
|
|
18547
18857
|
} else if (!unseenThreadIds.includes(parentId)) {
|
|
18548
18858
|
this.state.partialNext({ unseenThreadIds: unseenThreadIds.concat(parentId) });
|
|
@@ -18569,94 +18879,33 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18569
18879
|
return this.client.on("connection.recovered", throttledHandleConnectionRecovered).unsubscribe;
|
|
18570
18880
|
};
|
|
18571
18881
|
this.unregisterSubscriptions = () => {
|
|
18572
|
-
this.
|
|
18882
|
+
this.threadStore.values().forEach((thread) => thread.unregisterSubscriptions());
|
|
18573
18883
|
return super.unregisterSubscriptions();
|
|
18574
18884
|
};
|
|
18885
|
+
/**
|
|
18886
|
+
* Loads the first page, or re-queries a loaded list in place, sized to what is loaded plus the
|
|
18887
|
+
* unseen threads. Skipped once loaded unless forced or something changed; the list survives a
|
|
18888
|
+
* failure. Guarded only against another reload, as on `master`. A reload of a loaded list shows
|
|
18889
|
+
* no loading state; the first load shows through `paginator.isLoading`.
|
|
18890
|
+
*/
|
|
18575
18891
|
this.reload = async ({ force = false } = {}) => {
|
|
18576
|
-
|
|
18577
|
-
|
|
18578
|
-
if (!force &&
|
|
18579
|
-
|
|
18892
|
+
if (this.isReloadInFlight) return;
|
|
18893
|
+
const { isThreadOrderStale, unseenThreadIds } = this.state.getLatestValue();
|
|
18894
|
+
if (!force && this.paginator.hasResults && !unseenThreadIds.length && !isThreadOrderStale)
|
|
18895
|
+
return;
|
|
18896
|
+
const { items, pageSize } = this.paginator;
|
|
18897
|
+
const limit = (items?.length ?? 0) + unseenThreadIds.length;
|
|
18898
|
+
this.isReloadInFlight = true;
|
|
18580
18899
|
try {
|
|
18581
|
-
this.
|
|
18582
|
-
|
|
18583
|
-
|
|
18584
|
-
|
|
18585
|
-
|
|
18586
|
-
}
|
|
18587
|
-
}
|
|
18588
|
-
|
|
18589
|
-
limit: Math.min(limit, MAX_QUERY_THREADS_LIMIT) || MAX_QUERY_THREADS_LIMIT
|
|
18590
|
-
});
|
|
18591
|
-
const nextThreads = [];
|
|
18592
|
-
for (const incomingThread of response.threads) {
|
|
18593
|
-
const existingThread = this.threadsById[incomingThread.id];
|
|
18594
|
-
if (existingThread) {
|
|
18595
|
-
nextThreads.push(existingThread);
|
|
18596
|
-
if (existingThread.hasStaleState) {
|
|
18597
|
-
existingThread.hydrateState(incomingThread);
|
|
18598
|
-
}
|
|
18599
|
-
} else {
|
|
18600
|
-
nextThreads.push(incomingThread);
|
|
18601
|
-
}
|
|
18900
|
+
await this.paginator.reload({ limit: Math.min(limit, pageSize) || pageSize });
|
|
18901
|
+
} finally {
|
|
18902
|
+
this.isReloadInFlight = false;
|
|
18903
|
+
const error = this.paginator.lastQueryError;
|
|
18904
|
+
if (error) {
|
|
18905
|
+
logger10.withExtraTags("reload").error("Failed to reload the thread list.", { error });
|
|
18906
|
+
} else {
|
|
18907
|
+
this.state.partialNext({ isThreadOrderStale: false, unseenThreadIds: [] });
|
|
18602
18908
|
}
|
|
18603
|
-
this.state.next((current) => ({
|
|
18604
|
-
...current,
|
|
18605
|
-
threads: nextThreads,
|
|
18606
|
-
unseenThreadIds: [],
|
|
18607
|
-
isThreadOrderStale: false,
|
|
18608
|
-
pagination: {
|
|
18609
|
-
...current.pagination,
|
|
18610
|
-
isLoading: false,
|
|
18611
|
-
nextCursor: response.next ?? null
|
|
18612
|
-
},
|
|
18613
|
-
ready: true
|
|
18614
|
-
}));
|
|
18615
|
-
} catch (error) {
|
|
18616
|
-
logger10.withExtraTags("reload").error("Failed to reload the thread list.", { error });
|
|
18617
|
-
this.state.next((current) => ({
|
|
18618
|
-
...current,
|
|
18619
|
-
pagination: {
|
|
18620
|
-
...current.pagination,
|
|
18621
|
-
isLoading: false
|
|
18622
|
-
}
|
|
18623
|
-
}));
|
|
18624
|
-
}
|
|
18625
|
-
};
|
|
18626
|
-
this.queryThreads = (options = {}) => this.client.queryThreadsAndHydrate({
|
|
18627
|
-
limit: 25,
|
|
18628
|
-
participant_limit: 10,
|
|
18629
|
-
reply_limit: 10,
|
|
18630
|
-
watch: true,
|
|
18631
|
-
...options
|
|
18632
|
-
});
|
|
18633
|
-
this.loadNextPage = async (options = {}) => {
|
|
18634
|
-
const { pagination } = this.state.getLatestValue();
|
|
18635
|
-
if (pagination.isLoadingNext || !pagination.nextCursor) return;
|
|
18636
|
-
try {
|
|
18637
|
-
this.state.partialNext({ pagination: { ...pagination, isLoadingNext: true } });
|
|
18638
|
-
const response = await this.queryThreads({
|
|
18639
|
-
...options,
|
|
18640
|
-
next: pagination.nextCursor
|
|
18641
|
-
});
|
|
18642
|
-
this.state.next((current) => ({
|
|
18643
|
-
...current,
|
|
18644
|
-
threads: response.threads.length ? current.threads.concat(response.threads) : current.threads,
|
|
18645
|
-
pagination: {
|
|
18646
|
-
...current.pagination,
|
|
18647
|
-
nextCursor: response.next ?? null,
|
|
18648
|
-
isLoadingNext: false
|
|
18649
|
-
}
|
|
18650
|
-
}));
|
|
18651
|
-
} catch (error) {
|
|
18652
|
-
logger10.withExtraTags("loadNextPage").error("Failed to load the next page of threads.", { error });
|
|
18653
|
-
this.state.next((current) => ({
|
|
18654
|
-
...current,
|
|
18655
|
-
pagination: {
|
|
18656
|
-
...current.pagination,
|
|
18657
|
-
isLoadingNext: false
|
|
18658
|
-
}
|
|
18659
|
-
}));
|
|
18660
18909
|
}
|
|
18661
18910
|
};
|
|
18662
18911
|
this.configController = new ConfigController({
|
|
@@ -18664,7 +18913,11 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18664
18913
|
});
|
|
18665
18914
|
this.client = client;
|
|
18666
18915
|
this.state = new import_state_store21.StateStore(THREAD_MANAGER_INITIAL_STATE);
|
|
18667
|
-
this.
|
|
18916
|
+
this.paginator = new ThreadPaginator({ client, store: this.threadStore });
|
|
18917
|
+
this.paginator.state.subscribeWithSelector(
|
|
18918
|
+
({ items }) => ({ items }),
|
|
18919
|
+
({ items }) => items?.forEach(this.listenForDisposal)
|
|
18920
|
+
);
|
|
18668
18921
|
}
|
|
18669
18922
|
/**
|
|
18670
18923
|
* Resolved configuration, as a store — the shape every configurable class exposes
|
|
@@ -18698,21 +18951,13 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18698
18951
|
initializeConfig(config) {
|
|
18699
18952
|
this.configController.initialize(config);
|
|
18700
18953
|
}
|
|
18701
|
-
|
|
18702
|
-
|
|
18703
|
-
|
|
18704
|
-
|
|
18705
|
-
|
|
18706
|
-
|
|
18707
|
-
|
|
18708
|
-
newThreadsById[thread.id] = thread;
|
|
18709
|
-
return newThreadsById;
|
|
18710
|
-
},
|
|
18711
|
-
{}
|
|
18712
|
-
);
|
|
18713
|
-
this.threadsByIdGetterCache.threads = threads;
|
|
18714
|
-
this.threadsByIdGetterCache.threadsById = threadsById;
|
|
18715
|
-
return threadsById;
|
|
18954
|
+
/**
|
|
18955
|
+
* Every registered thread — the list's plus the opened ones.
|
|
18956
|
+
*
|
|
18957
|
+
* @internal
|
|
18958
|
+
*/
|
|
18959
|
+
get registeredThreads() {
|
|
18960
|
+
return this.threadStore.values();
|
|
18716
18961
|
}
|
|
18717
18962
|
};
|
|
18718
18963
|
|
|
@@ -20096,6 +20341,7 @@ var ConnectionRecoveryManager = class extends WithSubscriptions {
|
|
|
20096
20341
|
}
|
|
20097
20342
|
const channels = this.recoverableActiveChannels;
|
|
20098
20343
|
const threads = this.recoverableActiveThreads;
|
|
20344
|
+
this.markInactiveThreadsStale();
|
|
20099
20345
|
const socketDropBefore = this.client.wsConnection.state.getLatestValue().lastUnhealthyAt;
|
|
20100
20346
|
this.isRecovering = true;
|
|
20101
20347
|
logger12.withExtraTags("connectionRecovery").info(
|
|
@@ -20182,41 +20428,34 @@ var ConnectionRecoveryManager = class extends WithSubscriptions {
|
|
|
20182
20428
|
* Threads need their own pass because nothing else brings an open thread's replies back — a
|
|
20183
20429
|
* channel reload refreshes the main message list, `ChannelManager.recover()` refreshes the channel
|
|
20184
20430
|
* lists*, and `ThreadManager`'s own recovery refreshes the thread *list*, reusing existing
|
|
20185
|
-
* instances without rehydrating them unless they were separately marked stale
|
|
20186
|
-
* `user.watching.stop` does, never a reconnect).
|
|
20431
|
+
* instances without rehydrating them unless they were separately marked stale.
|
|
20187
20432
|
*
|
|
20188
|
-
* `active` is the filter that matters:
|
|
20189
|
-
* which is not what should be re-fetched on a reconnect — only what someone is actually
|
|
20433
|
+
* `active` is the filter that matters: the registry also holds every thread the list has paged
|
|
20434
|
+
* in, which is not what should be re-fetched on a reconnect — only what someone is actually
|
|
20435
|
+
* reading. An active thread is always registered, listed or not (`thread.activate()` registers it).
|
|
20190
20436
|
* Guarded on the owning channel the same way active channels are: a thread whose channel is being
|
|
20191
20437
|
* torn down has nothing to recover into.
|
|
20192
|
-
*
|
|
20193
|
-
* NOTE: `threadsById` is the thread LIST, not a thread cache. A thread opened from a message list
|
|
20194
|
-
* is constructed directly (`threadsById[id] ?? new Thread(...)`) and only reaches the list because
|
|
20195
|
-
* the UI SDKs adopt it there — prepended, once its replies have loaded — which is a workaround, not
|
|
20196
|
-
* a contract. Two consequences, both accepted for now and both narrow:
|
|
20197
|
-
*
|
|
20198
|
-
* - A reconnect landing before that adoption misses the thread. Self-limiting: the UI SDKs re-issue
|
|
20199
|
-
* the load while the reply list is still unloaded, so a thread that failed to load gets adopted on
|
|
20200
|
-
* the retry.
|
|
20201
|
-
* - `ThreadManager.reload()` rebuilds `state.threads` purely from the query response, so an adopted
|
|
20202
|
-
* thread absent from that response is evicted while still active. It cannot bite within the
|
|
20203
|
-
* reconnect that caused it: this getter is read before `connection.recovered` is dispatched, and
|
|
20204
|
-
* that event is what triggers the UI SDKs' list reload. It would take a LATER reconnect, after an
|
|
20205
|
-
* eviction, to miss the thread.
|
|
20206
|
-
*
|
|
20207
|
-
* The fix is a real off-list registry — see the commented-out `threadCache` in `ThreadManager` — at
|
|
20208
|
-
* which point this getter reads from that instead, with no change to the `active` filter.
|
|
20209
20438
|
*/
|
|
20210
20439
|
get recoverableActiveThreads() {
|
|
20211
20440
|
const threads = [];
|
|
20212
|
-
for (const thread of
|
|
20213
|
-
if (!thread) continue;
|
|
20441
|
+
for (const thread of this.client.threads.registeredThreads) {
|
|
20214
20442
|
const { active } = thread.state.getLatestValue();
|
|
20215
20443
|
if (!active || thread.channel.pendingDisposal) continue;
|
|
20216
20444
|
threads.push(thread);
|
|
20217
20445
|
}
|
|
20218
20446
|
return threads;
|
|
20219
20447
|
}
|
|
20448
|
+
/**
|
|
20449
|
+
* An opened thread stays registered and subscribed after it closes, but a drop loses events it
|
|
20450
|
+
* cannot replay. Flag only, no request: a listed one is rehydrated by the thread list's reload,
|
|
20451
|
+
* and any other reloads on its next `activate()`.
|
|
20452
|
+
*/
|
|
20453
|
+
markInactiveThreadsStale() {
|
|
20454
|
+
for (const thread of this.client.threads.registeredThreads) {
|
|
20455
|
+
const { active, isStateStale } = thread.state.getLatestValue();
|
|
20456
|
+
if (!active && !isStateStale) thread.state.partialNext({ isStateStale: true });
|
|
20457
|
+
}
|
|
20458
|
+
}
|
|
20220
20459
|
};
|
|
20221
20460
|
|
|
20222
20461
|
// src/connection/networkConnection/NetworkConnectionObserver.ts
|
|
@@ -20348,6 +20587,16 @@ var NetworkConnectionObserver = class extends WithSubscriptions {
|
|
|
20348
20587
|
get isOnline() {
|
|
20349
20588
|
return this.state.getLatestValue().isOnline;
|
|
20350
20589
|
}
|
|
20590
|
+
/**
|
|
20591
|
+
* Whether the installed reporter is the host default that mirrors this client's WebSocket
|
|
20592
|
+
* (`createWSConnectionNetworkStatusReporter`). Under it `isOnline === false` means "the socket is
|
|
20593
|
+
* down", not "the device is offline".
|
|
20594
|
+
*
|
|
20595
|
+
* @internal
|
|
20596
|
+
*/
|
|
20597
|
+
get usesDefaultWSConnectionNetworkStatusReporter() {
|
|
20598
|
+
return this.installedReporter !== void 0 && this.installedReporter === this.defaultReporter && this.defaultReporter !== browserNetworkStatusReporter;
|
|
20599
|
+
}
|
|
20351
20600
|
/**
|
|
20352
20601
|
* Installs the platform listener that reports device network status, unsubscribing whatever was
|
|
20353
20602
|
* there. Pass `null` to clear it, which leaves the last known status alone rather than reverting to
|
|
@@ -20399,6 +20648,7 @@ var NetworkConnectionObserver = class extends WithSubscriptions {
|
|
|
20399
20648
|
// src/connection/wsConnection/config.ts
|
|
20400
20649
|
var DEFAULT_WS_CONNECTION_CONFIG = deepFreezeConfig({
|
|
20401
20650
|
connectTimeoutMs: 15 * 1e3,
|
|
20651
|
+
enableWSFallback: false,
|
|
20402
20652
|
pingIntervalMs: 25 * 1e3,
|
|
20403
20653
|
healthCheckGracePeriodMs: 10 * 1e3,
|
|
20404
20654
|
offlineNotificationDisplayDelayMs: 5 * 1e3,
|
|
@@ -20986,7 +21236,190 @@ var StableWSConnection = class {
|
|
|
20986
21236
|
|
|
20987
21237
|
// src/connection/wsConnection/WSConnection.ts
|
|
20988
21238
|
var import_state_store25 = require("@stream-io/state-store");
|
|
20989
|
-
|
|
21239
|
+
|
|
21240
|
+
// src/connection/wsConnection/WSConnectionFallback.ts
|
|
21241
|
+
var import_axios4 = __toESM(require("axios"));
|
|
21242
|
+
var logger15 = chatLoggerSystem.getLogger("connection");
|
|
21243
|
+
var WSConnectionFallback = class {
|
|
21244
|
+
constructor({ client }) {
|
|
21245
|
+
/**
|
|
21246
|
+
* Applies a change in the device's network status. Stands in for v9's `window` online/offline
|
|
21247
|
+
* listeners: `WSConnection` routes the network-status store here once it has switched to this
|
|
21248
|
+
* long-poll.
|
|
21249
|
+
*
|
|
21250
|
+
* @internal
|
|
21251
|
+
*/
|
|
21252
|
+
this._applyNetworkStatus = (online) => {
|
|
21253
|
+
if (this.state === "DISCONNECTED" /* Disconnected */) return;
|
|
21254
|
+
this._log(`_applyNetworkStatus() - ${online ? "online" : "offline"}`);
|
|
21255
|
+
if (!online) {
|
|
21256
|
+
this._setState("CLOSED" /* Closed */);
|
|
21257
|
+
this.abortController?.abort();
|
|
21258
|
+
this.abortController = void 0;
|
|
21259
|
+
return;
|
|
21260
|
+
}
|
|
21261
|
+
if (this.state === "CLOSED" /* Closed */) {
|
|
21262
|
+
this.connect(true);
|
|
21263
|
+
}
|
|
21264
|
+
};
|
|
21265
|
+
/**
|
|
21266
|
+
* connect try to open a longpoll request
|
|
21267
|
+
*
|
|
21268
|
+
* @param reconnect - should be false for first call and true for subsequent calls to keep the connection alive and settle the connect promises
|
|
21269
|
+
*
|
|
21270
|
+
* @internal
|
|
21271
|
+
*/
|
|
21272
|
+
this.connect = async (reconnect = false) => {
|
|
21273
|
+
if (this.state === "CONNECTING" /* Connecting */) {
|
|
21274
|
+
this._log("connect() - connecting already in progress", { reconnect }, "warn");
|
|
21275
|
+
return;
|
|
21276
|
+
}
|
|
21277
|
+
if (this.state === "CONNECTED" /* Connected */) {
|
|
21278
|
+
this._log("connect() - already connected and polling", { reconnect }, "warn");
|
|
21279
|
+
return;
|
|
21280
|
+
}
|
|
21281
|
+
this._setState("CONNECTING" /* Connecting */);
|
|
21282
|
+
this.client.connectionIdManager.arm();
|
|
21283
|
+
try {
|
|
21284
|
+
const { event } = await this._req(
|
|
21285
|
+
// Authenticated by the request's `Authorization` header, so the message carries only a
|
|
21286
|
+
// placeholder token.
|
|
21287
|
+
{ json: this.client._buildWSAuthPayload() },
|
|
21288
|
+
{ timeout: 8e3 },
|
|
21289
|
+
// 8s
|
|
21290
|
+
reconnect
|
|
21291
|
+
);
|
|
21292
|
+
this.client.connectionIdManager.resolveConnectionId(event.connection_id);
|
|
21293
|
+
this._setState("CONNECTED" /* Connected */);
|
|
21294
|
+
this.client.dispatchEvent(event);
|
|
21295
|
+
this._poll();
|
|
21296
|
+
if (reconnect) {
|
|
21297
|
+
this.client._settleConnectPromises();
|
|
21298
|
+
}
|
|
21299
|
+
return event;
|
|
21300
|
+
} catch (err) {
|
|
21301
|
+
if (this.state !== "DISCONNECTED" /* Disconnected */)
|
|
21302
|
+
this._setState("CLOSED" /* Closed */);
|
|
21303
|
+
if (!import_axios4.default.isCancel(err)) this.client.connectionIdManager.rejectConnectionId(err);
|
|
21304
|
+
throw err;
|
|
21305
|
+
}
|
|
21306
|
+
};
|
|
21307
|
+
/**
|
|
21308
|
+
* Stops polling and tells the server to close `connectionId`.
|
|
21309
|
+
*
|
|
21310
|
+
* @param timeout - The close request's timeout, in milliseconds.
|
|
21311
|
+
* @param connectionId - The id to close. Passed in rather than read from
|
|
21312
|
+
* `client.connectionIdManager`, because `WSConnection.disconnect()` disconnects the old socket
|
|
21313
|
+
* first, and that drops the manager's id.
|
|
21314
|
+
*
|
|
21315
|
+
* @internal
|
|
21316
|
+
*/
|
|
21317
|
+
this.disconnect = async (timeout = 2e3, connectionId) => {
|
|
21318
|
+
this._setState("DISCONNECTED" /* Disconnected */);
|
|
21319
|
+
this.abortController?.abort();
|
|
21320
|
+
this.abortController = void 0;
|
|
21321
|
+
try {
|
|
21322
|
+
await this._req({ close: true, connection_id: connectionId }, { timeout }, false);
|
|
21323
|
+
this._log(`disconnect() - Closed connectionID`);
|
|
21324
|
+
} catch (err) {
|
|
21325
|
+
this._log(`disconnect() - Failed`, { err }, "error");
|
|
21326
|
+
}
|
|
21327
|
+
};
|
|
21328
|
+
this._req = async (params, config, retry) => {
|
|
21329
|
+
if (!this.abortController && !params.close) {
|
|
21330
|
+
this.abortController = new AbortController();
|
|
21331
|
+
}
|
|
21332
|
+
try {
|
|
21333
|
+
const res = await this.client.longPoll(params, {
|
|
21334
|
+
...config,
|
|
21335
|
+
signal: this.abortController?.signal
|
|
21336
|
+
});
|
|
21337
|
+
this.consecutiveFailures = 0;
|
|
21338
|
+
return res;
|
|
21339
|
+
} catch (error) {
|
|
21340
|
+
this.consecutiveFailures += 1;
|
|
21341
|
+
if (retry && isErrorRetryable(error)) {
|
|
21342
|
+
this._log(`_req() - Retryable error, retrying request`);
|
|
21343
|
+
await sleep(retryInterval(this.consecutiveFailures));
|
|
21344
|
+
if (params.connection_id && params.connection_id !== this.client.connectionIdManager.connectionId) {
|
|
21345
|
+
this._log(`_req() - Connection id changed, dropping the retry`);
|
|
21346
|
+
throw new import_axios4.CanceledError(
|
|
21347
|
+
"The connection id changed while the retry was waiting"
|
|
21348
|
+
);
|
|
21349
|
+
}
|
|
21350
|
+
return this._req(params, config, retry);
|
|
21351
|
+
}
|
|
21352
|
+
throw error;
|
|
21353
|
+
}
|
|
21354
|
+
};
|
|
21355
|
+
this._poll = async () => {
|
|
21356
|
+
while (this.state === "CONNECTED" /* Connected */) {
|
|
21357
|
+
try {
|
|
21358
|
+
const data = await this._req(
|
|
21359
|
+
{ connection_id: this.client.connectionIdManager.connectionId },
|
|
21360
|
+
{ timeout: 3e4 },
|
|
21361
|
+
// 30s => API responds in 20s if there is no event
|
|
21362
|
+
true
|
|
21363
|
+
);
|
|
21364
|
+
if (data.events?.length) {
|
|
21365
|
+
for (let i = 0; i < data.events.length; i++) {
|
|
21366
|
+
this.client.dispatchEvent(data.events[i]);
|
|
21367
|
+
}
|
|
21368
|
+
}
|
|
21369
|
+
} catch (error) {
|
|
21370
|
+
if (import_axios4.default.isCancel(error)) {
|
|
21371
|
+
this._log(`_poll() - axios canceled request`);
|
|
21372
|
+
return;
|
|
21373
|
+
}
|
|
21374
|
+
if (isConnectionIDError(error)) {
|
|
21375
|
+
this._log(`_poll() - ConnectionID error, connecting without ID...`);
|
|
21376
|
+
this._setState("DISCONNECTED" /* Disconnected */);
|
|
21377
|
+
this.connect(true);
|
|
21378
|
+
return;
|
|
21379
|
+
}
|
|
21380
|
+
if (isAPIError(error) && !isErrorRetryable(error)) {
|
|
21381
|
+
this._setState("CLOSED" /* Closed */);
|
|
21382
|
+
this.client.connectionIdManager.rejectConnectionId(error);
|
|
21383
|
+
return;
|
|
21384
|
+
}
|
|
21385
|
+
await sleep(retryInterval(this.consecutiveFailures));
|
|
21386
|
+
}
|
|
21387
|
+
}
|
|
21388
|
+
};
|
|
21389
|
+
this.client = client;
|
|
21390
|
+
this.state = "INIT" /* Init */;
|
|
21391
|
+
this.consecutiveFailures = 0;
|
|
21392
|
+
}
|
|
21393
|
+
/**
|
|
21394
|
+
* Whether a connect request is in flight.
|
|
21395
|
+
*
|
|
21396
|
+
* @internal
|
|
21397
|
+
*/
|
|
21398
|
+
get isConnecting() {
|
|
21399
|
+
return this.state === "CONNECTING" /* Connecting */;
|
|
21400
|
+
}
|
|
21401
|
+
_log(msg, extra = {}, level = "info") {
|
|
21402
|
+
const log = logger15.withExtraTags("connection_fallback");
|
|
21403
|
+
log[level]("WSConnectionFallback:" + msg, extra);
|
|
21404
|
+
}
|
|
21405
|
+
_setState(state) {
|
|
21406
|
+
this._log(`_setState() - ${state}`);
|
|
21407
|
+
const previous = this.state;
|
|
21408
|
+
this.state = state;
|
|
21409
|
+
if (previous === "CONNECTING" /* Connecting */ && this.state === "CONNECTED" /* Connected */) {
|
|
21410
|
+
this.client.wsConnection._setStatus({ isHealthy: true });
|
|
21411
|
+
}
|
|
21412
|
+
if (this.state === "CLOSED" /* Closed */ || this.state === "DISCONNECTED" /* Disconnected */) {
|
|
21413
|
+
this.client.connectionIdManager.invalidate();
|
|
21414
|
+
if (this.client.wsConnection._setStatus({ isHealthy: false })) {
|
|
21415
|
+
this.client._markActiveChannelsWatchInterrupted();
|
|
21416
|
+
}
|
|
21417
|
+
}
|
|
21418
|
+
}
|
|
21419
|
+
};
|
|
21420
|
+
|
|
21421
|
+
// src/connection/wsConnection/WSConnection.ts
|
|
21422
|
+
var logger16 = chatLoggerSystem.getLogger("client");
|
|
20990
21423
|
var WSConnection = class extends WithSubscriptions {
|
|
20991
21424
|
constructor({ client }) {
|
|
20992
21425
|
super();
|
|
@@ -21012,6 +21445,9 @@ var WSConnection = class extends WithSubscriptions {
|
|
|
21012
21445
|
*
|
|
21013
21446
|
* Read from the store rather than from an event, so there is one description of the device's
|
|
21014
21447
|
* network rather than two that can disagree.
|
|
21448
|
+
*
|
|
21449
|
+
* After an `enableWSFallback` switch it routes to {@link fallback} instead, leaving the
|
|
21450
|
+
* disconnected socket out of it.
|
|
21015
21451
|
*/
|
|
21016
21452
|
this.registerSubscriptions = () => {
|
|
21017
21453
|
if (!this.hasSubscriptions) {
|
|
@@ -21020,7 +21456,7 @@ var WSConnection = class extends WithSubscriptions {
|
|
|
21020
21456
|
({ isOnline }) => ({ isOnline }),
|
|
21021
21457
|
({ isOnline }) => {
|
|
21022
21458
|
if (typeof isOnline !== "boolean") return;
|
|
21023
|
-
this.connection?._applyNetworkStatus(isOnline);
|
|
21459
|
+
(this.fallback ?? this.connection)?._applyNetworkStatus(isOnline);
|
|
21024
21460
|
}
|
|
21025
21461
|
)
|
|
21026
21462
|
);
|
|
@@ -21073,7 +21509,7 @@ var WSConnection = class extends WithSubscriptions {
|
|
|
21073
21509
|
for (const correction of corrections) {
|
|
21074
21510
|
if (this.warnedClamps.get(correction.field) === correction.requested) continue;
|
|
21075
21511
|
this.warnedClamps.set(correction.field, correction.requested);
|
|
21076
|
-
|
|
21512
|
+
logger16.withExtraTags("wsConnection").warn(
|
|
21077
21513
|
`The WebSocket setting '${correction.field}' is outside its supported range; using ${correction.used}ms instead of ${correction.requested}ms.`,
|
|
21078
21514
|
{ bounds: WS_CONNECTION_CONFIG_BOUNDS[correction.field] }
|
|
21079
21515
|
);
|
|
@@ -21092,22 +21528,24 @@ var WSConnection = class extends WithSubscriptions {
|
|
|
21092
21528
|
this.configController.initialize(config);
|
|
21093
21529
|
}
|
|
21094
21530
|
/**
|
|
21095
|
-
* Is this WebSocket up
|
|
21096
|
-
*
|
|
21097
|
-
* connect.
|
|
21531
|
+
* Is this WebSocket up — or, after an `enableWSFallback` switch, the long-poll. Read from
|
|
21532
|
+
* {@link state} rather than from the current socket, so it survives the socket being replaced and
|
|
21533
|
+
* answers `false` rather than throwing before the first connect.
|
|
21098
21534
|
*/
|
|
21099
21535
|
get isHealthy() {
|
|
21100
21536
|
return this.state.getLatestValue().isHealthy;
|
|
21101
21537
|
}
|
|
21102
|
-
/** Whether a connection attempt is in flight. */
|
|
21538
|
+
/** Whether a connection attempt is in flight — the long-poll's after an `enableWSFallback` switch. */
|
|
21103
21539
|
get isConnecting() {
|
|
21104
|
-
return this.connection?.isConnecting ?? false;
|
|
21540
|
+
return (this.fallback ?? this.connection)?.isConnecting ?? false;
|
|
21105
21541
|
}
|
|
21106
21542
|
/**
|
|
21107
|
-
* Records this
|
|
21543
|
+
* Records this connection's status. Returns whether it changed.
|
|
21108
21544
|
*
|
|
21109
|
-
* {@link StableWSConnection}
|
|
21110
|
-
*
|
|
21545
|
+
* {@link StableWSConnection} routes **every** status transition through here — including
|
|
21546
|
+
* `disconnect()`, which `closeConnection()` uses, and the two error paths. The one other
|
|
21547
|
+
* caller is {@link fallback}, which takes over once `enableWSFallback` has switched to
|
|
21548
|
+
* long-polling.
|
|
21111
21549
|
*
|
|
21112
21550
|
* @internal
|
|
21113
21551
|
*/
|
|
@@ -21130,19 +21568,45 @@ var WSConnection = class extends WithSubscriptions {
|
|
|
21130
21568
|
* Creation lives here rather than in the constructor because a client that never calls
|
|
21131
21569
|
* `connectUser` should never build a socket, and here rather than in `client.connect()` because a
|
|
21132
21570
|
* field belongs to the object that owns it.
|
|
21571
|
+
*
|
|
21572
|
+
* With `enableWSFallback`, a socket that fails with a network error is replaced by the long-poll
|
|
21573
|
+
* ({@link fallback}), which every later call connects instead of building a socket.
|
|
21133
21574
|
*/
|
|
21134
|
-
connect(timeout) {
|
|
21575
|
+
async connect(timeout) {
|
|
21576
|
+
if (this.fallback) {
|
|
21577
|
+
return await this.fallback.connect();
|
|
21578
|
+
}
|
|
21135
21579
|
const next = this.buildConnection();
|
|
21136
21580
|
const previous = this.connection;
|
|
21137
21581
|
if (previous && previous !== next) {
|
|
21138
21582
|
void Promise.resolve(previous.disconnect()).catch((error) => {
|
|
21139
|
-
|
|
21583
|
+
logger16.withExtraTags("wsConnection").debug("Closing the replaced WebSocket failed; discarding it anyway.", {
|
|
21140
21584
|
error
|
|
21141
21585
|
});
|
|
21142
21586
|
});
|
|
21143
21587
|
}
|
|
21144
21588
|
this.connection = next;
|
|
21145
|
-
|
|
21589
|
+
try {
|
|
21590
|
+
return await next.connect(timeout);
|
|
21591
|
+
} catch (error) {
|
|
21592
|
+
const { networkConnection } = this.client;
|
|
21593
|
+
const isDeviceOffline = networkConnection.isOnline === false && !networkConnection.usesDefaultWSConnectionNetworkStatusReporter;
|
|
21594
|
+
if (this.config.enableWSFallback && isWSFailure(error) && !isDeviceOffline) {
|
|
21595
|
+
logger16.withExtraTags("connect").info("WS failed, fallback to longpoll");
|
|
21596
|
+
this.client.dispatchEvent({
|
|
21597
|
+
type: "connection.fallback_activated",
|
|
21598
|
+
mode: "longpoll"
|
|
21599
|
+
});
|
|
21600
|
+
next._destroyCurrentWSConnection();
|
|
21601
|
+
void next.disconnect();
|
|
21602
|
+
this.fallback = new WSConnectionFallback({ client: this.client });
|
|
21603
|
+
return await this.fallback.connect();
|
|
21604
|
+
}
|
|
21605
|
+
if (!isWSFailure(error)) {
|
|
21606
|
+
this.client.connectionIdManager.rejectConnectionId(error);
|
|
21607
|
+
}
|
|
21608
|
+
throw error;
|
|
21609
|
+
}
|
|
21146
21610
|
}
|
|
21147
21611
|
/**
|
|
21148
21612
|
* `config.connection` lets a caller supply a pre-built socket — used by unit tests, and the reason
|
|
@@ -21156,8 +21620,17 @@ var WSConnection = class extends WithSubscriptions {
|
|
|
21156
21620
|
}
|
|
21157
21621
|
return new StableWSConnection({ wsConnection: this });
|
|
21158
21622
|
}
|
|
21159
|
-
|
|
21160
|
-
|
|
21623
|
+
/**
|
|
21624
|
+
* Closes the socket and, after an `enableWSFallback` switch, the long-poll too, telling the
|
|
21625
|
+
* server to close its connection id. The long-poll's close request uses `timeout` as its timeout
|
|
21626
|
+
* (2s when omitted).
|
|
21627
|
+
*/
|
|
21628
|
+
async disconnect(timeout) {
|
|
21629
|
+
const connectionId = this.client.connectionIdManager.connectionId;
|
|
21630
|
+
await Promise.all([
|
|
21631
|
+
this.connection?.disconnect(timeout),
|
|
21632
|
+
this.fallback?.disconnect(timeout, connectionId)
|
|
21633
|
+
]);
|
|
21161
21634
|
}
|
|
21162
21635
|
/**
|
|
21163
21636
|
* @deprecated Report network status through `client.networkConnection.setStatus(isOnline)` instead,
|
|
@@ -21659,7 +22132,7 @@ var CONSTRUCTION_ONLY_CONFIG_PATHS = {
|
|
|
21659
22132
|
};
|
|
21660
22133
|
|
|
21661
22134
|
// src/configuration/InstanceConfigurationRegistry.ts
|
|
21662
|
-
var
|
|
22135
|
+
var logger17 = chatLoggerSystem.getLogger("instance-configuration");
|
|
21663
22136
|
var InstanceConfigurationRegistry = class {
|
|
21664
22137
|
constructor() {
|
|
21665
22138
|
/**
|
|
@@ -21857,7 +22330,7 @@ var InstanceConfigurationRegistry = class {
|
|
|
21857
22330
|
try {
|
|
21858
22331
|
instance.reinitializeConfig?.();
|
|
21859
22332
|
} catch (error) {
|
|
21860
|
-
|
|
22333
|
+
logger17.error("reinitializeConfig threw during reset", error);
|
|
21861
22334
|
}
|
|
21862
22335
|
}
|
|
21863
22336
|
}
|
|
@@ -21915,7 +22388,7 @@ var InstanceConfigurationRegistry = class {
|
|
|
21915
22388
|
warnIfKeyLooksUnknown(key) {
|
|
21916
22389
|
if (INSTANCE_CONFIG_TREE_KEYS.includes(key)) return;
|
|
21917
22390
|
if (this.hasLiveInstances(key)) return;
|
|
21918
|
-
|
|
22391
|
+
logger17.withExtraTags(key).warn(
|
|
21919
22392
|
"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."
|
|
21920
22393
|
);
|
|
21921
22394
|
}
|
|
@@ -21942,7 +22415,7 @@ var InstanceConfigurationRegistry = class {
|
|
|
21942
22415
|
return !isEqual(getPath(registered, path), getPath(config, path));
|
|
21943
22416
|
});
|
|
21944
22417
|
if (late.length === 0) return;
|
|
21945
|
-
|
|
22418
|
+
logger17.withExtraTags(key).warn(
|
|
21946
22419
|
`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().`
|
|
21947
22420
|
);
|
|
21948
22421
|
}
|
|
@@ -23293,6 +23766,7 @@ var ChatApi = class {
|
|
|
23293
23766
|
}
|
|
23294
23767
|
async longPoll(request, requestOptions) {
|
|
23295
23768
|
const queryParams = {
|
|
23769
|
+
close: request?.close,
|
|
23296
23770
|
connection_id: request?.connection_id,
|
|
23297
23771
|
json: request?.json
|
|
23298
23772
|
};
|
|
@@ -23905,8 +24379,11 @@ var ChatApi = class {
|
|
|
23905
24379
|
function isString(value) {
|
|
23906
24380
|
return typeof value === "string" || value instanceof String;
|
|
23907
24381
|
}
|
|
23908
|
-
var
|
|
24382
|
+
var logger18 = chatLoggerSystem.getLogger("client");
|
|
23909
24383
|
var offlineDbLogger2 = chatLoggerSystem.getLogger("offline-db");
|
|
24384
|
+
var markThreadChannelWatched = ({ channel }) => {
|
|
24385
|
+
channel.watchStatus = ChannelWatchStatus.Watching;
|
|
24386
|
+
};
|
|
23910
24387
|
var StreamChat = class _StreamChat extends ChatApi {
|
|
23911
24388
|
/**
|
|
23912
24389
|
* Initializes a client.
|
|
@@ -23961,7 +24438,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
23961
24438
|
throw new Error('The "id" field on the user is missing');
|
|
23962
24439
|
}
|
|
23963
24440
|
if (this.userId === user.id && this.setUserPromise) {
|
|
23964
|
-
|
|
24441
|
+
logger18.withExtraTags("connectUser").warn(
|
|
23965
24442
|
"Detected consecutive calls to connectUser. Ideally, this function should only be called once."
|
|
23966
24443
|
);
|
|
23967
24444
|
return this.setUserPromise;
|
|
@@ -23972,7 +24449,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
23972
24449
|
);
|
|
23973
24450
|
}
|
|
23974
24451
|
if (this.node && !this.options.allowServerSideConnect) {
|
|
23975
|
-
|
|
24452
|
+
logger18.withExtraTags("connectUser").warn(
|
|
23976
24453
|
'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.'
|
|
23977
24454
|
);
|
|
23978
24455
|
}
|
|
@@ -24018,8 +24495,12 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24018
24495
|
* So when your app goes to background, you can call `client.closeConnection`.
|
|
24019
24496
|
* And when app comes back to foreground, call `client.openConnection`.
|
|
24020
24497
|
*
|
|
24498
|
+
* After an `enableWSFallback` switch it closes the long-poll as well, telling the server to
|
|
24499
|
+
* close its connection id.
|
|
24500
|
+
*
|
|
24021
24501
|
* @param timeout - Max number of milliseconds to wait for the WebSocket close event before forcefully assuming
|
|
24022
24502
|
* successful disconnection. See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent (optional).
|
|
24503
|
+
* The long-poll's close request uses it as its timeout (2s when omitted).
|
|
24023
24504
|
*/
|
|
24024
24505
|
this.closeConnection = async (timeout) => {
|
|
24025
24506
|
this._resetAIStateOnActiveChannels();
|
|
@@ -24043,7 +24524,8 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24043
24524
|
return Promise.resolve();
|
|
24044
24525
|
};
|
|
24045
24526
|
/**
|
|
24046
|
-
* Creates a new WebSocket connection with the current user.
|
|
24527
|
+
* Creates a new WebSocket connection with the current user. After an `enableWSFallback` switch it
|
|
24528
|
+
* reconnects the long-poll instead: the client never goes back to the WebSocket.
|
|
24047
24529
|
*
|
|
24048
24530
|
* @returns The WebSocket connect promise, or an empty resolved promise if a connection is already active.
|
|
24049
24531
|
*/
|
|
@@ -24054,11 +24536,11 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24054
24536
|
);
|
|
24055
24537
|
}
|
|
24056
24538
|
if (this.wsConnection?.isConnecting && this.wsPromise) {
|
|
24057
|
-
|
|
24539
|
+
logger18.withExtraTags("openConnection").debug("A connection attempt is already in progress.");
|
|
24058
24540
|
return this.wsPromise;
|
|
24059
24541
|
}
|
|
24060
24542
|
if (this.wsConnection?.isHealthy && this.connectionIdManager.connectionId) {
|
|
24061
|
-
|
|
24543
|
+
logger18.withExtraTags("openConnection").debug("openConnection was called twice; a healthy connection already exists.");
|
|
24062
24544
|
return;
|
|
24063
24545
|
}
|
|
24064
24546
|
this.clientId = `${this.userId}--${randomId()}`;
|
|
@@ -24115,7 +24597,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24115
24597
|
* @returns The close-connection promise.
|
|
24116
24598
|
*/
|
|
24117
24599
|
this.disconnectUser = (timeout) => {
|
|
24118
|
-
|
|
24600
|
+
logger18.withExtraTags("disconnectUser").info("Disconnecting the client.");
|
|
24119
24601
|
delete this.user;
|
|
24120
24602
|
delete this._user;
|
|
24121
24603
|
const teardownReason = new Error(
|
|
@@ -24123,8 +24605,8 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24123
24605
|
);
|
|
24124
24606
|
this._rejectPendingWsPromise(teardownReason);
|
|
24125
24607
|
this.wsPromise = null;
|
|
24126
|
-
this.connectionIdManager.rejectConnectionId(teardownReason);
|
|
24127
24608
|
const closePromise = this.closeConnection(timeout);
|
|
24609
|
+
this.connectionIdManager.rejectConnectionId(teardownReason);
|
|
24128
24610
|
for (const channel of Object.values(this.activeChannels)) {
|
|
24129
24611
|
channel._disconnect();
|
|
24130
24612
|
}
|
|
@@ -24138,9 +24620,9 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24138
24620
|
this.unsubscribeClientConfiguration?.();
|
|
24139
24621
|
this.unsubscribeClientConfiguration = void 0;
|
|
24140
24622
|
closePromise.finally(() => {
|
|
24141
|
-
this.tokenManager.reset();
|
|
24623
|
+
if (!this.userId) this.tokenManager.reset();
|
|
24142
24624
|
}).catch(
|
|
24143
|
-
(err) =>
|
|
24625
|
+
(err) => logger18.withExtraTags("disconnectUser").error("The close promise rejected during disconnect.", { error: err })
|
|
24144
24626
|
);
|
|
24145
24627
|
return closePromise;
|
|
24146
24628
|
};
|
|
@@ -24158,7 +24640,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24158
24640
|
*/
|
|
24159
24641
|
this.connectAnonymousUser = () => {
|
|
24160
24642
|
if (this.node && !this.options.allowServerSideConnect) {
|
|
24161
|
-
|
|
24643
|
+
logger18.withExtraTags("connectAnonymousUser").warn(
|
|
24162
24644
|
'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.'
|
|
24163
24645
|
);
|
|
24164
24646
|
}
|
|
@@ -24311,7 +24793,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24311
24793
|
...this.listeners.get(event.type) ?? []
|
|
24312
24794
|
];
|
|
24313
24795
|
for (const listener of listeners) {
|
|
24314
|
-
invokeEventListener(listener, event,
|
|
24796
|
+
invokeEventListener(listener, event, logger18);
|
|
24315
24797
|
}
|
|
24316
24798
|
};
|
|
24317
24799
|
/**
|
|
@@ -24322,15 +24804,16 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24322
24804
|
* Requests no longer settle against these: waiting for a connection id is the
|
|
24323
24805
|
* {@link ConnectionIdManager}'s job, applied centrally in `ApiClient`.
|
|
24324
24806
|
*
|
|
24325
|
-
* Called by `StableWSConnection._reconnect()
|
|
24326
|
-
*
|
|
24327
|
-
*
|
|
24328
|
-
*
|
|
24807
|
+
* Called by `StableWSConnection._reconnect()`, and by `WSConnectionFallback.connect(true)`
|
|
24808
|
+
* for the long-poll's own reconnects. Recovery itself is owned by
|
|
24809
|
+
* {@link ConnectionRecoveryManager}, which subscribes to the connection lifecycle and so
|
|
24810
|
+
* covers every reconnect path — including `closeConnection()` → `openConnection()` (mobile
|
|
24811
|
+
* backgrounding), which reaches neither.
|
|
24329
24812
|
*
|
|
24330
24813
|
* @internal
|
|
24331
24814
|
*/
|
|
24332
24815
|
this._settleConnectPromises = () => {
|
|
24333
|
-
|
|
24816
|
+
logger18.withExtraTags("_settleConnectPromises").info(
|
|
24334
24817
|
`Connection re-established with connection ID ${this.connectionIdManager.connectionId}.`
|
|
24335
24818
|
);
|
|
24336
24819
|
if (this._wsPromiseSettled) {
|
|
@@ -24420,7 +24903,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24420
24903
|
return this.userAgent;
|
|
24421
24904
|
}
|
|
24422
24905
|
if (!this.cachedUserAgent) {
|
|
24423
|
-
const version = "10.0.0-rc.
|
|
24906
|
+
const version = "10.0.0-rc.17";
|
|
24424
24907
|
const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
|
|
24425
24908
|
const { name: appName, version: appVersion } = this.appIdentifier ?? {};
|
|
24426
24909
|
const { os, model: deviceModel } = this.deviceIdentifier ?? {};
|
|
@@ -24450,11 +24933,21 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24450
24933
|
*
|
|
24451
24934
|
* @returns The JSON-encoded auth message.
|
|
24452
24935
|
*/
|
|
24453
|
-
this._buildWSAuthMessage = () => JSON.stringify(
|
|
24454
|
-
|
|
24455
|
-
|
|
24456
|
-
|
|
24457
|
-
|
|
24936
|
+
this._buildWSAuthMessage = () => JSON.stringify(this._buildWSAuthPayload(this.tokenManager.getToken()));
|
|
24937
|
+
/**
|
|
24938
|
+
* The auth message itself — what {@link _buildWSAuthMessage} encodes for the WebSocket, and what
|
|
24939
|
+
* the long-poll fallback sends as `longPoll()`'s `json` query param.
|
|
24940
|
+
*
|
|
24941
|
+
* @private
|
|
24942
|
+
*
|
|
24943
|
+
* @param token - The token the message carries. The long-poll passes none: it authenticates
|
|
24944
|
+
* through the request's `Authorization` header instead.
|
|
24945
|
+
*/
|
|
24946
|
+
this._buildWSAuthPayload = (token) => ({
|
|
24947
|
+
// The server requires a non-empty token, but skips JWT parsing for any string that is not
|
|
24948
|
+
// shaped like one. Anonymous users have no token, and the long-poll does not send one, so both
|
|
24949
|
+
// send a placeholder the server accepts and ignores.
|
|
24950
|
+
token: token || "anonymous",
|
|
24458
24951
|
// `connect()` rejects before reaching this when `_user` is unset.
|
|
24459
24952
|
user_details: this._user,
|
|
24460
24953
|
products: ["chat"]
|
|
@@ -24477,7 +24970,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24477
24970
|
this.browser = options.browser ?? typeof window !== "undefined";
|
|
24478
24971
|
this.node = !this.browser;
|
|
24479
24972
|
this.options = { ...options };
|
|
24480
|
-
this.axiosInstance =
|
|
24973
|
+
this.axiosInstance = import_axios5.default.create({
|
|
24481
24974
|
timeout: 3e3,
|
|
24482
24975
|
withCredentials: false,
|
|
24483
24976
|
...this.options.axiosRequestConfig,
|
|
@@ -24734,14 +25227,14 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24734
25227
|
const key = callbackOrNothing ? callbackOrString : "all";
|
|
24735
25228
|
const callback = callbackOrNothing ? callbackOrNothing : callbackOrString;
|
|
24736
25229
|
const set = this.listeners.get(key) ?? /* @__PURE__ */ new Set();
|
|
24737
|
-
|
|
25230
|
+
logger18.withExtraTags("on").debug(`Attaching a listener for the "${key}" event.`);
|
|
24738
25231
|
set.add(callback);
|
|
24739
25232
|
if (!this.listeners.has(key)) {
|
|
24740
25233
|
this.listeners.set(key, set);
|
|
24741
25234
|
}
|
|
24742
25235
|
return {
|
|
24743
25236
|
unsubscribe: () => {
|
|
24744
|
-
|
|
25237
|
+
logger18.withExtraTags("on").debug(`Removing the listener for the "${key}" event.`);
|
|
24745
25238
|
set.delete(callback);
|
|
24746
25239
|
if (!set.size) {
|
|
24747
25240
|
this.listeners.delete(key);
|
|
@@ -24752,7 +25245,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24752
25245
|
off(callbackOrString, callbackOrNothing) {
|
|
24753
25246
|
const key = callbackOrNothing ? callbackOrString : "all";
|
|
24754
25247
|
const callback = callbackOrNothing ? callbackOrNothing : callbackOrString;
|
|
24755
|
-
|
|
25248
|
+
logger18.withExtraTags("off").debug(`Removing the listener for the "${key}" event.`);
|
|
24756
25249
|
const set = this.listeners.get(key);
|
|
24757
25250
|
set?.delete(callback);
|
|
24758
25251
|
if (!set?.size) {
|
|
@@ -24762,7 +25255,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24762
25255
|
_handleClientEvent(event) {
|
|
24763
25256
|
const client = this;
|
|
24764
25257
|
const postListenerCallbacks = [];
|
|
24765
|
-
|
|
25258
|
+
logger18.withExtraTags("_handleClientEvent").debug(`Received an event of type "${event.type}".`, { event });
|
|
24766
25259
|
if (event.type === "user.presence.changed" || event.type === "user.updated" || event.type === "user.deleted") {
|
|
24767
25260
|
this._handleUserEvent(event);
|
|
24768
25261
|
}
|
|
@@ -24839,10 +25332,13 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24839
25332
|
* Only `Watching` is demoted: a channel the consumer stopped on purpose, or one that was torn
|
|
24840
25333
|
* down, stays `NotWatching` and must not be resurrected by a reconnect.
|
|
24841
25334
|
*
|
|
24842
|
-
* Invoked from two places, because neither covers the other:
|
|
24843
|
-
* for an abnormal close/error, and
|
|
24844
|
-
*
|
|
24845
|
-
*
|
|
25335
|
+
* Invoked from two places on the WebSocket, because neither covers the other:
|
|
25336
|
+
* `StableWSConnection._setHealth(false)` for an abnormal close/error, and
|
|
25337
|
+
* `closeConnection()` for a deliberate shutdown (e.g. mobile backgrounding), whose
|
|
25338
|
+
* `disconnect()` writes the status through `_applyHealth` and so never reaches
|
|
25339
|
+
* `_setHealth`. After an `enableWSFallback` switch the long-poll calls it too, from
|
|
25340
|
+
* `WSConnectionFallback._setState()` whenever going closed or disconnected takes the
|
|
25341
|
+
* status offline.
|
|
24846
25342
|
*/
|
|
24847
25343
|
_markActiveChannelsWatchInterrupted() {
|
|
24848
25344
|
for (const cid in this.activeChannels) {
|
|
@@ -24891,14 +25387,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24891
25387
|
if (!this.clientId) {
|
|
24892
25388
|
throw Error("Property clientId is not set");
|
|
24893
25389
|
}
|
|
24894
|
-
|
|
24895
|
-
return await this.wsConnection.connect();
|
|
24896
|
-
} catch (error) {
|
|
24897
|
-
if (!isWSFailure(error)) {
|
|
24898
|
-
this.connectionIdManager.rejectConnectionId(error);
|
|
24899
|
-
}
|
|
24900
|
-
throw error;
|
|
24901
|
-
}
|
|
25390
|
+
return await this.wsConnection.connect();
|
|
24902
25391
|
}
|
|
24903
25392
|
/**
|
|
24904
25393
|
* Queries users and watches user presence.
|
|
@@ -24915,6 +25404,31 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24915
25404
|
this.state.updateUsers(data.users);
|
|
24916
25405
|
return data;
|
|
24917
25406
|
}
|
|
25407
|
+
/**
|
|
25408
|
+
* Stops watching a channel on this client's connection.
|
|
25409
|
+
*
|
|
25410
|
+
* It carries no `watch` flag, so the request layer does not hold it, but it is connection-scoped
|
|
25411
|
+
* by definition: it tells the server which connection should stop watching. So it waits for this
|
|
25412
|
+
* client's connection id exactly as a watching request does — through a handshake or a reconnect,
|
|
25413
|
+
* until the caller's abort signal fires — and throws when there is no connection and none is being
|
|
25414
|
+
* established.
|
|
25415
|
+
*
|
|
25416
|
+
* @param ...args - `[request, requestOptions]`. `request.connection_id` is replaced by this
|
|
25417
|
+
* client's connection id.
|
|
25418
|
+
* @returns The server response.
|
|
25419
|
+
*/
|
|
25420
|
+
async stopWatchingChannel(...args) {
|
|
25421
|
+
const [request, requestOptions] = args;
|
|
25422
|
+
const signal = requestOptions?.signal;
|
|
25423
|
+
const connectionId = await this.connectionIdManager.getConnectionId(
|
|
25424
|
+
// Check if signal is still usable - if it is read from offline DB, it has lost `addEventListener`.
|
|
25425
|
+
typeof signal?.addEventListener === "function" ? signal : void 0
|
|
25426
|
+
);
|
|
25427
|
+
return super.stopWatchingChannel(
|
|
25428
|
+
{ ...request, connection_id: connectionId },
|
|
25429
|
+
requestOptions
|
|
25430
|
+
);
|
|
25431
|
+
}
|
|
24918
25432
|
/**
|
|
24919
25433
|
* Queries channels and returns the full API response including top-level metadata such as
|
|
24920
25434
|
* `predefined_filter`.
|
|
@@ -25369,12 +25883,11 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
25369
25883
|
const response = await this.queryThreads(requestBody, requestOptions);
|
|
25370
25884
|
const parentMessages = response.threads.map((thread) => thread.parent_message);
|
|
25371
25885
|
this.polls.hydratePollCache(parentMessages);
|
|
25372
|
-
|
|
25373
|
-
|
|
25374
|
-
|
|
25375
|
-
|
|
25376
|
-
|
|
25377
|
-
};
|
|
25886
|
+
const threads = response.threads.map(
|
|
25887
|
+
(thread) => new Thread({ client: this, threadData: thread })
|
|
25888
|
+
);
|
|
25889
|
+
if (optionsWithDefaults.watch) threads.forEach(markThreadChannelWatched);
|
|
25890
|
+
return { threads, next: response.next };
|
|
25378
25891
|
}
|
|
25379
25892
|
/**
|
|
25380
25893
|
* Returns the thread of a message by its ID, wrapped in a hydrated `Thread` instance.
|
|
@@ -25406,7 +25919,9 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
25406
25919
|
},
|
|
25407
25920
|
requestOptions
|
|
25408
25921
|
);
|
|
25409
|
-
|
|
25922
|
+
const thread = new Thread({ client: this, threadData: response.thread });
|
|
25923
|
+
if (optionsWithDefaults.watch) markThreadChannelWatched(thread);
|
|
25924
|
+
return thread;
|
|
25410
25925
|
}
|
|
25411
25926
|
/**
|
|
25412
25927
|
* Sets the user agent string.
|
|
@@ -25841,6 +26356,11 @@ var WS_CONNECTION_FIELDS = {
|
|
|
25841
26356
|
kind: "value",
|
|
25842
26357
|
type: "number"
|
|
25843
26358
|
},
|
|
26359
|
+
enableWSFallback: {
|
|
26360
|
+
description: "Falls back to HTTP long-polling (`/api/v2/longpoll`) when the WebSocket cannot connect, for networks that block WebSockets. The WebSocket connects within `connectTimeoutMs`, as without the flag; lower it to switch sooner. Dispatches `connection.fallback_activated` when it switches, and stays on long-poll from then on. Defaults to `false`. Was `StreamChatOptions.enableWSFallback`.",
|
|
26361
|
+
kind: "value",
|
|
26362
|
+
type: "boolean"
|
|
26363
|
+
},
|
|
25844
26364
|
offlineNotificationDisplayDelayMs: {
|
|
25845
26365
|
description: "How long a drop must last before a UI reports it. Most drops resolve in under a second, so a banner showing all of them makes a working application look broken. Nothing in this package waits on it: it is here so the UI SDKs share one value. Zero holds nothing back, landing on the next task.",
|
|
25846
26366
|
kind: "value",
|
|
@@ -25899,7 +26419,7 @@ var CLIENT_FIELDS = {
|
|
|
25899
26419
|
kind: "group"
|
|
25900
26420
|
},
|
|
25901
26421
|
wsConnection: {
|
|
25902
|
-
description: "This client's WebSocket: how long to wait for it, how often to ping it, how long it may go quiet before being declared dead,
|
|
26422
|
+
description: "This client's WebSocket: how long to wait for it, how often to ping it, how long it may go quiet before being declared dead, how long a drop must last before a UI reports it, and whether to fall back to long-polling when it cannot connect.",
|
|
25903
26423
|
fields: WS_CONNECTION_FIELDS,
|
|
25904
26424
|
kind: "group"
|
|
25905
26425
|
}
|
|
@@ -26123,8 +26643,8 @@ var OfflineError = class extends Error {
|
|
|
26123
26643
|
};
|
|
26124
26644
|
|
|
26125
26645
|
// src/offline-support/offline_sync_manager.ts
|
|
26126
|
-
var
|
|
26127
|
-
var
|
|
26646
|
+
var import_axios6 = require("axios");
|
|
26647
|
+
var logger19 = chatLoggerSystem.getLogger("offline-db");
|
|
26128
26648
|
var OfflineDBSyncManager = class {
|
|
26129
26649
|
constructor({
|
|
26130
26650
|
client,
|
|
@@ -26168,7 +26688,7 @@ var OfflineDBSyncManager = class {
|
|
|
26168
26688
|
}
|
|
26169
26689
|
);
|
|
26170
26690
|
} catch (error) {
|
|
26171
|
-
|
|
26691
|
+
logger19.withExtraTags("init").error("Failed to initialize the offline DB sync manager.", { error });
|
|
26172
26692
|
}
|
|
26173
26693
|
};
|
|
26174
26694
|
/**
|
|
@@ -26258,7 +26778,7 @@ var OfflineDBSyncManager = class {
|
|
|
26258
26778
|
const { syncMaxEventCount } = this;
|
|
26259
26779
|
const exceedsLimit = typeof syncMaxEventCount === "number" && syncMaxEventCount > 0 && result.events.length > syncMaxEventCount;
|
|
26260
26780
|
if (exceedsLimit) {
|
|
26261
|
-
|
|
26781
|
+
logger19.withExtraTags("sync").warn(
|
|
26262
26782
|
`Skipping sync event replay: received ${result.events.length} events, which exceeds the configured limit of ${syncMaxEventCount}. Visible channels are refreshed on reconnect instead.`
|
|
26263
26783
|
);
|
|
26264
26784
|
} else {
|
|
@@ -26278,8 +26798,8 @@ var OfflineDBSyncManager = class {
|
|
|
26278
26798
|
lastSyncedAt: (/* @__PURE__ */ new Date()).toString()
|
|
26279
26799
|
});
|
|
26280
26800
|
} catch (e) {
|
|
26281
|
-
|
|
26282
|
-
if ((0,
|
|
26801
|
+
logger19.withExtraTags("syncAndExecutePendingTasks").error("An error occurred while syncing the database.", { error: e });
|
|
26802
|
+
if ((0, import_axios6.isAxiosError)(e) && e.code === "ECONNABORTED") {
|
|
26283
26803
|
return;
|
|
26284
26804
|
}
|
|
26285
26805
|
const error = e;
|
|
@@ -26319,7 +26839,7 @@ var OfflineDBSyncManager = class {
|
|
|
26319
26839
|
|
|
26320
26840
|
// src/offline-support/offline_support_api.ts
|
|
26321
26841
|
var import_state_store30 = require("@stream-io/state-store");
|
|
26322
|
-
var
|
|
26842
|
+
var logger20 = chatLoggerSystem.getLogger("offline-db");
|
|
26323
26843
|
var AbstractOfflineDB = class {
|
|
26324
26844
|
constructor({
|
|
26325
26845
|
client,
|
|
@@ -26344,7 +26864,7 @@ var AbstractOfflineDB = class {
|
|
|
26344
26864
|
}
|
|
26345
26865
|
} catch (error) {
|
|
26346
26866
|
this.state.partialNext({ initialized: false, userId: void 0 });
|
|
26347
|
-
|
|
26867
|
+
logger20.withExtraTags("init").error("Failed to initialize the offline database.", { error });
|
|
26348
26868
|
}
|
|
26349
26869
|
};
|
|
26350
26870
|
/**
|
|
@@ -26436,7 +26956,7 @@ var AbstractOfflineDB = class {
|
|
|
26436
26956
|
}
|
|
26437
26957
|
}
|
|
26438
26958
|
if (!channelData) {
|
|
26439
|
-
|
|
26959
|
+
logger20.withExtraTags("queriesWithChannelGuard").warn(
|
|
26440
26960
|
`Received a "${type}" event for a non-initialized channel that is not in the database. Skipping the event.`,
|
|
26441
26961
|
{ event }
|
|
26442
26962
|
);
|
|
@@ -26447,7 +26967,7 @@ var AbstractOfflineDB = class {
|
|
|
26447
26967
|
execute: false
|
|
26448
26968
|
});
|
|
26449
26969
|
if (!channelQuery) {
|
|
26450
|
-
|
|
26970
|
+
logger20.withExtraTags("queriesWithChannelGuard").warn(
|
|
26451
26971
|
`Could not create channel queries on a "${type}" event for an initialized channel that is not in the database. Skipping the event.`,
|
|
26452
26972
|
{ event }
|
|
26453
26973
|
);
|
|
@@ -26642,7 +27162,7 @@ var AbstractOfflineDB = class {
|
|
|
26642
27162
|
}) => {
|
|
26643
27163
|
const channel = this.client.activeChannels[cid];
|
|
26644
27164
|
if (!channel?.initialized || channel.pendingDisposal) {
|
|
26645
|
-
|
|
27165
|
+
logger20.withExtraTags("upsertMessageWithChannelGuard", cid).warn("Skipping message persistence: the channel is not in the database.");
|
|
26646
27166
|
return [];
|
|
26647
27167
|
}
|
|
26648
27168
|
const queries = [
|
|
@@ -27057,13 +27577,12 @@ var AbstractOfflineDB = class {
|
|
|
27057
27577
|
* message ordered against everything queued after it.
|
|
27058
27578
|
*
|
|
27059
27579
|
* Keyed on a queued `send-message` task existing for this id - the only reliable statement of "the
|
|
27060
|
-
* server has never seen this message". NOT on the edited message's status:
|
|
27061
|
-
*
|
|
27062
|
-
* field, so nothing arriving here carries one.
|
|
27580
|
+
* server has never seen this message". NOT on the edited message's status: `failed` is also what a
|
|
27581
|
+
* rejected edit of a message the server has carries.
|
|
27063
27582
|
*
|
|
27064
|
-
*
|
|
27065
|
-
*
|
|
27066
|
-
* `[undefined]`.
|
|
27583
|
+
* Both payloads are already `MessageRequest`s (the send's from `localMessageToNewMessagePayload`, the
|
|
27584
|
+
* edit's from `toUpdatedMessagePayload`), which is why they merge by a plain spread: normalizing again
|
|
27585
|
+
* would map an already-flattened `mentioned_users` back over itself as `[undefined]`.
|
|
27067
27586
|
*
|
|
27068
27587
|
* With nothing to fold into, the edit is queued on its own and replays as an ordinary update.
|
|
27069
27588
|
*/
|