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.
@@ -382,6 +382,7 @@ var Campaign = class {
382
382
  };
383
383
 
384
384
  // src/channel_batch_updater.ts
385
+ var isChannelBatchDataUpdateOptions = (update) => "data" in update || "custom_set" in update || "custom_unset" in update;
385
386
  var ChannelBatchUpdater = class {
386
387
  constructor(client) {
387
388
  this.client = client;
@@ -520,19 +521,14 @@ var ChannelBatchUpdater = class {
520
521
  filter
521
522
  });
522
523
  }
523
- // Data operations
524
- /**
525
- * updateData - Update data on channels matching the filter
526
- *
527
- * @param {UpdateChannelsBatchFilters} filter Filter to select channels
528
- * @param {BatchChannelDataUpdate} data Data to update
529
- * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
530
- */
531
- async updateData(filter, data) {
524
+ async updateData(filter, update) {
525
+ const { data, custom_set, custom_unset } = isChannelBatchDataUpdateOptions(update) ? update : { data: update };
532
526
  return await this.client.updateChannelsBatch({
533
527
  operation: "updateData",
534
528
  filter,
535
- data
529
+ ...data && { data },
530
+ ...custom_set && { custom_set },
531
+ ...custom_unset && { custom_unset }
536
532
  });
537
533
  }
538
534
  };
@@ -5914,10 +5910,11 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
5914
5910
  const linkPreviews = linkPreviewsManager.loadingPreviews.length > 0 ? [] : linkPreviewsManager.loadedPreviews.map(
5915
5911
  (preview) => LinkPreviewsManager.getPreviewData(preview)
5916
5912
  );
5917
- const attachments = (state.message.attachments ?? []).concat(
5913
+ const messageAttachments = (state.message.attachments ?? []).concat(
5918
5914
  linkPreviews
5919
5915
  );
5920
- if (!attachments.length) return forward();
5916
+ const localAttachments = (state.localMessage.attachments ?? []).concat(linkPreviews);
5917
+ if (!messageAttachments.length && !localAttachments.length) return forward();
5921
5918
  const sendOptions = { ...state.sendOptions };
5922
5919
  const skip_enrich_url = !someLinkPreviewsLoading && linkPreviews.length > 0 || someLinkPreviewsDismissed;
5923
5920
  if (skip_enrich_url) {
@@ -5925,14 +5922,14 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
5925
5922
  }
5926
5923
  return next({
5927
5924
  ...state,
5928
- message: {
5925
+ message: messageAttachments.length ? {
5929
5926
  ...state.message,
5930
- attachments
5931
- },
5932
- localMessage: {
5927
+ attachments: messageAttachments
5928
+ } : state.message,
5929
+ localMessage: localAttachments.length ? {
5933
5930
  ...state.localMessage,
5934
- attachments
5935
- },
5931
+ attachments: localAttachments
5932
+ } : state.localMessage,
5936
5933
  sendOptions
5937
5934
  });
5938
5935
  }
@@ -13161,6 +13158,242 @@ var ThreadManager = class extends WithSubscriptions {
13161
13158
  }
13162
13159
  };
13163
13160
 
