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.
@@ -351,6 +351,7 @@ var Campaign = class {
351
351
  };
352
352
 
353
353
  // src/channel_batch_updater.ts
354
+ var isChannelBatchDataUpdateOptions = (update) => "data" in update || "custom_set" in update || "custom_unset" in update;
354
355
  var ChannelBatchUpdater = class {
355
356
  constructor(client) {
356
357
  this.client = client;
@@ -489,19 +490,14 @@ var ChannelBatchUpdater = class {
489
490
  filter
490
491
  });
491
492
  }
492
- // Data operations
493
- /**
494
- * updateData - Update data on channels matching the filter
495
- *
496
- * @param {UpdateChannelsBatchFilters} filter Filter to select channels
497
- * @param {BatchChannelDataUpdate} data Data to update
498
- * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
499
- */
500
- async updateData(filter, data) {
493
+ async updateData(filter, update) {
494
+ const { data, custom_set, custom_unset } = isChannelBatchDataUpdateOptions(update) ? update : { data: update };
501
495
  return await this.client.updateChannelsBatch({
502
496
  operation: "updateData",
503
497
  filter,
504
- data
498
+ ...data && { data },
499
+ ...custom_set && { custom_set },
500
+ ...custom_unset && { custom_unset }
505
501
  });
506
502
  }
507
503
  };
@@ -5883,10 +5879,11 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
5883
5879
  const linkPreviews = linkPreviewsManager.loadingPreviews.length > 0 ? [] : linkPreviewsManager.loadedPreviews.map(
5884
5880
  (preview) => LinkPreviewsManager.getPreviewData(preview)
5885
5881
  );
5886
- const attachments = (state.message.attachments ?? []).concat(
5882
+ const messageAttachments = (state.message.attachments ?? []).concat(
5887
5883
  linkPreviews
5888
5884
  );
5889
- if (!attachments.length) return forward();
5885
+ const localAttachments = (state.localMessage.attachments ?? []).concat(linkPreviews);
5886
+ if (!messageAttachments.length && !localAttachments.length) return forward();
5890
5887
  const sendOptions = { ...state.sendOptions };
5891
5888
  const skip_enrich_url = !someLinkPreviewsLoading && linkPreviews.length > 0 || someLinkPreviewsDismissed;
5892
5889
  if (skip_enrich_url) {
@@ -5894,14 +5891,14 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
5894
5891
  }
5895
5892
  return next({
5896
5893
  ...state,
5897
- message: {
5894
+ message: messageAttachments.length ? {
5898
5895
  ...state.message,
5899
- attachments
5900
- },
5901
- localMessage: {
5896
+ attachments: messageAttachments
5897
+ } : state.message,
5898
+ localMessage: localAttachments.length ? {
5902
5899
  ...state.localMessage,
5903
- attachments
5904
- },
5900
+ attachments: localAttachments
5901
+ } : state.localMessage,
5905
5902
  sendOptions
5906
5903
  });
5907
5904
  }
@@ -13130,6 +13127,242 @@ var ThreadManager = class extends WithSubscriptions {
13130
13127
  }
13131
13128
  };
13132
13129
 
