stream-chat 9.52.1 → 9.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -159,6 +159,7 @@ var Campaign = class {
159
159
  };
160
160
 
161
161
  // src/channel_batch_updater.ts
162
+ var isChannelBatchDataUpdateOptions = (update) => "data" in update || "custom_set" in update || "custom_unset" in update;
162
163
  var ChannelBatchUpdater = class {
163
164
  constructor(client) {
164
165
  this.client = client;
@@ -297,19 +298,14 @@ var ChannelBatchUpdater = class {
297
298
  filter
298
299
  });
299
300
  }
300
- // Data operations
301
- /**
302
- * updateData - Update data on channels matching the filter
303
- *
304
- * @param {UpdateChannelsBatchFilters} filter Filter to select channels
305
- * @param {BatchChannelDataUpdate} data Data to update
306
- * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
307
- */
308
- async updateData(filter, data) {
301
+ async updateData(filter, update) {
302
+ const { data, custom_set, custom_unset } = isChannelBatchDataUpdateOptions(update) ? update : { data: update };
309
303
  return await this.client.updateChannelsBatch({
310
304
  operation: "updateData",
311
305
  filter,
312
- data
306
+ ...data && { data },
307
+ ...custom_set && { custom_set },
308
+ ...custom_unset && { custom_unset }
313
309
  });
314
310
  }
315
311
  };
@@ -5691,10 +5687,11 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
5691
5687
  const linkPreviews = linkPreviewsManager.loadingPreviews.length > 0 ? [] : linkPreviewsManager.loadedPreviews.map(
5692
5688
  (preview) => LinkPreviewsManager.getPreviewData(preview)
5693
5689
  );
5694
- const attachments = (state.message.attachments ?? []).concat(
5690
+ const messageAttachments = (state.message.attachments ?? []).concat(
5695
5691
  linkPreviews
5696
5692
  );
5697
- if (!attachments.length) return forward();
5693
+ const localAttachments = (state.localMessage.attachments ?? []).concat(linkPreviews);
5694
+ if (!messageAttachments.length && !localAttachments.length) return forward();
5698
5695
  const sendOptions = { ...state.sendOptions };
5699
5696
  const skip_enrich_url = !someLinkPreviewsLoading && linkPreviews.length > 0 || someLinkPreviewsDismissed;
5700
5697
  if (skip_enrich_url) {
@@ -5702,14 +5699,14 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
5702
5699
  }
5703
5700
  return next({
5704
5701
  ...state,
5705
- message: {
5702
+ message: messageAttachments.length ? {
5706
5703
  ...state.message,
5707
- attachments
5708
- },
5709
- localMessage: {
5704
+ attachments: messageAttachments
5705
+ } : state.message,
5706
+ localMessage: localAttachments.length ? {
5710
5707
  ...state.localMessage,
5711
- attachments
5712
- },
5708
+ attachments: localAttachments
5709
+ } : state.localMessage,
5713
5710
  sendOptions
5714
5711
  });
5715
5712
  }
@@ -12938,6 +12935,242 @@ var ThreadManager = class extends WithSubscriptions {
12938
12935
  }
12939
12936
  };
12940
12937
 
