@snowcone-app/ui 0.9.4 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowcone-app/ui",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "React components for merchandise visualization and customization",
5
5
  "keywords": [
6
6
  "react",
@@ -62,6 +62,9 @@ const DEFAULT_ARTBOARD_SIZE = 2000;
62
62
  // Enforce a conservative cap to prevent memory crashes
63
63
  const MAX_CONCURRENT_BLOBS = 5;
64
64
 
65
+ /** How long a frame whose blob: URL was revoked waits for its newer frame before counting as a real failure. */
66
+ const FRAME_SUPERSEDE_GRACE_MS = 2000;
67
+
65
68
  /** `current` in order, then any of `extra` not already in it. */
66
69
  function mergeMockupIds(current: readonly string[], extra: readonly string[]): string[] {
67
70
  const out = [...current];
@@ -943,6 +946,32 @@ export function RealtimeProvider({
943
946
  ) {
944
947
  return;
945
948
  }
949
+ // The FRAME (not the background) failed to load from a revoked blob:
950
+ // URL. The SDK revokes a mockup's frame only when that mockup's next
951
+ // frame arrives, so the newer frame exists even if React state hasn't
952
+ // delivered it here yet (prod 2026-11-11, phone: this race switched
953
+ // cropped delivery off for the whole session). Wait for it; if none
954
+ // shows up, it really failed.
955
+ const message = err instanceof Error ? err.message : String(err);
956
+ if (
957
+ result.imageUrl.startsWith("blob:") &&
958
+ message.startsWith("E_OVERLAY_FRAME_LOAD") &&
959
+ message.includes(result.imageUrl.slice(0, 160))
960
+ ) {
961
+ const failedUrl = result.imageUrl;
962
+ setTimeout(() => {
963
+ if (gen !== sceneGenRef.current) return;
964
+ if (compositeQueueRef.current.has(result.mockupId)) return;
965
+ if (processedFrameRef.current.get(result.mockupId) !== failedUrl) return;
966
+ console.error(
967
+ `[RealtimeProvider] E_OVERLAY_COMPOSITE_FAILED: frame for ${result.mockupId} failed to load and no newer frame arrived within ${FRAME_SUPERSEDE_GRACE_MS} ms — full-frame delivery for the rest of this session.`,
968
+ err
969
+ );
970
+ cropCapableRef.current = false;
971
+ rerenderFullFrame(result.mockupId);
972
+ }, FRAME_SUPERSEDE_GRACE_MS);
973
+ return;
974
+ }
946
975
  console.error(
947
976
  "[RealtimeProvider] E_OVERLAY_COMPOSITE_FAILED: cropped-frame composite failed — full-frame delivery for the rest of this session.",
948
977
  err
@@ -412,5 +412,46 @@ describe("RealtimeProvider cropped delivery", () => {
412
412
  expect(h.rerender).not.toHaveBeenCalled();
413
413
  errSpy.mockRestore();
414
414
  });
415
+
416
+ it("a frame whose blob: URL was revoked waits for its newer frame instead of switching the session to full frames (prod, phone, 2026-11-11)", async () => {
417
+ const { rerender } = render(<Harness version={1} />);
418
+ await waitFor(() => expect(h.sendConfig).toHaveBeenCalled());
419
+ await seedAndFlip(rerender);
420
+ h.sendConfig.mockClear();
421
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
422
+
423
+ // v3's frame was revoked by the SDK (its v4 already arrived there) before React state delivered v4 here.
424
+ h.compositeCroppedFrame.mockImplementationOnce(() =>
425
+ Promise.reject(new Error("E_OVERLAY_FRAME_LOAD: image failed to load: blob:v3-m1"))
426
+ );
427
+ h.state.results = [croppedResult("m1", "blob:v3-m1"), fullResult("m2", "https://r/m2-full")];
428
+ await act(async () => rerender(<Harness version={4} />));
429
+ // v4 lands within the grace window and composites.
430
+ h.state.results = [croppedResult("m1", "blob:v4-m1"), fullResult("m2", "https://r/m2-full")];
431
+ await act(async () => rerender(<Harness version={5} />));
432
+ await act(async () => { await new Promise((r) => setTimeout(r, 2300)); });
433
+ expect(h.compositeCroppedFrame).toHaveBeenLastCalledWith(expect.objectContaining({ frameUrl: "blob:v4-m1" }));
434
+ expect(h.sendConfig).not.toHaveBeenCalled(); // cropped delivery stays on, nothing re-rendered
435
+ errSpy.mockRestore();
436
+ });
437
+
438
+ it("a revoked frame with no newer frame within the grace window is a real failure: full-frame re-render", async () => {
439
+ const { rerender } = render(<Harness version={1} />);
440
+ await waitFor(() => expect(h.sendConfig).toHaveBeenCalled());
441
+ await seedAndFlip(rerender);
442
+ h.sendConfig.mockClear();
443
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
444
+ h.compositeCroppedFrame.mockImplementationOnce(() =>
445
+ Promise.reject(new Error("E_OVERLAY_FRAME_LOAD: image failed to load: blob:v3-m1"))
446
+ );
447
+ h.state.results = [croppedResult("m1", "blob:v3-m1"), fullResult("m2", "https://r/m2-full")];
448
+ await act(async () => rerender(<Harness version={4} />));
449
+ await act(async () => { await new Promise((r) => setTimeout(r, 200)); });
450
+ expect(h.sendConfig).not.toHaveBeenCalled(); // still inside the grace window
451
+ await act(async () => { await new Promise((r) => setTimeout(r, 2100)); });
452
+ await waitFor(() => expect(sentConfigs().at(-1)?.mockupIds).toEqual(["m1", "m2"]));
453
+ expect(sentConfigs().every((c) => c.overlayCrop === undefined)).toBe(true);
454
+ errSpy.mockRestore();
455
+ });
415
456
  });
416
457