@snowcone-app/ui 0.9.5 → 0.9.6
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/CHANGELOG.md +6 -0
- package/dist/index.cjs +29 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +29 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/patterns/RealtimeProvider.tsx +38 -4
- package/src/patterns/__tests__/RealtimeProvider.cropDelivery.test.tsx +24 -0
package/package.json
CHANGED
|
@@ -64,6 +64,8 @@ const MAX_CONCURRENT_BLOBS = 5;
|
|
|
64
64
|
|
|
65
65
|
/** How long a frame whose blob: URL was revoked waits for its newer frame before counting as a real failure. */
|
|
66
66
|
const FRAME_SUPERSEDE_GRACE_MS = 2000;
|
|
67
|
+
/** The box accepts one `rerender` per this many ms (rc#840; more are refused with E_RERENDER_RATE_LIMITED). */
|
|
68
|
+
const RERENDER_MIN_INTERVAL_MS = 550;
|
|
67
69
|
|
|
68
70
|
/** `current` in order, then any of `extra` not already in it. */
|
|
69
71
|
function mergeMockupIds(current: readonly string[], extra: readonly string[]): string[] {
|
|
@@ -617,6 +619,10 @@ export function RealtimeProvider({
|
|
|
617
619
|
// Mockups a `rerender` was sent for, until it is answered or refused.
|
|
618
620
|
const pendingRerenderRef = useRef<string[]>([]);
|
|
619
621
|
const forceRerenderByConfigRef = useRef<((failed: string[]) => void) | null>(null);
|
|
622
|
+
// Pacing of `rerender` requests (the box allows one per 500 ms).
|
|
623
|
+
const lastRerenderAtRef = useRef(0);
|
|
624
|
+
const rerenderQueueRef = useRef<string[]>([]);
|
|
625
|
+
const rerenderTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
620
626
|
// Cropped frames that could not be shown, waiting for one batched
|
|
621
627
|
// full-frame re-render (see rerenderFullFrame).
|
|
622
628
|
const fullFrameRerenderIdsRef = useRef<Set<string>>(new Set());
|
|
@@ -743,6 +749,35 @@ export function RealtimeProvider({
|
|
|
743
749
|
);
|
|
744
750
|
forceRerenderByConfigRef.current = forceRerenderByConfig;
|
|
745
751
|
|
|
752
|
+
/**
|
|
753
|
+
* Ask the box to re-render `ids`, never faster than it allows (one request
|
|
754
|
+
* per RERENDER_MIN_INTERVAL_MS; it refuses more with E_RERENDER_RATE_LIMITED,
|
|
755
|
+
* prod 2026-11-11). Requests inside the interval wait for the remainder and
|
|
756
|
+
* go out as ONE request with every picture that failed meanwhile.
|
|
757
|
+
*/
|
|
758
|
+
const sendRerenderPaced = useCallback(
|
|
759
|
+
(ids: string[]) => {
|
|
760
|
+
rerenderQueueRef.current = mergeMockupIds(rerenderQueueRef.current, ids);
|
|
761
|
+
if (rerenderTimerRef.current) return;
|
|
762
|
+
const wait = Math.max(0, lastRerenderAtRef.current + RERENDER_MIN_INTERVAL_MS - Date.now());
|
|
763
|
+
const flush = () => {
|
|
764
|
+
rerenderTimerRef.current = null;
|
|
765
|
+
const batch = rerenderQueueRef.current;
|
|
766
|
+
rerenderQueueRef.current = [];
|
|
767
|
+
if (batch.length === 0) return;
|
|
768
|
+
lastRerenderAtRef.current = Date.now();
|
|
769
|
+
pendingRerenderRef.current = mergeMockupIds(pendingRerenderRef.current, batch);
|
|
770
|
+
if (!rerenderRaw(batch)) {
|
|
771
|
+
pendingRerenderRef.current = [];
|
|
772
|
+
forceRerenderByConfigRef.current?.(batch);
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
if (wait === 0) flush();
|
|
776
|
+
else rerenderTimerRef.current = setTimeout(flush, wait);
|
|
777
|
+
},
|
|
778
|
+
[rerenderRaw]
|
|
779
|
+
);
|
|
780
|
+
|
|
746
781
|
const rerenderFullFrame = useCallback(
|
|
747
782
|
(mockupId: string) => {
|
|
748
783
|
fullFrameRerenderIdsRef.current.add(mockupId);
|
|
@@ -769,14 +804,13 @@ export function RealtimeProvider({
|
|
|
769
804
|
// for exactly the failed pictures. A refusal (E_RERENDER_*) falls
|
|
770
805
|
// back to the config way (see onError).
|
|
771
806
|
sendFullFrameConfig(ids);
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
pendingRerenderRef.current = [];
|
|
807
|
+
sendRerenderPaced(failed);
|
|
808
|
+
return;
|
|
775
809
|
}
|
|
776
810
|
forceRerenderByConfig(failed);
|
|
777
811
|
}, 0);
|
|
778
812
|
},
|
|
779
|
-
[sendFullFrameConfig, forceRerenderByConfig,
|
|
813
|
+
[sendFullFrameConfig, forceRerenderByConfig, sendRerenderPaced]
|
|
780
814
|
);
|
|
781
815
|
|
|
782
816
|
/** Flip to cropped delivery once every configured mockup has a frozen
|
|
@@ -453,5 +453,29 @@ describe("RealtimeProvider cropped delivery", () => {
|
|
|
453
453
|
expect(sentConfigs().every((c) => c.overlayCrop === undefined)).toBe(true);
|
|
454
454
|
errSpy.mockRestore();
|
|
455
455
|
});
|
|
456
|
+
|
|
457
|
+
it("never asks the box to rerender faster than it allows: failures 100 ms apart become 2 requests >= 500 ms apart", async () => {
|
|
458
|
+
const { rerender } = render(<Harness version={1} />);
|
|
459
|
+
await waitFor(() => expect(h.sendConfig).toHaveBeenCalled());
|
|
460
|
+
const v = { versions: { Front: 1 } };
|
|
461
|
+
h.state.results = [{ ...fullResult("m1", "https://r/m1-full"), ...v }, { ...fullResult("m2", "https://r/m2-full"), ...v }];
|
|
462
|
+
await act(async () => rerender(<Harness version={2} />));
|
|
463
|
+
await waitFor(() => expect(sentConfigs().some((c) => c.overlayCrop === "auto")).toBe(true));
|
|
464
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
465
|
+
h.compositeCroppedFrame.mockRejectedValue(new Error("E_OVERLAY_ENCODE_FAILED"));
|
|
466
|
+
const calls: number[] = [];
|
|
467
|
+
h.rerender.mockImplementation(() => { calls.push(Date.now()); return true; });
|
|
468
|
+
|
|
469
|
+
h.state.results = [croppedResult("m1", "https://r/m1-crop-a")];
|
|
470
|
+
await act(async () => rerender(<Harness version={3} />));
|
|
471
|
+
await act(async () => { await new Promise((r) => setTimeout(r, 100)); });
|
|
472
|
+
h.state.results = [croppedResult("m1", "https://r/m1-crop-a"), croppedResult("m2", "https://r/m2-crop-a")];
|
|
473
|
+
await act(async () => rerender(<Harness version={4} />));
|
|
474
|
+
await act(async () => { await new Promise((r) => setTimeout(r, 900)); });
|
|
475
|
+
|
|
476
|
+
expect(h.rerender.mock.calls.map((c) => c[0])).toEqual([["m1"], ["m2"]]);
|
|
477
|
+
expect(calls[1] - calls[0]).toBeGreaterThanOrEqual(500);
|
|
478
|
+
errSpy.mockRestore();
|
|
479
|
+
});
|
|
456
480
|
});
|
|
457
481
|
|