13161
+ // src/poll_optimistic_votes.ts
13162
+ var CATCH_UP_TIMEOUT_MS = 3e3;
13163
+ var OptimisticPollVotes = class {
13164
+ constructor({ client, poll }) {
13165
+ // Own vote changes not confirmed by a WS event yet
13166
+ this.pendingCount = 0;
13167
+ // Own vote changes whose requests succeeded since the server's vote state was last shown
13168
+ this.succeededChanges = [];
13169
+ // Vote requests with this tag are sent one after another. A symbol, so it is unique per instance.
13170
+ this.requestTag = /* @__PURE__ */ Symbol("poll-votes");
13171
+ this.withServerVotes = (pollState) => ({
13172
+ ...pollState,
13173
+ ...this.serverVoteState
13174
+ });
13175
+ /**
13176
+ * Casts a vote for an option, shown right away through a placeholder vote. In polls with unique
13177
+ * votes, it replaces the existing own votes.
13178
+ */
13179
+ this.castVote = (optionId, messageId) => {
13180
+ const sendRequest = () => this.client.castPollVote(messageId, this.poll.id, { option_id: optionId });
13181
+ const state = this.poll.data;
13182
+ if (state.ownVotesByOptionId[optionId]) return this.send(sendRequest);
13183
+ const replacedVotes = state.enforce_unique_vote ? Object.values(state.ownVotesByOptionId) : [];
13184
+ this.poll.state.partialNext(
13185
+ applyOwnVoteDelta(state, {
13186
+ add: [this.createPlaceholderVote(optionId)],
13187
+ remove: replacedVotes
13188
+ })
13189
+ );
13190
+ return this.track(sendRequest, (response) => ({
13191
+ optionId,
13192
+ vote: response.vote
13193
+ }));
13194
+ };
13195
+ // Removes a vote, right away.
13196
+ this.removeVote = (voteId, messageId) => {
13197
+ const sendRequest = (id) => this.client.removePollVote(messageId, this.poll.id, id);
13198
+ const state = this.poll.data;
13199
+ const vote = Object.values(state.ownVotesByOptionId).find(({ id }) => id === voteId);
13200
+ if (!vote?.option_id) return this.send(() => sendRequest(voteId));
13201
+ const optionId = vote.option_id;
13202
+ this.poll.state.partialNext(applyOwnVoteDelta(state, { remove: [vote] }));
13203
+ return this.track(
13204
+ // runs when the removal's turn comes, so a preceding cast of the vote has finished
13205
+ () => {
13206
+ const serverVoteId = this.getServerVoteId(vote);
13207
+ return serverVoteId ? sendRequest(serverVoteId) : Promise.resolve(void 0);
13208
+ },
13209
+ () => ({ optionId })
13210
+ );
13211
+ };
13212
+ // Sends a vote request once the previous one has finished, whether it succeeded or not.
13213
+ this.send = async (sendRequest) => {
13214
+ try {
13215
+ return await withoutConcurrency(this.requestTag, sendRequest);
13216
+ } finally {
13217
+ if (!hasPending(this.requestTag)) this.scheduleCatchUp();
13218
+ }
13219
+ };
13220
+ // Stores the vote state of a WS vote event and returns the vote fields to show, if any.
13221
+ this.handleVoteEvent = (poll, {
13222
+ confirmsOwnVote,
13223
+ ownVotesByOptionId
13224
+ }) => {
13225
+ this.serverVoteState = {
13226
+ latest_votes_by_option: poll.latest_votes_by_option,
13227
+ ownVotesByOptionId,
13228
+ vote_count: poll.vote_count,
13229
+ vote_counts_by_option: poll.vote_counts_by_option
13230
+ };
13231
+ if (confirmsOwnVote) this.pendingCount = Math.max(this.pendingCount - 1, 0);
13232
+ return this.pendingCount > 0 ? {} : this.takeServerVoteState();
13233
+ };
13234
+ // Fresh server state (e.g. a refetch) replaces everything, pending changes are dropped.
13235
+ this.reset = (pollState) => {
13236
+ this.serverVoteState = pickVoteState(pollState);
13237
+ this.pendingCount = 0;
13238
+ this.succeededChanges = [];
13239
+ clearTimeout(this.catchUpTimeout);
13240
+ };
13241
+ // Tracks an own vote change until its request settles. `sendRequest` resolves to undefined
13242
+ // when no request was needed.
13243
+ this.track = async (sendRequest, toChange) => {
13244
+ this.pendingCount += 1;
13245
+ clearTimeout(this.catchUpTimeout);
13246
+ let response;
13247
+ try {
13248
+ response = await this.send(async () => {
13249
+ const result = await sendRequest();
13250
+ if (result !== void 0) this.succeededChanges.push(toChange(result));
13251
+ return result;
13252
+ });
13253
+ } catch (error) {
13254
+ this.settleWithoutEvent();
13255
+ throw error;
13256
+ }
13257
+ if (response === void 0) this.settleWithoutEvent();
13258
+ return response;
13259
+ };
13260
+ // A failed or skipped request gets no WS event, so it counts as confirmed right away.
13261
+ this.settleWithoutEvent = () => {
13262
+ this.pendingCount = Math.max(this.pendingCount - 1, 0);
13263
+ if (this.pendingCount === 0) this.poll.state.partialNext(this.takeServerVoteState());
13264
+ };
13265
+ // The vote shown for a cast until the server's vote arrives
13266
+ this.createPlaceholderVote = (optionId) => {
13267
+ const now = (/* @__PURE__ */ new Date()).toISOString();
13268
+ return {
13269
+ created_at: now,
13270
+ id: `local-${generateUUIDv4()}`,
13271
+ option_id: optionId,
13272
+ poll_id: this.poll.id,
13273
+ updated_at: now,
13274
+ user: this.client.user,
13275
+ user_id: this.client.userID
13276
+ };
13277
+ };
13278
+ /**
13279
+ * The server id of a vote to remove, once everything sent before the removal has finished. Casts
13280
+ * and removals of an option alternate, so the option's last succeeded change tells: a cast is the
13281
+ * vote being removed (e.g. a placeholder), a removal means the cast in between failed. Without
13282
+ * one, the server's own vote of the option is the vote being removed.
13283
+ */
13284
+ this.getServerVoteId = (vote) => {
13285
+ for (let i = this.succeededChanges.length - 1; i >= 0; i--) {
13286
+ const change = this.succeededChanges[i];
13287
+ if (change.optionId === vote.option_id) return change.vote?.id;
13288
+ }
13289
+ return this.serverVoteState.ownVotesByOptionId[vote.option_id]?.id;
13290
+ };
13291
+ this.takeServerVoteState = () => {
13292
+ clearTimeout(this.catchUpTimeout);
13293
+ this.succeededChanges = [];
13294
+ return {
13295
+ ...this.serverVoteState,
13296
+ maxVotedOptionIds: getMaxVotedOptionIds(this.serverVoteState.vote_counts_by_option)
13297
+ };
13298
+ };
13299
+ this.scheduleCatchUp = () => {
13300
+ clearTimeout(this.catchUpTimeout);
13301
+ if (this.pendingCount === 0) return;
13302
+ this.catchUpTimeout = setTimeout(this.applySucceededChanges, CATCH_UP_TIMEOUT_MS);
13303
+ };
13304
+ /**
13305
+ * A WS event of an own vote change got lost, so the server's own votes are stale. All requests
13306
+ * have finished (the timeout starts once nothing is left to send and a new request cancels it),
13307
+ * so applying the succeeded changes in order gives the server's own votes (changes whose events
13308
+ * did arrive end up the same). They are shown with the vote counts of the last WS event, which
13309
+ * contain the other users' votes too.
13310
+ */
13311
+ this.applySucceededChanges = () => {
13312
+ const { enforce_unique_vote } = this.poll.data;
13313
+ let ownVotesByOptionId = { ...this.serverVoteState.ownVotesByOptionId };
13314
+ for (const { optionId, vote } of this.succeededChanges) {
13315
+ if (!vote) {
13316
+ delete ownVotesByOptionId[optionId];
13317
+ } else if (enforce_unique_vote) {
13318
+ ownVotesByOptionId = { [optionId]: vote };
13319
+ } else {
13320
+ ownVotesByOptionId[optionId] = vote;
13321
+ }
13322
+ }
13323
+ this.serverVoteState = { ...this.serverVoteState, ownVotesByOptionId };
13324
+ this.pendingCount = 0;
13325
+ this.poll.state.partialNext(this.takeServerVoteState());
13326
+ };
13327
+ this.client = client;
13328
+ this.poll = poll;
13329
+ this.serverVoteState = pickVoteState(poll.data);
13330
+ }
13331
+ get serverOwnVotes() {
13332
+ return this.serverVoteState.ownVotesByOptionId;
13333
+ }
13334
+ };
13335
+ function pickVoteState(state) {
13336
+ return {
13337
+ latest_votes_by_option: state.latest_votes_by_option,
13338
+ ownVotesByOptionId: state.ownVotesByOptionId,
13339
+ vote_count: state.vote_count,
13340
+ vote_counts_by_option: state.vote_counts_by_option
13341
+ };
13342
+ }
13343
+ function applyOwnVoteDelta(state, { add = [], remove = [] }) {
13344
+ const ownVotesByOptionId = { ...state.ownVotesByOptionId };
13345
+ const voteCountsByOption = { ...state.vote_counts_by_option };
13346
+ const latestVotesByOption = state.latest_votes_by_option ? { ...state.latest_votes_by_option } : state.latest_votes_by_option;
13347
+ let voteCount = state.vote_count ?? 0;
13348
+ for (const vote of remove) {
13349
+ const optionId = vote.option_id;
13350
+ if (!optionId) continue;
13351
+ delete ownVotesByOptionId[optionId];
13352
+ const optionVoteCount = (voteCountsByOption[optionId] ?? 0) - 1;
13353
+ if (optionVoteCount > 0) {
13354
+ voteCountsByOption[optionId] = optionVoteCount;
13355
+ } else {
13356
+ delete voteCountsByOption[optionId];
13357
+ }
13358
+ voteCount = Math.max(voteCount - 1, 0);
13359
+ if (latestVotesByOption?.[optionId]) {
13360
+ latestVotesByOption[optionId] = latestVotesByOption[optionId].filter(
13361
+ (latestVote) => latestVote.id !== vote.id
13362
+ );
13363
+ }
13364
+ }
13365
+ for (const vote of add) {
13366
+ const optionId = vote.option_id;
13367
+ if (!optionId) continue;
13368
+ ownVotesByOptionId[optionId] = vote;
13369
+ voteCountsByOption[optionId] = (voteCountsByOption[optionId] ?? 0) + 1;
13370
+ voteCount += 1;
13371
+ if (latestVotesByOption && state.voting_visibility !== "anonymous") {
13372
+ latestVotesByOption[optionId] = [vote, ...latestVotesByOption[optionId] ?? []];
13373
+ }
13374
+ }
13375
+ return {
13376
+ latest_votes_by_option: latestVotesByOption,
13377
+ maxVotedOptionIds: getMaxVotedOptionIds(voteCountsByOption),
13378
+ ownVotesByOptionId,
13379
+ vote_count: voteCount,
13380
+ vote_counts_by_option: voteCountsByOption
13381
+ };
13382
+ }
13383
+ function getMaxVotedOptionIds(voteCountsByOption) {
13384
+ let maxVotes = 0;
13385
+ let winningOptions = [];
13386
+ for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
13387
+ if (count > maxVotes) {
13388
+ winningOptions = [id];
13389
+ maxVotes = count;
13390
+ } else if (count === maxVotes) {
13391
+ winningOptions.push(id);
13392
+ }
13393
+ }
13394
+ return winningOptions;
13395
+ }
13396
+
13164
13397
  // src/poll.ts