13130
+ // src/poll_optimistic_votes.ts
13131
+ var CATCH_UP_TIMEOUT_MS = 3e3;
13132
+ var OptimisticPollVotes = class {
13133
+ constructor({ client, poll }) {
13134
+ // Own vote changes not confirmed by a WS event yet
13135
+ this.pendingCount = 0;
13136
+ // Own vote changes whose requests succeeded since the server's vote state was last shown
13137
+ this.succeededChanges = [];
13138
+ // Vote requests with this tag are sent one after another. A symbol, so it is unique per instance.
13139
+ this.requestTag = /* @__PURE__ */ Symbol("poll-votes");
13140
+ this.withServerVotes = (pollState) => ({
13141
+ ...pollState,
13142
+ ...this.serverVoteState
13143
+ });
13144
+ /**
13145
+ * Casts a vote for an option, shown right away through a placeholder vote. In polls with unique
13146
+ * votes, it replaces the existing own votes.
13147
+ */
13148
+ this.castVote = (optionId, messageId) => {
13149
+ const sendRequest = () => this.client.castPollVote(messageId, this.poll.id, { option_id: optionId });
13150
+ const state = this.poll.data;
13151
+ if (state.ownVotesByOptionId[optionId]) return this.send(sendRequest);
13152
+ const replacedVotes = state.enforce_unique_vote ? Object.values(state.ownVotesByOptionId) : [];
13153
+ this.poll.state.partialNext(
13154
+ applyOwnVoteDelta(state, {
13155
+ add: [this.createPlaceholderVote(optionId)],
13156
+ remove: replacedVotes
13157
+ })
13158
+ );
13159
+ return this.track(sendRequest, (response) => ({
13160
+ optionId,
13161
+ vote: response.vote
13162
+ }));
13163
+ };
13164
+ // Removes a vote, right away.
13165
+ this.removeVote = (voteId, messageId) => {
13166
+ const sendRequest = (id) => this.client.removePollVote(messageId, this.poll.id, id);
13167
+ const state = this.poll.data;
13168
+ const vote = Object.values(state.ownVotesByOptionId).find(({ id }) => id === voteId);
13169
+ if (!vote?.option_id) return this.send(() => sendRequest(voteId));
13170
+ const optionId = vote.option_id;
13171
+ this.poll.state.partialNext(applyOwnVoteDelta(state, { remove: [vote] }));
13172
+ return this.track(
13173
+ // runs when the removal's turn comes, so a preceding cast of the vote has finished
13174
+ () => {
13175
+ const serverVoteId = this.getServerVoteId(vote);
13176
+ return serverVoteId ? sendRequest(serverVoteId) : Promise.resolve(void 0);
13177
+ },
13178
+ () => ({ optionId })
13179
+ );
13180
+ };
13181
+ // Sends a vote request once the previous one has finished, whether it succeeded or not.
13182
+ this.send = async (sendRequest) => {
13183
+ try {
13184
+ return await withoutConcurrency(this.requestTag, sendRequest);
13185
+ } finally {
13186
+ if (!hasPending(this.requestTag)) this.scheduleCatchUp();
13187
+ }
13188
+ };
13189
+ // Stores the vote state of a WS vote event and returns the vote fields to show, if any.
13190
+ this.handleVoteEvent = (poll, {
13191
+ confirmsOwnVote,
13192
+ ownVotesByOptionId
13193
+ }) => {
13194
+ this.serverVoteState = {
13195
+ latest_votes_by_option: poll.latest_votes_by_option,
13196
+ ownVotesByOptionId,
13197
+ vote_count: poll.vote_count,
13198
+ vote_counts_by_option: poll.vote_counts_by_option
13199
+ };
13200
+ if (confirmsOwnVote) this.pendingCount = Math.max(this.pendingCount - 1, 0);
13201
+ return this.pendingCount > 0 ? {} : this.takeServerVoteState();
13202
+ };
13203
+ // Fresh server state (e.g. a refetch) replaces everything, pending changes are dropped.
13204
+ this.reset = (pollState) => {
13205
+ this.serverVoteState = pickVoteState(pollState);
13206
+ this.pendingCount = 0;
13207
+ this.succeededChanges = [];
13208
+ clearTimeout(this.catchUpTimeout);
13209
+ };
13210
+ // Tracks an own vote change until its request settles. `sendRequest` resolves to undefined
13211
+ // when no request was needed.
13212
+ this.track = async (sendRequest, toChange) => {
13213
+ this.pendingCount += 1;
13214
+ clearTimeout(this.catchUpTimeout);
13215
+ let response;
13216
+ try {
13217
+ response = await this.send(async () => {
13218
+ const result = await sendRequest();
13219
+ if (result !== void 0) this.succeededChanges.push(toChange(result));
13220
+ return result;
13221
+ });
13222
+ } catch (error) {
13223
+ this.settleWithoutEvent();
13224
+ throw error;
13225
+ }
13226
+ if (response === void 0) this.settleWithoutEvent();
13227
+ return response;
13228
+ };
13229
+ // A failed or skipped request gets no WS event, so it counts as confirmed right away.
13230
+ this.settleWithoutEvent = () => {
13231
+ this.pendingCount = Math.max(this.pendingCount - 1, 0);
13232
+ if (this.pendingCount === 0) this.poll.state.partialNext(this.takeServerVoteState());
13233
+ };
13234
+ // The vote shown for a cast until the server's vote arrives
13235
+ this.createPlaceholderVote = (optionId) => {
13236
+ const now = (/* @__PURE__ */ new Date()).toISOString();
13237
+ return {
13238
+ created_at: now,
13239
+ id: `local-${generateUUIDv4()}`,
13240
+ option_id: optionId,
13241
+ poll_id: this.poll.id,
13242
+ updated_at: now,
13243
+ user: this.client.user,
13244
+ user_id: this.client.userID
13245
+ };
13246
+ };
13247
+ /**
13248
+ * The server id of a vote to remove, once everything sent before the removal has finished. Casts
13249
+ * and removals of an option alternate, so the option's last succeeded change tells: a cast is the
13250
+ * vote being removed (e.g. a placeholder), a removal means the cast in between failed. Without
13251
+ * one, the server's own vote of the option is the vote being removed.
13252
+ */
13253
+ this.getServerVoteId = (vote) => {
13254
+ for (let i = this.succeededChanges.length - 1; i >= 0; i--) {
13255
+ const change = this.succeededChanges[i];
13256
+ if (change.optionId === vote.option_id) return change.vote?.id;
13257
+ }
13258
+ return this.serverVoteState.ownVotesByOptionId[vote.option_id]?.id;
13259
+ };
13260
+ this.takeServerVoteState = () => {
13261
+ clearTimeout(this.catchUpTimeout);
13262
+ this.succeededChanges = [];
13263
+ return {
13264
+ ...this.serverVoteState,
13265
+ maxVotedOptionIds: getMaxVotedOptionIds(this.serverVoteState.vote_counts_by_option)
13266
+ };
13267
+ };
13268
+ this.scheduleCatchUp = () => {
13269
+ clearTimeout(this.catchUpTimeout);
13270
+ if (this.pendingCount === 0) return;
13271
+ this.catchUpTimeout = setTimeout(this.applySucceededChanges, CATCH_UP_TIMEOUT_MS);
13272
+ };
13273
+ /**
13274
+ * A WS event of an own vote change got lost, so the server's own votes are stale. All requests
13275
+ * have finished (the timeout starts once nothing is left to send and a new request cancels it),
13276
+ * so applying the succeeded changes in order gives the server's own votes (changes whose events
13277
+ * did arrive end up the same). They are shown with the vote counts of the last WS event, which
13278
+ * contain the other users' votes too.
13279
+ */
13280
+ this.applySucceededChanges = () => {
13281
+ const { enforce_unique_vote } = this.poll.data;
13282
+ let ownVotesByOptionId = { ...this.serverVoteState.ownVotesByOptionId };
13283
+ for (const { optionId, vote } of this.succeededChanges) {
13284
+ if (!vote) {
13285
+ delete ownVotesByOptionId[optionId];
13286
+ } else if (enforce_unique_vote) {
13287
+ ownVotesByOptionId = { [optionId]: vote };
13288
+ } else {
13289
+ ownVotesByOptionId[optionId] = vote;
13290
+ }
13291
+ }
13292
+ this.serverVoteState = { ...this.serverVoteState, ownVotesByOptionId };
13293
+ this.pendingCount = 0;
13294
+ this.poll.state.partialNext(this.takeServerVoteState());
13295
+ };
13296
+ this.client = client;
13297
+ this.poll = poll;
13298
+ this.serverVoteState = pickVoteState(poll.data);
13299
+ }
13300
+ get serverOwnVotes() {
13301
+ return this.serverVoteState.ownVotesByOptionId;
13302
+ }
13303
+ };
13304
+ function pickVoteState(state) {
13305
+ return {
13306
+ latest_votes_by_option: state.latest_votes_by_option,
13307
+ ownVotesByOptionId: state.ownVotesByOptionId,
13308
+ vote_count: state.vote_count,
13309
+ vote_counts_by_option: state.vote_counts_by_option
13310
+ };
13311
+ }
13312
+ function applyOwnVoteDelta(state, { add = [], remove = [] }) {
13313
+ const ownVotesByOptionId = { ...state.ownVotesByOptionId };
13314
+ const voteCountsByOption = { ...state.vote_counts_by_option };
13315
+ const latestVotesByOption = state.latest_votes_by_option ? { ...state.latest_votes_by_option } : state.latest_votes_by_option;
13316
+ let voteCount = state.vote_count ?? 0;
13317
+ for (const vote of remove) {
13318
+ const optionId = vote.option_id;
13319
+ if (!optionId) continue;
13320
+ delete ownVotesByOptionId[optionId];
13321
+ const optionVoteCount = (voteCountsByOption[optionId] ?? 0) - 1;
13322
+ if (optionVoteCount > 0) {
13323
+ voteCountsByOption[optionId] = optionVoteCount;
13324
+ } else {
13325
+ delete voteCountsByOption[optionId];
13326
+ }
13327
+ voteCount = Math.max(voteCount - 1, 0);
13328
+ if (latestVotesByOption?.[optionId]) {
13329
+ latestVotesByOption[optionId] = latestVotesByOption[optionId].filter(
13330
+ (latestVote) => latestVote.id !== vote.id
13331
+ );
13332
+ }
13333
+ }
13334
+ for (const vote of add) {
13335
+ const optionId = vote.option_id;
13336
+ if (!optionId) continue;
13337
+ ownVotesByOptionId[optionId] = vote;
13338
+ voteCountsByOption[optionId] = (voteCountsByOption[optionId] ?? 0) + 1;
13339
+ voteCount += 1;
13340
+ if (latestVotesByOption && state.voting_visibility !== "anonymous") {
13341
+ latestVotesByOption[optionId] = [vote, ...latestVotesByOption[optionId] ?? []];
13342
+ }
13343
+ }
13344
+ return {
13345
+ latest_votes_by_option: latestVotesByOption,
13346
+ maxVotedOptionIds: getMaxVotedOptionIds(voteCountsByOption),
13347
+ ownVotesByOptionId,
13348
+ vote_count: voteCount,
13349
+ vote_counts_by_option: voteCountsByOption
13350
+ };
13351
+ }
13352
+ function getMaxVotedOptionIds(voteCountsByOption) {
13353
+ let maxVotes = 0;
13354
+ let winningOptions = [];
13355
+ for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
13356
+ if (count > maxVotes) {
13357
+ winningOptions = [id];
13358
+ maxVotes = count;
13359
+ } else if (count === maxVotes) {
13360
+ winningOptions.push(id);
13361
+ }
13362
+ }
13363
+ return winningOptions;
13364
+ }
13365
+
13133
13366
  // src/poll.ts
