stream-chat 9.2.0-offline-support-beta.2 → 9.2.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.
Files changed (36) hide show
  1. package/dist/cjs/index.browser.cjs +378 -198
  2. package/dist/cjs/index.browser.cjs.map +3 -3
  3. package/dist/cjs/index.node.cjs +378 -198
  4. package/dist/cjs/index.node.cjs.map +3 -3
  5. package/dist/esm/index.js +378 -198
  6. package/dist/esm/index.js.map +3 -3
  7. package/dist/types/channel_manager.d.ts +2 -2
  8. package/dist/types/channel_state.d.ts +1 -1
  9. package/dist/types/client.d.ts +2 -2
  10. package/dist/types/constants.d.ts +1 -1
  11. package/dist/types/index.d.ts +1 -1
  12. package/dist/types/messageComposer/attachmentManager.d.ts +2 -0
  13. package/dist/types/offline-support/index.d.ts +3 -0
  14. package/dist/types/offline-support/offline_support_api.d.ts +558 -0
  15. package/dist/types/offline-support/offline_sync_manager.d.ts +63 -0
  16. package/dist/types/offline-support/types.d.ts +327 -0
  17. package/dist/types/types.d.ts +1 -5
  18. package/dist/types/utils.d.ts +2 -3
  19. package/package.json +8 -10
  20. package/src/channel.ts +9 -5
  21. package/src/channel_manager.ts +12 -10
  22. package/src/channel_state.ts +20 -9
  23. package/src/client.ts +8 -10
  24. package/src/constants.ts +1 -1
  25. package/src/index.ts +1 -1
  26. package/src/messageComposer/attachmentManager.ts +45 -25
  27. package/src/messageComposer/fileUtils.ts +1 -1
  28. package/src/messageComposer/messageComposer.ts +1 -1
  29. package/src/offline-support/index.ts +3 -0
  30. package/src/offline-support/offline_support_api.ts +1104 -0
  31. package/src/offline-support/offline_sync_manager.ts +190 -0
  32. package/src/offline-support/types.ts +403 -0
  33. package/src/types.ts +1 -5
  34. package/src/utils.ts +10 -8
  35. package/dist/types/offline_support_api.d.ts +0 -351
  36. package/src/offline_support_api.ts +0 -1078
@@ -2550,7 +2550,7 @@ var RESERVED_UPDATED_MESSAGE_FIELDS = {
2550
2550
  };
2551
2551
  var LOCAL_MESSAGE_FIELDS = { error: true };
2552
2552
  var DEFAULT_QUERY_CHANNELS_RETRY_COUNT = 3;
2553
- var DEFAULT_QUERY_CHANNELS_SECONDS_BETWEEN_RETRIES = 1;
2553
+ var DEFAULT_QUERY_CHANNELS_MS_BETWEEN_RETRIES = 1e3;
2554
2554
 
2555
2555
  // src/utils.ts