13165
13398
  var isPollUpdatedEvent = (e) => e.type === "poll.updated";
13166
13399
  var isPollClosedEventEvent = (e) => e.type === "poll.closed";
@@ -13170,6 +13403,9 @@ var isPollVoteRemovedEvent = (e) => e.type === "poll.vote_removed";
13170
13403
  var isVoteAnswer = (vote) => !!vote?.answer_text;
13171
13404
  var Poll = class {
13172
13405
  constructor({ client, poll }) {
13406
+ // Set whenever server data (WS event or rehydration) confirms that the poll is closed,
13407
+ // so that a failed optimistic close does not reopen a poll that is actually closed.
13408
+ this.closeConfirmed = false;
13173
13409
  this.getInitialStateFromPollResponse = (poll) => {
13174
13410
  const { own_votes, id, ...pollResponseForState } = poll;
13175
13411
  const { ownAnswer, ownVotes } = own_votes?.reduce(
@@ -13193,25 +13429,47 @@ var Poll = class {
13193
13429
  ownVotesByOptionId: getOwnVotesByOptionId(ownVotes)
13194
13430
  };
13195
13431
  };
13432
+ // persists only the server's votes, never the optimistic ones
13196
13433
  this.upsertOfflineDb = () => {
13434
+ const serverState = this.optimisticVotes.withServerVotes(this.data);
13197
13435
  this.client.offlineDb?.executeQuerySafely(
13198
- (db) => db.upsertPoll({ poll: mapPollStateToResponse(this) }),
13436
+ (db) => db.upsertPoll({ poll: mapPollStateToResponse(this, serverState) }),
13199
13437
  { method: "upsertPoll" }
13200
13438
  );
13201
13439
  };
13202
13440
  this.reinitializeState = (poll) => {
13203
- this.state.partialNext(this.getInitialStateFromPollResponse(poll));
13441
+ if (poll.is_closed) this.closeConfirmed = true;
13442
+ const initialState = this.getInitialStateFromPollResponse(poll);
13443
+ this.optimisticVotes.reset(initialState);
13444
+ this.state.partialNext(initialState);
13445
+ };
13446
+ // Hands the vote state of a WS vote event to the optimistic votes, which decide whether to show it.
13447
+ this.updateFromVoteEvent = (event, ownVotesByOptionId, answerState) => {
13448
+ const isOwnVote = event.poll_vote.user_id === this.client.userID;
13449
+ const shownVoteState = this.optimisticVotes.handleVoteEvent(event.poll, {
13450
+ confirmsOwnVote: isOwnVote && !isVoteAnswer(event.poll_vote) && !!event.poll_vote.option_id,
13451
+ ownVotesByOptionId
13452
+ });
13453
+ this.state.partialNext({
13454
+ ...answerState,
13455
+ answers_count: event.poll.answers_count,
13456
+ lastActivityAt: new Date(event.created_at),
13457
+ ...shownVoteState
13458
+ });
13459
+ this.upsertOfflineDb();
13204
13460
  };
13205
13461
  this.handlePollUpdated = (event) => {
13206
13462
  if (event.poll?.id && event.poll.id !== this.id) return;
13207
13463
  if (!isPollUpdatedEvent(event)) return;
13208
13464
  const { id, ...pollData } = extractPollData(event.poll);
13465
+ if (pollData.is_closed) this.closeConfirmed = true;
13209
13466
  this.state.partialNext({ ...pollData, lastActivityAt: new Date(event.created_at) });
13210
13467
  this.upsertOfflineDb();
13211
13468
  };
13212
13469
  this.handlePollClosed = (event) => {
13213
13470
  if (event.poll?.id && event.poll.id !== this.id) return;
13214
13471
  if (!isPollClosedEventEvent(event)) return;
13472
+ this.closeConfirmed = true;
13215
13473
  this.state.partialNext({
13216
13474
  is_closed: true,
13217
13475
  lastActivityAt: new Date(event.created_at)
@@ -13225,8 +13483,7 @@ var Poll = class {
13225
13483
  const isOwnVote = event.poll_vote.user_id === this.client.userID;
13226
13484
  let latestAnswers = [...currentState.latest_answers];
13227
13485
  let ownAnswer = currentState.ownAnswer;
13228
- const ownVotesByOptionId = currentState.ownVotesByOptionId;
13229
- let maxVotedOptionIds = currentState.maxVotedOptionIds;
13486
+ const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
13230
13487
  if (isOwnVote) {
13231
13488
  if (isVoteAnswer(event.poll_vote)) {
13232
13489
  ownAnswer = event.poll_vote;
@@ -13236,19 +13493,11 @@ var Poll = class {
13236
13493
  }
13237
13494
  if (isVoteAnswer(event.poll_vote)) {
13238
13495
  latestAnswers = [event.poll_vote, ...latestAnswers];
13239
- } else {
13240
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13241
13496
  }
13242
- const pollEnrichData = extractPollEnrichedData(event.poll);
13243
- this.state.partialNext({
13244
- ...pollEnrichData,
13497
+ this.updateFromVoteEvent(event, ownVotesByOptionId, {
13245
13498
  latest_answers: latestAnswers,
13246
- lastActivityAt: new Date(event.created_at),
13247
- ownAnswer,
13248
- ownVotesByOptionId,
13249
- maxVotedOptionIds
13499
+ ownAnswer
13250
13500
  });
13251
- this.upsertOfflineDb();
13252
13501
  };
13253
13502
  this.handleVoteChanged = (event) => {
13254
13503
  if (event.poll?.id && event.poll.id !== this.id) return;
@@ -13257,8 +13506,7 @@ var Poll = class {
13257
13506
  const isOwnVote = event.poll_vote.user_id === this.client.userID;
13258
13507
  let latestAnswers = [...currentState.latest_answers];
13259
13508
  let ownAnswer = currentState.ownAnswer;
13260
- let ownVotesByOptionId = currentState.ownVotesByOptionId;
13261
- let maxVotedOptionIds = currentState.maxVotedOptionIds;
13509
+ let ownVotesByOptionId = this.optimisticVotes.serverOwnVotes;
13262
13510
  if (isOwnVote) {
13263
13511
  if (isVoteAnswer(event.poll_vote)) {
13264
13512
  latestAnswers = [
@@ -13282,23 +13530,14 @@ var Poll = class {
13282
13530
  if (ownAnswer?.id === event.poll_vote.id) {
13283
13531
  ownAnswer = void 0;
13284
13532
  }
13285
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13286
13533
  }
13287
13534
  } else if (isVoteAnswer(event.poll_vote)) {
13288
13535
  latestAnswers = [event.poll_vote, ...latestAnswers];
13289
- } else {
13290
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13291
13536
  }
13292
- const pollEnrichData = extractPollEnrichedData(event.poll);
13293
- this.state.partialNext({
13294
- ...pollEnrichData,
13537
+ this.updateFromVoteEvent(event, ownVotesByOptionId, {
13295
13538
  latest_answers: latestAnswers,
13296
- lastActivityAt: new Date(event.created_at),
13297
- ownAnswer,
13298
- ownVotesByOptionId,
13299
- maxVotedOptionIds
13539
+ ownAnswer
13300
13540
  });
13301
- this.upsertOfflineDb();
13302
13541
  };
13303
13542
  this.handleVoteRemoved = (event) => {
13304
13543
  if (event.poll?.id && event.poll.id !== this.id) return;
@@ -13307,29 +13546,19 @@ var Poll = class {
13307
13546
  const isOwnVote = event.poll_vote.user_id === this.client.userID;
13308
13547
  let latestAnswers = [...currentState.latest_answers];
13309
13548
  let ownAnswer = currentState.ownAnswer;
13310
- const ownVotesByOptionId = { ...currentState.ownVotesByOptionId };
13311
- let maxVotedOptionIds = currentState.maxVotedOptionIds;
13549
+ const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
13312
13550
  if (isVoteAnswer(event.poll_vote)) {
13313
13551
  latestAnswers = latestAnswers.filter((answer) => answer.id !== event.poll_vote.id);
13314
13552
  if (isOwnVote) {
13315
13553
  ownAnswer = void 0;
13316
13554
  }
13317
- } else {
13318
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13319
- if (isOwnVote && event.poll_vote.option_id) {
13320
- delete ownVotesByOptionId[event.poll_vote.option_id];
13321
- }
13555
+ } else if (isOwnVote && event.poll_vote.option_id) {
13556
+ delete ownVotesByOptionId[event.poll_vote.option_id];
13322
13557
  }
13323
- const pollEnrichData = extractPollEnrichedData(event.poll);
13324
- this.state.partialNext({
13325
- ...pollEnrichData,
13558
+ this.updateFromVoteEvent(event, ownVotesByOptionId, {
13326
13559
  latest_answers: latestAnswers,
13327
- lastActivityAt: new Date(event.created_at),
13328
- ownAnswer,
13329
- ownVotesByOptionId,
13330
- maxVotedOptionIds
13560
+ ownAnswer
13331
13561
  });
13332
- this.upsertOfflineDb();
13333
13562
  };
13334
13563
  this.query = async (id) => {
13335
13564
  const { poll } = await this.client.getPoll(id);
@@ -13338,11 +13567,33 @@ var Poll = class {
13338
13567
  };
13339
13568
  this.update = async (data) => await this.client.updatePoll({ ...data, id: this.id });
13340
13569
  this.partialUpdate = async (partialPollObject) => await this.client.partialUpdatePoll(this.id, partialPollObject);
13341
- this.close = async () => await this.client.closePoll(this.id);
13570
+ /**
13571
+ * Closes the poll. The poll is marked as closed optimistically and the change is
13572
+ * reverted if the request fails, unless the server confirmed the close meanwhile.
13573
+ * Does nothing if the poll is already closed.
13574
+ */
13575
+ this.close = async () => {
13576
+ if (this.data.is_closed) return;
13577
+ this.closeConfirmed = false;
13578
+ this.state.partialNext({ is_closed: true });
13579
+ try {
13580
+ return await this.client.closePoll(this.id);
13581
+ } catch (error) {
13582
+ if (!this.closeConfirmed) {
13583
+ this.state.partialNext({ is_closed: false });
13584
+ }
13585
+ throw error;
13586
+ }
13587
+ };
13342
13588
  this.delete = async () => await this.client.deletePoll(this.id);
13343
13589
  this.createOption = async (option) => await this.client.createPollOption(this.id, option);
13344
13590
  this.updateOption = async (option) => await this.client.updatePollOption(this.id, option);
13345
13591
  this.deleteOption = async (optionId) => await this.client.deletePollOption(this.id, optionId);
13592
+ /**
13593
+ * Casts a vote for an option. The vote is shown right away through a local placeholder vote,
13594
+ * which the server's vote replaces once the WS events of all pending vote changes arrived.
13595
+ * If the request fails, the change is reverted.
13596
+ */
13346
13597
  this.castVote = async (optionId, messageId) => {
13347
13598
  const { max_votes_allowed, ownVotesByOptionId } = this.data;
13348
13599
  const reachedVoteLimit = max_votes_allowed && max_votes_allowed === Object.keys(ownVotesByOptionId).length;
@@ -13359,11 +13610,14 @@ var Poll = class {
13359
13610
  });
13360
13611
  return;
13361
13612
  }
13362
- return await this.client.castPollVote(messageId, this.id, {
13363
- option_id: optionId
13364
- });
13613
+ return await this.optimisticVotes.castVote(optionId, messageId);
13365
13614
  };
13366
- this.removeVote = async (voteId, messageId) => await this.client.removePollVote(messageId, this.id, voteId);
13615
+ /**
13616
+ * Removes a vote. The vote is removed right away and restored if the request fails. Vote
13617
+ * requests are sent in order, so removing a vote whose cast is not confirmed yet uses the vote
13618
+ * id from the cast's response; if that cast failed, there is nothing to remove.
13619
+ */
13620
+ this.removeVote = async (voteId, messageId) => await this.optimisticVotes.removeVote(voteId, messageId);
13367
13621
  this.addAnswer = async (answerText, messageId) => await this.client.addPollAnswer(messageId, this.id, answerText);
13368
13622
  this.removeAnswer = async (answerId, messageId) => await this.client.removePollVote(messageId, this.id, answerId);
13369
13623
  this.queryAnswers = async (params) => await this.client.queryPollAnswers(
@@ -13381,24 +13635,12 @@ var Poll = class {
13381
13635
  this.client = client;
13382
13636
  this.id = poll.id;
13383
13637
  this.state = new StateStore(this.getInitialStateFromPollResponse(poll));
13638
+ this.optimisticVotes = new OptimisticPollVotes({ client: this.client, poll: this });
13384
13639
  }
13385
13640
  get data() {
13386
13641
  return this.state.getLatestValue();
13387
13642
  }
13388
13643
  };
13389
- function getMaxVotedOptionIds(voteCountsByOption) {
13390
- let maxVotes = 0;
13391
- let winningOptions = [];
13392
- for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
13393
- if (count > maxVotes) {
13394
- winningOptions = [id];
13395
- maxVotes = count;
13396
- } else if (count === maxVotes) {
13397
- winningOptions.push(id);
13398
- }
13399
- }
13400
- return winningOptions;
13401
- }
13402
13644
  function getOwnVotesByOptionId(ownVotes) {
13403
13645
  return !ownVotes ? {} : ownVotes.reduce((acc, vote) => {
13404
13646
  if (isVoteAnswer(vote) || !vote.option_id) return acc;
@@ -13420,7 +13662,7 @@ function extractPollData(pollResponse) {
13420
13662
  voting_visibility: pollResponse.voting_visibility
13421
13663
  };
13422
13664
  }
13423
- function mapPollStateToResponse(poll) {
13665
+ function mapPollStateToResponse(poll, state = poll.data) {
13424
13666
  const {
13425
13667
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
13426
13668
  lastActivityAt,
@@ -13429,7 +13671,7 @@ function mapPollStateToResponse(poll) {
13429
13671
  ownVotesByOptionId,
13430
13672
  ownAnswer,
13431
13673
  ...restState
13432
- } = poll.data;
13674
+ } = state;
13433
13675
  const ownVotes = [
13434
13676
  ...Object.values(ownVotesByOptionId),
13435
13677
  ...ownAnswer ? [ownAnswer] : []
@@ -14587,6 +14829,8 @@ var StreamChat = class _StreamChat {
14587
14829
  * The main use case for SDKs who do independent state recovery for channels.
14588
14830
  */
14589
14831
  this.preventThreadCleanup = false;
14832
+ this._wsPromiseSettled = true;
14833
+ this._wsConnectId = 0;
14590
14834
  this.nextRequestAbortController = null;
14591
14835
  /**
14592
14836
  * @private
@@ -14632,17 +14876,20 @@ var StreamChat = class _StreamChat {
14632
14876
  const setTokenPromise = this._setToken(user, userTokenOrProvider);
14633
14877
  this._setUser(user);
14634
14878
  const wsPromise = this.openConnection();
14635
- this.setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
14879
+ const setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
14636
14880
  (result) => result[1]
14637
14881
  // We only return connection promise;
14638
14882
  );
14883
+ this.setUserPromise = setUserPromise;
14639
14884
  try {
14640
- return await this.setUserPromise;
14885
+ return await setUserPromise;
14641
14886
  } catch (err) {
14642
- if (this.persistUserOnConnectionFailure) {
14643
- this.closeConnection();
14644
- } else {
14645
- this.disconnectUser();
14887
+ if (this.setUserPromise === setUserPromise && this.userID === user.id) {
14888
+ if (this.persistUserOnConnectionFailure) {
14889
+ this.closeConnection();
14890
+ } else {
14891
+ this.disconnectUser();
14892
+ }
14646
14893
  }
14647
14894
  throw err;
14648
14895
  }
@@ -14738,9 +14985,50 @@ var StreamChat = class _StreamChat {
14738
14985
  return;
14739
14986
  }
14740
14987
  this.clientID = `${this.userID}--${randomId()}`;
14741
- this.wsPromise = this.connect();
14988
+ const wsPromise = this._bindWsPromise(this.connect());
14742
14989
  this._startCleaning();
14743
- return this.wsPromise;
14990
+ return wsPromise;
14991
+ };
14992
+ /**
14993
+ * Rejects the shared `wsPromise`, invalidates any in-flight connect attempt
14994
+ */
14995
+ this._rejectPendingWsPromise = (reason) => {
14996
+ if (!this._wsPromiseSettled) {
14997
+ this._wsConnectId += 1;
14998
+ this._wsPromiseSettled = true;
14999
+ this.wsPromise?.catch(() => {
15000
+ });
15001
+ this._rejectWsPromise?.(reason);
15002
+ }
15003
+ };
15004
+ /**
15005
+ * Keep a single pending `wsPromise` across close/reopen so callers that already
15006
+ * `await this.wsPromise` are not stranded when `openConnection` starts a new connect.
15007
+ */
15008
+ this._bindWsPromise = (connectPromise) => {
15009
+ const connectId = ++this._wsConnectId;
15010
+ let pending = this.wsPromise;
15011
+ if (this._wsPromiseSettled || !pending) {
15012
+ this._wsPromiseSettled = false;
15013
+ pending = new Promise((resolve, reject) => {
15014
+ this._resolveWsPromise = resolve;
15015
+ this._rejectWsPromise = reject;
15016
+ });
15017
+ this.wsPromise = pending;
15018
+ }
15019
+ connectPromise.then(
15020
+ (value) => {
15021
+ if (connectId !== this._wsConnectId) return;
15022
+ this._wsPromiseSettled = true;
15023
+ this._resolveWsPromise?.(value);
15024
+ },
15025
+ (error) => {
15026
+ if (connectId !== this._wsConnectId) return;
15027
+ this._wsPromiseSettled = true;
15028
+ this._rejectWsPromise?.(error);
15029
+ }
15030
+ );
15031
+ return pending;
14744
15032
  };
14745
15033
  /**
14746
15034
  * @deprecated Please use client.openConnction instead.
@@ -14774,6 +15062,10 @@ var StreamChat = class _StreamChat {
14774
15062
  delete this._user;
14775
15063
  delete this.userID;
14776
15064
  this.anonymous = false;
15065
+ this._rejectPendingWsPromise(
15066
+ new Error("Connection was closed because disconnectUser() was called")
15067
+ );
15068
+ this.wsPromise = null;
14777
15069
  const closePromise = this.closeConnection(timeout);
14778
15070
  for (const channel of Object.values(this.activeChannels)) {
14779
15071
  channel._disconnect();
@@ -15044,7 +15336,9 @@ var StreamChat = class _StreamChat {
15044
15336
  type: "connection.recovered"
15045
15337
  });
15046
15338
  }
15047
- this.wsPromise = Promise.resolve();
15339
+ if (this._wsPromiseSettled) {
15340
+ this.wsPromise = Promise.resolve();
15341
+ }
15048
15342
  this.setUserPromise = Promise.resolve();
15049
15343
  };
15050
15344
  /**
@@ -15162,7 +15456,7 @@ var StreamChat = class _StreamChat {
15162
15456
  return this.userAgent;
15163
15457
  }
15164
15458
  if (!this.cachedUserAgent) {
15165
- const version = "9.52.1";
15459
+ const version = "9.54.0";
15166
15460
  const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
15167
15461
  const { name: appName, version: appVersion } = this.appIdentifier ?? {};
15168
15462
  const { os, model: deviceModel } = this.deviceIdentifier ?? {};
@@ -18404,6 +18698,10 @@ var StreamChat = class _StreamChat {
18404
18698
  /**
18405
18699
  * Update Channels Batch
18406
18700
  *
18701
+ * For the `updateData` operation, `data.custom` replaces the channel's whole
18702
+ * custom object, while the root-level `custom_set` / `custom_unset` patch
18703
+ * individual keys and leave the rest untouched.
18704
+ *
18407
18705
  * @param {UpdateChannelsBatchOptions} payload for updating channels in batch
18408
18706
  * @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
18409
18707
  */