@veltdev/react 5.0.0-beta.1 → 5.0.0-beta.10

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 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
- script.onload = function () {
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
- if (callback) {
143
- callback();
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.1';
215
+ var VELT_SDK_VERSION = '5.0.0-beta.10';
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.1': 'sha384-fMN3ZF/Xls6kPb0BgrFjPT+Vy4pbJf6UttL5aF1sfdY0xfdp/kSpxdLeirA6H3II',
221
+ '5.0.0-beta.10': 'sha384-lfrhhXJI+7qwt11axkvxSE3P5wrcprWI2qU28Y3nIiSlfZ9E9QV9zAUsXffx7e01',
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);
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
+ }, []);
220
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
- loadVelt(function () {
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
- }, version, staging, develop, config === null || config === void 0 ? void 0 : config.proxyDomain, integrity, integrityValue);
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);
@@ -1625,8 +1776,8 @@ var VeltInlineReactionsSection = function (props) {
1625
1776
  };
1626
1777
 
1627
1778
  var VeltCommentComposer = function (props) {
1628
- var darkMode = props.darkMode, variant = props.variant, shadowDom = props.shadowDom, dialogVariant = props.dialogVariant, context = props.context, locationId = props.locationId, documentId = props.documentId, folderId = props.folderId;
1629
- return (React__default["default"].createElement("velt-comment-composer", { variant: variant, "dialog-variant": dialogVariant, "shadow-dom": [true, false].includes(shadowDom) ? (shadowDom ? 'true' : 'false') : undefined, "dark-mode": [true, false].includes(darkMode) ? (darkMode ? 'true' : 'false') : undefined, context: context ? JSON.stringify(context) : undefined, "location-id": locationId, "document-id": documentId, "folder-id": folderId }));
1779
+ var darkMode = props.darkMode, variant = props.variant, shadowDom = props.shadowDom, dialogVariant = props.dialogVariant, context = props.context, locationId = props.locationId, documentId = props.documentId, folderId = props.folderId, placeholder = props.placeholder, targetElementId = props.targetElementId;
1780
+ return (React__default["default"].createElement("velt-comment-composer", { variant: variant, "dialog-variant": dialogVariant, "shadow-dom": [true, false].includes(shadowDom) ? (shadowDom ? 'true' : 'false') : undefined, "dark-mode": [true, false].includes(darkMode) ? (darkMode ? 'true' : 'false') : undefined, context: context ? JSON.stringify(context) : undefined, "location-id": locationId, "document-id": documentId, "folder-id": folderId, placeholder: placeholder, "target-element-id": targetElementId }));
1630
1781
  };
1631
1782
 
