@snowcone-app/ui 0.9.3 → 0.9.5

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
@@ -2930,6 +2930,8 @@ interface MockupResult {
2930
2930
  imageUrl: string;
2931
2931
  /** The canvas version this frame was rendered from (see getCanvasVersion). */
2932
2932
  requestVersion?: number;
2933
+ /** Per-placement versions folded into this frame (servers with per-placement stamps; see getLatestSentVersions). */
2934
+ versions?: Record<string, number>;
2933
2935
  }
2934
2936
  type RealtimeState = RealtimeContextValue;
2935
2937
  interface RealtimeContextValue {
@@ -3052,6 +3054,12 @@ interface RealtimeContextValue {
3052
3054
  * older design. Read it after a `subscribeBlobSent` notification.
3053
3055
  */
3054
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>;
3055
3063
  flushPendingBlobs: () => void;
3056
3064
  hasPendingBlobs: () => boolean;
3057
3065
  subscribePipelineSettled: (callback: () => void) => () => void;
package/dist/index.d.ts CHANGED
@@ -2930,6 +2930,8 @@ interface MockupResult {
2930
2930
  imageUrl: string;
2931
2931
  /** The canvas version this frame was rendered from (see getCanvasVersion). */
2932
2932
  requestVersion?: number;
2933
+ /** Per-placement versions folded into this frame (servers with per-placement stamps; see getLatestSentVersions). */
2934
+ versions?: Record<string, number>;
2933
2935
  }
2934
2936
  type RealtimeState = RealtimeContextValue;
2935
2937
  interface RealtimeContextValue {
@@ -3052,6 +3054,12 @@ interface RealtimeContextValue {
3052
3054
  * older design. Read it after a `subscribeBlobSent` notification.
3053
3055
  */
3054
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>;
3055
3063
  flushPendingBlobs: () => void;
3056
3064
  hasPendingBlobs: () => boolean;
3057
3065
  subscribePipelineSettled: (callback: () => void) => () => void;
package/dist/index.js CHANGED
@@ -5703,6 +5703,7 @@ 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
+ var FRAME_SUPERSEDE_GRACE_MS = 2e3;
5706
5707
  function mergeMockupIds(current, extra) {
5707
5708
  const out = [...current];
5708
5709
  for (const id of extra) if (!out.includes(id)) out.push(id);
@@ -5786,6 +5787,12 @@ function RealtimeProvider({
5786
5787
  },
5787
5788
  onError: (error, detail) => {
5788
5789
  console.error(`[RealtimeProvider] Render error [${detail.code}]:`, error);
5790
+ if (detail.code.startsWith("E_RERENDER_") && pendingRerenderRef.current.length > 0) {
5791
+ const ids = pendingRerenderRef.current;
5792
+ pendingRerenderRef.current = [];
5793
+ forceRerenderByConfigRef.current?.(ids);
5794
+ return;
5795
+ }
5789
5796
  if (detail.mockupId) {
5790
5797
  const id = detail.mockupId;
5791
5798
  setUnavailableMockups((prev) => prev.get(id)?.code === detail.code ? prev : new Map(prev).set(id, detail));
@@ -5811,7 +5818,9 @@ function RealtimeProvider({
5811
5818
  disconnect,
5812
5819
  flushPendingBlobs,
5813
5820
  hasPendingBlobs,
5814
- getLastSentVersion
5821
+ getLastSentVersion,
5822
+ getLatestSentVersions,
5823
+ rerender: rerenderRaw
5815
5824
  } = realtimeMockup;
5816
5825
  const mockupResultsRef = useRef12([]);
5817
5826
  const mockupSubscribersRef = useRef12(
@@ -5849,6 +5858,9 @@ function RealtimeProvider({
5849
5858
  const compositeBusyRef = useRef12(/* @__PURE__ */ new Set());
5850
5859
  const compositeCanvasRef = useRef12(null);
5851
5860
  const requestedMockupIdsRef = useRef12([]);
5861
+ const serverStampsVersionsRef = useRef12(false);
5862
+ const pendingRerenderRef = useRef12([]);
5863
+ const forceRerenderByConfigRef = useRef12(null);
5852
5864
  const fullFrameRerenderIdsRef = useRef12(/* @__PURE__ */ new Set());
5853
5865
  const fullFrameRerenderTimerRef = useRef12(null);
5854
5866
  useEffect17(
@@ -5901,6 +5913,36 @@ function RealtimeProvider({
5901
5913
  if (!croppedActiveRef.current) return;
5902
5914
  sendCropConfig(false);
5903
5915
  }, [sendCropConfig]);
5916
+ const sendFullFrameConfig = useCallback10(
5917
+ (mockupIds) => {
5918
+ const base = lastConfigBaseRef.current;
5919
+ if (!base) return;
5920
+ sendConfigRaw({
5921
+ ...base,
5922
+ mockupIds,
5923
+ placementSettings: { ...placementSettingsOverridesRef.current }
5924
+ });
5925
+ },
5926
+ [sendConfigRaw]
5927
+ );
5928
+ const forceRerenderByConfig = useCallback10(
5929
+ (failed) => {
5930
+ const inForce = requestedMockupIdsRef.current;
5931
+ const fp = (list) => [...list].sort().join(",");
5932
+ const step = fp(failed) !== fp(inForce) ? failed : inForce.slice(0, -1);
5933
+ if (step.length === 0) {
5934
+ console.error(
5935
+ `[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.`
5936
+ );
5937
+ sendFullFrameConfig(inForce);
5938
+ return;
5939
+ }
5940
+ sendFullFrameConfig(step);
5941
+ sendFullFrameConfig(inForce);
5942
+ },
5943
+ [sendFullFrameConfig]
5944
+ );
5945
+ forceRerenderByConfigRef.current = forceRerenderByConfig;
5904
5946
  const rerenderFullFrame = useCallback10(
5905
5947
  (mockupId) => {
5906
5948
  fullFrameRerenderIdsRef.current.add(mockupId);
@@ -5909,35 +5951,26 @@ function RealtimeProvider({
5909
5951
  fullFrameRerenderTimerRef.current = null;
5910
5952
  const failed = [...fullFrameRerenderIdsRef.current];
5911
5953
  fullFrameRerenderIdsRef.current.clear();
5912
- const base = lastConfigBaseRef.current;
5913
- if (!base || failed.length === 0) return;
5954
+ if (!lastConfigBaseRef.current || failed.length === 0) return;
5914
5955
  const previous = requestedMockupIdsRef.current;
5915
5956
  const ids = mergeMockupIds(previous, failed);
5916
5957
  croppedActiveRef.current = false;
5917
5958
  requestedMockupIdsRef.current = ids;
5918
- const send = (mockupIds) => sendConfigRaw({
5919
- ...base,
5920
- mockupIds,
5921
- placementSettings: { ...placementSettingsOverridesRef.current }
5922
- });
5923
- const fingerprint = (list) => [...list].sort().join(",");
5924
- if (fingerprint(ids) !== fingerprint(previous)) {
5925
- send(ids);
5959
+ const fp = (list) => [...list].sort().join(",");
5960
+ if (fp(ids) !== fp(previous)) {
5961
+ sendFullFrameConfig(ids);
5926
5962
  return;
5927
5963
  }
5928
- const step = fingerprint(failed) !== fingerprint(previous) ? failed : ids.slice(0, -1);
5929
- if (step.length === 0) {
5930
- console.error(
5931
- `[RealtimeProvider] E_OVERLAY_RERENDER_UNAVAILABLE: cannot force a full-frame re-render of ${failed.join(", ")} (single-mockup session); it updates on the next edit.`
5932
- );
5933
- send(ids);
5934
- return;
5964
+ if (serverStampsVersionsRef.current) {
5965
+ sendFullFrameConfig(ids);
5966
+ pendingRerenderRef.current = failed;
5967
+ if (rerenderRaw(failed)) return;
5968
+ pendingRerenderRef.current = [];
5935
5969
  }
5936
- send(step);
5937
- send(ids);
5970
+ forceRerenderByConfig(failed);
5938
5971
  }, 0);
5939
5972
  },
5940
- [sendConfigRaw]
5973
+ [sendFullFrameConfig, forceRerenderByConfig, rerenderRaw]
5941
5974
  );
5942
5975
  const maybeFlipCrop = useCallback10(() => {
5943
5976
  if (!croppedDeliveryEnabled || !cropCapableRef.current) return;
@@ -5972,6 +6005,10 @@ function RealtimeProvider({
5972
6005
  else merged.push(result);
5973
6006
  });
5974
6007
  mockupResultsRef.current = merged;
6008
+ if (pendingRerenderRef.current.length > 0) {
6009
+ const delivered = new Set(results.map((r) => r.mockupId));
6010
+ pendingRerenderRef.current = pendingRerenderRef.current.filter((id) => !delivered.has(id));
6011
+ }
5975
6012
  const clearedPlacements = /* @__PURE__ */ new Set();
5976
6013
  results.forEach((result) => {
5977
6014
  const wasPending = pendingMockupIdsRef.current.has(result.mockupId);
@@ -6059,6 +6096,22 @@ function RealtimeProvider({
6059
6096
  if (gen !== sceneGenRef.current || compositeQueueRef.current.has(result.mockupId) || processedFrameRef.current.get(result.mockupId) !== result.imageUrl) {
6060
6097
  return;
6061
6098
  }
6099
+ const message = err instanceof Error ? err.message : String(err);
6100
+ if (result.imageUrl.startsWith("blob:") && message.startsWith("E_OVERLAY_FRAME_LOAD") && message.includes(result.imageUrl.slice(0, 160))) {
6101
+ const failedUrl = result.imageUrl;
6102
+ setTimeout(() => {
6103
+ if (gen !== sceneGenRef.current) return;
6104
+ if (compositeQueueRef.current.has(result.mockupId)) return;
6105
+ if (processedFrameRef.current.get(result.mockupId) !== failedUrl) return;
6106
+ console.error(
6107
+ `[RealtimeProvider] E_OVERLAY_COMPOSITE_FAILED: frame for ${result.mockupId} failed to load and no newer frame arrived within ${FRAME_SUPERSEDE_GRACE_MS} ms \u2014 full-frame delivery for the rest of this session.`,
6108
+ err
6109
+ );
6110
+ cropCapableRef.current = false;
6111
+ rerenderFullFrame(result.mockupId);
6112
+ }, FRAME_SUPERSEDE_GRACE_MS);
6113
+ return;
6114
+ }
6062
6115
  console.error(
6063
6116
  "[RealtimeProvider] E_OVERLAY_COMPOSITE_FAILED: cropped-frame composite failed \u2014 full-frame delivery for the rest of this session.",
6064
6117
  err
@@ -6093,6 +6146,9 @@ function RealtimeProvider({
6093
6146
  }
6094
6147
  console.log(`[RTP] rawMockupResults changed: ${rawMockupResults.length} result(s)`, rawMockupResults.map((r) => `${r.mockupId}${r.imageUrl ? " \u2713url" : " \u2717NO-URL"}${r.cropRect ? " [crop]" : ""}`));
6095
6148
  const displayable = [];
6149
+ if (!serverStampsVersionsRef.current && rawMockupResults.some((r) => r.versions && typeof r.versions === "object")) {
6150
+ serverStampsVersionsRef.current = true;
6151
+ }
6096
6152
  rawMockupResults.forEach((result) => {
6097
6153
  if (!result.imageUrl) {
6098
6154
  console.log(`[RTP] Result for ${result.mockupId} has NO imageUrl \u2014 not caching, pending NOT cleared`);
@@ -6575,6 +6631,9 @@ function RealtimeProvider({
6575
6631
  const getLastSentVersionRef = useRef12(getLastSentVersion);
6576
6632
  getLastSentVersionRef.current = getLastSentVersion;
6577
6633
  const getCanvasVersion = useCallback10(() => getLastSentVersionRef.current?.() ?? 0, []);
6634
+ const getLatestSentVersionsRef = useRef12(getLatestSentVersions);
6635
+ getLatestSentVersionsRef.current = getLatestSentVersions;
6636
+ const getLatestSentVersionsStable = useCallback10(() => getLatestSentVersionsRef.current?.() ?? {}, []);
6578
6637
  const stableSubscribePipelineSettled = useCallback10(
6579
6638
  (callback) => {
6580
6639
  return subscribePipelineSettledRef.current(callback);
@@ -6624,6 +6683,7 @@ function RealtimeProvider({
6624
6683
  getMockupUrl: getMockupUrl3,
6625
6684
  getMockupUrls,
6626
6685
  getCanvasVersion,
6686
+ getLatestSentVersions: getLatestSentVersionsStable,
6627
6687
  isMockupLoading,
6628
6688
  registerBlobUrl,
6629
6689
  // WebRTC streaming
@@ -6656,6 +6716,7 @@ function RealtimeProvider({
6656
6716
  getMockupUrl3,
6657
6717
  getMockupUrls,
6658
6718
  getCanvasVersion,
6719
+ getLatestSentVersionsStable,
6659
6720
  isMockupLoading,
6660
6721
  registerBlobUrl
6661
6722
  ]
@@ -10451,6 +10512,8 @@ var HeroProductImage = memo3(function HeroProductImage2({
10451
10512
  };
10452
10513
  }, [mode, realtimeContext?.subscribePendingBlob, placement]);
10453
10514
  const renderVersionRef = useRef23(0);
10515
+ const renderVersionsRef = useRef23(null);
10516
+ const getLatestSentVersions = realtimeContext?.getLatestSentVersions;
10454
10517
  const refusalCodeRef = useRef23(null);
10455
10518
  refusalCodeRef.current = unavailable?.code ?? null;
10456
10519
  const sessionErrorCodeRef = useRef23(null);
@@ -10467,11 +10530,13 @@ var HeroProductImage = memo3(function HeroProductImage2({
10467
10530
  return;
10468
10531
  }
10469
10532
  const rendered = renderVersionRef.current;
10470
- const behind = rendered < canvas;
10533
+ const versions = renderVersionsRef.current;
10534
+ const sent = versions ? getLatestSentVersions?.() : void 0;
10535
+ const behind = versions && sent ? Object.entries(sent).some(([placement2, v]) => (versions[placement2] ?? -1) < v) : rendered < canvas;
10471
10536
  node.dataset.canvasVersion = String(canvas);
10472
10537
  node.dataset.renderVersion = String(rendered);
10473
10538
  node.dataset.renderState = refusalCodeRef.current || behind && sessionErrorCodeRef.current ? "refused" : behind ? "stale" : "current";
10474
- }, [getCanvasVersion]);
10539
+ }, [getCanvasVersion, getLatestSentVersions]);
10475
10540
  useEffect32(() => {
10476
10541
  applyRenderStamp();
10477
10542
  });
@@ -10481,8 +10546,9 @@ var HeroProductImage = memo3(function HeroProductImage2({
10481
10546
  if (!actualMockupId || !subscribeSent || !subscribeResult) return;
10482
10547
  const offSent = subscribeSent(() => applyRenderStamp());
10483
10548
  const offResult = subscribeResult(actualMockupId, (result) => {
10484
- if (typeof result?.requestVersion === "number" && result.requestVersion > renderVersionRef.current) {
10549
+ if (typeof result?.requestVersion === "number" && result.requestVersion >= renderVersionRef.current) {
10485
10550
  renderVersionRef.current = result.requestVersion;
10551
+ renderVersionsRef.current = result.versions ?? null;
10486
10552
  }
10487
10553
  applyRenderStamp();
10488
10554
  });