2556
2556
  function logChatPromiseExecution(promise, name) {
@@ -3254,16 +3254,17 @@ var promoteChannel = ({
3254
3254
  };
3255
3255
  var isDate2 = (value) => !!value.getTime;
3256
3256
  var isLocalMessage = (message) => isDate2(message.created_at);
3257
- var waitSeconds = (seconds) => new Promise((resolve) => {
3258
- setTimeout(resolve, seconds * 1e3);
3259
- });
3260
3257
  var runDetached = (callback, options) => {
3261
3258
  const { context, onSuccessCallback = () => void 0, onErrorCallback } = options ?? {};
3262
3259
  const defaultOnError = (error) => {
3263
3260
  console.log(`An error has occurred in context ${context}: ${error}`);
3264
3261
  };
3265
3262
  const onError = onErrorCallback ?? defaultOnError;
3266
- callback.then(onSuccessCallback).catch(onError);
3263
+ let promise = callback;
3264
+ if (onSuccessCallback) {
3265
+ promise = promise.then(onSuccessCallback);
3266
+ }
3267
+ promise.catch(onError);
3267
3268
  };
3268
3269
 
3269
3270
  // src/channel_state.ts
@@ -3539,12 +3540,14 @@ var ChannelState = class {
3539
3540
  };
3540
3541
  this._updateMessage(updateData, (msg) => {
3541
3542
  if (messageWithReaction) {
3543
+ const updatedMessage = { ...messageWithReaction };
3542
3544
  messageWithReaction.own_reactions = this._addOwnReactionToMessage(
3543
3545
  msg.own_reactions,
3544
3546
  reaction,
3545
3547
  enforce_unique
3546
3548
  );
3547
- return this.formatMessage(messageWithReaction);
3549
+ updatedMessage.own_reactions = this._channel.getClient().userID === reaction.user_id ? messageWithReaction.own_reactions : msg.own_reactions;
3550
+ return this.formatMessage(updatedMessage);
3548
3551
  }
3549
3552
  if (messageFromState) {
3550
3553
  return this._addReactionToState(messageFromState, reaction, enforce_unique);
@@ -3599,16 +3602,15 @@ var ChannelState = class {
3599
3602
  return messageFromState;
3600
3603
  }
3601
3604
  _addOwnReactionToMessage(ownReactions, reaction, enforce_unique) {
3602
- if (this._channel.getClient().userID !== reaction.user_id) {
3603
- return ownReactions;
3604
- }
3605
3605
  if (enforce_unique) {
3606
3606
  ownReactions = [];
3607
3607
  } else {
3608
3608
  ownReactions = this._removeOwnReactionFromMessage(ownReactions, reaction);
3609
3609
  }
3610
3610
  ownReactions = ownReactions || [];
3611
- ownReactions.push(reaction);
3611
+ if (this._channel.getClient().userID === reaction.user_id) {
3612
+ ownReactions.push(reaction);
3613
+ }
3612
3614
  return ownReactions;
3613
3615
  }
3614
3616
  _removeOwnReactionFromMessage(ownReactions, reaction) {
@@ -3727,9 +3729,8 @@ var ChannelState = class {
3727
3729
  (msg) => msg.id === message.id
3728
3730
  );
3729
3731
  if (msgIndex !== -1) {
3730
- this.messageSets[messageSetIndex].messages[msgIndex] = updateFunc(
3731
- this.messageSets[messageSetIndex].messages[msgIndex]
3732
- );
3732
+ const upMsg = updateFunc(this.messageSets[messageSetIndex].messages[msgIndex]);
3733
+ this.messageSets[messageSetIndex].messages[msgIndex] = upMsg;
3733
3734
  }
3734
3735
  }
3735
3736
  }
@@ -3989,7 +3990,7 @@ var isFile2 = (fileLike) => !!fileLike.lastModified && !("uri" in fileLike);
3989
3990
  var isFileList2 = (obj) => {
3990
3991
  if (obj === null || obj === void 0) return false;
3991
3992
  if (typeof obj !== "object") return false;
3992
- return obj instanceof FileList || "item" in obj && "length" in obj && !Array.isArray(obj);
3993
+ return typeof FileList !== "undefined" && obj instanceof FileList || "item" in obj && "length" in obj && !Array.isArray(obj);
3993
3994
  };
3994
3995
  var isBlobButNotFile = (obj) => obj instanceof Blob && !(obj instanceof File);
3995
3996
  var isFileReference = (obj) => obj !== null && typeof obj === "object" && !isFile2(obj) && !isBlobButNotFile(obj) && typeof obj.name === "string" && typeof obj.uri === "string" && typeof obj.size === "number" && typeof obj.type === "string";
@@ -4476,29 +4477,51 @@ var AttachmentManager = class {
4476
4477
  const attachmentsById = this.attachmentsById;
4477
4478
  return this.attachments.indexOf(attachmentsById[localId]);
4478
4479
  };
4480
+ this.prepareAttachmentUpdate = (attachmentToUpdate) => {
4481
+ const stateAttachments = this.attachments;
4482
+ const attachments = [...this.attachments];
4483
+ const attachmentIndex = this.getAttachmentIndex(attachmentToUpdate.localMetadata.id);
4484
+ if (attachmentIndex === -1) return null;
4485
+ const merged = mergeWithDiff(
4486
+ stateAttachments[attachmentIndex],
4487
+ attachmentToUpdate
4488
+ );
4489
+ const updatesOnMerge = merged.diff && Object.keys(merged.diff.children).length;
4490
+ if (updatesOnMerge) {
4491
+ const localAttachment = ensureIsLocalAttachment(merged.result);
4492
+ if (localAttachment) {
4493
+ attachments.splice(attachmentIndex, 1, localAttachment);
4494
+ return attachments;
4495
+ }
4496
+ }
4497
+ return null;
4498
+ };
4499
+ this.updateAttachment = (attachmentToUpdate) => {
4500
+ const updatedAttachments = this.prepareAttachmentUpdate(attachmentToUpdate);
4501
+ if (updatedAttachments) {
4502
+ this.state.partialNext({ attachments: updatedAttachments });
4503
+ }
4504
+ };
4479
4505
  this.upsertAttachments = (attachmentsToUpsert) => {
4480
4506
  if (!attachmentsToUpsert.length) return;
4481
- const currentAttachments = this.attachments;
4482
- const newAttachments = [...currentAttachments];
4507
+ let attachments = [...this.attachments];
4508
+ let hasUpdates = false;
4483
4509
  attachmentsToUpsert.forEach((attachment) => {
4484
- const targetAttachmentIndex = this.getAttachmentIndex(attachment.localMetadata?.id);
4485
- if (targetAttachmentIndex < 0) {
4486
- const localAttachment = ensureIsLocalAttachment(attachment);
4487
- if (localAttachment) newAttachments.push(localAttachment);
4510
+ const updatedAttachments = this.prepareAttachmentUpdate(attachment);
4511
+ if (updatedAttachments) {
4512
+ attachments = updatedAttachments;
4513
+ hasUpdates = true;
4488
4514
  } else {
4489
- const merged = mergeWithDiff(
4490
- currentAttachments[targetAttachmentIndex],
4491
- attachment
4492
- );
4493
- const updatesOnMerge = merged.diff && Object.keys(merged.diff.children).length;
4494
- if (updatesOnMerge) {
4495
- const localAttachment = ensureIsLocalAttachment(merged.result);
4496
- if (localAttachment)
4497
- newAttachments.splice(targetAttachmentIndex, 1, localAttachment);
4515
+ const localAttachment = ensureIsLocalAttachment(attachment);
4516
+ if (localAttachment) {
4517
+ attachments.push(localAttachment);
4518
+ hasUpdates = true;
4498
4519
  }
4499
4520
  }
4500
4521
  });
4501
- this.state.partialNext({ attachments: newAttachments });
4522
+ if (hasUpdates) {
4523
+ this.state.partialNext({ attachments });
4524
+ }
4502
4525
  };
4503
4526
  this.removeAttachments = (localAttachmentIds) => {
4504
4527
  this.state.partialNext({
@@ -4526,7 +4549,7 @@ var AttachmentManager = class {
4526
4549
  } = uploadConfig;
4527
4550
  const sizeLimit = size_limit || DEFAULT_UPLOAD_SIZE_LIMIT_BYTES;
4528
4551
  const mimeType = fileLike.type;
4529
- if (isFile2(fileLike)) {
4552
+ if (isFile2(fileLike) || isFileReference(fileLike)) {
4530
4553
  if (allowed_file_extensions?.length && !allowed_file_extensions.some(
4531
4554
  (ext) => fileLike.name.toLowerCase().endsWith(ext.toLowerCase())
4532
4555
  )) {
@@ -4672,7 +4695,7 @@ var AttachmentManager = class {
4672
4695
  uploadState: "failed"
4673
4696
  }
4674
4697
  };
4675
- this.upsertAttachments([failedAttachment]);
4698
+ this.updateAttachment(failedAttachment);
4676
4699
  return failedAttachment;
4677
4700
  }
4678
4701
  if (!response) {
@@ -4698,7 +4721,7 @@ var AttachmentManager = class {
4698
4721
  if (response.thumb_url) {
4699
4722
  uploadedAttachment.thumb_url = response.thumb_url;
4700
4723
  }
4701
- this.upsertAttachments([uploadedAttachment]);
4724
+ this.updateAttachment(uploadedAttachment);
4702
4725
  return uploadedAttachment;
4703
4726
  };
4704
4727
  this.uploadFiles = async (files) => {
@@ -7648,7 +7671,7 @@ var _MessageComposer = class _MessageComposer extends WithSubscriptions {
7648
7671
  this.addUnsubscribeFunction(this.subscribeCustomDataManagerStateChanged());
7649
7672
  this.addUnsubscribeFunction(this.subscribeMessageComposerStateChanged());
7650
7673
  this.addUnsubscribeFunction(this.subscribeMessageComposerConfigStateChanged());
7651
- return this.unregisterSubscriptions;
7674
+ return this.unregisterSubscriptions.bind(this);
7652
7675
  };
7653
7676
  this.subscribeMessageUpdated = () => {
7654
7677
  const eventTypes = [
@@ -8372,10 +8395,12 @@ var Channel = class {
8372
8395
  type: reactionType,
8373
8396
  user_id: this.getClient().userID ?? user_id
8374
8397
  };
8375
- await offlineDb.deleteReaction({
8376
- message,
8377
- reaction
8378
- });
8398
+ if (message) {
8399
+ await offlineDb.deleteReaction({
8400
+ message,
8401
+ reaction
8402
+ });
8403
+ }
8379
8404
  return await offlineDb.queueTask({
8380
8405
  task: {
8381
8406
  channelId: this.id,
@@ -8847,7 +8872,7 @@ var Channel = class {
8847
8872
  });
8848
8873
  }
8849
8874
  _isTypingIndicatorsEnabled() {
8850
- if (!this.getConfig()?.typing_events) {
8875
+ if (!this.getConfig()?.typing_events || !this.getClient().wsConnection?.isHealthy) {
8851
8876
  return false;
8852
8877
  }
8853
8878
  return this.getClient().user?.privacy_settings?.typing_indicators?.enabled ?? true;
@@ -11726,7 +11751,7 @@ var ChannelManager = class extends WithSubscriptions {
11726
11751
  this.setOptions = (options = {}) => {
11727
11752
  this.options = { ...DEFAULT_CHANNEL_MANAGER_OPTIONS, ...options };
11728
11753
  };
11729
- this.queryChannelsRequest = async (payload, retryCount = 0) => {
11754
+ this.executeChannelsQuery = async (payload, retryCount = 0) => {
11730
11755
  const { filters, sort, options, stateOptions } = payload;
11731
11756
  const { offset, limit } = {
11732
11757
  ...DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS,
@@ -11770,8 +11795,8 @@ var ChannelManager = class extends WithSubscriptions {
11770
11795
  this.state.partialNext({ error: wrappedError });
11771
11796
  return;
11772
11797
  }
11773
- await waitSeconds(DEFAULT_QUERY_CHANNELS_SECONDS_BETWEEN_RETRIES);
11774
- return this.queryChannelsRequest(payload, retryCount + 1);
11798
+ await sleep(DEFAULT_QUERY_CHANNELS_MS_BETWEEN_RETRIES);
11799
+ return this.executeChannelsQuery(payload, retryCount + 1);
11775
11800
  }
11776
11801
  };
11777
11802
  this.queryChannels = async (filters, sort = [], options = {}, stateOptions = {}) => {
@@ -11779,10 +11804,12 @@ var ChannelManager = class extends WithSubscriptions {
11779
11804
  pagination: { isLoading, filters: filtersFromState },
11780
11805
  initialized
11781
11806
  } = this.state.getLatestValue();
11782
- if (isLoading && !this.options.abortInFlightQuery && JSON.stringify(filtersFromState) === JSON.stringify(filters)) {
11807
+ if (isLoading && !this.options.abortInFlightQuery && // TODO: Figure a proper way to either deeply compare these or
11808
+ // create hashes from each.
11809
+ JSON.stringify(filtersFromState) === JSON.stringify(filters)) {
11783
11810
  return;
11784
11811
  }
11785
- const queryChannelsRequestPayload = { filters, sort, options, stateOptions };
11812
+ const executeChannelsQueryPayload = { filters, sort, options, stateOptions };
11786
11813
  try {
11787
11814
  this.stateOptions = stateOptions;
11788
11815
  this.state.next((currentState) => ({
@@ -11817,13 +11844,13 @@ var ChannelManager = class extends WithSubscriptions {
11817
11844
  this.client.offlineDb.syncManager.scheduleSyncStatusChangeCallback(
11818
11845
  this.id,
11819
11846
  async () => {
11820
- await this.queryChannelsRequest(queryChannelsRequestPayload);
11847
+ await this.executeChannelsQuery(executeChannelsQueryPayload);
11821
11848
  }
11822
11849
  );
11823
11850
  return;
11824
11851
  }
11825
11852
  }
11826
- await this.queryChannelsRequest(queryChannelsRequestPayload);
11853
+ await this.executeChannelsQuery(executeChannelsQueryPayload);
11827
11854
  } catch (error) {
11828
11855
  this.client.logger("error", error.message);
11829
11856
  this.state.next((currentState) => ({
@@ -13166,13 +13193,11 @@ var StreamChat = class _StreamChat {
13166
13193
  if (event.type === "notification.mutes_updated" && event.me?.mutes) {
13167
13194
  this.mutedUsers = event.me.mutes;
13168
13195
  }
13169
- if (event.type === "notification.mark_read") {
13170
- if (event.unread_channels === 0) {
13171
- const activeChannelKeys = Object.keys(this.activeChannels);
13172
- activeChannelKeys.forEach(
13173
- (activeChannelKey) => this.activeChannels[activeChannelKey].state.unreadCount = 0
13174
- );
13175
- }
13196
+ if (event.type === "notification.mark_read" && event.unread_channels === 0) {
13197
+ const activeChannelKeys = Object.keys(this.activeChannels);
13198
+ activeChannelKeys.forEach(
13199
+ (activeChannelKey) => this.activeChannels[activeChannelKey].state.unreadCount = 0
13200
+ );
13176
13201
  }
13177
13202
  if ((event.type === "channel.deleted" || event.type === "notification.channel_deleted") && event.cid) {
13178
13203
  const { cid } = event;
@@ -14050,7 +14075,7 @@ var StreamChat = class _StreamChat {
14050
14075
  data
14051
14076
  );
14052
14077
  }
14053
- deleteChannelType(channelType) {
14078
+ DBDeleteChannelType(channelType) {
14054
14079
  return this.delete(
14055
14080
  this.baseURL + `/channeltypes/${encodeURIComponent(channelType)}`
14056
14081
  );
@@ -14391,7 +14416,7 @@ var StreamChat = class _StreamChat {
14391
14416
  if (this.userAgent) {
14392
14417
  return this.userAgent;
14393
14418
  }
14394
- const version = "9.2.0-offline-support-beta.2";
14419
+ const version = "9.2.0";
14395
14420
  const clientBundle = "browser-cjs";
14396
14421
  let userAgentString = "";
14397
14422
  if (this.sdkIdentifier) {
@@ -15567,9 +15592,154 @@ var BuiltinPermissions = {
15567
15592
  UseFrozenChannel: "Send messages and reactions to frozen channels"
15568
15593
  };
15569
15594
 
15570
- // src/offline_support_api.ts
15595
+ // src/offline-support/offline_sync_manager.ts
15596
+ var OfflineDBSyncManager = class {
15597
+ constructor({
15598
+ client,
15599
+ offlineDb
15600
+ }) {
15601
+ this.syncStatus = false;
15602
+ this.connectionChangedListener = null;
15603
+ this.syncStatusListeners = [];
15604
+ this.scheduledSyncStatusCallbacks = /* @__PURE__ */ new Map();
15605
+ /**
15606
+ * Initializes the sync manager. Should only be called once per session.
15607
+ *
15608
+ * Cleans up old listeners if re-initialized to avoid memory leaks.
15609
+ * Starts syncing immediately if already connected, otherwise waits for reconnection.
15610
+ */
15611
+ this.init = async () => {
15612
+ try {
15613
+ if (this.client.user?.id && this.client.wsConnection?.isHealthy) {
15614
+ await this.syncAndExecutePendingTasks();
15615
+ await this.invokeSyncStatusListeners(true);
15616
+ }
15617
+ if (this.connectionChangedListener) {
15618
+ this.connectionChangedListener.unsubscribe();
15619
+ }
15620
+ this.connectionChangedListener = this.client.on(
15621
+ "connection.changed",
15622
+ async (event) => {
15623
+ if (event.online) {
15624
+ await this.syncAndExecutePendingTasks();
15625
+ await this.invokeSyncStatusListeners(true);
15626
+ } else {
15627
+ await this.invokeSyncStatusListeners(false);
15628
+ }
15629
+ }
15630
+ );
15631
+ } catch (error) {
15632
+ console.log("Error in DBSyncManager.init: ", error);
15633
+ }
15634
+ };
15635
+ /**
15636
+ * Registers a listener that is called whenever the sync status changes.
15637
+ *
15638
+ * @param listener - A callback invoked with the new sync status (`true` or `false`).
15639
+ * @returns An object with an `unsubscribe` function to remove the listener.
15640
+ */
15641
+ this.onSyncStatusChange = (listener) => {
15642
+ this.syncStatusListeners.push(listener);
15643
+ return {
15644
+ unsubscribe: () => {
15645
+ this.syncStatusListeners = this.syncStatusListeners.filter(
15646
+ (el) => el !== listener
15647
+ );
15648
+ }
15649
+ };
15650
+ };
15651
+ /**
15652
+ * Schedules a one-time callback to be invoked after the next successful sync.
15653
+ *
15654
+ * @param tag - A unique key to identify and manage the callback.
15655
+ * @param callback - An async function to run after sync.
15656
+ */
15657
+ this.scheduleSyncStatusChangeCallback = (tag, callback) => {
15658
+ this.scheduledSyncStatusCallbacks.set(tag, callback);
15659
+ };
15660
+ /**
15661
+ * Invokes all registered sync status listeners and executes any scheduled sync callbacks.
15662
+ *
15663
+ * @param status - The new sync status (`true` or `false`).
15664
+ */
15665
+ this.invokeSyncStatusListeners = async (status) => {
15666
+ this.syncStatus = status;
15667
+ this.syncStatusListeners.forEach((l) => l(status));
15668
+ if (status) {
15669
+ const promises = Array.from(this.scheduledSyncStatusCallbacks.values()).map(
15670
+ (cb) => cb()
15671
+ );
15672
+ await Promise.all(promises);
15673
+ this.scheduledSyncStatusCallbacks.clear();
15674
+ }
15675
+ };
15676
+ /**
15677
+ * Performs synchronization with the Stream backend.
15678
+ *
15679
+ * This includes downloading events since the last sync, updating the local DB,
15680
+ * and handling sync failures (e.g., if syncing beyond the allowed retention window).
15681
+ */
15682
+ this.sync = async () => {
15683
+ if (!this.client?.user) {
15684
+ return;
15685
+ }
15686
+ try {
15687
+ const cids = await this.offlineDb.getAllChannelCids();
15688
+ if (cids.length === 0) {
15689
+ return;
15690
+ }
15691
+ const lastSyncedAt = await this.offlineDb.getLastSyncedAt({
15692
+ userId: this.client.user.id
15693
+ });
15694
+ if (lastSyncedAt) {
15695
+ const lastSyncedAtDate = new Date(lastSyncedAt);
15696
+ const nowDate = /* @__PURE__ */ new Date();
15697
+ const diff = Math.floor(
15698
+ (nowDate.getTime() - lastSyncedAtDate.getTime()) / (1e3 * 60 * 60 * 24)
15699
+ );
15700
+ if (diff > 30) {
15701
+ await this.offlineDb.resetDB();
15702
+ } else {
15703
+ const result = await this.client.sync(cids, lastSyncedAtDate.toISOString());
15704
+ const queryPromises = result.events.map(
15705
+ (event) => this.offlineDb.handleEvent({ event, execute: false })
15706
+ );
15707
+ const queriesArray = await Promise.all(queryPromises);
15708
+ const queries = queriesArray.flat();
15709
+ if (queries.length) {
15710
+ await this.offlineDb.executeSqlBatch(queries);
15711
+ }
15712
+ }
15713
+ }
15714
+ await this.offlineDb.upsertUserSyncStatus({
15715
+ userId: this.client.user.id,
15716
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toString()
15717
+ });
15718
+ } catch (e) {
15719
+ console.log("An error has occurred while syncing the DB.", e);
15720
+ await this.offlineDb.resetDB();
15721
+ }
15722
+ };
15723
+ /**
15724
+ * Executes any tasks that were queued while offline and then performs a sync.
15725
+ */
15726
+ this.syncAndExecutePendingTasks = async () => {
15727
+ await this.offlineDb.executePendingTasks();
15728
+ await this.sync();
15729
+ };
15730
+ this.client = client;
15731
+ this.offlineDb = offlineDb;
15732
+ }
15733
+ };
15734
+
15735
+ // src/offline-support/offline_support_api.ts
15571
15736
  var AbstractOfflineDB = class {
15572
15737
  constructor({ client }) {
15738
+ /**
15739
+ * Initializes the DB as well as its syncManager for a given userId.
15740
+ * It will update the DBs reactive state with initialization values.
15741
+ * @param userId - the user ID for which we want to initialize
15742
+ */
15573
15743
  this.init = async (userId) => {
15574
15744
  try {
15575
15745
  if (!this.shouldInitialize(userId)) {
@@ -15586,6 +15756,15 @@ var AbstractOfflineDB = class {
15586
15756
  console.log("Error Initializing DB:", error);
15587
15757
  }
15588
15758
  };
15759
+ /**
15760
+ * A utility method used to execute a query in a detached manner. The callback
15761
+ * passed uses a reference to the DB itself and will handle errors gracefully
15762
+ * and silently. Only really meant to be used for write queries that need to
15763
+ * be run in synchronous functions.
15764
+ * @param queryCallback - a callback wrapping all query logic that is to be executed
15765
+ * @param method - a utility parameter used for proper logging (will make sure the method
15766
+ * is logged on failure)
15767
+ */
15589
15768
  this.executeQuerySafely = (queryCallback, { method }) => {
15590
15769
  const { initialized } = this.state.getLatestValue();
15591
15770
  if (!initialized) {
@@ -15593,6 +15772,24 @@ var AbstractOfflineDB = class {
15593
15772
  }
15594
15773
  runDetached(queryCallback(this), { context: `OfflineDB(${method})` });
15595
15774
  };
15775
+ /**
15776
+ * A utility method used to guard a certain DB query with the possible non-existance
15777
+ * of a channel inside of the DB. If the channel we want to guard against does not exist
15778
+ * in the DB yet, it will try to:
15779
+ *
15780
+ * 1. Use the channel from the WS event
15781
+ * 2. Use the channel from state
15782
+ *
15783
+ * and upsert the channels in the DB.
15784
+ *
15785
+ * If both fail, it will not execute the query as it would result in a foreign key constraint
15786
+ * error.
15787
+ *
15788
+ * @param event - the WS event we are trying to process
15789
+ * @param execute - whether to immediately execute the operation.
15790
+ * @param forceUpdate - whether to upsert the channel data anyway
15791
+ * @param createQueries - a callback function to creation of the queries that we want to execute
15792
+ */
15596
15793
  this.queriesWithChannelGuard = async ({
15597
15794
  event,
15598
15795
  execute = true,
@@ -15630,7 +15827,7 @@ var AbstractOfflineDB = class {
15630
15827
  return newQueries;
15631
15828
  } else {
15632
15829
  console.warn(
15633
- `Couldnt create channel queries on ${type} event for an initialized channel that is not in DB, skipping event`,
15830
+ `Couldn't create channel queries on ${type} event for an initialized channel that is not in DB, skipping event`,
15634
15831
  { event }
15635
15832
  );
15636
15833
  return [];
@@ -15645,8 +15842,14 @@ var AbstractOfflineDB = class {
15645
15842
  }
15646
15843
  return await createQueries(execute);
15647
15844
  };
15648
- // TODO: Check why this isn't working properly for read state - something is not
15649
- // getting populated as it should :'(
15845
+ /**
15846
+ * Handles a message.new event. Will always use a channel guard for the inner queries
15847
+ * and it is going to make sure that both messages and reads are upserted. It will not
15848
+ * try to fetch the reads from the DB first and it will rely on channel.state to handle
15849
+ * the number of unreads.
15850
+ * @param event - the WS event we are trying to process
15851
+ * @param execute - whether to immediately execute the operation.
15852
+ */
15650
15853
  this.handleNewMessage = async ({
15651
15854
  event,
15652
15855
  execute = true
@@ -15692,6 +15895,12 @@ var AbstractOfflineDB = class {
15692
15895
  }
15693
15896
  return finalQueries;
15694
15897
  };
15898
+ /**
15899
+ * A handler for message deletion. It provides a channel guard and determines whether
15900
+ * it should hard delete or soft delete the message.
15901
+ * @param event - the WS event we are trying to process
15902
+ * @param execute - whether to immediately execute the operation.
15903
+ */
15695
15904
  this.handleDeleteMessage = async ({
15696
15905
  event,
15697
15906
  execute = true
@@ -15707,12 +15916,11 @@ var AbstractOfflineDB = class {
15707
15916
  return [];
15708
15917
  };
15709
15918
  /**
15710
- * TODO: Write docs. Here and in all other places in the API.
15711
- * This method is used for removing a message that has already failed from the
15712
- * state as well as the DB. We want to drop all pending tasks as well as finally
15713
- * hard delete the message from the DB.
15714
- * @param messageId
15715
- * @param execute
15919
+ * A utility method used for removing a message that has already failed from the
15920
+ * state as well as the DB. We want to drop all pending tasks and finally hard
15921
+ * delete the message from the DB.
15922
+ * @param messageId - the message id of the message we want to remove
15923
+ * @param execute - whether to immediately execute the operation.
15716
15924
  */
15717
15925
  this.handleRemoveMessage = async ({
15718
15926
  messageId,
@@ -15732,6 +15940,16 @@ var AbstractOfflineDB = class {
15732
15940
  }
15733
15941
  return queries;
15734
15942
  };
15943
+ /**
15944
+ * A utility method to handle read events. It will calculate the state of the reads if
15945
+ * present in the event, or optionally rely on the hard override in unreadMessages.
15946
+ * The unreadMessages argument is useful for cases where we know the exact number of unreads
15947
+ * (for example reading an entire channel), but `unread_messages` might not necessarily exist
15948
+ * in the event (or it exists with a stale value if we know what we want to ultimately update to).
15949
+ * @param event - the WS event we are trying to process
15950
+ * @param unreadMessages - an override of unread_messages that will be preferred when upserting reads
15951
+ * @param execute - whether to immediately execute the operation.
15952
+ */
15735
15953
  this.handleRead = async ({
15736
15954
  event,
15737
15955
  unreadMessages,
@@ -15764,6 +15982,13 @@ var AbstractOfflineDB = class {
15764
15982
  }
15765
15983
  return [];
15766
15984
  };
15985
+ /**
15986
+ * A utility method used to handle member events. It guards the processing
15987
+ * of each event with a channel guard and also forces an update of member_count
15988
+ * for the respective channel if applicable.
15989
+ * @param event - the WS event we are trying to process
15990
+ * @param execute - whether to immediately execute the operation.
15991
+ */
15767
15992
  this.handleMemberEvent = async ({
15768
15993
  event,
15769
15994
  execute = true
@@ -15786,6 +16011,12 @@ var AbstractOfflineDB = class {
15786
16011
  }
15787
16012
  return [];
15788
16013
  };
16014
+ /**
16015
+ * A utility method used to handle message.updated events. It guards each
16016
+ * event handler within a channel guard.
16017
+ * @param event - the WS event we are trying to process
16018
+ * @param execute - whether to immediately execute the operation.
16019
+ */
15789
16020
  this.handleMessageUpdatedEvent = async ({
15790
16021
  event,
15791
16022
  execute = true
@@ -15803,8 +16034,10 @@ var AbstractOfflineDB = class {
15803
16034
  * An event handler for channel.visible and channel.hidden events. We need a separate
15804
16035
  * handler because event.channel.hidden does not arrive with the baseline event, so a
15805
16036
  * simple upsertion is not enough.
15806
- * @param event
15807
- * @param execute
16037
+ * It will update the hidden property of a channel to true if handling the `channel.hidden`
16038
+ * event and to false if handling `channel.visible`.
16039
+ * @param event - the WS event we are trying to process
16040
+ * @param execute - whether to immediately execute the operation.
15808
16041
  */
15809
16042
  this.handleChannelVisibilityEvent = async ({
15810
16043
  event,
@@ -15820,6 +16053,13 @@ var AbstractOfflineDB = class {
15820
16053
  }
15821
16054
  return [];
15822
16055
  };
16056
+ /**
16057
+ * A utility handler used to handle channel.truncated events. It handles both
16058
+ * removing all messages and relying on truncated_at as well. It will also upsert
16059
+ * reads adequately (and calculate the correct unread messages when truncating).
16060
+ * @param event - the WS event we are trying to process
16061
+ * @param execute - whether to immediately execute the operation.
16062
+ */
15823
16063
  this.handleChannelTruncatedEvent = async ({
15824
16064
  event,
15825
16065
  execute = true
@@ -15861,6 +16101,15 @@ var AbstractOfflineDB = class {
15861
16101
  }
15862
16102
  return [];
15863
16103
  };
16104
+ /**
16105
+ * A utility handler for all reaction events. It wraps the inner queries
16106
+ * within a channel guard and maps them like so:
16107
+ * - reaction.new -> insertReaction
16108
+ * - reaction.updated -> updateReaction
16109
+ * - reaction.deleted -> deleteReaction
16110
+ * @param event - the WS event we are trying to process
16111
+ * @param execute - whether to immediately execute the operation.
16112
+ */
15864
16113
  this.handleReactionEvent = async ({
15865
16114
  event,
15866
16115
  execute = true
@@ -15889,6 +16138,13 @@ var AbstractOfflineDB = class {
15889
16138
  (executeOverride) => reactionMethod({ message, reaction, execute: executeOverride })
15890
16139
  );
15891
16140
  };
16141
+ /**
16142
+ * A generic event handler that decides which DB API to invoke based on
16143
+ * event.type for all events we are currently handling. It is used to both
16144
+ * react on WS events as well as process the sync API events.
16145
+ * @param event - the WS event we are trying to process
16146
+ * @param execute - whether to immediately execute the operation.
16147
+ */
15892
16148
  this.handleEvent = async ({
15893
16149
  event,
15894
16150
  execute = true
@@ -15929,9 +16185,24 @@ var AbstractOfflineDB = class {
15929
16185
  }
15930
16186
  return [];
15931
16187
  };
16188
+ /**
16189
+ * A method used to enqueue a pending task if the execution of it fails.
16190
+ * It will try to do the following:
16191
+ *
16192
+ * 1. Execute the task immediately
16193
+ * 2. If this fails, checks if the failure was due to something valid for a pending task
16194
+ * 3. If it is, it will insert the task in the pending tasks table
16195
+ *
16196
+ * It will return the response from the execution if it succeeded.
16197
+ * @param task - the pending task we want to execute
16198
+ */
15932
16199
  this.queueTask = async ({ task }) => {
15933
16200
  let response;
15934
16201
  try {
16202
+ if (!this.client.wsConnection?.isHealthy) {
16203
+ await this.addPendingTask(task);
16204
+ return;
16205
+ }
15935
16206
  response = await this.executeTask({ task });
15936
16207
  } catch (e) {
15937
16208
  if (!this.shouldSkipQueueingTask(e)) {
@@ -15941,9 +16212,26 @@ var AbstractOfflineDB = class {
15941
16212
  }
15942
16213
  return response;
15943
16214
  };
15944
- // Error code 4 - bad request data
15945
- // Error code 17 - missing own_capabilities to execute the task
16215
+ /**
16216
+ * A utility method that determines if a failed task should be added to the
16217
+ * queue based on its error.
16218
+ * Error code 4 - bad request data
16219
+ * Error code 17 - missing own_capabilities to execute the task
16220
+ * @param error
16221
+ */
15946
16222
  this.shouldSkipQueueingTask = (error) => error?.response?.data?.code === 4 || error?.response?.data?.code === 17;
16223
+ /**
16224
+ * Executes a task from the list of supported pending tasks. Currently supported pending tasks
16225
+ * are:
16226
+ * - Deleting a message
16227
+ * - Sending a reaction
16228
+ * - Removing a reaction
16229
+ * - Sending a message
16230
+ * It will throw if we try to execute a pending task that is not supported.
16231
+ * @param task - The task we want to execute
16232
+ * @param isPendingTask - a control value telling us if it's an actual pending task being executed
16233
+ * or delayed execution
16234
+ */
15947
16235
  this.executeTask = async ({ task }, isPendingTask = false) => {
15948
16236
  if (task.type === "delete-message") {
15949
16237
  return await this.client._deleteMessage(...task.payload);
@@ -15951,9 +16239,6 @@ var AbstractOfflineDB = class {
15951
16239
  const { channelType, channelId } = task;
15952
16240
  if (channelType && channelId) {
15953
16241
  const channel = this.client.channel(channelType, channelId);
15954
- if (!channel.initialized) {
15955
- await channel.watch();
15956
- }
15957
16242
  if (task.type === "send-reaction") {
15958
16243
  return await channel._sendReaction(...task.payload);
15959
16244
  }
@@ -15961,17 +16246,30 @@ var AbstractOfflineDB = class {
15961
16246
  return await channel._deleteReaction(...task.payload);
15962
16247
  }
15963
16248
  if (task.type === "send-message") {
15964
- const newMessage = await channel._sendMessage(...task.payload);
15965
- if (isPendingTask) {
15966
- channel.state.addMessageSorted(newMessage.message, true);
16249
+ const newMessageResponse = await channel._sendMessage(...task.payload);
16250
+ const newMessage = newMessageResponse?.message;
16251
+ if (isPendingTask && newMessage) {
16252
+ if (newMessage?.parent_id) {
16253
+ this.client.threads.threadsById[newMessage.parent_id]?.upsertReplyLocally({
16254
+ message: newMessage,
16255
+ timestampChanged: true
16256
+ });
16257
+ }
16258
+ channel.state.addMessageSorted(newMessage, true);
15967
16259
  }
15968
- return newMessage;
16260
+ return newMessageResponse;
15969
16261
  }
15970
16262
  }
15971
16263
  throw new Error(
15972
16264
  `Tried to execute invalid pending task type (${task.type}) while synchronizing the database.`
15973
16265
  );
15974
16266
  };
16267
+ /**
16268
+ * A utility method used to execute all pending tasks. As each task succeeds execution,
16269
+ * it is going to be removed from the DB. If the execution failed due to a valid reason
16270
+ * it is going to remove the pending task from the DB even if execution fails, otherwise
16271
+ * it will keep it for the next time we try to execute all pending taks.
16272
+ */
15975
16273
  this.executePendingTasks = async () => {
15976
16274
  const queue = await this.getPendingTasks();
15977
16275
  for (const task of queue) {
@@ -16003,133 +16301,15 @@ var AbstractOfflineDB = class {
16003
16301
  userId: this.client.userID
16004
16302
  });
16005
16303
  }
16304
+ /**
16305
+ * Checks whether the DB should be initialized or if it has been initialized already.
16306
+ * @param {string} userId - the user ID for which we want to check initialization
16307
+ */
16006
16308
  shouldInitialize(userId) {
16007
16309
  const { userId: userIdFromState, initialized } = this.state.getLatestValue();
16008
16310
  return userId === userIdFromState && initialized;
16009
16311
  }
16010
16312
  };
16011
- var OfflineDBSyncManager = class {
16012
- constructor({
16013
- client,
16014
- offlineDb
16015
- }) {
16016
- this.syncStatus = false;
16017
- this.connectionChangedListener = null;
16018
- this.syncStatusListeners = [];
16019
- this.scheduledSyncStatusCallbacks = /* @__PURE__ */ new Map();
16020
- /**
16021
- * Initializes the OfflineDBSyncManager. This function should be called only once
16022
- * throughout the lifetime of SDK. If it is performed more than once for whatever
16023
- * reason, it will run cleanup on its listeners to prevent memory leaks.
16024
- */
16025
- this.init = async () => {
16026
- try {
16027
- if (this.client.user?.id && this.client.wsConnection?.isHealthy) {
16028
- await this.syncAndExecutePendingTasks();
16029
- await this.invokeSyncStatusListeners(true);
16030
- }
16031
- if (this.connectionChangedListener) {
16032
- this.connectionChangedListener.unsubscribe();
16033
- }
16034
- this.connectionChangedListener = this.client.on(
16035
- "connection.changed",
16036
- async (event) => {
16037
- if (event.online) {
16038
- await this.syncAndExecutePendingTasks();
16039
- await this.invokeSyncStatusListeners(true);
16040
- } else {
16041
- await this.invokeSyncStatusListeners(false);
16042
- }
16043
- }
16044
- );
16045
- } catch (error) {
16046
- console.log("Error in DBSyncManager.init: ", error);
16047
- }
16048
- };
16049
- /**
16050
- * Subscribes a listener for sync status change.
16051
- *
16052
- * @param listener {function}
16053
- * @returns {function} to unsubscribe the listener.
16054
- */
16055
- this.onSyncStatusChange = (listener) => {
16056
- this.syncStatusListeners.push(listener);
16057
- return {
16058
- unsubscribe: () => {
16059
- this.syncStatusListeners = this.syncStatusListeners.filter(
16060
- (el) => el !== listener
16061
- );
16062
- }
16063
- };
16064
- };
16065
- this.scheduleSyncStatusChangeCallback = (tag, callback) => {
16066
- this.scheduledSyncStatusCallbacks.set(tag, callback);
16067
- };
16068
- this.invokeSyncStatusListeners = async (status) => {
16069
- this.syncStatus = status;
16070
- this.syncStatusListeners.forEach((l) => l(status));
16071
- if (status) {
16072
- const promises = Array.from(this.scheduledSyncStatusCallbacks.values()).map(
16073
- (cb) => cb()
16074
- );
16075
- await Promise.all(promises);
16076
- this.scheduledSyncStatusCallbacks.clear();
16077
- }
16078
- };
16079
- this.sync = async () => {
16080
- if (!this.client?.user) {
16081
- return;
16082
- }
16083
- const cids = await this.offlineDb.getAllChannelCids();
16084
- if (cids.length === 0) {
16085
- return;
16086
- }
16087
- console.log("MIDWAY MARK :D");
16088
- const lastSyncedAt = await this.offlineDb.getLastSyncedAt({
16089
- userId: this.client.user.id
16090
- });
16091
- console.log("LAST SYNC AT: ", lastSyncedAt);
16092
- if (lastSyncedAt) {
16093
- const lastSyncedAtDate = new Date(lastSyncedAt);
16094
- const nowDate = /* @__PURE__ */ new Date();
16095
- const diff = Math.floor(
16096
- (nowDate.getTime() - lastSyncedAtDate.getTime()) / (1e3 * 60 * 60 * 24)
16097
- );
16098
- console.log("DIFF: ", diff);
16099
- if (diff > 30) {
16100
- await this.offlineDb.resetDB();
16101
- } else {
16102
- try {
16103
- console.log("ABOUT TO CALL SYNC API");
16104
- const result = await this.client.sync(cids, lastSyncedAtDate.toISOString());
16105
- console.log("CALLED SYNC API", result.events);
16106
- const queryPromises = result.events.map(
16107
- (event) => this.offlineDb.handleEvent({ event, execute: false })
16108
- );
16109
- const queriesArray = await Promise.all(queryPromises);
16110
- const queries = queriesArray.flat();
16111
- if (queries.length) {
16112
- await this.offlineDb.executeSqlBatch(queries);
16113
- }
16114
- } catch (e) {
16115
- console.log("An error has occurred while syncing the DB.", e);
16116
- await this.offlineDb.resetDB();
16117
- }
16118
- }
16119
- }
16120
- await this.offlineDb.upsertUserSyncStatus({
16121
- userId: this.client.user.id,
16122
- lastSyncedAt: (/* @__PURE__ */ new Date()).toString()
16123
- });
16124
- };
16125
- this.syncAndExecutePendingTasks = async () => {
16126
- await this.offlineDb.executePendingTasks();
16127
- await this.sync();
16128
- };
16129
- this.client = client;
16130
- this.offlineDb = offlineDb;
16131
- }
16132
- };
16133
16313
 
16134
16314
  // src/utils/FixedSizeQueueCache.ts
16135
16315
  var FixedSizeQueueCache = class {