13134
13367
  var isPollUpdatedEvent = (e) => e.type === "poll.updated";
13135
13368
  var isPollClosedEventEvent = (e) => e.type === "poll.closed";
@@ -13139,6 +13372,9 @@ var isPollVoteRemovedEvent = (e) => e.type === "poll.vote_removed";
13139
13372
  var isVoteAnswer = (vote) => !!vote?.answer_text;
13140
13373
  var Poll = class {
13141
13374
  constructor({ client, poll }) {
13375
+ // Set whenever server data (WS event or rehydration) confirms that the poll is closed,
13376
+ // so that a failed optimistic close does not reopen a poll that is actually closed.
13377
+ this.closeConfirmed = false;
13142
13378
  this.getInitialStateFromPollResponse = (poll) => {
13143
13379
  const { own_votes, id, ...pollResponseForState } = poll;
13144
13380
  const { ownAnswer, ownVotes } = own_votes?.reduce(
@@ -13162,25 +13398,47 @@ var Poll = class {
13162
13398
  ownVotesByOptionId: getOwnVotesByOptionId(ownVotes)
13163
13399
  };
13164
13400
  };
13401
+ // persists only the server's votes, never the optimistic ones
13165
13402
  this.upsertOfflineDb = () => {
13403
+ const serverState = this.optimisticVotes.withServerVotes(this.data);
13166
13404
  this.client.offlineDb?.executeQuerySafely(
13167
- (db) => db.upsertPoll({ poll: mapPollStateToResponse(this) }),
13405
+ (db) => db.upsertPoll({ poll: mapPollStateToResponse(this, serverState) }),
13168
13406
  { method: "upsertPoll" }
13169
13407
  );
13170
13408
  };
13171
13409
  this.reinitializeState = (poll) => {
13172
- this.state.partialNext(this.getInitialStateFromPollResponse(poll));
13410
+ if (poll.is_closed) this.closeConfirmed = true;
13411
+ const initialState = this.getInitialStateFromPollResponse(poll);
13412
+ this.optimisticVotes.reset(initialState);
13413
+ this.state.partialNext(initialState);
13414
+ };
13415
+ // Hands the vote state of a WS vote event to the optimistic votes, which decide whether to show it.
13416
+ this.updateFromVoteEvent = (event, ownVotesByOptionId, answerState) => {
13417
+ const isOwnVote = event.poll_vote.user_id === this.client.userID;
13418
+ const shownVoteState = this.optimisticVotes.handleVoteEvent(event.poll, {
13419
+ confirmsOwnVote: isOwnVote && !isVoteAnswer(event.poll_vote) && !!event.poll_vote.option_id,
13420
+ ownVotesByOptionId
13421
+ });
13422
+ this.state.partialNext({
13423
+ ...answerState,
13424
+ answers_count: event.poll.answers_count,
13425
+ lastActivityAt: new Date(event.created_at),
13426
+ ...shownVoteState
13427
+ });
13428
+ this.upsertOfflineDb();
13173
13429
  };
13174
13430
  this.handlePollUpdated = (event) => {
13175
13431
  if (event.poll?.id && event.poll.id !== this.id) return;
13176
13432
  if (!isPollUpdatedEvent(event)) return;
13177
13433
  const { id, ...pollData } = extractPollData(event.poll);
13434
+ if (pollData.is_closed) this.closeConfirmed = true;
13178
13435
  this.state.partialNext({ ...pollData, lastActivityAt: new Date(event.created_at) });
13179
13436
  this.upsertOfflineDb();
13180
13437
  };
13181
13438
  this.handlePollClosed = (event) => {
13182
13439
  if (event.poll?.id && event.poll.id !== this.id) return;
13183
13440
  if (!isPollClosedEventEvent(event)) return;
13441
+ this.closeConfirmed = true;
13184
13442
  this.state.partialNext({
13185
13443
  is_closed: true,
13186
13444
  lastActivityAt: new Date(event.created_at)
@@ -13194,8 +13452,7 @@ var Poll = class {
13194
13452
  const isOwnVote = event.poll_vote.user_id === this.client.userID;
13195
13453
  let latestAnswers = [...currentState.latest_answers];
13196
13454
  let ownAnswer = currentState.ownAnswer;
13197
- const ownVotesByOptionId = currentState.ownVotesByOptionId;
13198
- let maxVotedOptionIds = currentState.maxVotedOptionIds;
13455
+ const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
13199
13456
  if (isOwnVote) {
13200
13457
  if (isVoteAnswer(event.poll_vote)) {
13201
13458
  ownAnswer = event.poll_vote;
@@ -13205,19 +13462,11 @@ var Poll = class {
13205
13462
  }
13206
13463
  if (isVoteAnswer(event.poll_vote)) {
13207
13464
  latestAnswers = [event.poll_vote, ...latestAnswers];
13208
- } else {
13209
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13210
13465
  }
13211
- const pollEnrichData = extractPollEnrichedData(event.poll);
13212
- this.state.partialNext({
13213
- ...pollEnrichData,
13466
+ this.updateFromVoteEvent(event, ownVotesByOptionId, {
13214
13467
  latest_answers: latestAnswers,
13215
- lastActivityAt: new Date(event.created_at),
13216
- ownAnswer,
13217
- ownVotesByOptionId,
13218
- maxVotedOptionIds
13468
+ ownAnswer
13219
13469
  });
13220
- this.upsertOfflineDb();
13221
13470
  };
13222
13471
  this.handleVoteChanged = (event) => {
13223
13472
  if (event.poll?.id && event.poll.id !== this.id) return;
@@ -13226,8 +13475,7 @@ var Poll = class {
13226
13475
  const isOwnVote = event.poll_vote.user_id === this.client.userID;
13227
13476
  let latestAnswers = [...currentState.latest_answers];
13228
13477
  let ownAnswer = currentState.ownAnswer;
13229
- let ownVotesByOptionId = currentState.ownVotesByOptionId;
13230
- let maxVotedOptionIds = currentState.maxVotedOptionIds;
13478
+ let ownVotesByOptionId = this.optimisticVotes.serverOwnVotes;
13231
13479
  if (isOwnVote) {
13232
13480
  if (isVoteAnswer(event.poll_vote)) {
13233
13481
  latestAnswers = [
@@ -13251,23 +13499,14 @@ var Poll = class {
13251
13499
  if (ownAnswer?.id === event.poll_vote.id) {
13252
13500
  ownAnswer = void 0;
13253
13501
  }
13254
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13255
13502
  }
13256
13503
  } else if (isVoteAnswer(event.poll_vote)) {
13257
13504
  latestAnswers = [event.poll_vote, ...latestAnswers];
13258
- } else {
13259
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13260
13505
  }
13261
- const pollEnrichData = extractPollEnrichedData(event.poll);
13262
- this.state.partialNext({
13263
- ...pollEnrichData,
13506
+ this.updateFromVoteEvent(event, ownVotesByOptionId, {
13264
13507
  latest_answers: latestAnswers,
13265
- lastActivityAt: new Date(event.created_at),
13266
- ownAnswer,
13267
- ownVotesByOptionId,
13268
- maxVotedOptionIds
13508
+ ownAnswer
13269
13509
  });
13270
- this.upsertOfflineDb();
13271
13510
  };
13272
13511
  this.handleVoteRemoved = (event) => {
13273
13512
  if (event.poll?.id && event.poll.id !== this.id) return;
@@ -13276,29 +13515,19 @@ var Poll = class {
13276
13515
  const isOwnVote = event.poll_vote.user_id === this.client.userID;
13277
13516
  let latestAnswers = [...currentState.latest_answers];
13278
13517
  let ownAnswer = currentState.ownAnswer;
13279
- const ownVotesByOptionId = { ...currentState.ownVotesByOptionId };
13280
- let maxVotedOptionIds = currentState.maxVotedOptionIds;
13518
+ const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
13281
13519
  if (isVoteAnswer(event.poll_vote)) {
13282
13520
  latestAnswers = latestAnswers.filter((answer) => answer.id !== event.poll_vote.id);
13283
13521
  if (isOwnVote) {
13284
13522
  ownAnswer = void 0;
13285
13523
  }
13286
- } else {
13287
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13288
- if (isOwnVote && event.poll_vote.option_id) {
13289
- delete ownVotesByOptionId[event.poll_vote.option_id];
13290
- }
13524
+ } else if (isOwnVote && event.poll_vote.option_id) {
13525
+ delete ownVotesByOptionId[event.poll_vote.option_id];
13291
13526
  }
13292
- const pollEnrichData = extractPollEnrichedData(event.poll);
13293
- this.state.partialNext({
13294
- ...pollEnrichData,
13527
+ this.updateFromVoteEvent(event, ownVotesByOptionId, {
13295
13528
  latest_answers: latestAnswers,
13296
- lastActivityAt: new Date(event.created_at),
13297
- ownAnswer,
13298
- ownVotesByOptionId,
13299
- maxVotedOptionIds
13529
+ ownAnswer
13300
13530
  });
13301
- this.upsertOfflineDb();
13302
13531
  };
13303
13532
  this.query = async (id) => {
13304
13533
  const { poll } = await this.client.getPoll(id);
@@ -13307,11 +13536,33 @@ var Poll = class {
13307
13536
  };
13308
13537
  this.update = async (data) => await this.client.updatePoll({ ...data, id: this.id });
13309
13538
  this.partialUpdate = async (partialPollObject) => await this.client.partialUpdatePoll(this.id, partialPollObject);
13310
- this.close = async () => await this.client.closePoll(this.id);
13539
+ /**
13540
+ * Closes the poll. The poll is marked as closed optimistically and the change is
13541
+ * reverted if the request fails, unless the server confirmed the close meanwhile.
13542
+ * Does nothing if the poll is already closed.
13543
+ */
13544
+ this.close = async () => {
13545
+ if (this.data.is_closed) return;
13546
+ this.closeConfirmed = false;
13547
+ this.state.partialNext({ is_closed: true });
13548
+ try {
13549
+ return await this.client.closePoll(this.id);
13550
+ } catch (error) {
13551
+ if (!this.closeConfirmed) {
13552
+ this.state.partialNext({ is_closed: false });
13553
+ }
13554
+ throw error;
13555
+ }
13556
+ };
13311
13557
  this.delete = async () => await this.client.deletePoll(this.id);
13312
13558
  this.createOption = async (option) => await this.client.createPollOption(this.id, option);
13313
13559
  this.updateOption = async (option) => await this.client.updatePollOption(this.id, option);
13314
13560
  this.deleteOption = async (optionId) => await this.client.deletePollOption(this.id, optionId);
13561
+ /**
13562
+ * Casts a vote for an option. The vote is shown right away through a local placeholder vote,
13563
+ * which the server's vote replaces once the WS events of all pending vote changes arrived.
13564
+ * If the request fails, the change is reverted.
13565
+ */
13315
13566
  this.castVote = async (optionId, messageId) => {
13316
13567
  const { max_votes_allowed, ownVotesByOptionId } = this.data;
13317
13568
  const reachedVoteLimit = max_votes_allowed && max_votes_allowed === Object.keys(ownVotesByOptionId).length;
@@ -13328,11 +13579,14 @@ var Poll = class {
13328
13579
  });
13329
13580
  return;
13330
13581
  }
13331
- return await this.client.castPollVote(messageId, this.id, {
13332
- option_id: optionId
13333
- });
13582
+ return await this.optimisticVotes.castVote(optionId, messageId);
13334
13583
  };
13335
- this.removeVote = async (voteId, messageId) => await this.client.removePollVote(messageId, this.id, voteId);
13584
+ /**
13585
+ * Removes a vote. The vote is removed right away and restored if the request fails. Vote
13586
+ * requests are sent in order, so removing a vote whose cast is not confirmed yet uses the vote
13587
+ * id from the cast's response; if that cast failed, there is nothing to remove.
13588
+ */
13589
+ this.removeVote = async (voteId, messageId) => await this.optimisticVotes.removeVote(voteId, messageId);
13336
13590
  this.addAnswer = async (answerText, messageId) => await this.client.addPollAnswer(messageId, this.id, answerText);
13337
13591
  this.removeAnswer = async (answerId, messageId) => await this.client.removePollVote(messageId, this.id, answerId);
13338
13592
  this.queryAnswers = async (params) => await this.client.queryPollAnswers(
@@ -13350,24 +13604,12 @@ var Poll = class {
13350
13604
  this.client = client;
13351
13605
  this.id = poll.id;
13352
13606
  this.state = new StateStore(this.getInitialStateFromPollResponse(poll));
13607
+ this.optimisticVotes = new OptimisticPollVotes({ client: this.client, poll: this });
13353
13608
  }
13354
13609
  get data() {
13355
13610
  return this.state.getLatestValue();
13356
13611
  }
13357
13612
  };
13358
- function getMaxVotedOptionIds(voteCountsByOption) {
13359
- let maxVotes = 0;
13360
- let winningOptions = [];
13361
- for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
13362
- if (count > maxVotes) {
13363
- winningOptions = [id];
13364
- maxVotes = count;
13365
- } else if (count === maxVotes) {
13366
- winningOptions.push(id);
13367
- }
13368
- }
13369
- return winningOptions;
13370
- }
13371
13613
  function getOwnVotesByOptionId(ownVotes) {
13372
13614
  return !ownVotes ? {} : ownVotes.reduce((acc, vote) => {
13373
13615
  if (isVoteAnswer(vote) || !vote.option_id) return acc;
@@ -13389,7 +13631,7 @@ function extractPollData(pollResponse) {
13389
13631
  voting_visibility: pollResponse.voting_visibility
13390
13632
  };
13391
13633
  }
13392
- function mapPollStateToResponse(poll) {
13634
+ function mapPollStateToResponse(poll, state = poll.data) {
13393
13635
  const {
13394
13636
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
13395
13637
  lastActivityAt,
@@ -13398,7 +13640,7 @@ function mapPollStateToResponse(poll) {
13398
13640
  ownVotesByOptionId,
13399
13641
  ownAnswer,
13400
13642
  ...restState
13401
- } = poll.data;
13643
+ } = state;
13402
13644
  const ownVotes = [
13403
13645
  ...Object.values(ownVotesByOptionId),
13404
13646
  ...ownAnswer ? [ownAnswer] : []
@@ -14556,6 +14798,8 @@ var StreamChat = class _StreamChat {
14556
14798
  * The main use case for SDKs who do independent state recovery for channels.
14557
14799
  */
14558
14800
  this.preventThreadCleanup = false;
14801
+ this._wsPromiseSettled = true;
14802
+ this._wsConnectId = 0;
14559
14803
  this.nextRequestAbortController = null;
14560
14804
  /**
14561
14805
  * @private
@@ -14601,17 +14845,20 @@ var StreamChat = class _StreamChat {
14601
14845
  const setTokenPromise = this._setToken(user, userTokenOrProvider);
14602
14846
  this._setUser(user);
14603
14847
  const wsPromise = this.openConnection();
14604
- this.setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
14848
+ const setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
14605
14849
  (result) => result[1]
14606
14850
  // We only return connection promise;
14607
14851
  );
14852
+ this.setUserPromise = setUserPromise;
14608
14853
  try {
14609
- return await this.setUserPromise;
14854
+ return await setUserPromise;
14610
14855
  } catch (err) {
14611
- if (this.persistUserOnConnectionFailure) {
14612
- this.closeConnection();
14613
- } else {
14614
- this.disconnectUser();
14856
+ if (this.setUserPromise === setUserPromise && this.userID === user.id) {
14857
+ if (this.persistUserOnConnectionFailure) {
14858
+ this.closeConnection();
14859
+ } else {
14860
+ this.disconnectUser();
14861
+ }
14615
14862
  }
14616
14863
  throw err;
14617
14864
  }
@@ -14707,9 +14954,50 @@ var StreamChat = class _StreamChat {
14707
14954
  return;
14708
14955
  }
14709
14956
  this.clientID = `${this.userID}--${randomId()}`;
14710
- this.wsPromise = this.connect();
14957
+ const wsPromise = this._bindWsPromise(this.connect());
14711
14958
  this._startCleaning();
14712
- return this.wsPromise;
14959
+ return wsPromise;
14960
+ };
14961
+ /**
14962
+ * Rejects the shared `wsPromise`, invalidates any in-flight connect attempt
14963
+ */
14964
+ this._rejectPendingWsPromise = (reason) => {
14965
+ if (!this._wsPromiseSettled) {
14966
+ this._wsConnectId += 1;
14967
+ this._wsPromiseSettled = true;
14968
+ this.wsPromise?.catch(() => {
14969
+ });
14970
+ this._rejectWsPromise?.(reason);
14971
+ }
14972
+ };
14973
+ /**
14974
+ * Keep a single pending `wsPromise` across close/reopen so callers that already
14975
+ * `await this.wsPromise` are not stranded when `openConnection` starts a new connect.
14976
+ */
14977
+ this._bindWsPromise = (connectPromise) => {
14978
+ const connectId = ++this._wsConnectId;
14979
+ let pending = this.wsPromise;
14980
+ if (this._wsPromiseSettled || !pending) {
14981
+ this._wsPromiseSettled = false;
14982
+ pending = new Promise((resolve, reject) => {
14983
+ this._resolveWsPromise = resolve;
14984
+ this._rejectWsPromise = reject;
14985
+ });
14986
+ this.wsPromise = pending;
14987
+ }
14988
+ connectPromise.then(
14989
+ (value) => {
14990
+ if (connectId !== this._wsConnectId) return;
14991
+ this._wsPromiseSettled = true;
14992
+ this._resolveWsPromise?.(value);
14993
+ },
14994
+ (error) => {
14995
+ if (connectId !== this._wsConnectId) return;
14996
+ this._wsPromiseSettled = true;
14997
+ this._rejectWsPromise?.(error);
14998
+ }
14999
+ );
15000
+ return pending;
14713
15001
  };
14714
15002
  /**
14715
15003
  * @deprecated Please use client.openConnction instead.
@@ -14743,6 +15031,10 @@ var StreamChat = class _StreamChat {
14743
15031
  delete this._user;
14744
15032
  delete this.userID;
14745
15033
  this.anonymous = false;
15034
+ this._rejectPendingWsPromise(
15035
+ new Error("Connection was closed because disconnectUser() was called")
15036
+ );
15037
+ this.wsPromise = null;
14746
15038
  const closePromise = this.closeConnection(timeout);
14747
15039
  for (const channel of Object.values(this.activeChannels)) {
14748
15040
  channel._disconnect();
@@ -15013,7 +15305,9 @@ var StreamChat = class _StreamChat {
15013
15305
  type: "connection.recovered"
15014
15306
  });
15015
15307
  }
15016
- this.wsPromise = Promise.resolve();
15308
+ if (this._wsPromiseSettled) {
15309
+ this.wsPromise = Promise.resolve();
15310
+ }
15017
15311
  this.setUserPromise = Promise.resolve();
15018
15312
  };
15019
15313
  /**
@@ -15131,7 +15425,7 @@ var StreamChat = class _StreamChat {
15131
15425
  return this.userAgent;
15132
15426
  }
15133
15427
  if (!this.cachedUserAgent) {
15134
- const version = "9.52.1";
15428
+ const version = "9.54.0";
15135
15429
  const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
15136
15430
  const { name: appName, version: appVersion } = this.appIdentifier ?? {};
15137
15431
  const { os, model: deviceModel } = this.deviceIdentifier ?? {};
@@ -18373,6 +18667,10 @@ var StreamChat = class _StreamChat {
18373
18667
  /**
18374
18668
  * Update Channels Batch
18375
18669
  *
18670
+ * For the `updateData` operation, `data.custom` replaces the channel's whole
18671
+ * custom object, while the root-level `custom_set` / `custom_unset` patch
18672
+ * individual keys and leave the rest untouched.
18673
+ *
18376
18674
  * @param {UpdateChannelsBatchOptions} payload for updating channels in batch
18377
18675
  * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
18378
18676
  */