@veltdev/react 5.0.0-beta.2 → 5.0.0-beta.4

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.
@@ -1,5 +1,5 @@
1
1
  /// <reference types="react" />
2
- export declare const VELT_SDK_VERSION = "5.0.0-beta.2";
2
+ export declare const VELT_SDK_VERSION = "5.0.0-beta.4";
3
3
  export declare const VELT_SDK_INIT_EVENT = "onVeltInit";
4
4
  export declare const VELT_TAB_ID = "veltTabId";
5
5
  export declare const INTEGRITY_MAP: Record<string, string>;
@@ -1,2 +1,28 @@
1
- declare const loadVelt: (callback: Function, version?: string, staging?: boolean, develop?: boolean, proxyDomain?: string, integrity?: boolean, integrityValue?: string) => void;
1
+ /**
2
+ * Loads the Velt script dynamically and calls the callback when ready.
3
+ * Returns a cleanup function to remove the event listener if component unmounts.
4
+ *
5
+ * ## Issue (Rapid Mount/Unmount with Slow Network)
6
+ *
7
+ * When VeltProvider is rapidly mounted/unmounted (e.g., conditional rendering,
8
+ * React Strict Mode, or route changes) while the Velt script is still loading
9
+ * over a slow network, multiple issues can occur:
10
+ *
11
+ * 1. First component mount creates the script tag and starts loading
12
+ * 2. Component unmounts before script loads (script tag remains in DOM)
13
+ * 3. Second component mount finds existing script tag
14
+ * 4. OLD BEHAVIOR: Immediately triggered callback assuming script was loaded
15
+ * 5. PROBLEM: Script wasn't actually loaded yet (still loading due to network delay)
16
+ * 6. This caused initVelt to run with window.Velt = undefined
17
+ *
18
+ * ## Fix
19
+ *
20
+ * 1. Check if window.Velt exists before triggering callback for existing scripts
21
+ * 2. If script tag exists but window.Velt doesn't, attach a new load event listener
22
+ * 3. Return a cleanup function that removes the event listener when component unmounts
23
+ * 4. This prevents orphaned callbacks from firing on destroyed component instances
24
+ *
25
+ * @returns Cleanup function to remove event listener (call on component unmount)
26
+ */
27
+ declare const loadVelt: (callback: Function, version?: string, staging?: boolean, develop?: boolean, proxyDomain?: string, integrity?: boolean, integrityValue?: string) => (() => void);
2
28
  export default loadVelt;
package/esm/index.js CHANGED
@@ -92,12 +92,44 @@ function useVeltClient() {
92
92
  return useContext(VeltContext);
93
93
  }
94
94
 