12938
+ // src/poll_optimistic_votes.ts
12939
+ var CATCH_UP_TIMEOUT_MS = 3e3;
12940
+ var OptimisticPollVotes = class {
12941
+ constructor({ client, poll }) {
12942
+ // Own vote changes not confirmed by a WS event yet
12943
+ this.pendingCount = 0;
12944
+ // Own vote changes whose requests succeeded since the server's vote state was last shown
12945
+ this.succeededChanges = [];
12946
+ // Vote requests with this tag are sent one after another. A symbol, so it is unique per instance.
12947
+ this.requestTag = /* @__PURE__ */ Symbol("poll-votes");
12948
+ this.withServerVotes = (pollState) => ({
12949
+ ...pollState,
12950
+ ...this.serverVoteState
12951
+ });
12952
+ /**
12953
+ * Casts a vote for an option, shown right away through a placeholder vote. In polls with unique
12954
+ * votes, it replaces the existing own votes.
12955
+ */
12956
+ this.castVote = (optionId, messageId) => {
12957
+ const sendRequest = () => this.client.castPollVote(messageId, this.poll.id, { option_id: optionId });
12958
+ const state = this.poll.data;
12959
+ if (state.ownVotesByOptionId[optionId]) return this.send(sendRequest);
12960
+ const replacedVotes = state.enforce_unique_vote ? Object.values(state.ownVotesByOptionId) : [];
12961
+ this.poll.state.partialNext(
12962
+ applyOwnVoteDelta(state, {
12963
+ add: [this.createPlaceholderVote(optionId)],
12964
+ remove: replacedVotes
12965
+ })
12966
+ );
12967
+ return this.track(sendRequest, (response) => ({
12968
+ optionId,
12969
+ vote: response.vote
12970
+ }));
12971
+ };
12972
+ // Removes a vote, right away.
12973
+ this.removeVote = (voteId, messageId) => {
12974
+ const sendRequest = (id) => this.client.removePollVote(messageId, this.poll.id, id);
12975
+ const state = this.poll.data;
12976
+ const vote = Object.values(state.ownVotesByOptionId).find(({ id }) => id === voteId);
12977
+ if (!vote?.option_id) return this.send(() => sendRequest(voteId));
12978
+ const optionId = vote.option_id;
12979
+ this.poll.state.partialNext(applyOwnVoteDelta(state, { remove: [vote] }));
12980
+ return this.track(
12981
+ // runs when the removal's turn comes, so a preceding cast of the vote has finished
12982
+ () => {
12983
+ const serverVoteId = this.getServerVoteId(vote);
12984
+ return serverVoteId ? sendRequest(serverVoteId) : Promise.resolve(void 0);
12985
+ },
12986
+ () => ({ optionId })
12987
+ );
12988
+ };
12989
+ // Sends a vote request once the previous one has finished, whether it succeeded or not.
12990
+ this.send = async (sendRequest) => {
12991
+ try {
12992
+ return await withoutConcurrency(this.requestTag, sendRequest);
12993
+ } finally {
12994
+ if (!hasPending(this.requestTag)) this.scheduleCatchUp();
12995
+ }
12996
+ };
12997
+ // Stores the vote state of a WS vote event and returns the vote fields to show, if any.
12998
+ this.handleVoteEvent = (poll, {
12999
+ confirmsOwnVote,
13000
+ ownVotesByOptionId
13001
+ }) => {
13002
+ this.serverVoteState = {
13003
+ latest_votes_by_option: poll.latest_votes_by_option,
13004
+ ownVotesByOptionId,
13005
+ vote_count: poll.vote_count,
13006
+ vote_counts_by_option: poll.vote_counts_by_option
13007
+ };
13008
+ if (confirmsOwnVote) this.pendingCount = Math.max(this.pendingCount - 1, 0);
13009
+ return this.pendingCount > 0 ? {} : this.takeServerVoteState();
13010
+ };
13011
+ // Fresh server state (e.g. a refetch) replaces everything, pending changes are dropped.
13012
+ this.reset = (pollState) => {
13013
+ this.serverVoteState = pickVoteState(pollState);
13014
+ this.pendingCount = 0;
13015
+ this.succeededChanges = [];
13016
+ clearTimeout(this.catchUpTimeout);
13017
+ };
13018
+ // Tracks an own vote change until its request settles. `sendRequest` resolves to undefined
13019
+ // when no request was needed.
13020
+ this.track = async (sendRequest, toChange) => {
13021
+ this.pendingCount += 1;
13022
+ clearTimeout(this.catchUpTimeout);
13023
+ let response;
13024
+ try {
13025
+ response = await this.send(async () => {
13026
+ const result = await sendRequest();
13027
+ if (result !== void 0) this.succeededChanges.push(toChange(result));
13028
+ return result;
13029
+ });
13030
+ } catch (error) {
13031
+ this.settleWithoutEvent();
13032
+ throw error;
13033
+ }
13034
+ if (response === void 0) this.settleWithoutEvent();
13035
+ return response;
13036
+ };
13037
+ // A failed or skipped request gets no WS event, so it counts as confirmed right away.
13038
+ this.settleWithoutEvent = () => {
13039
+ this.pendingCount = Math.max(this.pendingCount - 1, 0);
13040
+ if (this.pendingCount === 0) this.poll.state.partialNext(this.takeServerVoteState());
13041
+ };
13042
+ // The vote shown for a cast until the server's vote arrives
13043
+ this.createPlaceholderVote = (optionId) => {
13044
+ const now = (/* @__PURE__ */ new Date()).toISOString();
13045
+ return {
13046
+ created_at: now,
13047
+ id: `local-${generateUUIDv4()}`,
13048
+ option_id: optionId,
13049
+ poll_id: this.poll.id,
13050
+ updated_at: now,
13051
+ user: this.client.user,
13052
+ user_id: this.client.userID
13053
+ };
13054
+ };
13055
+ /**
13056
+ * The server id of a vote to remove, once everything sent before the removal has finished. Casts
13057
+ * and removals of an option alternate, so the option's last succeeded change tells: a cast is the
13058
+ * vote being removed (e.g. a placeholder), a removal means the cast in between failed. Without
13059
+ * one, the server's own vote of the option is the vote being removed.
13060
+ */
13061
+ this.getServerVoteId = (vote) => {
13062
+ for (let i = this.succeededChanges.length - 1; i >= 0; i--) {
13063
+ const change = this.succeededChanges[i];
13064
+ if (change.optionId === vote.option_id) return change.vote?.id;
13065
+ }
13066
+ return this.serverVoteState.ownVotesByOptionId[vote.option_id]?.id;
13067
+ };
13068
+ this.takeServerVoteState = () => {
13069
+ clearTimeout(this.catchUpTimeout);
13070
+ this.succeededChanges = [];
13071
+ return {
13072
+ ...this.serverVoteState,
13073
+ maxVotedOptionIds: getMaxVotedOptionIds(this.serverVoteState.vote_counts_by_option)
13074
+ };
13075
+ };
13076
+ this.scheduleCatchUp = () => {
13077
+ clearTimeout(this.catchUpTimeout);
13078
+ if (this.pendingCount === 0) return;
13079
+ this.catchUpTimeout = setTimeout(this.applySucceededChanges, CATCH_UP_TIMEOUT_MS);
13080
+ };
13081
+ /**
13082
+ * A WS event of an own vote change got lost, so the server's own votes are stale. All requests
13083
+ * have finished (the timeout starts once nothing is left to send and a new request cancels it),
13084
+ * so applying the succeeded changes in order gives the server's own votes (changes whose events
13085
+ * did arrive end up the same). They are shown with the vote counts of the last WS event, which
13086
+ * contain the other users' votes too.
13087
+ */
13088
+ this.applySucceededChanges = () => {
13089
+ const { enforce_unique_vote } = this.poll.data;
13090
+ let ownVotesByOptionId = { ...this.serverVoteState.ownVotesByOptionId };
13091
+ for (const { optionId, vote } of this.succeededChanges) {
13092
+ if (!vote) {
13093
+ delete ownVotesByOptionId[optionId];
13094
+ } else if (enforce_unique_vote) {
13095
+ ownVotesByOptionId = { [optionId]: vote };
13096
+ } else {
13097
+ ownVotesByOptionId[optionId] = vote;
13098
+ }
13099
+ }
13100
+ this.serverVoteState = { ...this.serverVoteState, ownVotesByOptionId };
13101
+ this.pendingCount = 0;
13102
+ this.poll.state.partialNext(this.takeServerVoteState());
13103
+ };
13104
+ this.client = client;
13105
+ this.poll = poll;
13106
+ this.serverVoteState = pickVoteState(poll.data);
13107
+ }
13108
+ get serverOwnVotes() {
13109
+ return this.serverVoteState.ownVotesByOptionId;
13110
+ }
13111
+ };
13112
+ function pickVoteState(state) {
13113
+ return {
13114
+ latest_votes_by_option: state.latest_votes_by_option,
13115
+ ownVotesByOptionId: state.ownVotesByOptionId,
13116
+ vote_count: state.vote_count,
13117
+ vote_counts_by_option: state.vote_counts_by_option
13118
+ };
13119
+ }
13120
+ function applyOwnVoteDelta(state, { add = [], remove = [] }) {
13121
+ const ownVotesByOptionId = { ...state.ownVotesByOptionId };
13122
+ const voteCountsByOption = { ...state.vote_counts_by_option };
13123
+ const latestVotesByOption = state.latest_votes_by_option ? { ...state.latest_votes_by_option } : state.latest_votes_by_option;
13124
+ let voteCount = state.vote_count ?? 0;
13125
+ for (const vote of remove) {
13126
+ const optionId = vote.option_id;
13127
+ if (!optionId) continue;
13128
+ delete ownVotesByOptionId[optionId];
13129
+ const optionVoteCount = (voteCountsByOption[optionId] ?? 0) - 1;
13130
+ if (optionVoteCount > 0) {
13131
+ voteCountsByOption[optionId] = optionVoteCount;
13132
+ } else {
13133
+ delete voteCountsByOption[optionId];
13134
+ }
13135
+ voteCount = Math.max(voteCount - 1, 0);
13136
+ if (latestVotesByOption?.[optionId]) {
13137
+ latestVotesByOption[optionId] = latestVotesByOption[optionId].filter(
13138
+ (latestVote) => latestVote.id !== vote.id
13139
+ );
13140
+ }
13141
+ }
13142
+ for (const vote of add) {
13143
+ const optionId = vote.option_id;
13144
+ if (!optionId) continue;
13145
+ ownVotesByOptionId[optionId] = vote;
13146
+ voteCountsByOption[optionId] = (voteCountsByOption[optionId] ?? 0) + 1;
13147
+ voteCount += 1;
13148
+ if (latestVotesByOption && state.voting_visibility !== "anonymous") {
13149
+ latestVotesByOption[optionId] = [vote, ...latestVotesByOption[optionId] ?? []];
13150
+ }
13151
+ }
13152
+ return {
13153
+ latest_votes_by_option: latestVotesByOption,
13154
+ maxVotedOptionIds: getMaxVotedOptionIds(voteCountsByOption),
13155
+ ownVotesByOptionId,
13156
+ vote_count: voteCount,
13157
+ vote_counts_by_option: voteCountsByOption
13158
+ };
13159
+ }
13160
+ function getMaxVotedOptionIds(voteCountsByOption) {
13161
+ let maxVotes = 0;
13162
+ let winningOptions = [];
13163
+ for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
13164
+ if (count > maxVotes) {
13165
+ winningOptions = [id];
13166
+ maxVotes = count;
13167
+ } else if (count === maxVotes) {
13168
+ winningOptions.push(id);
13169
+ }
13170
+ }
13171
+ return winningOptions;
13172
+ }
13173
+
12941
13174
  // src/poll.ts