1632
1783
  var VeltSingleEditorModePanel = function (props) {
@@ -1647,9 +1798,9 @@ var VeltVideoEditor = function (props) {
1647
1798
  };
1648
1799
 
1649
1800
  var VeltCommentDialog = function (props) {
1650
- var annotationId = props.annotationId, defaultCondition = props.defaultCondition, inlineCommentSectionMode = props.inlineCommentSectionMode, commentPinSelected = props.commentPinSelected, fullExpanded = props.fullExpanded, shadowDom = props.shadowDom, children = props.children;
1801
+ var annotationId = props.annotationId, defaultCondition = props.defaultCondition, inlineCommentSectionMode = props.inlineCommentSectionMode, commentPinSelected = props.commentPinSelected, fullExpanded = props.fullExpanded, shadowDom = props.shadowDom, placeholder = props.placeholder, children = props.children;
1651
1802
  var ref = React.useRef(null);
1652
- return (React__default["default"].createElement("velt-comment-dialog", { ref: ref, "annotation-id": annotationId, "default-condition": [true, false].includes(defaultCondition) ? (defaultCondition ? 'true' : 'false') : undefined, "inline-comment-section-mode": [true, false].includes(inlineCommentSectionMode) ? (inlineCommentSectionMode ? 'true' : 'false') : undefined, "comment-pin-selected": [true, false].includes(commentPinSelected) ? (commentPinSelected ? 'true' : 'false') : undefined, "full-expanded": [true, false].includes(fullExpanded) ? (fullExpanded ? 'true' : 'false') : undefined, "shadow-dom": [true, false].includes(shadowDom) ? (shadowDom ? 'true' : 'false') : undefined }, children));
1803
+ return (React__default["default"].createElement("velt-comment-dialog", { ref: ref, "annotation-id": annotationId, "default-condition": [true, false].includes(defaultCondition) ? (defaultCondition ? 'true' : 'false') : undefined, "inline-comment-section-mode": [true, false].includes(inlineCommentSectionMode) ? (inlineCommentSectionMode ? 'true' : 'false') : undefined, "comment-pin-selected": [true, false].includes(commentPinSelected) ? (commentPinSelected ? 'true' : 'false') : undefined, "full-expanded": [true, false].includes(fullExpanded) ? (fullExpanded ? 'true' : 'false') : undefined, "shadow-dom": [true, false].includes(shadowDom) ? (shadowDom ? 'true' : 'false') : undefined, placeholder: placeholder }, children));
1653
1804
  };
1654
1805
 
1655
1806
  var VeltCommentDialogContextWrapper = function (props) {
@@ -2378,9 +2529,9 @@ var VeltCommentDialogSuggestionActionReject$1 = function (props) {
2378
2529
  };
2379
2530
 
2380
2531
  var VeltCommentDialogComposer$1 = function (props) {
2381
- var annotationId = props.annotationId, defaultCondition = props.defaultCondition, inlineCommentSectionMode = props.inlineCommentSectionMode, editMode = props.editMode, commentObj = props.commentObj, commentIndex = props.commentIndex, composerWireframe = props.composerWireframe, placeholder = props.placeholder, commentplaceholder = props.commentplaceholder, replyplaceholder = props.replyplaceholder, editplaceholder = props.editplaceholder, children = props.children;
2532
+ var annotationId = props.annotationId, defaultCondition = props.defaultCondition, inlineCommentSectionMode = props.inlineCommentSectionMode, editMode = props.editMode, commentObj = props.commentObj, commentIndex = props.commentIndex, composerWireframe = props.composerWireframe, placeholder = props.placeholder, commentplaceholder = props.commentplaceholder, replyplaceholder = props.replyplaceholder, editplaceholder = props.editplaceholder, targetElementId = props.targetElementId, children = props.children;
2382
2533
  var ref = React.useRef(null);
2383
- return (React__default["default"].createElement("velt-comment-dialog-composer", { ref: ref, "annotation-id": annotationId, "default-condition": [true, false].includes(defaultCondition) ? (defaultCondition ? 'true' : 'false') : undefined, "inline-comment-section-mode": [true, false].includes(inlineCommentSectionMode) ? (inlineCommentSectionMode ? 'true' : 'false') : undefined, "edit-mode": [true, false].includes(editMode) ? (editMode ? 'true' : 'false') : undefined, "comment-obj": commentObj ? (typeof commentObj === 'string' ? commentObj : JSON.stringify(commentObj)) : undefined, "comment-index": commentIndex !== undefined ? commentIndex.toString() : undefined, "composer-wireframe": composerWireframe ? JSON.stringify(composerWireframe) : undefined, placeholder: placeholder, "comment-placeholder": commentplaceholder, "reply-placeholder": replyplaceholder, "edit-placeholder": editplaceholder }, children));
2534
+ return (React__default["default"].createElement("velt-comment-dialog-composer", { ref: ref, "annotation-id": annotationId, "default-condition": [true, false].includes(defaultCondition) ? (defaultCondition ? 'true' : 'false') : undefined, "inline-comment-section-mode": [true, false].includes(inlineCommentSectionMode) ? (inlineCommentSectionMode ? 'true' : 'false') : undefined, "edit-mode": [true, false].includes(editMode) ? (editMode ? 'true' : 'false') : undefined, "comment-obj": commentObj ? (typeof commentObj === 'string' ? commentObj : JSON.stringify(commentObj)) : undefined, "comment-index": commentIndex !== undefined ? commentIndex.toString() : undefined, "composer-wireframe": composerWireframe ? JSON.stringify(composerWireframe) : undefined, placeholder: placeholder, "comment-placeholder": commentplaceholder, "reply-placeholder": replyplaceholder, "edit-placeholder": editplaceholder, "target-element-id": targetElementId }, children));
2384
2535
  };
2385
2536
 
2386
2537
  var VeltCommentDialogComposerActionButton$1 = function (props) {
@@ -7665,6 +7816,23 @@ function useCommentSidebarData() {
7665
7816
  }, [commentElement === null || commentElement === void 0 ? void 0 : commentElement.onCommentSidebarData]);
7666
7817
  return data;
7667
7818
  }
7819
+ function useSetContextProvider() {
7820
+ var _a = React__default["default"].useState(undefined), data = _a[0], setData = _a[1];
7821
+ var commentUtils = useCommentUtils();
7822
+ React__default["default"].useEffect(function () {
7823
+ if ((commentUtils === null || commentUtils === void 0 ? void 0 : commentUtils.setContextProvider) && data) {
7824
+ commentUtils.setContextProvider(data);
7825
+ }
7826
+ }, [commentUtils === null || commentUtils === void 0 ? void 0 : commentUtils.setContextProvider, data]);
7827
+ // Memoize the setContextProvider callback
7828
+ var setContextProviderCallback = React__default["default"].useCallback(function (provider) {
7829
+ // Wrap in arrow function to prevent React from treating provider as a functional update
7830
+ setData(function () { return provider; });
7831
+ }, []); // Empty dependency array since it only uses setState which is stable
7832
+ return {
7833
+ setContextProvider: setContextProviderCallback
7834
+ };
7835
+ }
7668
7836
 
7669
7837
  function useAddCommentAnnotation() {
7670
7838
  var commentElement = useCommentUtils();
@@ -9419,6 +9587,7 @@ exports.useRecordings = useRecordings;
9419
9587
  exports.useRejectCommentAnnotation = useRejectCommentAnnotation;
9420
9588
  exports.useResolveCommentAnnotation = useResolveCommentAnnotation;
9421
9589
  exports.useServerConnectionStateChangeHandler = useServerConnectionStateChangeHandler;
9590
+ exports.useSetContextProvider = useSetContextProvider;
9422
9591
  exports.useSetDocument = useSetDocument;
9423
9592
  exports.useSetDocumentId = useSetDocumentId;
9424
9593
  exports.useSetDocuments = useSetDocuments;