@repzo/widget-react-native 1.4.0 → 1.5.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 (51) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +36 -2
  3. package/lib/commonjs/api/client.js +29 -5
  4. package/lib/commonjs/api/client.js.map +1 -1
  5. package/lib/commonjs/index.js.map +1 -1
  6. package/lib/commonjs/state/user-update-queue.js +168 -0
  7. package/lib/commonjs/state/user-update-queue.js.map +1 -0
  8. package/lib/commonjs/state/widget-store.js +27 -4
  9. package/lib/commonjs/state/widget-store.js.map +1 -1
  10. package/lib/commonjs/types.js.map +1 -1
  11. package/lib/commonjs/ui/screens/chat/ChatView.js +14 -1
  12. package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
  13. package/lib/module/api/client.js +27 -4
  14. package/lib/module/api/client.js.map +1 -1
  15. package/lib/module/index.js.map +1 -1
  16. package/lib/module/state/user-update-queue.js +163 -0
  17. package/lib/module/state/user-update-queue.js.map +1 -0
  18. package/lib/module/state/widget-store.js +27 -4
  19. package/lib/module/state/widget-store.js.map +1 -1
  20. package/lib/module/types.js.map +1 -1
  21. package/lib/module/ui/screens/chat/ChatView.js +14 -1
  22. package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
  23. package/lib/typescript/commonjs/api/client.d.ts +8 -2
  24. package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
  25. package/lib/typescript/commonjs/index.d.ts +5 -0
  26. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/state/user-update-queue.d.ts +62 -0
  28. package/lib/typescript/commonjs/state/user-update-queue.d.ts.map +1 -0
  29. package/lib/typescript/commonjs/state/widget-store.d.ts +4 -0
  30. package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/types.d.ts +12 -0
  32. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
  34. package/lib/typescript/module/api/client.d.ts +8 -2
  35. package/lib/typescript/module/api/client.d.ts.map +1 -1
  36. package/lib/typescript/module/index.d.ts +5 -0
  37. package/lib/typescript/module/index.d.ts.map +1 -1
  38. package/lib/typescript/module/state/user-update-queue.d.ts +62 -0
  39. package/lib/typescript/module/state/user-update-queue.d.ts.map +1 -0
  40. package/lib/typescript/module/state/widget-store.d.ts +4 -0
  41. package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
  42. package/lib/typescript/module/types.d.ts +12 -0
  43. package/lib/typescript/module/types.d.ts.map +1 -1
  44. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/src/api/client.ts +33 -4
  47. package/src/index.tsx +5 -0
  48. package/src/state/user-update-queue.ts +178 -0
  49. package/src/state/widget-store.ts +25 -2
  50. package/src/types.ts +12 -0
  51. package/src/ui/screens/chat/ChatView.tsx +14 -1
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Paces `updateUser()` the way Intercom paces its web `update` (20 calls per
5
+ * 30 minutes), so a host that calls it on a timer or on every GPS fix can't
6
+ * flood Workstation.
7
+ *
8
+ * - Values that match what Workstation already has are not sent again.
9
+ * - Up to `burst` sends go out straight away; after that one more is allowed
10
+ * every `windowMs / burst` (90 s by default).
11
+ * - While a send is in flight, rate limited or out of budget, new values wait
12
+ * and merge into one bag, newest value per key. Nothing is lost: the latest
13
+ * of every key goes out on the next allowed send. Workstation merges the bag
14
+ * into the saved attributes per key, so sending only what changed is enough.
15
+ * - A 429 holds sends until its `Retry-After`. A failed connection or a 5xx
16
+ * keeps the values and tries again later.
17
+ */
18
+
19
+ import { RepzoWidgetError } from "../api/client.js";
20
+ export const USER_UPDATE_BURST = 20;
21
+ export const USER_UPDATE_WINDOW_MS = 30 * 60 * 1000;
22
+ /** Wait after a 429 without a usable `Retry-After`, or after a failed connection / 5xx. */
23
+ const DEFAULT_HOLD_MS = 60 * 1000;
24
+ export class UserUpdateQueue {
25
+ /** Values Workstation accepted for the current identity. */
26
+ sent = {};
27
+ /** Values waiting for the next allowed send. */
28
+ pending = null;
29
+ inFlight = false;
30
+ holdUntil = 0;
31
+ timer = null;
32
+ /** Bumped by reset(): a send that settles after it writes nothing back. */
33
+ generation = 0;
34
+ constructor(send, options = {}) {
35
+ this.send = send;
36
+ this.burst = Math.max(1, options.burst ?? USER_UPDATE_BURST);
37
+ this.refillMs = (options.windowMs ?? USER_UPDATE_WINDOW_MS) / this.burst;
38
+ this.now = options.now ?? Date.now;
39
+ this.tokens = this.burst;
40
+ this.refilledAt = this.now();
41
+ }
42
+
43
+ /**
44
+ * Resolves once the values are sent, or straight away when nothing changed
45
+ * or they were held for a later send. Rejects only when a send made right
46
+ * now fails with something other than a 429, as `updateUser()` always has.
47
+ */
48
+ update(attributes) {
49
+ const changes = {};
50
+ for (const [key, value] of Object.entries(attributes)) {
51
+ const known = this.pending && key in this.pending ? this.pending[key] : this.sent[key];
52
+ if (known !== value) changes[key] = value;
53
+ }
54
+ if (Object.keys(changes).length === 0) return Promise.resolve();
55
+ this.pending = {
56
+ ...this.pending,
57
+ ...changes
58
+ };
59
+ if (this.inFlight || this.now() < this.holdUntil || !this.takeToken()) {
60
+ this.schedule();
61
+ return Promise.resolve();
62
+ }
63
+ return this.sendPending();
64
+ }
65
+
66
+ /** The visitor's identity changed (anonymous → signed in): Workstation may hold other values now. */
67
+ forgetSent() {
68
+ this.sent = {};
69
+ }
70
+
71
+ /** Sign-out or a different user: drop everything waiting so it can't reach the next visitor. */
72
+ reset() {
73
+ this.generation += 1;
74
+ this.clearTimer();
75
+ this.sent = {};
76
+ this.pending = null;
77
+ this.inFlight = false;
78
+ this.holdUntil = 0;
79
+ }
80
+ destroy() {
81
+ this.reset();
82
+ }
83
+
84
+ /** The send budget is per device, not per visitor, so reset() leaves it alone. */
85
+ takeToken() {
86
+ this.refill();
87
+ if (this.tokens < 1) return false;
88
+ this.tokens -= 1;
89
+ return true;
90
+ }
91
+ refill() {
92
+ const now = this.now();
93
+ if (this.tokens >= this.burst) {
94
+ this.refilledAt = now;
95
+ return;
96
+ }
97
+ const earned = Math.floor((now - this.refilledAt) / this.refillMs);
98
+ if (earned <= 0) return;
99
+ this.tokens = Math.min(this.burst, this.tokens + earned);
100
+ this.refilledAt = this.tokens === this.burst ? now : this.refilledAt + earned * this.refillMs;
101
+ }
102
+ async sendPending() {
103
+ const attributes = this.pending ?? {};
104
+ const generation = this.generation;
105
+ this.pending = null;
106
+ this.inFlight = true;
107
+ try {
108
+ await this.send(attributes);
109
+ if (generation !== this.generation) return;
110
+ this.sent = {
111
+ ...this.sent,
112
+ ...attributes
113
+ };
114
+ } catch (error) {
115
+ if (generation !== this.generation) throw error;
116
+ const status = error instanceof RepzoWidgetError ? error.status : 0;
117
+ const retryable = status === 429 || status === 0 || status >= 500;
118
+ if (retryable) {
119
+ // Newer values that arrived meanwhile win over the ones that failed.
120
+ this.pending = {
121
+ ...attributes,
122
+ ...(this.pending ?? {})
123
+ };
124
+ const retryAfter = error instanceof RepzoWidgetError ? error.retryAfter : undefined;
125
+ const holdMs = status === 429 && retryAfter && retryAfter > 0 ? retryAfter * 1000 : DEFAULT_HOLD_MS;
126
+ this.holdUntil = Math.max(this.holdUntil, this.now() + holdMs);
127
+ }
128
+ // Rate limited: the values are kept and go out later, so the call succeeded.
129
+ if (status !== 429) throw error;
130
+ } finally {
131
+ if (generation === this.generation) {
132
+ this.inFlight = false;
133
+ if (this.pending) this.schedule();
134
+ }
135
+ }
136
+ }
137
+ schedule() {
138
+ if (this.timer || this.inFlight || !this.pending) return;
139
+ this.refill();
140
+ const now = this.now();
141
+ const tokenAt = this.tokens >= 1 ? now : this.refilledAt + this.refillMs;
142
+ const delay = Math.max(0, this.holdUntil - now, tokenAt - now);
143
+ this.timer = setTimeout(() => {
144
+ this.timer = null;
145
+ this.flush();
146
+ }, delay);
147
+ }
148
+ flush() {
149
+ if (this.inFlight || !this.pending) return;
150
+ if (this.now() < this.holdUntil || !this.takeToken()) {
151
+ this.schedule();
152
+ return;
153
+ }
154
+ this.sendPending().catch(error => {
155
+ console.warn('[repzo-widget] updateUser failed', error);
156
+ });
157
+ }
158
+ clearTimer() {
159
+ if (this.timer) clearTimeout(this.timer);
160
+ this.timer = null;
161
+ }
162
+ }
163
+ //# sourceMappingURL=user-update-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["RepzoWidgetError","USER_UPDATE_BURST","USER_UPDATE_WINDOW_MS","DEFAULT_HOLD_MS","UserUpdateQueue","sent","pending","inFlight","holdUntil","timer","generation","constructor","send","options","burst","Math","max","refillMs","windowMs","now","Date","tokens","refilledAt","update","attributes","changes","key","value","Object","entries","known","keys","length","Promise","resolve","takeToken","schedule","sendPending","forgetSent","reset","clearTimer","destroy","refill","earned","floor","min","error","status","retryable","retryAfter","undefined","holdMs","tokenAt","delay","setTimeout","flush","catch","console","warn","clearTimeout"],"sourceRoot":"../../../src","sources":["state/user-update-queue.ts"],"mappings":"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","ignoreList":[]}
@@ -24,9 +24,10 @@ import { clearRetainedState } from "../ui/hooks/use-retained-state.js";
24
24
  import { canRetryMessage, isLocalMessage } from "../ui/utils/conversations.js";
