@veltdev/react 5.0.0-beta.2 → 5.0.0-beta.3
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.
- package/cjs/index.js +158 -7
- package/cjs/index.js.map +1 -1
- package/cjs/types/constants.d.ts +1 -1
- package/cjs/types/loadSnippyly.d.ts +27 -1
- package/esm/index.js +158 -7
- package/esm/index.js.map +1 -1
- package/esm/types/constants.d.ts +1 -1
- package/esm/types/loadSnippyly.d.ts +27 -1
- package/package.json +1 -1
package/cjs/types/constants.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
|
-
export declare const VELT_SDK_VERSION = "5.0.0-beta.
|
|
2
|
+
export declare const VELT_SDK_VERSION = "5.0.0-beta.3";
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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.
|
|
207
|
+
var VELT_SDK_VERSION = '5.0.0-beta.3';
|
|
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.
|
|
213
|
+
'5.0.0-beta.3': 'sha384-3BZ1+ii93NtGRH5pECR6FaGd6IJpz4VUHE503QnPQcIIb+nuw6C0HGSxbt0D9h9x',
|
|
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
|
-
|
|
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
|
-
}
|
|
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);
|