@snowcone-app/ui 0.9.2 → 0.9.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.
package/dist/index.d.cts CHANGED
@@ -2928,6 +2928,10 @@ declare function useShopOptional(): ShopContextValue | undefined;
2928
2928
  interface MockupResult {
2929
2929
  mockupId: string;
2930
2930
  imageUrl: string;
2931
+ /** The canvas version this frame was rendered from (see getCanvasVersion). */
2932
+ requestVersion?: number;
2933
+ /** Per-placement versions folded into this frame (servers with per-placement stamps; see getLatestSentVersions). */
2934
+ versions?: Record<string, number>;
2931
2935
  }
2932
2936
  type RealtimeState = RealtimeContextValue;
2933
2937
  interface RealtimeContextValue {
@@ -3043,6 +3047,19 @@ interface RealtimeContextValue {
3043
3047
  * subsequent blobs to the same placement.
3044
3048
  */
3045
3049
  subscribeBlobSent: (callback: (placement: string) => void) => () => void;
3050
+ /**
3051
+ * The canvas version the session last sent (0 before the first edit
3052
+ * reaches the server). Every rendered result carries the `requestVersion`
3053
+ * it was rendered from: a picture whose version is below this one shows an
3054
+ * older design. Read it after a `subscribeBlobSent` notification.
3055
+ */
3056
+ getCanvasVersion: () => number;
3057
+ /**
3058
+ * The latest canvas version sent per placement. With a server that stamps
3059
+ * `versions` on every frame, a picture is current exactly when, for every
3060
+ * placement here, its frame's `versions[placement]` is at least this.
3061
+ */
3062
+ getLatestSentVersions: () => Record<string, number>;
3046
3063
  flushPendingBlobs: () => void;
3047
3064
  hasPendingBlobs: () => boolean;
3048
3065
  subscribePipelineSettled: (callback: () => void) => () => void;
package/dist/index.d.ts CHANGED
@@ -2928,6 +2928,10 @@ declare function useShopOptional(): ShopContextValue | undefined;
2928
2928
  interface MockupResult {
2929
2929
  mockupId: string;
2930
2930
  imageUrl: string;
2931
+ /** The canvas version this frame was rendered from (see getCanvasVersion). */
2932
+ requestVersion?: number;
2933
+ /** Per-placement versions folded into this frame (servers with per-placement stamps; see getLatestSentVersions). */
2934
+ versions?: Record<string, number>;
2931
2935
  }
2932
2936
  type RealtimeState = RealtimeContextValue;
2933
2937
  interface RealtimeContextValue {
@@ -3043,6 +3047,19 @@ interface RealtimeContextValue {
3043
3047
  * subsequent blobs to the same placement.
3044
3048
  */
3045
3049
  subscribeBlobSent: (callback: (placement: string) => void) => () => void;
3050
+ /**
3051
+ * The canvas version the session last sent (0 before the first edit
3052
+ * reaches the server). Every rendered result carries the `requestVersion`
3053
+ * it was rendered from: a picture whose version is below this one shows an
3054
+ * older design. Read it after a `subscribeBlobSent` notification.
3055
+ */
3056
+ getCanvasVersion: () => number;
3057
+ /**
3058
+ * The latest canvas version sent per placement. With a server that stamps
3059
+ * `versions` on every frame, a picture is current exactly when, for every
3060
+ * placement here, its frame's `versions[placement]` is at least this.
3061
+ */
3062
+ getLatestSentVersions: () => Record<string, number>;
3046
3063
  flushPendingBlobs: () => void;
3047
3064
  hasPendingBlobs: () => boolean;
3048
3065
  subscribePipelineSettled: (callback: () => void) => () => void;
package/dist/index.js CHANGED
@@ -5703,6 +5703,11 @@ async function compositeCroppedFrame(args) {
5703
5703
  import { jsx as jsx52 } from "react/jsx-runtime";
5704
5704
  var DEFAULT_ARTBOARD_SIZE = 2e3;
5705
5705
  var MAX_CONCURRENT_BLOBS = 5;
5706
+ function mergeMockupIds(current, extra) {
5707
+ const out = [...current];
5708
+ for (const id of extra) if (!out.includes(id)) out.push(id);
5709
+ return out;
5710
+ }
5706
5711
  var RealtimeContext = createContext8(
5707
5712
  void 0
5708
5713
  );
@@ -5781,6 +5786,12 @@ function RealtimeProvider({
5781
5786
  },
5782
5787
  onError: (error, detail) => {
5783
5788
  console.error(`[RealtimeProvider] Render error [${detail.code}]:`, error);
5789
+ if (detail.code.startsWith("E_RERENDER_") && pendingRerenderRef.current.length > 0) {
5790
+ const ids = pendingRerenderRef.current;
5791
+ pendingRerenderRef.current = [];
5792
+ forceRerenderByConfigRef.current?.(ids);
5793
+ return;
5794
+ }
5784
5795
  if (detail.mockupId) {
5785
5796
  const id = detail.mockupId;
5786
5797
  setUnavailableMockups((prev) => prev.get(id)?.code === detail.code ? prev : new Map(prev).set(id, detail));
@@ -5806,7 +5817,9 @@ function RealtimeProvider({
5806
5817
  disconnect,
5807
5818
  flushPendingBlobs,
5808
5819
  hasPendingBlobs,
5809
- getLastSentVersion
5820
+ getLastSentVersion,
5821
+ getLatestSentVersions,
5822
+ rerender: rerenderRaw
5810
5823
  } = realtimeMockup;
5811
5824
  const mockupResultsRef = useRef12([]);
5812
5825
  const mockupSubscribersRef = useRef12(
@@ -5843,6 +5856,18 @@ function RealtimeProvider({
5843
5856
  const compositeQueueRef = useRef12(/* @__PURE__ */ new Map());
5844
5857
  const compositeBusyRef = useRef12(/* @__PURE__ */ new Set());
5845
5858
  const compositeCanvasRef = useRef12(null);
5859
+ const requestedMockupIdsRef = useRef12([]);
5860
+ const serverStampsVersionsRef = useRef12(false);
5861
+ const pendingRerenderRef = useRef12([]);
5862
+ const forceRerenderByConfigRef = useRef12(null);
5863
+ const fullFrameRerenderIdsRef = useRef12(/* @__PURE__ */ new Set());
5864
+ const fullFrameRerenderTimerRef = useRef12(null);
5865
+ useEffect17(
5866
+ () => () => {
5867
+ if (fullFrameRerenderTimerRef.current) clearTimeout(fullFrameRerenderTimerRef.current);
5868
+ },
5869
+ []
5870
+ );
5846
5871
  const registerBlobUrl = useCallback10((blobOrUrl, placementName) => {
5847
5872
  let url;
5848
5873
  if (blobOrUrl instanceof Blob) {
@@ -5874,6 +5899,7 @@ function RealtimeProvider({
5874
5899
  const base = lastConfigBaseRef.current;
5875
5900
  if (!base) return;
5876
5901
  croppedActiveRef.current = withCrop;
5902
+ requestedMockupIdsRef.current = base.mockupIds ?? [];
5877
5903
  sendConfigRaw({
5878
5904
  ...base,
5879
5905
  placementSettings: { ...placementSettingsOverridesRef.current },
@@ -5886,6 +5912,65 @@ function RealtimeProvider({
5886
5912
  if (!croppedActiveRef.current) return;
5887
5913
  sendCropConfig(false);
5888
5914
  }, [sendCropConfig]);
5915
+ const sendFullFrameConfig = useCallback10(
5916
+ (mockupIds) => {
5917
+ const base = lastConfigBaseRef.current;
5918
+ if (!base) return;
5919
+ sendConfigRaw({
5920
+ ...base,
5921
+ mockupIds,
5922
+ placementSettings: { ...placementSettingsOverridesRef.current }
5923
+ });
5924
+ },
5925
+ [sendConfigRaw]
5926
+ );
5927
+ const forceRerenderByConfig = useCallback10(
5928
+ (failed) => {
5929
+ const inForce = requestedMockupIdsRef.current;
5930
+ const fp = (list) => [...list].sort().join(",");
5931
+ const step = fp(failed) !== fp(inForce) ? failed : inForce.slice(0, -1);
5932
+ if (step.length === 0) {
5933
+ console.error(
5934
+ `[RealtimeProvider] E_OVERLAY_RERENDER_UNAVAILABLE: cannot force a full-frame re-render of ${failed.join(", ")} (single-mockup session on a box without \`rerender\`); it updates on the next edit.`
5935
+ );
5936
+ sendFullFrameConfig(inForce);
5937
+ return;
5938
+ }
5939
+ sendFullFrameConfig(step);
5940
+ sendFullFrameConfig(inForce);
5941
+ },
5942
+ [sendFullFrameConfig]
5943
+ );
5944
+ forceRerenderByConfigRef.current = forceRerenderByConfig;
5945
+ const rerenderFullFrame = useCallback10(
5946
+ (mockupId) => {
5947
+ fullFrameRerenderIdsRef.current.add(mockupId);
5948
+ if (fullFrameRerenderTimerRef.current) return;
5949
+ fullFrameRerenderTimerRef.current = setTimeout(() => {
5950
+ fullFrameRerenderTimerRef.current = null;
5951
+ const failed = [...fullFrameRerenderIdsRef.current];
5952
+ fullFrameRerenderIdsRef.current.clear();
5953
+ if (!lastConfigBaseRef.current || failed.length === 0) return;
5954
+ const previous = requestedMockupIdsRef.current;
5955
+ const ids = mergeMockupIds(previous, failed);
5956
+ croppedActiveRef.current = false;
5957
+ requestedMockupIdsRef.current = ids;
5958
+ const fp = (list) => [...list].sort().join(",");
5959
+ if (fp(ids) !== fp(previous)) {
5960
+ sendFullFrameConfig(ids);
5961
+ return;
5962
+ }
5963
+ if (serverStampsVersionsRef.current) {
5964
+ sendFullFrameConfig(ids);
5965
+ pendingRerenderRef.current = failed;
5966
+ if (rerenderRaw(failed)) return;
5967
+ pendingRerenderRef.current = [];
5968
+ }
5969
+ forceRerenderByConfig(failed);
5970
+ }, 0);
5971
+ },
5972
+ [sendFullFrameConfig, forceRerenderByConfig, rerenderRaw]
5973
+ );
5889
5974
  const maybeFlipCrop = useCallback10(() => {
5890
5975
  if (!croppedDeliveryEnabled || !cropCapableRef.current) return;
5891
5976
  if (croppedActiveRef.current) return;
@@ -5919,6 +6004,10 @@ function RealtimeProvider({
5919
6004
  else merged.push(result);
5920
6005
  });
5921
6006
  mockupResultsRef.current = merged;
6007
+ if (pendingRerenderRef.current.length > 0) {
6008
+ const delivered = new Set(results.map((r) => r.mockupId));
6009
+ pendingRerenderRef.current = pendingRerenderRef.current.filter((id) => !delivered.has(id));
6010
+ }
5922
6011
  const clearedPlacements = /* @__PURE__ */ new Set();
5923
6012
  results.forEach((result) => {
5924
6013
  const wasPending = pendingMockupIdsRef.current.has(result.mockupId);
@@ -5985,8 +6074,7 @@ function RealtimeProvider({
5985
6074
  console.error(
5986
6075
  `[RealtimeProvider] E_OVERLAY_NO_BACKGROUND: cropped frame for ${result.mockupId} has no frozen background \u2014 falling back to full-frame and re-rendering it.`
5987
6076
  );
5988
- unflipCrop();
5989
- updateMockupIdsRaw([result.mockupId]);
6077
+ rerenderFullFrame(result.mockupId);
5990
6078
  return;
5991
6079
  }
5992
6080
  try {
@@ -6004,16 +6092,18 @@ function RealtimeProvider({
6004
6092
  if (gen !== sceneGenRef.current) return;
6005
6093
  deliverResults([{ ...result, imageUrl: registerBlobUrl(blob) }]);
6006
6094
  } catch (err) {
6095
+ if (gen !== sceneGenRef.current || compositeQueueRef.current.has(result.mockupId) || processedFrameRef.current.get(result.mockupId) !== result.imageUrl) {
6096
+ return;
6097
+ }
6007
6098
  console.error(
6008
6099
  "[RealtimeProvider] E_OVERLAY_COMPOSITE_FAILED: cropped-frame composite failed \u2014 full-frame delivery for the rest of this session.",
6009
6100
  err
6010
6101
  );
6011
6102
  cropCapableRef.current = false;
6012
- unflipCrop();
6013
- updateMockupIdsRaw([result.mockupId]);
6103
+ rerenderFullFrame(result.mockupId);
6014
6104
  }
6015
6105
  },
6016
- [deliverResults, registerBlobUrl, unflipCrop, updateMockupIdsRaw]
6106
+ [deliverResults, registerBlobUrl, rerenderFullFrame]
6017
6107
  );
6018
6108
  const pumpCompositeQueue = useCallback10(
6019
6109
  (mockupId) => {
@@ -6039,6 +6129,9 @@ function RealtimeProvider({
6039
6129
  }
6040
6130
  console.log(`[RTP] rawMockupResults changed: ${rawMockupResults.length} result(s)`, rawMockupResults.map((r) => `${r.mockupId}${r.imageUrl ? " \u2713url" : " \u2717NO-URL"}${r.cropRect ? " [crop]" : ""}`));
6041
6131
  const displayable = [];
6132
+ if (!serverStampsVersionsRef.current && rawMockupResults.some((r) => r.versions && typeof r.versions === "object")) {
6133
+ serverStampsVersionsRef.current = true;
6134
+ }
6042
6135
  rawMockupResults.forEach((result) => {
6043
6136
  if (!result.imageUrl) {
6044
6137
  console.log(`[RTP] Result for ${result.mockupId} has NO imageUrl \u2014 not caching, pending NOT cleared`);
@@ -6201,6 +6294,7 @@ function RealtimeProvider({
6201
6294
  });
6202
6295
  }
6203
6296
  lastConfigBaseRef.current = configPayload;
6297
+ requestedMockupIdsRef.current = mockupIds;
6204
6298
  sendConfigRaw(
6205
6299
  croppedActiveRef.current ? { ...configPayload, overlayCrop: "auto" } : configPayload
6206
6300
  );
@@ -6260,6 +6354,7 @@ function RealtimeProvider({
6260
6354
  if (croppedActiveRef.current && mockupIds.some((id) => !backgroundsRef.current.has(id))) {
6261
6355
  unflipCrop();
6262
6356
  }
6357
+ requestedMockupIdsRef.current = [...mockupIds];
6263
6358
  return updateMockupIdsRaw(mockupIds);
6264
6359
  },
6265
6360
  [updateMockupIdsRaw, unflipCrop]
@@ -6516,6 +6611,12 @@ function RealtimeProvider({
6516
6611
  },
6517
6612
  []
6518
6613
  );
6614
+ const getLastSentVersionRef = useRef12(getLastSentVersion);
6615
+ getLastSentVersionRef.current = getLastSentVersion;
6616
+ const getCanvasVersion = useCallback10(() => getLastSentVersionRef.current?.() ?? 0, []);
6617
+ const getLatestSentVersionsRef = useRef12(getLatestSentVersions);
6618
+ getLatestSentVersionsRef.current = getLatestSentVersions;
6619
+ const getLatestSentVersionsStable = useCallback10(() => getLatestSentVersionsRef.current?.() ?? {}, []);
6519
6620
  const stableSubscribePipelineSettled = useCallback10(
6520
6621
  (callback) => {
6521
6622
  return subscribePipelineSettledRef.current(callback);
@@ -6564,6 +6665,8 @@ function RealtimeProvider({
6564
6665
  // Centralized URL management
6565
6666
  getMockupUrl: getMockupUrl3,
6566
6667
  getMockupUrls,
6668
+ getCanvasVersion,
6669
+ getLatestSentVersions: getLatestSentVersionsStable,
6567
6670
  isMockupLoading,
6568
6671
  registerBlobUrl,
6569
6672
  // WebRTC streaming
@@ -6595,6 +6698,8 @@ function RealtimeProvider({
6595
6698
  hasPendingBlobs,
6596
6699
  getMockupUrl3,
6597
6700
  getMockupUrls,
6701
+ getCanvasVersion,
6702
+ getLatestSentVersionsStable,
6598
6703
  isMockupLoading,
6599
6704
  registerBlobUrl
6600
6705
  ]
@@ -7471,17 +7576,33 @@ function MockupPriorityProvider({ children }) {
7471
7576
  if (!realtimeContext?.updateMockupIds) {
7472
7577
  return;
7473
7578
  }
7579
+ const mobile = mobileCarouselRef.current;
7580
+ const universe = mobile && mobile.mockupIds.length > 0 ? mobile.mockupIds : registrationOrderRef.current;
7581
+ const inView = [];
7582
+ if (level1MockupRef.current) {
7583
+ inView.push(level1MockupRef.current);
7584
+ if (!mobile || mobile.mockupIds.length === 0) {
7585
+ visibilityMapRef.current.forEach((info, id) => {
7586
+ if (info.visibleArea > 0 && !inView.includes(id)) inView.push(id);
7587
+ });
7588
+ }
7589
+ }
7474
7590
  let mockupIds;
7475
7591
  switch (level) {
7476
7592
  case 1:
7477
- mockupIds = level1MockupRef.current ? [level1MockupRef.current] : [];
7593
+ mockupIds = inView;
7478
7594
  break;
7479
7595
  case 2:
7480
- mockupIds = [...level2MockupsRef.current];
7596
+ mockupIds = level2MockupsRef.current.filter((id) => !inView.includes(id));
7481
7597
  break;
7482
- case 3:
7483
- mockupIds = [...level3QueueRef.current];
7598
+ case 3: {
7599
+ const earlier = /* @__PURE__ */ new Set([...inView, ...level2MockupsRef.current]);
7600
+ mockupIds = [
7601
+ ...level3QueueRef.current.filter((id) => !earlier.has(id)),
7602
+ ...universe.filter((id) => !earlier.has(id) && !level3QueueRef.current.includes(id))
7603
+ ];
7484
7604
  break;
7605
+ }
7485
7606
  default:
7486
7607
  mockupIds = [];
7487
7608
  }
@@ -7506,6 +7627,16 @@ function MockupPriorityProvider({ children }) {
7506
7627
  const isStillDragging = timeSinceLastPendingBlob < DRAG_STOP_DEBOUNCE_MS;
7507
7628
  if (currentLevel === 1 && isStillDragging) {
7508
7629
  currentRenderingLevelRef.current = null;
7630
+ if (levelAdvanceTimeoutRef.current) {
7631
+ clearTimeout(levelAdvanceTimeoutRef.current);
7632
+ }
7633
+ levelAdvanceTimeoutRef.current = setTimeout(() => {
7634
+ levelAdvanceTimeoutRef.current = null;
7635
+ const quiet = Date.now() - lastPendingBlobTimeRef.current >= DRAG_STOP_DEBOUNCE_MS;
7636
+ if (quiet && currentRenderingLevelRef.current === null && pendingMockupsRef.current.size === 0) {
7637
+ requestMockupsForLevel(2);
7638
+ }
7639
+ }, DRAG_STOP_DEBOUNCE_MS - timeSinceLastPendingBlob + 10);
7509
7640
  return;
7510
7641
  }
7511
7642
  if (isStillDragging) {
@@ -10363,6 +10494,57 @@ var HeroProductImage = memo3(function HeroProductImage2({
10363
10494
  unsubscribe?.();
10364
10495
  };
10365
10496
  }, [mode, realtimeContext?.subscribePendingBlob, placement]);
10497
+ const renderVersionRef = useRef23(0);
10498
+ const renderVersionsRef = useRef23(null);
10499
+ const getLatestSentVersions = realtimeContext?.getLatestSentVersions;
10500
+ const refusalCodeRef = useRef23(null);
10501
+ refusalCodeRef.current = unavailable?.code ?? null;
10502
+ const sessionErrorCodeRef = useRef23(null);
10503
+ sessionErrorCodeRef.current = realtimeContext?.renderError?.code ?? null;
10504
+ const getCanvasVersion = realtimeContext?.getCanvasVersion;
10505
+ const applyRenderStamp = useCallback18(() => {
10506
+ const node = containerRef.current;
10507
+ if (!node) return;
10508
+ const canvas = getCanvasVersion?.() ?? 0;
10509
+ if (canvas <= 0) {
10510
+ delete node.dataset.canvasVersion;
10511
+ delete node.dataset.renderVersion;
10512
+ delete node.dataset.renderState;
10513
+ return;
10514
+ }
10515
+ const rendered = renderVersionRef.current;
10516
+ const versions = renderVersionsRef.current;
10517
+ const sent = versions ? getLatestSentVersions?.() : void 0;
10518
+ const behind = versions && sent ? Object.entries(sent).some(([placement2, v]) => (versions[placement2] ?? -1) < v) : rendered < canvas;
10519
+ node.dataset.canvasVersion = String(canvas);
10520
+ node.dataset.renderVersion = String(rendered);
10521
+ node.dataset.renderState = refusalCodeRef.current || behind && sessionErrorCodeRef.current ? "refused" : behind ? "stale" : "current";
10522
+ }, [getCanvasVersion, getLatestSentVersions]);
10523
+ useEffect32(() => {
10524
+ applyRenderStamp();
10525
+ });
10526
+ useEffect32(() => {
10527
+ const subscribeSent = realtimeContext?.subscribeBlobSent;
10528
+ const subscribeResult = realtimeContext?.subscribeMockupResultById;
10529
+ if (!actualMockupId || !subscribeSent || !subscribeResult) return;
10530
+ const offSent = subscribeSent(() => applyRenderStamp());
10531
+ const offResult = subscribeResult(actualMockupId, (result) => {
10532
+ if (typeof result?.requestVersion === "number" && result.requestVersion >= renderVersionRef.current) {
10533
+ renderVersionRef.current = result.requestVersion;
10534
+ renderVersionsRef.current = result.versions ?? null;
10535
+ }
10536
+ applyRenderStamp();
10537
+ });
10538
+ return () => {
10539
+ offSent?.();
10540
+ offResult?.();
10541
+ };
10542
+ }, [
10543
+ actualMockupId,
10544
+ realtimeContext?.subscribeBlobSent,
10545
+ realtimeContext?.subscribeMockupResultById,
10546
+ applyRenderStamp
10547
+ ]);
10366
10548
  useEffect32(() => {
10367
10549
  if (!priorityContext || !actualMockupId || !placement) return;
10368
10550
  priorityContext.registerMockup(actualMockupId, placement, initialPriority);