12942
13175
  var isPollUpdatedEvent = (e) => e.type === "poll.updated";
12943
13176
  var isPollClosedEventEvent = (e) => e.type === "poll.closed";
@@ -12947,6 +13180,9 @@ var isPollVoteRemovedEvent = (e) => e.type === "poll.vote_removed";
12947
13180
  var isVoteAnswer = (vote) => !!vote?.answer_text;
12948
13181
  var Poll = class {
12949
13182
  constructor({ client, poll }) {
13183
+ // Set whenever server data (WS event or rehydration) confirms that the poll is closed,
13184
+ // so that a failed optimistic close does not reopen a poll that is actually closed.
13185
+ this.closeConfirmed = false;
12950
13186
  this.getInitialStateFromPollResponse = (poll) => {
12951
13187
  const { own_votes, id, ...pollResponseForState } = poll;
12952
13188
  const { ownAnswer, ownVotes } = own_votes?.reduce(
@@ -12970,25 +13206,47 @@ var Poll = class {
12970
13206
  ownVotesByOptionId: getOwnVotesByOptionId(ownVotes)
12971
13207
  };
12972
13208
  };
13209
+ // persists only the server's votes, never the optimistic ones
12973
13210
  this.upsertOfflineDb = () => {
13211
+ const serverState = this.optimisticVotes.withServerVotes(this.data);
12974
13212
  this.client.offlineDb?.executeQuerySafely(
12975
- (db) => db.upsertPoll({ poll: mapPollStateToResponse(this) }),
13213
+ (db) => db.upsertPoll({ poll: mapPollStateToResponse(this, serverState) }),
12976
13214
  { method: "upsertPoll" }
12977
13215
  );
12978
13216
  };
12979
13217
  this.reinitializeState = (poll) => {
12980
- this.state.partialNext(this.getInitialStateFromPollResponse(poll));
13218
+ if (poll.is_closed) this.closeConfirmed = true;
13219
+ const initialState = this.getInitialStateFromPollResponse(poll);
13220
+ this.optimisticVotes.reset(initialState);
13221
+ this.state.partialNext(initialState);
13222
+ };
13223
+ // Hands the vote state of a WS vote event to the optimistic votes, which decide whether to show it.
13224
+ this.updateFromVoteEvent = (event, ownVotesByOptionId, answerState) => {
13225
+ const isOwnVote = event.poll_vote.user_id === this.client.userID;
13226
+ const shownVoteState = this.optimisticVotes.handleVoteEvent(event.poll, {
13227
+ confirmsOwnVote: isOwnVote && !isVoteAnswer(event.poll_vote) && !!event.poll_vote.option_id,
13228
+ ownVotesByOptionId
13229
+ });
13230
+ this.state.partialNext({
13231
+ ...answerState,
13232
+ answers_count: event.poll.answers_count,
13233
+ lastActivityAt: new Date(event.created_at),
13234
+ ...shownVoteState
13235
+ });
13236
+ this.upsertOfflineDb();
12981
13237
  };
12982
13238
  this.handlePollUpdated = (event) => {
12983
13239
  if (event.poll?.id && event.poll.id !== this.id) return;
12984
13240
  if (!isPollUpdatedEvent(event)) return;
12985
13241
  const { id, ...pollData } = extractPollData(event.poll);
13242
+ if (pollData.is_closed) this.closeConfirmed = true;
12986
13243
  this.state.partialNext({ ...pollData, lastActivityAt: new Date(event.created_at) });
12987
13244
  this.upsertOfflineDb();
12988
13245
  };
12989
13246
  this.handlePollClosed = (event) => {
12990
13247
  if (event.poll?.id && event.poll.id !== this.id) return;
12991
13248
  if (!isPollClosedEventEvent(event)) return;
13249
+ this.closeConfirmed = true;
12992
13250
  this.state.partialNext({
12993
13251
  is_closed: true,
12994
13252
  lastActivityAt: new Date(event.created_at)
@@ -13002,8 +13260,7 @@ var Poll = class {
13002
13260
  const isOwnVote = event.poll_vote.user_id === this.client.userID;
13003
13261
  let latestAnswers = [...currentState.latest_answers];
13004
13262
  let ownAnswer = currentState.ownAnswer;
13005
- const ownVotesByOptionId = currentState.ownVotesByOptionId;
13006
- let maxVotedOptionIds = currentState.maxVotedOptionIds;
13263
+ const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
13007
13264
  if (isOwnVote) {
13008
13265
  if (isVoteAnswer(event.poll_vote)) {
13009
13266
  ownAnswer = event.poll_vote;
@@ -13013,19 +13270,11 @@ var Poll = class {
13013
13270
  }
13014
13271
  if (isVoteAnswer(event.poll_vote)) {
13015
13272
  latestAnswers = [event.poll_vote, ...latestAnswers];
13016
- } else {
13017
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13018
13273
  }
13019
- const pollEnrichData = extractPollEnrichedData(event.poll);
13020
- this.state.partialNext({
13021
- ...pollEnrichData,
13274
+ this.updateFromVoteEvent(event, ownVotesByOptionId, {
13022
13275
  latest_answers: latestAnswers,
13023
- lastActivityAt: new Date(event.created_at),
13024
- ownAnswer,
13025
- ownVotesByOptionId,
13026
- maxVotedOptionIds
13276
+ ownAnswer
13027
13277
  });
13028
- this.upsertOfflineDb();
13029
13278
  };
13030
13279
  this.handleVoteChanged = (event) => {
13031
13280
  if (event.poll?.id && event.poll.id !== this.id) return;
@@ -13034,8 +13283,7 @@ var Poll = class {
13034
13283
  const isOwnVote = event.poll_vote.user_id === this.client.userID;
13035
13284
  let latestAnswers = [...currentState.latest_answers];
13036
13285
  let ownAnswer = currentState.ownAnswer;
13037
- let ownVotesByOptionId = currentState.ownVotesByOptionId;
13038
- let maxVotedOptionIds = currentState.maxVotedOptionIds;
13286
+ let ownVotesByOptionId = this.optimisticVotes.serverOwnVotes;
13039
13287
  if (isOwnVote) {
13040
13288
  if (isVoteAnswer(event.poll_vote)) {
13041
13289
  latestAnswers = [
@@ -13059,23 +13307,14 @@ var Poll = class {
13059
13307
  if (ownAnswer?.id === event.poll_vote.id) {
13060
13308
  ownAnswer = void 0;
13061
13309
  }
13062
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13063
13310
  }
13064
13311
  } else if (isVoteAnswer(event.poll_vote)) {
13065
13312
  latestAnswers = [event.poll_vote, ...latestAnswers];
13066
- } else {
13067
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13068
13313
  }
13069
- const pollEnrichData = extractPollEnrichedData(event.poll);
13070
- this.state.partialNext({
13071
- ...pollEnrichData,
13314
+ this.updateFromVoteEvent(event, ownVotesByOptionId, {
13072
13315
  latest_answers: latestAnswers,
13073
- lastActivityAt: new Date(event.created_at),
13074
- ownAnswer,
13075
- ownVotesByOptionId,
13076
- maxVotedOptionIds
13316
+ ownAnswer
13077
13317
  });
13078
- this.upsertOfflineDb();
13079
13318
  };
13080
13319
  this.handleVoteRemoved = (event) => {
13081
13320
  if (event.poll?.id && event.poll.id !== this.id) return;
@@ -13084,29 +13323,19 @@ var Poll = class {
13084
13323
  const isOwnVote = event.poll_vote.user_id === this.client.userID;
13085
13324
  let latestAnswers = [...currentState.latest_answers];
13086
13325
  let ownAnswer = currentState.ownAnswer;
13087
- const ownVotesByOptionId = { ...currentState.ownVotesByOptionId };
13088
- let maxVotedOptionIds = currentState.maxVotedOptionIds;
13326
+ const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
13089
13327
  if (isVoteAnswer(event.poll_vote)) {
13090
13328
  latestAnswers = latestAnswers.filter((answer) => answer.id !== event.poll_vote.id);
13091
13329
  if (isOwnVote) {
13092
13330
  ownAnswer = void 0;
13093
13331
  }
13094
- } else {
13095
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13096
- if (isOwnVote && event.poll_vote.option_id) {
13097
- delete ownVotesByOptionId[event.poll_vote.option_id];
13098
- }
13332
+ } else if (isOwnVote && event.poll_vote.option_id) {
13333
+ delete ownVotesByOptionId[event.poll_vote.option_id];
13099
13334
  }
13100
- const pollEnrichData = extractPollEnrichedData(event.poll);
13101
- this.state.partialNext({
13102
- ...pollEnrichData,
13335
+ this.updateFromVoteEvent(event, ownVotesByOptionId, {
13103
13336
  latest_answers: latestAnswers,
13104
- lastActivityAt: new Date(event.created_at),
13105
- ownAnswer,
13106
- ownVotesByOptionId,
13107
- maxVotedOptionIds
13337
+ ownAnswer
13108
13338
  });
13109
- this.upsertOfflineDb();
13110
13339
  };
13111
13340
  this.query = async (id) => {
13112
13341
  const { poll } = await this.client.getPoll(id);
@@ -13115,11 +13344,33 @@ var Poll = class {
13115
13344
  };
13116
13345
  this.update = async (data) => await this.client.updatePoll({ ...data, id: this.id });
13117
13346
  this.partialUpdate = async (partialPollObject) => await this.client.partialUpdatePoll(this.id, partialPollObject);
13118
- this.close = async () => await this.client.closePoll(this.id);
13347
+ /**
13348
+ * Closes the poll. The poll is marked as closed optimistically and the change is
13349
+ * reverted if the request fails, unless the server confirmed the close meanwhile.
13350
+ * Does nothing if the poll is already closed.
13351
+ */
13352
+ this.close = async () => {
13353
+ if (this.data.is_closed) return;
13354
+ this.closeConfirmed = false;
13355
+ this.state.partialNext({ is_closed: true });
13356
+ try {
13357
+ return await this.client.closePoll(this.id);
13358
+ } catch (error) {
13359
+ if (!this.closeConfirmed) {
13360
+ this.state.partialNext({ is_closed: false });
13361
+ }
13362
+ throw error;
13363
+ }
13364
+ };
13119
13365
  this.delete = async () => await this.client.deletePoll(this.id);
13120
13366
  this.createOption = async (option) => await this.client.createPollOption(this.id, option);
13121
13367
  this.updateOption = async (option) => await this.client.updatePollOption(this.id, option);
13122
13368
  this.deleteOption = async (optionId) => await this.client.deletePollOption(this.id, optionId);
13369
+ /**
13370
+ * Casts a vote for an option. The vote is shown right away through a local placeholder vote,
13371
+ * which the server's vote replaces once the WS events of all pending vote changes arrived.
13372
+ * If the request fails, the change is reverted.
13373
+ */
13123
13374
  this.castVote = async (optionId, messageId) => {
13124
13375
  const { max_votes_allowed, ownVotesByOptionId } = this.data;
13125
13376
  const reachedVoteLimit = max_votes_allowed && max_votes_allowed === Object.keys(ownVotesByOptionId).length;
@@ -13136,11 +13387,14 @@ var Poll = class {
13136
13387
  });
13137
13388
  return;
13138
13389
  }
13139
- return await this.client.castPollVote(messageId, this.id, {
13140
- option_id: optionId
13141
- });
13390
+ return await this.optimisticVotes.castVote(optionId, messageId);
13142
13391
  };
13143
- this.removeVote = async (voteId, messageId) => await this.client.removePollVote(messageId, this.id, voteId);
13392
+ /**
13393
+ * Removes a vote. The vote is removed right away and restored if the request fails. Vote
13394
+ * requests are sent in order, so removing a vote whose cast is not confirmed yet uses the vote
13395
+ * id from the cast's response; if that cast failed, there is nothing to remove.
13396
+ */
13397
+ this.removeVote = async (voteId, messageId) => await this.optimisticVotes.removeVote(voteId, messageId);
13144
13398
  this.addAnswer = async (answerText, messageId) => await this.client.addPollAnswer(messageId, this.id, answerText);
13145
13399
  this.removeAnswer = async (answerId, messageId) => await this.client.removePollVote(messageId, this.id, answerId);
13146
13400
  this.queryAnswers = async (params) => await this.client.queryPollAnswers(
@@ -13158,24 +13412,12 @@ var Poll = class {
13158
13412
  this.client = client;
13159
13413
  this.id = poll.id;
13160
13414
  this.state = new StateStore(this.getInitialStateFromPollResponse(poll));
13415
+ this.optimisticVotes = new OptimisticPollVotes({ client: this.client, poll: this });
13161
13416
  }
13162
13417
  get data() {
13163
13418
  return this.state.getLatestValue();
13164
13419
  }
13165
13420
  };
13166
- function getMaxVotedOptionIds(voteCountsByOption) {
13167
- let maxVotes = 0;
13168
- let winningOptions = [];
13169
- for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
13170
- if (count > maxVotes) {
13171
- winningOptions = [id];
13172
- maxVotes = count;
13173
- } else if (count === maxVotes) {
13174
- winningOptions.push(id);
13175
- }
13176
- }
13177
- return winningOptions;
13178
- }
13179
13421
  function getOwnVotesByOptionId(ownVotes) {
13180
13422
  return !ownVotes ? {} : ownVotes.reduce((acc, vote) => {
13181
13423
  if (isVoteAnswer(vote) || !vote.option_id) return acc;
@@ -13197,7 +13439,7 @@ function extractPollData(pollResponse) {
13197
13439
  voting_visibility: pollResponse.voting_visibility
13198
13440
  };
13199
13441
  }
13200
- function mapPollStateToResponse(poll) {
13442
+ function mapPollStateToResponse(poll, state = poll.data) {
13201
13443
  const {
13202
13444
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
13203
13445
  lastActivityAt,
@@ -13206,7 +13448,7 @@ function mapPollStateToResponse(poll) {
13206
13448
  ownVotesByOptionId,
13207
13449
  ownAnswer,
13208
13450
  ...restState
13209
- } = poll.data;
13451
+ } = state;
13210
13452
  const ownVotes = [
13211
13453
  ...Object.values(ownVotesByOptionId),
13212
13454
  ...ownAnswer ? [ownAnswer] : []
@@ -14364,6 +14606,8 @@ var StreamChat = class _StreamChat {
14364
14606
  * The main use case for SDKs who do independent state recovery for channels.
14365
14607
  */
14366
14608
  this.preventThreadCleanup = false;
14609
+ this._wsPromiseSettled = true;
14610
+ this._wsConnectId = 0;
14367
14611
  this.nextRequestAbortController = null;
14368
14612
  /**
14369
14613
  * @private
@@ -14409,17 +14653,20 @@ var StreamChat = class _StreamChat {
14409
14653
  const setTokenPromise = this._setToken(user, userTokenOrProvider);
14410
14654
  this._setUser(user);
14411
14655
  const wsPromise = this.openConnection();
14412
- this.setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
14656
+ const setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
14413
14657
  (result) => result[1]
14414
14658
  // We only return connection promise;
14415
14659
  );
14660
+ this.setUserPromise = setUserPromise;
14416
14661
  try {
14417
- return await this.setUserPromise;
14662
+ return await setUserPromise;
14418
14663
  } catch (err) {
14419
- if (this.persistUserOnConnectionFailure) {
14420
- this.closeConnection();
14421
- } else {
14422
- this.disconnectUser();
14664
+ if (this.setUserPromise === setUserPromise && this.userID === user.id) {
14665
+ if (this.persistUserOnConnectionFailure) {
14666
+ this.closeConnection();
14667
+ } else {
14668
+ this.disconnectUser();
14669
+ }
14423
14670
  }
14424
14671
  throw err;
14425
14672
  }
@@ -14515,9 +14762,50 @@ var StreamChat = class _StreamChat {
14515
14762
  return;
14516
14763
  }
14517
14764
  this.clientID = `${this.userID}--${randomId()}`;
14518
- this.wsPromise = this.connect();
14765
+ const wsPromise = this._bindWsPromise(this.connect());
14519
14766
  this._startCleaning();
14520
- return this.wsPromise;
14767
+ return wsPromise;
14768
+ };
14769
+ /**
14770
+ * Rejects the shared `wsPromise`, invalidates any in-flight connect attempt
14771
+ */
14772
+ this._rejectPendingWsPromise = (reason) => {
14773
+ if (!this._wsPromiseSettled) {
14774
+ this._wsConnectId += 1;
14775
+ this._wsPromiseSettled = true;
14776
+ this.wsPromise?.catch(() => {
14777
+ });
14778
+ this._rejectWsPromise?.(reason);
14779
+ }
14780
+ };
14781
+ /**
14782
+ * Keep a single pending `wsPromise` across close/reopen so callers that already
14783
+ * `await this.wsPromise` are not stranded when `openConnection` starts a new connect.
14784
+ */
14785
+ this._bindWsPromise = (connectPromise) => {
14786
+ const connectId = ++this._wsConnectId;
14787
+ let pending = this.wsPromise;
14788
+ if (this._wsPromiseSettled || !pending) {
14789
+ this._wsPromiseSettled = false;
14790
+ pending = new Promise((resolve, reject) => {
14791
+ this._resolveWsPromise = resolve;
14792
+ this._rejectWsPromise = reject;
14793
+ });
14794
+ this.wsPromise = pending;
14795
+ }
14796
+ connectPromise.then(
14797
+ (value) => {
14798
+ if (connectId !== this._wsConnectId) return;
14799
+ this._wsPromiseSettled = true;
14800
+ this._resolveWsPromise?.(value);
14801
+ },
14802
+ (error) => {
14803
+ if (connectId !== this._wsConnectId) return;
14804
+ this._wsPromiseSettled = true;
14805
+ this._rejectWsPromise?.(error);
14806
+ }
14807
+ );
14808
+ return pending;
14521
14809
  };
14522
14810
  /**
14523
14811
  * @deprecated Please use client.openConnction instead.
@@ -14551,6 +14839,10 @@ var StreamChat = class _StreamChat {
14551
14839
  delete this._user;
14552
14840
  delete this.userID;
14553
14841
  this.anonymous = false;
14842
+ this._rejectPendingWsPromise(
14843
+ new Error("Connection was closed because disconnectUser() was called")
14844
+ );
14845
+ this.wsPromise = null;
14554
14846
  const closePromise = this.closeConnection(timeout);
14555
14847
  for (const channel of Object.values(this.activeChannels)) {
14556
14848
  channel._disconnect();
@@ -14821,7 +15113,9 @@ var StreamChat = class _StreamChat {
14821
15113
  type: "connection.recovered"
14822
15114
  });
14823
15115
  }
14824
- this.wsPromise = Promise.resolve();
15116
+ if (this._wsPromiseSettled) {
15117
+ this.wsPromise = Promise.resolve();
15118
+ }
14825
15119
  this.setUserPromise = Promise.resolve();
14826
15120
  };
14827
15121
  /**
@@ -14939,7 +15233,7 @@ var StreamChat = class _StreamChat {
14939
15233
  return this.userAgent;
14940
15234
  }
14941
15235
  if (!this.cachedUserAgent) {
14942
- const version = "9.52.1";
15236
+ const version = "9.54.0";
14943
15237
  const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
14944
15238
  const { name: appName, version: appVersion } = this.appIdentifier ?? {};
14945
15239
  const { os, model: deviceModel } = this.deviceIdentifier ?? {};
@@ -18181,6 +18475,10 @@ var StreamChat = class _StreamChat {
18181
18475
  /**
18182
18476
  * Update Channels Batch
18183
18477
  *
18478
+ * For the `updateData` operation, `data.custom` replaces the channel's whole
18479
+ * custom object, while the root-level `custom_set` / `custom_unset` patch
18480
+ * individual keys and leave the rest untouched.
18481
+ *
18184
18482
  * @param {UpdateChannelsBatchOptions} payload for updating channels in batch
18185
18483
  * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
18186
18484
  */