25
25
  import { stripHtml } from "../ui/utils/html.js";
26
26
  import { setOpenUrlHandler } from "../ui/utils/safe-url.js";
27
+ import { UserUpdateQueue } from "./user-update-queue.js";
27
28
 
28
29
  /** Sent on every init; must match `package.json` (tests/version.test.js checks). */
29
- const SDK_VERSION = '@repzo/widget-react-native@1.4.0';
30
+ const SDK_VERSION = '@repzo/widget-react-native@1.5.0';
30
31
 
31
32
  /** The platform this session reports — mobile sessions get the Mobile SDKs overrides. */
32
33
  function sessionPlatform() {
@@ -124,6 +125,11 @@ const USER_SCOPED_STATE = {
124
125
  };
125
126
  export class WidgetStore {
126
127
  client = new WidgetApiClient();
128
+
129
+ /** Paces `updateUser()` — see `boot({ throttleUpdateUser })`. */
130
+ userUpdates = new UserUpdateQueue(customAttributes => this.client.identify({
131
+ customAttributes
132
+ }));
127
133
  events = new EventEmitter();
128
134
  push = new PushManager(this.client, conversationId => {
129
135
  void this.openConversation(conversationId);
@@ -144,6 +150,8 @@ export class WidgetStore {
144
150
  photoAlbumsEnabled = true;
145
151
  /** `boot({ youPrefix })` — "You:" before the visitor's own newest message in the lists. */
146
152
  youPrefixEnabled = true;
153
+ /** `boot({ throttleUpdateUser })` — skip unchanged `updateUser()` values and pace the rest. */
154
+ throttleUpdateUserEnabled = true;
147
155
 
148
156
  /** Posts whose view was reported this session (the server also dedupes per day). */
149
157
  viewedPostIds = new Set();
@@ -356,6 +364,9 @@ export class WidgetStore {
356
364
  }
357
365
  if (options.photoAlbums !== undefined) this.photoAlbumsEnabled = options.photoAlbums !== false;
358
366
  if (options.youPrefix !== undefined) this.youPrefixEnabled = options.youPrefix !== false;
367
+ if (options.throttleUpdateUser !== undefined) {
368
+ this.throttleUpdateUserEnabled = options.throttleUpdateUser !== false;
369
+ }
359
370
  if (options.posts !== undefined) {
360
371
  const wasEnabled = this.postsEnabled;
361
372
  this.postsEnabled = options.posts !== false;
@@ -526,6 +537,10 @@ export class WidgetStore {
526
537
  const onScreenId = this.state.activeConversationId;
527
538
  if (onScreenId && this.isThreadOnScreen(onScreenId)) delete unreadByConversation[onScreenId];
528
539
  this.persistUnread(unreadByConversation);
540
+
541
+ // A new identity (anonymous → signed in) may hold other attributes, so
542
+ // nothing is skipped as "already sent" for it.
543
+ if (result.identityId !== this.state.identityId) this.userUpdates.forgetSent();
529
544
  this.setState(current => ({
530
545
  booted: true,
531
546
  loading: false,
@@ -754,9 +769,13 @@ export class WidgetStore {
754
769
  if (!this.state.booted) await this.bootPromise;
755
770
  const customAttributes = sanitizeAttributes(attributes);
756
771
  if (Object.keys(customAttributes).length === 0) return;
757
- await this.client.identify({
758
- customAttributes
759
- });
772
+ if (!this.throttleUpdateUserEnabled) {
773
+ await this.client.identify({
774
+ customAttributes
775
+ });
776
+ return;
777
+ }
778
+ await this.userUpdates.update(customAttributes);
760
779
  }
761
780
 
762
781
  /**
@@ -768,6 +787,7 @@ export class WidgetStore {
768
787
  this.loggedOut = true;
769
788
  this.currentUser = null;
770
789
  this.pendingConversationId = null;
790
+ this.userUpdates.reset();
771
791
  this.realtime.close();
772
792
  this.clearTypingTimer();
773
793
  this.resetThreadTracking();
@@ -799,6 +819,8 @@ export class WidgetStore {
799
819
  }
800
820
  async wipeSessionUnguarded() {
801
821
  this.realtime.close();
822
+ // Attributes waiting for the old visitor must never reach the next one.
823
+ this.userUpdates.reset();
802
824
  this.clearTypingTimer();
803
825
  this.resetThreadTracking();
804
826
  this.appStateSubscription?.remove();
@@ -2609,6 +2631,7 @@ export class WidgetStore {
2609
2631
  // ─── Teardown (tests / hot reload) ──────────────────────────────────────
2610
2632
 
2611
2633
  destroy() {
2634
+ this.userUpdates.destroy();
2612
2635
  this.clearPostRecheck();
2613
2636
  this.clearPostThanks();
2614
2637
  this.realtime.close();