95
+ /**
96
+ * Loads the Velt script dynamically and calls the callback when ready.
97
+ * Returns a cleanup function to remove the event listener if component unmounts.
98
+ *
99
+ * ## Issue (Rapid Mount/Unmount with Slow Network)
100
+ *
101
+ * When VeltProvider is rapidly mounted/unmounted (e.g., conditional rendering,
102
+ * React Strict Mode, or route changes) while the Velt script is still loading
103
+ * over a slow network, multiple issues can occur:
104
+ *
105
+ * 1. First component mount creates the script tag and starts loading
106
+ * 2. Component unmounts before script loads (script tag remains in DOM)
107
+ * 3. Second component mount finds existing script tag
108
+ * 4. OLD BEHAVIOR: Immediately triggered callback assuming script was loaded
109
+ * 5. PROBLEM: Script wasn't actually loaded yet (still loading due to network delay)
110
+ * 6. This caused initVelt to run with window.Velt = undefined
111
+ *
112
+ * ## Fix
113
+ *
114
+ * 1. Check if window.Velt exists before triggering callback for existing scripts
115
+ * 2. If script tag exists but window.Velt doesn't, attach a new load event listener
116
+ * 3. Return a cleanup function that removes the event listener when component unmounts
117
+ * 4. This prevents orphaned callbacks from firing on destroyed component instances
118
+ *
119
+ * @returns Cleanup function to remove event listener (call on component unmount)
120
+ */
95
121
  var loadVelt = function (callback, version, staging, develop, proxyDomain, integrity, integrityValue) {
96
122
  if (version === void 0) { version = 'latest'; }
97
123
  if (staging === void 0) { staging = false; }
98
124
  if (develop === void 0) { develop = false; }
99
125
  var existingScript = document.getElementById('veltScript');
126
+ // Store reference to the handler so we can remove it on cleanup.
127
+ // This is essential for proper cleanup - we need the exact same function
128
+ // reference to remove the event listener later.
129
+ var loadHandler = null;
130
+ var scriptElement = null;
100
131
  if (!existingScript) {
132
+ // No script tag exists - this is the first component to request Velt
101
133
  var script = document.createElement('script');
102
134
  if (staging) {
103
135
  script.src = "https://us-central1-snipply-sdk-staging.cloudfunctions.net/getprivatenpmpackagefile?packageName=sdk-staging&packageVersion=".concat((!version || version === 'latest') ? '1.0.1' : version, "&filePath=velt.js&orgName=@veltdev");
@@ -124,26 +156,61 @@ var loadVelt = function (callback, version, staging, develop, proxyDomain, integ
124
156
  script.crossOrigin = 'anonymous';
125
157
  }
126
158
  document.body.appendChild(script);
127
- script.onload = function () {
159
+ // Create handler and store reference for cleanup.
160
+ // We store the reference so we can remove this exact listener on cleanup.
161
+ loadHandler = function () {
128
162
  if (callback) {
129
163
  callback();
130
164
  }
131
165
  };
166
+ scriptElement = script;
167
+ script.addEventListener('load', loadHandler);
132
168
  }
133
169
  else {
134
- if (callback) {
135
- callback();
170
+ // Script tag already exists in DOM (created by a previous component instance).
171
+ // We need to check if it's actually finished loading or still in progress.
172
+ if (window.Velt) {
173
+ // Script has finished loading - window.Velt is available.
174
+ // Trigger callback directly without attaching any event listener.
175
+ if (callback) {
176
+ callback();
177
+ }
178
+ }
179
+ else {
180
+ // IMPORTANT: Script tag exists but window.Velt is undefined.
181
+ // This means the script is still loading (slow network scenario).
182
+ //
183
+ // Previous bug: We used to trigger callback immediately here,
184
+ // which caused initVelt to run with window.Velt = undefined.
185
+ //
186
+ // Fix: Attach a load event listener to wait for the script to finish loading.
187
+ // Using addEventListener (not onload) to avoid overwriting existing handlers.
188
+ loadHandler = function () {
189
+ if (callback) {
190
+ callback();
191
+ }
192
+ };
193
+ scriptElement = existingScript;
194
+ existingScript.addEventListener('load', loadHandler);
136
195
  }
137
196
  }
197
+ // Return cleanup function to remove event listener when component unmounts.
198
+ // This prevents the callback from firing on destroyed component instances,
199
+ // which would cause React state updates on unmounted components.
200
+ return function () {
201
+ if (loadHandler && scriptElement) {
202
+ scriptElement.removeEventListener('load', loadHandler);
203
+ }
204
+ };
138
205
  };
139
206
 
140
- var VELT_SDK_VERSION = '5.0.0-beta.2';
207
+ var VELT_SDK_VERSION = '5.0.0-beta.4';
141
208
  var VELT_SDK_INIT_EVENT = 'onVeltInit';
142
209
  var VELT_TAB_ID = 'veltTabId';
143
210
  // integrity map for the Velt SDK
144
211
  // Note: generate integrity hashes with: https://www.srihash.org/
145
212
  var INTEGRITY_MAP = {
146
- '5.0.0-beta.2': 'sha384-P/mj/JAXeU39QESjJFT6yJxpd5CIMt1DuS3dL4T5t5x0/163T/HONIKocpSIEXQ9',
213
+ '5.0.0-beta.4': 'sha384-3c/ckSQY9J/UDZeuhsbMw7BCZtjIL/P6Xx3zMHsk73bP7Lgy9F7Aij31avS1hEDE',
147
214
  };
148
215
 
149
216
  var validProps = ['veltIf', 'veltClass', 'className', 'variant'];
@@ -209,7 +276,43 @@ var SnippylyProvider = function (props) {
209
276
  var prevUserDataProviderRef = useRef(undefined);
210
277
  var prevDataProvidersRef = useRef(undefined);
211
278
  var prevPermissionProviderRef = useRef(undefined);
279
+ /**
280
+ * Tracks whether the component is currently mounted.
281
+ * Used to prevent state updates and async operations on unmounted components.
282
+ *
283
+ * ## Issue (Rapid Mount/Unmount)
284
+ * When VeltProvider is rapidly mounted/unmounted (e.g., conditional rendering
285
+ * based on state changes, React Strict Mode, or route transitions), multiple
286
+ * component instances can have async operations (like Velt.init()) in flight.
287
+ * When these async operations complete, they try to call setClient() on
288
+ * unmounted component instances, causing React warnings and incorrect state.
289
+ *
290
+ * ## Fix
291
+ * Track mount state and check it before/after async operations to bail out
292
+ * if the component has been unmounted.
293
+ */
294
+ var isMountedRef = useRef(true);
212
295
  useEffect(function () {
296
+ // IMPORTANT: Reset isMountedRef to true on every mount/remount.
297
+ // This is critical for React Strict Mode which simulates unmount/remount:
298
+ //
299
+ // React Strict Mode sequence:
300
+ // 1. Mount → isMountedRef = true (initial value)
301
+ // 2. Effect runs
302
+ // 3. Strict Mode simulates unmount → cleanup sets isMountedRef = false
303
+ // 4. Strict Mode simulates remount → effect runs again
304
+ //
305
+ // Without this reset, step 4 would still see isMountedRef = false from step 3,
306
+ // causing initVelt to skip even though the component is actually mounted.
307
+ isMountedRef.current = true;
308
+ // Cleanup: mark component as unmounted
309
+ return function () {
310
+ isMountedRef.current = false;
311
+ };
312
+ }, []);
313
+ useEffect(function () {
314
+ // Store cleanup function returned from loadVelt to remove event listeners on unmount
315
+ var cleanupLoadVelt;
213
316
  if (apiKey) {
214
317
  var staging = config === null || config === void 0 ? void 0 : config.staging;
215
318
  var develop = config === null || config === void 0 ? void 0 : config.develop;
@@ -228,10 +331,34 @@ var SnippylyProvider = function (props) {
228
331
  }
229
332
  }
230
333
  }
231
- loadVelt(function () {
334
+ // Rapid mount/unmount issue found in OpenEnvoy client code with slow network.
335
+ //
336
+ // Scenario: VeltProvider is conditionally rendered and toggles rapidly while
337
+ // the Velt script is loading over a slow network connection.
338
+ //
339
+ // We check if Velt is already loaded (window.Velt exists) and call initVelt
340
+ // directly. If not loaded, we call loadVelt which handles the script loading
341
+ // and returns a cleanup function to remove event listeners.
342
+ if (window === null || window === void 0 ? void 0 : window.Velt) {
232
343
  initVelt();
233
- }, version, staging, develop, config === null || config === void 0 ? void 0 : config.proxyDomain, integrity, integrityValue);
344
+ }
345
+ else {
346
+ // loadVelt returns a cleanup function that removes the load event listener.
347
+ // This prevents the callback from firing on destroyed component instances.
348
+ cleanupLoadVelt = loadVelt(function () {
349
+ initVelt();
350
+ }, version, staging, develop, config === null || config === void 0 ? void 0 : config.proxyDomain, integrity, integrityValue);
351
+ }
234
352
  }
353
+ // Cleanup: remove event listener when component unmounts.
354
+ // This is critical for rapid mount/unmount scenarios - without this cleanup,
355
+ // the load callback would fire on destroyed component instances and try to
356
+ // call initVelt, which would then try to update state on an unmounted component.
357
+ return function () {
358
+ if (cleanupLoadVelt) {
359
+ cleanupLoadVelt();
360
+ }
361
+ };
235
362
  }, []);
236
363
  useEffect(function () {
237
364
  if (!deepCompare(prevUserDataProviderRef.current, userDataProvider)) {
@@ -306,12 +433,29 @@ var SnippylyProvider = function (props) {
306
433
  }
307
434
  }
308
435
  }, [client, permissionProvider]);
436
+ /**
437
+ * Initializes the Velt SDK with proper mount state checks.
438
+ *
439
+ * ## Async Safety Pattern
440
+ * This function contains async operations (Velt.init). During the await,
441
+ * the component might unmount. We must check isMountedRef:
442
+ * 1. BEFORE starting - skip if already unmounted
443
+ * 2. AFTER await completes - abort if unmounted during the wait
444
+ *
445
+ * Without these checks, setClient() would be called on unmounted components,
446
+ * causing React warnings and potential memory leaks.
447
+ */
309
448
  var initVelt = function () { return __awaiter(void 0, void 0, void 0, function () {
310
449
  var velt, event;
311
450
  var _a, _b;
312
451
  return __generator(this, function (_c) {
313
452
  switch (_c.label) {
314
453
  case 0:
454
+ // CHECK 1: Skip initialization if component has already been unmounted.
455
+ // This handles the case where cleanup ran before initVelt was called.
456
+ if (!isMountedRef.current) {
457
+ return [2 /*return*/];
458
+ }
315
459
  if (!config) return [3 /*break*/, 2];
316
460
  if (config.staging) {
317
461
  delete config.staging;
@@ -334,6 +478,13 @@ var SnippylyProvider = function (props) {
334
478
  velt = _c.sent();
335
479
  _c.label = 4;
336
480
  case 4:
481
+ // CHECK 2: Abort if component unmounted during the await.
482
+ // Velt.init() is async and can take time. The component might have
483
+ // been unmounted while we were waiting. Check again before proceeding
484
+ // with state updates and side effects.
485
+ if (!isMountedRef.current) {
486
+ return [2 /*return*/];
487
+ }
337
488
  // Set language
338
489
  if (language && (velt === null || velt === void 0 ? void 0 : velt.setLanguage)) {
339
490
  velt === null || velt === void 0 ? void 0 : velt.setLanguage(language);