@snowcone-app/ui 0.4.13 → 0.4.14

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.4.13",
3
+ "version": "0.4.14",
4
4
  "description": "React components for merchandise visualization and customization",
5
5
  "keywords": [
6
6
  "react",
@@ -446,6 +446,29 @@ class CanvasExportService {
446
446
  return;
447
447
  }
448
448
 
449
+ // Leading edge: the FIRST change after idle goes out immediately. Editor
450
+ // actions (add, undo, nudge) emit two state changes ~250ms apart, and a
451
+ // trailing-only debounce restarted on the second one — nothing left the
452
+ // browser for ~550ms on every discrete action (measured on prod). Sending
453
+ // the first state now gives a frame ~a round-trip later; the trailing
454
+ // settle below still sends the final state, and the server cancels a
455
+ // superseded in-flight render, so a follow-up change costs no extra
456
+ // composite when it arrives quickly. Continuous activity (typing, drags)
457
+ // is unaffected: only an idle→active transition is leading.
458
+ const isIdle = !this.stateChangeThrottleTimeout && (!hasSentBefore || timeSinceLastSend >= settleMs);
459
+ if (isIdle) {
460
+ console.log(`[CES] handleStateChange #${callId}: LEADING send (idle ${hasSentBefore ? timeSinceLastSend : 'forever'}ms)`);
461
+ this.sendStateNow(state);
462
+ // Arm the settle timer so a follow-up change within settleMs still
463
+ // coalesces into one trailing send; with no follow-up it finds nothing
464
+ // pending and sends nothing.
465
+ this.stateChangeThrottleTimeout = setTimeout(() => {
466
+ this.stateChangeThrottleTimeout = null;
467
+ if (this.pendingCanvasState) this.sendStateNow(this.pendingCanvasState);
468
+ }, settleMs);
469
+ return;
470
+ }
471
+
449
472
  // Settle timer: reset on every call, fires settleMs after last change.
450
473
  // Handles discrete actions (type a letter, nudge, color pick).
451
474
  if (this.stateChangeThrottleTimeout) {
@@ -0,0 +1,60 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { canvasExportService } from "../CanvasExportService";
4
+
5
+ const state = (n: number) => ({
6
+ elements: [{ type: "shape", n }],
7
+ artboards: [{ name: "Front", width: 100, height: 100 }],
8
+ activeArtboard: "Front",
9
+ });
10
+
11
+ describe("CanvasExportService — leading-edge send", () => {
12
+ let send: ReturnType<typeof vi.fn>;
13
+ beforeEach(() => {
14
+ vi.useFakeTimers();
15
+ vi.spyOn(console, "log").mockImplementation(() => {});
16
+ canvasExportService.reset();
17
+ send = vi.fn(() => true);
18
+ canvasExportService.configure({
19
+ isRealtimeEnabled: true,
20
+ sendCanvasState: send as never,
21
+ serializeStateForServer: ((s: unknown) => s) as never,
22
+ mockupCount: 1,
23
+ placementName: "Front",
24
+ });
25
+ });
26
+ afterEach(() => {
27
+ vi.useRealTimers();
28
+ vi.restoreAllMocks();
29
+ });
30
+
31
+ it("sends the first change after idle immediately, then the settled state once", () => {
32
+ canvasExportService.handleStateChange(state(1));
33
+ expect(send).toHaveBeenCalledTimes(1); // leading — no 300ms wait
34
+
35
+ vi.advanceTimersByTime(250);
36
+ canvasExportService.handleStateChange(state(2)); // the editor's follow-up emission
37
+ expect(send).toHaveBeenCalledTimes(1);
38
+ vi.advanceTimersByTime(299);
39
+ expect(send).toHaveBeenCalledTimes(1);
40
+ vi.advanceTimersByTime(1);
41
+ expect(send).toHaveBeenCalledTimes(2); // trailing, with the final state
42
+ expect((send.mock.calls[1][1] as { elements: { n: number }[] }).elements[0].n).toBe(2);
43
+ });
44
+
45
+ it("a lone change sends exactly once (no duplicate trailing send)", () => {
46
+ canvasExportService.handleStateChange(state(1));
47
+ vi.advanceTimersByTime(2000);
48
+ expect(send).toHaveBeenCalledTimes(1);
49
+ });
50
+
51
+ it("a burst is leading + trailing only, not one per change", () => {
52
+ for (let i = 0; i < 12; i++) {
53
+ canvasExportService.handleStateChange(state(i));
54
+ vi.advanceTimersByTime(50);
55
+ }
56
+ expect(send).toHaveBeenCalledTimes(1);
57
+ vi.advanceTimersByTime(300);
58
+ expect(send).toHaveBeenCalledTimes(2);
59
+ });
60
+ });