@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/index.js
CHANGED
|
@@ -100,12 +100,44 @@ function useVeltClient() {
|
|
|
100
100
|
return React.useContext(VeltContext);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Loads the Velt script dynamically and calls the callback when ready.
|
|
105
|
+
* Returns a cleanup function to remove the event listener if component unmounts.
|
|
106
|
+
*
|
|
107
|
+
* ## Issue (Rapid Mount/Unmount with Slow Network)
|
|
108
|
+
*
|
|
109
|
+
* When VeltProvider is rapidly mounted/unmounted (e.g., conditional rendering,
|
|
110
|
+
* React Strict Mode, or route changes) while the Velt script is still loading
|
|
111
|
+
* over a slow network, multiple issues can occur:
|
|
112
|
+
*
|
|
113
|
+
* 1. First component mount creates the script tag and starts loading
|
|
114
|
+
* 2. Component unmounts before script loads (script tag remains in DOM)
|
|
115
|
+
* 3. Second component mount finds existing script tag
|
|
116
|
+
* 4. OLD BEHAVIOR: Immediately triggered callback assuming script was loaded
|
|
117
|
+
* 5. PROBLEM: Script wasn't actually loaded yet (still loading due to network delay)
|
|
118
|
+
* 6. This caused initVelt to run with window.Velt = undefined
|
|
119
|
+
*
|
|
120
|
+
* ## Fix
|
|
121
|
+
*
|
|
122
|
+
* 1. Check if window.Velt exists before triggering callback for existing scripts
|
|
123
|
+
* 2. If script tag exists but window.Velt doesn't, attach a new load event listener
|
|
124
|
+
* 3. Return a cleanup function that removes the event listener when component unmounts
|
|
125
|
+
* 4. This prevents orphaned callbacks from firing on destroyed component instances
|
|
126
|
+
*
|
|
127
|
+
* @returns Cleanup function to remove event listener (call on component unmount)
|
|
128
|
+
*/
|
|
103
129
|
var loadVelt = function (callback, version, staging, develop, proxyDomain, integrity, integrityValue) {
|
|
104
130
|
if (version === void 0) { version = 'latest'; }
|
|
105
131
|
if (staging === void 0) { staging = false; }
|
|
106
132
|
if (develop === void 0) { develop = false; }
|
|
107
133
|
var existingScript = document.getElementById('veltScript');
|
|
134
|
+
// Store reference to the handler so we can remove it on cleanup.
|
|
135
|
+
// This is essential for proper cleanup - we need the exact same function
|
|
136
|
+
// reference to remove the event listener later.
|
|
137
|
+
var loadHandler = null;
|
|
138
|
+
var scriptElement = null;
|
|
108
139
|
if (!existingScript) {
|
|
140
|
+
// No script tag exists - this is the first component to request Velt
|
|
109
141
|
var script = document.createElement('script');
|
|
110
142
|
if (staging) {
|
|
111
143
|
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");
|
|
@@ -132,26 +164,61 @@ var loadVelt = function (callback, version, staging, develop, proxyDomain, integ
|
|
|
132
164
|
script.crossOrigin = 'anonymous';
|
|
133
165
|
}
|
|
134
166
|
document.body.appendChild(script);
|
|
135
|
-
|
|
167
|
+
// Create handler and store reference for cleanup.
|
|
168
|
+
// We store the reference so we can remove this exact listener on cleanup.
|
|
169
|
+
loadHandler = function () {
|
|
136
170
|
if (callback) {
|
|
137
171
|
callback();
|
|
138
172
|
}
|
|
139
173
|
};
|
|
174
|
+
scriptElement = script;
|
|
175
|
+
script.addEventListener('load', loadHandler);
|
|
140
176
|
}
|
|
141
177
|
else {
|
|
142
|
-
|
|
143
|
-
|
|
178
|
+
// Script tag already exists in DOM (created by a previous component instance).
|
|
179
|
+
// We need to check if it's actually finished loading or still in progress.
|
|
180
|
+
if (window.Velt) {
|
|
181
|
+
// Script has finished loading - window.Velt is available.
|
|
182
|
+
// Trigger callback directly without attaching any event listener.
|
|
183
|
+
if (callback) {
|
|
184
|
+
callback();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// IMPORTANT: Script tag exists but window.Velt is undefined.
|
|
189
|
+
// This means the script is still loading (slow network scenario).
|
|
190
|
+
//
|
|
191
|
+
// Previous bug: We used to trigger callback immediately here,
|
|
192
|
+
// which caused initVelt to run with window.Velt = undefined.
|
|
193
|
+
//
|
|
194
|
+
// Fix: Attach a load event listener to wait for the script to finish loading.
|
|
195
|
+
// Using addEventListener (not onload) to avoid overwriting existing handlers.
|
|
196
|
+
loadHandler = function () {
|
|
197
|
+
if (callback) {
|
|
198
|
+
callback();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
scriptElement = existingScript;
|
|
202
|
+
existingScript.addEventListener('load', loadHandler);
|
|
144
203
|
}
|
|
145
204
|
}
|
|
205
|
+
// Return cleanup function to remove event listener when component unmounts.
|
|
206
|
+
// This prevents the callback from firing on destroyed component instances,
|
|
207
|
+
// which would cause React state updates on unmounted components.
|
|
208
|
+
return function () {
|
|
209
|
+
if (loadHandler && scriptElement) {
|
|
210
|
+
scriptElement.removeEventListener('load', loadHandler);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
146
213
|
};
|
|
147
214
|
|
|
148
|
-
var VELT_SDK_VERSION = '5.0.0-beta.
|
|
215
|
+
var VELT_SDK_VERSION = '5.0.0-beta.3';
|
|
149
216
|
var VELT_SDK_INIT_EVENT = 'onVeltInit';
|
|
150
217
|
var VELT_TAB_ID = 'veltTabId';
|
|
151
218
|
// integrity map for the Velt SDK
|
|
152
219
|
// Note: generate integrity hashes with: https://www.srihash.org/
|
|
153
220
|
var INTEGRITY_MAP = {
|
|
154
|
-
'5.0.0-beta.
|
|
221
|
+
'5.0.0-beta.3': 'sha384-3BZ1+ii93NtGRH5pECR6FaGd6IJpz4VUHE503QnPQcIIb+nuw6C0HGSxbt0D9h9x',
|
|
155
222
|
};
|
|
156
223
|
|
|
157
224
|
var validProps = ['veltIf', 'veltClass', 'className', 'variant'];
|
|
@@ -217,7 +284,43 @@ var SnippylyProvider = function (props) {
|
|
|
217
284
|
var prevUserDataProviderRef = React.useRef(undefined);
|
|
218
285
|
var prevDataProvidersRef = React.useRef(undefined);
|
|
219
286
|
var prevPermissionProviderRef = React.useRef(undefined);
|
|
287
|
+
/**
|
|
288
|
+
* Tracks whether the component is currently mounted.
|
|
289
|
+
* Used to prevent state updates and async operations on unmounted components.
|
|
290
|
+
*
|
|
291
|
+
* ## Issue (Rapid Mount/Unmount)
|
|
292
|
+
* When VeltProvider is rapidly mounted/unmounted (e.g., conditional rendering
|
|
293
|
+
* based on state changes, React Strict Mode, or route transitions), multiple
|
|
294
|
+
* component instances can have async operations (like Velt.init()) in flight.
|
|
295
|
+
* When these async operations complete, they try to call setClient() on
|
|
296
|
+
* unmounted component instances, causing React warnings and incorrect state.
|
|
297
|
+
*
|
|
298
|
+
* ## Fix
|
|
299
|
+
* Track mount state and check it before/after async operations to bail out
|
|
300
|
+
* if the component has been unmounted.
|
|
301
|
+
*/
|
|
302
|
+
var isMountedRef = React.useRef(true);
|
|
220
303
|
React.useEffect(function () {
|
|
304
|
+
// IMPORTANT: Reset isMountedRef to true on every mount/remount.
|
|
305
|
+
// This is critical for React Strict Mode which simulates unmount/remount:
|
|
306
|
+
//
|
|
307
|
+
// React Strict Mode sequence:
|
|
308
|
+
// 1. Mount → isMountedRef = true (initial value)
|
|
309
|
+
// 2. Effect runs
|
|
310
|
+
// 3. Strict Mode simulates unmount → cleanup sets isMountedRef = false
|
|
311
|
+
// 4. Strict Mode simulates remount → effect runs again
|
|
312
|
+
//
|
|
313
|
+
// Without this reset, step 4 would still see isMountedRef = false from step 3,
|
|
314
|
+
// causing initVelt to skip even though the component is actually mounted.
|
|
315
|
+
isMountedRef.current = true;
|
|
316
|
+
// Cleanup: mark component as unmounted
|
|
317
|
+
return function () {
|
|
318
|
+
isMountedRef.current = false;
|
|
319
|
+
};
|
|
320
|
+
}, []);
|
|
321
|
+
React.useEffect(function () {
|
|
322
|
+
// Store cleanup function returned from loadVelt to remove event listeners on unmount
|
|
323
|
+
var cleanupLoadVelt;
|
|
221
324
|
if (apiKey) {
|
|
222
325
|
var staging = config === null || config === void 0 ? void 0 : config.staging;
|
|
223
326
|
var develop = config === null || config === void 0 ? void 0 : config.develop;
|
|
@@ -236,10 +339,34 @@ var SnippylyProvider = function (props) {
|
|
|
236
339
|
}
|
|
237
340
|
}
|
|
238
341
|
}
|
|
239
|
-
|
|
342
|
+
// Rapid mount/unmount issue found in OpenEnvoy client code with slow network.
|
|
343
|
+
//
|
|
344
|
+
// Scenario: VeltProvider is conditionally rendered and toggles rapidly while
|
|
345
|
+
// the Velt script is loading over a slow network connection.
|
|
346
|
+
//
|
|
347
|
+
// We check if Velt is already loaded (window.Velt exists) and call initVelt
|
|
348
|
+
// directly. If not loaded, we call loadVelt which handles the script loading
|
|
349
|
+
// and returns a cleanup function to remove event listeners.
|
|
350
|
+
if (window === null || window === void 0 ? void 0 : window.Velt) {
|
|
240
351
|
initVelt();
|
|
241
|
-
}
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
// loadVelt returns a cleanup function that removes the load event listener.
|
|
355
|
+
// This prevents the callback from firing on destroyed component instances.
|
|
356
|
+
cleanupLoadVelt = loadVelt(function () {
|
|
357
|
+
initVelt();
|
|
358
|
+
}, version, staging, develop, config === null || config === void 0 ? void 0 : config.proxyDomain, integrity, integrityValue);
|
|
359
|
+
}
|
|
242
360
|
}
|
|
361
|
+
// Cleanup: remove event listener when component unmounts.
|
|
362
|
+
// This is critical for rapid mount/unmount scenarios - without this cleanup,
|
|
363
|
+
// the load callback would fire on destroyed component instances and try to
|
|
364
|
+
// call initVelt, which would then try to update state on an unmounted component.
|
|
365
|
+
return function () {
|
|
366
|
+
if (cleanupLoadVelt) {
|
|
367
|
+
cleanupLoadVelt();
|
|
368
|
+
}
|
|
369
|
+
};
|
|
243
370
|
}, []);
|
|
244
371
|
React.useEffect(function () {
|
|
245
372
|
if (!deepCompare(prevUserDataProviderRef.current, userDataProvider)) {
|
|
@@ -314,12 +441,29 @@ var SnippylyProvider = function (props) {
|
|
|
314
441
|
}
|
|
315
442
|
}
|
|
316
443
|
}, [client, permissionProvider]);
|
|
444
|
+
/**
|
|
445
|
+
* Initializes the Velt SDK with proper mount state checks.
|
|
446
|
+
*
|
|
447
|
+
* ## Async Safety Pattern
|
|
448
|
+
* This function contains async operations (Velt.init). During the await,
|
|
449
|
+
* the component might unmount. We must check isMountedRef:
|
|
450
|
+
* 1. BEFORE starting - skip if already unmounted
|
|
451
|
+
* 2. AFTER await completes - abort if unmounted during the wait
|
|
452
|
+
*
|
|
453
|
+
* Without these checks, setClient() would be called on unmounted components,
|
|
454
|
+
* causing React warnings and potential memory leaks.
|
|
455
|
+
*/
|
|
317
456
|
var initVelt = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
318
457
|
var velt, event;
|
|
319
458
|
var _a, _b;
|
|
320
459
|
return __generator(this, function (_c) {
|
|
321
460
|
switch (_c.label) {
|
|
322
461
|
case 0:
|
|
462
|
+
// CHECK 1: Skip initialization if component has already been unmounted.
|
|
463
|
+
// This handles the case where cleanup ran before initVelt was called.
|
|
464
|
+
if (!isMountedRef.current) {
|
|
465
|
+
return [2 /*return*/];
|
|
466
|
+
}
|
|
323
467
|
if (!config) return [3 /*break*/, 2];
|
|
324
468
|
if (config.staging) {
|
|
325
469
|
delete config.staging;
|
|
@@ -342,6 +486,13 @@ var SnippylyProvider = function (props) {
|
|
|
342
486
|
velt = _c.sent();
|
|
343
487
|
_c.label = 4;
|
|
344
488
|
case 4:
|
|
489
|
+
// CHECK 2: Abort if component unmounted during the await.
|
|
490
|
+
// Velt.init() is async and can take time. The component might have
|
|
491
|
+
// been unmounted while we were waiting. Check again before proceeding
|
|
492
|
+
// with state updates and side effects.
|
|
493
|
+
if (!isMountedRef.current) {
|
|
494
|
+
return [2 /*return*/];
|
|
495
|
+
}
|
|
345
496
|
// Set language
|
|
346
497
|
if (language && (velt === null || velt === void 0 ? void 0 : velt.setLanguage)) {
|
|
347
498
|
velt === null || velt === void 0 ? void 0 : velt.setLanguage(language);
|