@snowcone-app/ui 0.4.12 → 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/CHANGELOG.md +12 -0
- package/dist/index.cjs +10 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/services/CanvasExportService.ts +23 -0
- package/src/services/__tests__/CanvasExportService.leadingEdge.test.ts +60 -0
package/package.json
CHANGED
|
@@ -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
|
+
});
|