@snowcone-app/ui 0.4.14 → 0.8.0
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 +41 -0
- package/dist/index.cjs +143 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -4
- package/dist/index.d.ts +59 -4
- package/dist/index.js +143 -38
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/CanvasIsolationBoundary.tsx +6 -0
- package/src/composed/HeroProductImage.tsx +15 -0
- package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +2 -1
- package/src/ecommerce/stories/PDPLayout.stories.tsx +2 -1
- package/src/patterns/RealtimeProvider.tsx +84 -3
- package/src/primitives/stories/Accordion.stories.tsx +2 -2
- package/src/services/CanvasExportService.ts +96 -50
- package/src/services/__tests__/CanvasExportService.leadingEdge.test.ts +93 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#2249](https://github.com/snowcone-app/snowcone-monorepo/pull/2249) [`19f7ee5`](https://github.com/snowcone-app/snowcone-monorepo/commit/19f7ee583638ccedd8fd5f309ad3e5ba6a06c0f9) Thanks [@kevinsproles](https://github.com/kevinsproles)! - A mockup whose scene failed render parity is refused by the server (`E_MOCKUP_NOT_BLESSED`) instead of silently rendering on the slow path. `RealtimeRenderError` gains `mockupId` for per-mockup errors; `RealtimeProvider` exposes `unavailableMockups` (mockupId → error) and `HeroProductImage` shows that mockup as unavailable (`data-mockup-unavailable`) while the others keep streaming.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`19f7ee5`](https://github.com/snowcone-app/snowcone-monorepo/commit/19f7ee583638ccedd8fd5f309ad3e5ba6a06c0f9)]:
|
|
12
|
+
- @snowcone-app/sdk@0.23.0
|
|
13
|
+
|
|
14
|
+
## 0.7.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- [#2240](https://github.com/snowcone-app/snowcone-monorepo/pull/2240) [`61c4826`](https://github.com/snowcone-app/snowcone-monorepo/commit/61c48267d77b93628ef525df5a6aee66421d67f7) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Realtime send policy: pipeline depth two. `CanvasExportService` now keeps up to two edits in flight (`maxInFlight`, default 2) before holding the latest state — the box's window is ~30 ms of a ~96 ms edit→frame, so one in flight left it idle ~70% of every drag cycle. `RealtimeProvider` settles once per landed send (every mockup of a `requestVersion`) instead of on the first frame after any send, using the SDK's new `getLastSentVersion()` on the realtime service and hook.
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- Updated dependencies [[`61c4826`](https://github.com/snowcone-app/snowcone-monorepo/commit/61c48267d77b93628ef525df5a6aee66421d67f7)]:
|
|
23
|
+
- @snowcone-app/sdk@0.22.0
|
|
24
|
+
|
|
25
|
+
## 0.6.0
|
|
26
|
+
|
|
27
|
+
### Minor Changes
|
|
28
|
+
|
|
29
|
+
- [#2231](https://github.com/snowcone-app/snowcone-monorepo/pull/2231) [`ecc5bde`](https://github.com/snowcone-app/snowcone-monorepo/commit/ecc5bde83c091a5c0de179903b18c67d7c674fc0) Thanks [@kevinsproles](https://github.com/kevinsproles)! - `RealtimeProvider` accepts `initialGrant` — a session grant minted server-side while the page rendered. The first connect spends it (if it has more than 30 s of life left) instead of paying a grant round trip; renewals and reconnects still go through `grantUrl`.
|
|
30
|
+
|
|
31
|
+
## 0.5.0
|
|
32
|
+
|
|
33
|
+
### Minor Changes
|
|
34
|
+
|
|
35
|
+
- [#2230](https://github.com/snowcone-app/snowcone-monorepo/pull/2230) [`9f99c8a`](https://github.com/snowcone-app/snowcone-monorepo/commit/9f99c8a362f38b26d57c62a80a3845080338478f) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Realtime editor send policy is now one-in-flight: a change goes out immediately when no frame is in flight; while a frame is in flight the latest state is held and sent the moment that frame lands (`subscribePipelineSettled`), so the render pipeline is never idle during a drag and never more than one edit behind. `handleInteractionEnd()` (exported from the canvas isolation boundary) flushes a held state on pointer release. Replaces the fixed 300 ms settle / 1 s max-wait debounce.
|
|
36
|
+
|
|
37
|
+
## 0.4.15
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- Updated dependencies [[`aaa73cc`](https://github.com/snowcone-app/snowcone-monorepo/commit/aaa73ccb4b2d4c7c107b66f18be29f67032422b8)]:
|
|
42
|
+
- @snowcone-app/sdk@0.21.0
|
|
43
|
+
|
|
3
44
|
## 0.4.14
|
|
4
45
|
|
|
5
46
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -412,7 +412,7 @@ var brand_default = {
|
|
|
412
412
|
display_name: "Snowcone",
|
|
413
413
|
tagline: "Your art, on everything.",
|
|
414
414
|
seo_title: "Snowcone \u2014 Your art, on everything.",
|
|
415
|
-
legal_entity: "
|
|
415
|
+
legal_entity: "Art Shop AI LLC",
|
|
416
416
|
lang: "en",
|
|
417
417
|
wordmark: "/assets/wordmark-en.svg"
|
|
418
418
|
},
|
|
@@ -422,7 +422,7 @@ var brand_default = {
|
|
|
422
422
|
display_name: "\u96EA\u7ED8 (Snowcone)",
|
|
423
423
|
tagline: "\u5728\u96EA\u767D\u4E4B\u4E0A\uFF0C\u7ED8\u51FA\u4F60\u7684\u8272\u5F69\u3002",
|
|
424
424
|
seo_title: "\u96EA\u7ED8 (Snowcone) \u2014 \u5728\u96EA\u767D\u4E4B\u4E0A\uFF0C\u7ED8\u51FA\u4F60\u7684\u8272\u5F69",
|
|
425
|
-
legal_entity: "
|
|
425
|
+
legal_entity: "Art Shop AI LLC",
|
|
426
426
|
lang: "zh",
|
|
427
427
|
wordmark: "/assets/wordmark-zh.svg"
|
|
428
428
|
},
|
|
@@ -432,7 +432,7 @@ var brand_default = {
|
|
|
432
432
|
display_name: "Snowcone",
|
|
433
433
|
tagline: "Tu arte, en todo.",
|
|
434
434
|
seo_title: "Snowcone \u2014 Tu arte, en todo.",
|
|
435
|
-
legal_entity: "
|
|
435
|
+
legal_entity: "Art Shop AI LLC",
|
|
436
436
|
lang: "es",
|
|
437
437
|
wordmark: "/assets/wordmark-en.svg"
|
|
438
438
|
},
|
|
@@ -442,7 +442,7 @@ var brand_default = {
|
|
|
442
442
|
display_name: "Snowcone",
|
|
443
443
|
tagline: "Votre art, sur tout.",
|
|
444
444
|
seo_title: "Snowcone \u2014 Votre art, sur tout.",
|
|
445
|
-
legal_entity: "
|
|
445
|
+
legal_entity: "Art Shop AI LLC",
|
|
446
446
|
lang: "fr",
|
|
447
447
|
wordmark: "/assets/wordmark-en.svg"
|
|
448
448
|
}
|
|
@@ -6006,13 +6006,18 @@ function RealtimeProvider({
|
|
|
6006
6006
|
autoEnable = false,
|
|
6007
6007
|
shop,
|
|
6008
6008
|
grantUrl,
|
|
6009
|
+
initialGrant,
|
|
6009
6010
|
delivery = "cropped"
|
|
6010
6011
|
}) {
|
|
6011
6012
|
const productContext = useProductOptional();
|
|
6012
6013
|
const mockupBg = useMockupBackgroundOptional();
|
|
6013
6014
|
const [isEnabled, setIsEnabled] = (0, import_react25.useState)(autoEnable);
|
|
6014
6015
|
const useSessionGrant = Boolean(shop && grantUrl);
|
|
6016
|
+
const initialGrantRef = (0, import_react25.useRef)(initialGrant);
|
|
6015
6017
|
const getToken = (0, import_react25.useCallback)(async () => {
|
|
6018
|
+
const minted = initialGrantRef.current;
|
|
6019
|
+
initialGrantRef.current = void 0;
|
|
6020
|
+
if (minted && minted.expiresAt * 1e3 - Date.now() > 3e4) return minted;
|
|
6016
6021
|
const res = await fetch(grantUrl, {
|
|
6017
6022
|
method: "POST",
|
|
6018
6023
|
headers: { "Content-Type": "application/json" },
|
|
@@ -6035,6 +6040,9 @@ function RealtimeProvider({
|
|
|
6035
6040
|
const [spreadCropsVersion, setSpreadCropsVersion] = (0, import_react25.useState)(0);
|
|
6036
6041
|
const lastSentSpreadCropsRef = (0, import_react25.useRef)("null");
|
|
6037
6042
|
const lastSentColorsRef = (0, import_react25.useRef)({});
|
|
6043
|
+
const [unavailableMockups, setUnavailableMockups] = (0, import_react25.useState)(
|
|
6044
|
+
() => /* @__PURE__ */ new Map()
|
|
6045
|
+
);
|
|
6038
6046
|
const realtimeMockup = (0, import_react26.useRealtimeMockup)({
|
|
6039
6047
|
wsUrl,
|
|
6040
6048
|
getToken: useSessionGrant ? getToken : void 0,
|
|
@@ -6064,6 +6072,10 @@ function RealtimeProvider({
|
|
|
6064
6072
|
},
|
|
6065
6073
|
onError: (error, detail) => {
|
|
6066
6074
|
console.error(`[RealtimeProvider] Render error [${detail.code}]:`, error);
|
|
6075
|
+
if (detail.mockupId) {
|
|
6076
|
+
const id = detail.mockupId;
|
|
6077
|
+
setUnavailableMockups((prev) => prev.get(id)?.code === detail.code ? prev : new Map(prev).set(id, detail));
|
|
6078
|
+
}
|
|
6067
6079
|
}
|
|
6068
6080
|
});
|
|
6069
6081
|
const subscribeRTCTiming = (0, import_react25.useCallback)((_callback) => {
|
|
@@ -6084,7 +6096,8 @@ function RealtimeProvider({
|
|
|
6084
6096
|
connect,
|
|
6085
6097
|
disconnect,
|
|
6086
6098
|
flushPendingBlobs,
|
|
6087
|
-
hasPendingBlobs
|
|
6099
|
+
hasPendingBlobs,
|
|
6100
|
+
getLastSentVersion
|
|
6088
6101
|
} = realtimeMockup;
|
|
6089
6102
|
const mockupResultsRef = (0, import_react25.useRef)([]);
|
|
6090
6103
|
const mockupSubscribersRef = (0, import_react25.useRef)(
|
|
@@ -6106,6 +6119,8 @@ function RealtimeProvider({
|
|
|
6106
6119
|
const blobQueuedGenRef = (0, import_react25.useRef)(0);
|
|
6107
6120
|
const hasBlobInFlightRef = (0, import_react25.useRef)(false);
|
|
6108
6121
|
const pipelineResetCooldownRef = (0, import_react25.useRef)(false);
|
|
6122
|
+
const dueByVersionRef = (0, import_react25.useRef)(/* @__PURE__ */ new Map());
|
|
6123
|
+
const configuredMockupIdsRef = (0, import_react25.useRef)([]);
|
|
6109
6124
|
const mockupUrlCacheRef = (0, import_react25.useRef)(/* @__PURE__ */ new Map());
|
|
6110
6125
|
const registeredBlobUrlsRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
|
|
6111
6126
|
const pendingPlacementsRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
|
|
@@ -6224,7 +6239,29 @@ function RealtimeProvider({
|
|
|
6224
6239
|
console.log(`[RTP] No subscribers for ${result.mockupId} \u2014 result received but nobody listening`);
|
|
6225
6240
|
}
|
|
6226
6241
|
});
|
|
6227
|
-
|
|
6242
|
+
let settledVersions = 0;
|
|
6243
|
+
const due = dueByVersionRef.current;
|
|
6244
|
+
results.forEach((result) => {
|
|
6245
|
+
const v = result.requestVersion;
|
|
6246
|
+
if (v === void 0) return;
|
|
6247
|
+
const entry = due.get(v);
|
|
6248
|
+
if (!entry) return;
|
|
6249
|
+
if (entry.due <= 1) {
|
|
6250
|
+
due.delete(v);
|
|
6251
|
+
settledVersions += 1;
|
|
6252
|
+
} else {
|
|
6253
|
+
entry.due -= 1;
|
|
6254
|
+
}
|
|
6255
|
+
});
|
|
6256
|
+
due.forEach((entry, v) => {
|
|
6257
|
+
if (now - entry.at > 1e4) due.delete(v);
|
|
6258
|
+
});
|
|
6259
|
+
if (settledVersions > 0) {
|
|
6260
|
+
if (!pipelineResetCooldownRef.current) {
|
|
6261
|
+
hasBlobInFlightRef.current = false;
|
|
6262
|
+
for (let i = 0; i < settledVersions; i++) pipelineSettledSubscribersRef.current.forEach((cb) => cb());
|
|
6263
|
+
}
|
|
6264
|
+
} else if (due.size === 0 && hasBlobInFlightRef.current && !hasPendingBlobs() && !pipelineResetCooldownRef.current) {
|
|
6228
6265
|
hasBlobInFlightRef.current = false;
|
|
6229
6266
|
pipelineSettledSubscribersRef.current.forEach((cb) => cb());
|
|
6230
6267
|
}
|
|
@@ -6353,11 +6390,17 @@ function RealtimeProvider({
|
|
|
6353
6390
|
canvasBlobsSentRef.current += 1;
|
|
6354
6391
|
lastBlobSentTimeRef.current = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
6355
6392
|
isPendingMockupsRef.current = true;
|
|
6393
|
+
const version = getLastSentVersion();
|
|
6394
|
+
if (version > 0) {
|
|
6395
|
+
const configured = configuredMockupIdsRef.current.length;
|
|
6396
|
+
const expected = Math.max(1, Math.min(configured || Infinity, mockupCount || Infinity));
|
|
6397
|
+
dueByVersionRef.current.set(version, { due: Number.isFinite(expected) ? expected : 1, at: Date.now() });
|
|
6398
|
+
}
|
|
6356
6399
|
pendingBlobSubscribersRef.current.forEach((cb) => cb(placement));
|
|
6357
6400
|
}
|
|
6358
6401
|
return result;
|
|
6359
6402
|
},
|
|
6360
|
-
[sendCanvasStateRaw]
|
|
6403
|
+
[sendCanvasStateRaw, getLastSentVersion]
|
|
6361
6404
|
);
|
|
6362
6405
|
const sendColorBlobTracked = (0, import_react25.useCallback)(
|
|
6363
6406
|
(placement, color) => {
|
|
@@ -6377,6 +6420,8 @@ function RealtimeProvider({
|
|
|
6377
6420
|
return () => {
|
|
6378
6421
|
disconnect();
|
|
6379
6422
|
configSentRef.current = false;
|
|
6423
|
+
dueByVersionRef.current.clear();
|
|
6424
|
+
setUnavailableMockups((prev) => prev.size ? /* @__PURE__ */ new Map() : prev);
|
|
6380
6425
|
lastSentVariantIdRef.current = null;
|
|
6381
6426
|
lastSentBgRef.current = void 0;
|
|
6382
6427
|
lastSentSpreadCropsRef.current = "null";
|
|
@@ -6420,6 +6465,7 @@ function RealtimeProvider({
|
|
|
6420
6465
|
}
|
|
6421
6466
|
});
|
|
6422
6467
|
}
|
|
6468
|
+
configuredMockupIdsRef.current = mockupIds;
|
|
6423
6469
|
const configPayload = useSessionGrant ? {
|
|
6424
6470
|
productId,
|
|
6425
6471
|
mockupIds,
|
|
@@ -6635,6 +6681,7 @@ function RealtimeProvider({
|
|
|
6635
6681
|
() => {
|
|
6636
6682
|
pipelineGenRef.current++;
|
|
6637
6683
|
hasBlobInFlightRef.current = false;
|
|
6684
|
+
dueByVersionRef.current.clear();
|
|
6638
6685
|
pipelineResetCooldownRef.current = true;
|
|
6639
6686
|
requestAnimationFrame(() => {
|
|
6640
6687
|
pipelineResetCooldownRef.current = false;
|
|
@@ -6773,6 +6820,7 @@ function RealtimeProvider({
|
|
|
6773
6820
|
isConnected,
|
|
6774
6821
|
isConfigured,
|
|
6775
6822
|
renderError,
|
|
6823
|
+
unavailableMockups,
|
|
6776
6824
|
// Read from ref - this is a snapshot at render time, NOT reactive
|
|
6777
6825
|
// For reactive updates, use getMockupResultsImmediate() or subscribe functions
|
|
6778
6826
|
get mockupResults() {
|
|
@@ -6818,6 +6866,7 @@ function RealtimeProvider({
|
|
|
6818
6866
|
isConnected,
|
|
6819
6867
|
isConfigured,
|
|
6820
6868
|
renderError,
|
|
6869
|
+
unavailableMockups,
|
|
6821
6870
|
// mockupResults removed from deps - it's a getter that reads from ref
|
|
6822
6871
|
canvasExportSize,
|
|
6823
6872
|
mockupWidth,
|
|
@@ -8658,9 +8707,18 @@ var CanvasExportService = class _CanvasExportService {
|
|
|
8658
8707
|
// Debounce state for handleStateChange
|
|
8659
8708
|
stateChangeThrottleTimeout = null;
|
|
8660
8709
|
stateChangeLastSendTime = 0;
|
|
8661
|
-
/**
|
|
8662
|
-
|
|
8663
|
-
|
|
8710
|
+
/** Send policy: send times of the edits whose frames have not landed yet, oldest first. */
|
|
8711
|
+
inFlightSentAt = [];
|
|
8712
|
+
maxInFlight = 2;
|
|
8713
|
+
// CanvasExportServiceConfig.maxInFlight
|
|
8714
|
+
subscribePipelineSettled = null;
|
|
8715
|
+
unsubscribePipelineSettled = null;
|
|
8716
|
+
/**
|
|
8717
|
+
* Safety net for the one-in-flight policy: a held state never waits longer
|
|
8718
|
+
* than this for the in-flight frame (a swallowed render error would
|
|
8719
|
+
* otherwise hold the pipeline forever), and a change arriving while a
|
|
8720
|
+
* frame has been in flight this long is sent regardless.
|
|
8721
|
+
*/
|
|
8664
8722
|
static MAX_WAIT_MS = 1e3;
|
|
8665
8723
|
// Internal state for tracking exports
|
|
8666
8724
|
lastExportedArtwork = null;
|
|
@@ -8702,6 +8760,14 @@ var CanvasExportService = class _CanvasExportService {
|
|
|
8702
8760
|
if (config.disableRealtime !== void 0) {
|
|
8703
8761
|
this.disableRealtime = config.disableRealtime;
|
|
8704
8762
|
}
|
|
8763
|
+
if (config.maxInFlight !== void 0) {
|
|
8764
|
+
this.maxInFlight = config.maxInFlight;
|
|
8765
|
+
}
|
|
8766
|
+
if (config.subscribePipelineSettled !== void 0 && config.subscribePipelineSettled !== this.subscribePipelineSettled) {
|
|
8767
|
+
this.unsubscribePipelineSettled?.();
|
|
8768
|
+
this.subscribePipelineSettled = config.subscribePipelineSettled;
|
|
8769
|
+
this.unsubscribePipelineSettled = config.subscribePipelineSettled?.(this.onPipelineSettled) ?? null;
|
|
8770
|
+
}
|
|
8705
8771
|
if (config.clearImageBitmapCache !== void 0) {
|
|
8706
8772
|
this.clearImageBitmapCache = config.clearImageBitmapCache;
|
|
8707
8773
|
}
|
|
@@ -8905,42 +8971,62 @@ var CanvasExportService = class _CanvasExportService {
|
|
|
8905
8971
|
console.log(`[CES] handleStateChange #${callId}: not ready (realtime=${this.isRealtimeEnabled} sendState=${!!this.sendCanvasState} serialize=${!!this.serializeStateForServer}) \u2014 state cached`);
|
|
8906
8972
|
return;
|
|
8907
8973
|
}
|
|
8908
|
-
|
|
8909
|
-
const
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
if (isActiveDrag && timeSinceLastSend >= maxWaitMs) {
|
|
8914
|
-
console.log(`[CES] handleStateChange #${callId}: MAX-WAIT FLUSH (${timeSinceLastSend}ms >= ${maxWaitMs}ms)`);
|
|
8915
|
-
if (this.stateChangeThrottleTimeout) {
|
|
8916
|
-
clearTimeout(this.stateChangeThrottleTimeout);
|
|
8917
|
-
this.stateChangeThrottleTimeout = null;
|
|
8918
|
-
}
|
|
8974
|
+
this.dropStalledSends(now);
|
|
8975
|
+
const inFlight = this.inFlightSentAt.length;
|
|
8976
|
+
if (inFlight < this.maxInFlight) {
|
|
8977
|
+
console.log(`[CES] handleStateChange #${callId}: SEND now (${inFlight} in flight)`);
|
|
8978
|
+
this.clearFlushTimer();
|
|
8919
8979
|
this.sendStateNow(state);
|
|
8920
8980
|
return;
|
|
8921
8981
|
}
|
|
8922
|
-
const
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
clearTimeout(this.stateChangeThrottleTimeout);
|
|
8982
|
+
const oldestMs = now - this.inFlightSentAt[0];
|
|
8983
|
+
console.log(`[CES] handleStateChange #${callId}: HELD (${inFlight} in flight, oldest ${oldestMs}ms) \u2014 sends when one lands`);
|
|
8984
|
+
this.armFlushTimer(_CanvasExportService.MAX_WAIT_MS - oldestMs);
|
|
8985
|
+
};
|
|
8986
|
+
/** A send landed (every mockup of it): free its slot, release the held state if any. */
|
|
8987
|
+
onPipelineSettled = () => {
|
|
8988
|
+
this.inFlightSentAt.shift();
|
|
8989
|
+
this.clearFlushTimer();
|
|
8990
|
+
if (this.pendingCanvasState && this.isRealtimeEnabled) {
|
|
8991
|
+
console.log(`[CES] pipeline settled \u2192 sending held state`);
|
|
8992
|
+
this.sendStateNow(this.pendingCanvasState);
|
|
8934
8993
|
}
|
|
8935
|
-
|
|
8994
|
+
};
|
|
8995
|
+
/**
|
|
8996
|
+
* The pointer was released (drag end, slider release, key up). Send the held
|
|
8997
|
+
* state NOW instead of waiting for the in-flight frame: the server coalesces
|
|
8998
|
+
* (latest wins), and the "let go" frame is the one the shopper waits for.
|
|
8999
|
+
* A no-op when nothing is held, so it is safe on every pointerup.
|
|
9000
|
+
*/
|
|
9001
|
+
handleInteractionEnd = () => {
|
|
9002
|
+
if (!this.pendingCanvasState || !this.isRealtimeEnabled || !this.sendCanvasState || !this.serializeStateForServer) return;
|
|
9003
|
+
if (this.inFlightSentAt.length < this.maxInFlight) return;
|
|
9004
|
+
console.log(`[CES] interaction end \u2192 flushing held state`);
|
|
9005
|
+
this.clearFlushTimer();
|
|
9006
|
+
this.sendStateNow(this.pendingCanvasState);
|
|
9007
|
+
};
|
|
9008
|
+
armFlushTimer(ms) {
|
|
9009
|
+
this.clearFlushTimer();
|
|
8936
9010
|
this.stateChangeThrottleTimeout = setTimeout(() => {
|
|
8937
9011
|
this.stateChangeThrottleTimeout = null;
|
|
8938
|
-
console.log(`[CES] handleStateChange #${callId}: SETTLE FIRED at ${Date.now()} (${Date.now() - now}ms after call)`);
|
|
8939
9012
|
if (this.pendingCanvasState) {
|
|
9013
|
+
console.log(`[CES] flush timer \u2192 sending held state (no frame landed within ${_CanvasExportService.MAX_WAIT_MS}ms)`);
|
|
9014
|
+
this.dropStalledSends(Date.now());
|
|
8940
9015
|
this.sendStateNow(this.pendingCanvasState);
|
|
8941
9016
|
}
|
|
8942
|
-
},
|
|
8943
|
-
}
|
|
9017
|
+
}, Math.max(0, ms));
|
|
9018
|
+
}
|
|
9019
|
+
/** A send whose frame has not landed within MAX_WAIT_MS no longer occupies a slot. */
|
|
9020
|
+
dropStalledSends(now) {
|
|
9021
|
+
if (this.inFlightSentAt.length === 0) return;
|
|
9022
|
+
this.inFlightSentAt = this.inFlightSentAt.filter((t) => now - t < _CanvasExportService.MAX_WAIT_MS);
|
|
9023
|
+
}
|
|
9024
|
+
clearFlushTimer() {
|
|
9025
|
+
if (this.stateChangeThrottleTimeout) {
|
|
9026
|
+
clearTimeout(this.stateChangeThrottleTimeout);
|
|
9027
|
+
this.stateChangeThrottleTimeout = null;
|
|
9028
|
+
}
|
|
9029
|
+
}
|
|
8944
9030
|
sendStateNow(state) {
|
|
8945
9031
|
if (!this.sendCanvasState || !this.serializeStateForServer) {
|
|
8946
9032
|
console.log(`[CES] sendStateNow SKIPPED: sendCanvasState=${!!this.sendCanvasState} serializeStateForServer=${!!this.serializeStateForServer}`);
|
|
@@ -8965,6 +9051,7 @@ var CanvasExportService = class _CanvasExportService {
|
|
|
8965
9051
|
if (sent) {
|
|
8966
9052
|
this.pendingCanvasState = null;
|
|
8967
9053
|
this.jsonModeSentSuccessfully = true;
|
|
9054
|
+
this.inFlightSentAt.push(Date.now());
|
|
8968
9055
|
console.log(`[CES] JSON state sent for ${artboards.length} artboard(s)`);
|
|
8969
9056
|
} else {
|
|
8970
9057
|
console.log(`[CES] JSON state send FAILED for all ${artboards.length} artboard(s) \u2014 state remains pending, jsonEverSent=${this.jsonModeSentSuccessfully}`);
|
|
@@ -8986,6 +9073,7 @@ var CanvasExportService = class _CanvasExportService {
|
|
|
8986
9073
|
this.stateChangeThrottleTimeout = null;
|
|
8987
9074
|
}
|
|
8988
9075
|
this.stateChangeLastSendTime = 0;
|
|
9076
|
+
this.inFlightSentAt = [];
|
|
8989
9077
|
this.jsonModeSentSuccessfully = false;
|
|
8990
9078
|
this.pendingCanvasState = null;
|
|
8991
9079
|
if (this.clearImageBitmapCache) {
|
|
@@ -9021,7 +9109,10 @@ function CanvasIsolationConfigurator({
|
|
|
9021
9109
|
// Wire CES's "I sent nothing" signal to the realtime context's
|
|
9022
9110
|
// cancellation broadcaster. Roll back any optimistic
|
|
9023
9111
|
// notifyPendingExport that didn't end up producing a real export.
|
|
9024
|
-
notifyExportSkipped: realtimeContext?.notifyPendingExportCancelled
|
|
9112
|
+
notifyExportSkipped: realtimeContext?.notifyPendingExportCancelled,
|
|
9113
|
+
// One-in-flight send policy: the held state goes out when the last
|
|
9114
|
+
// send's frame lands.
|
|
9115
|
+
subscribePipelineSettled: realtimeContext?.subscribePipelineSettled
|
|
9025
9116
|
});
|
|
9026
9117
|
}, [
|
|
9027
9118
|
realtimeContext?.sendCanvasBlob,
|
|
@@ -9033,6 +9124,7 @@ function CanvasIsolationConfigurator({
|
|
|
9033
9124
|
realtimeContext?.enableRealtime,
|
|
9034
9125
|
realtimeContext?.disableRealtime,
|
|
9035
9126
|
realtimeContext?.notifyPendingExportCancelled,
|
|
9127
|
+
realtimeContext?.subscribePipelineSettled,
|
|
9036
9128
|
placementNameProp,
|
|
9037
9129
|
onArtworkChange,
|
|
9038
9130
|
clearImageBitmapCache
|
|
@@ -9078,6 +9170,8 @@ function useIsolatedCanvas() {
|
|
|
9078
9170
|
handleExport: canvasExportService.handleExport,
|
|
9079
9171
|
/** Stable state change handler - pass to SnowconeCanvas onChange for JSON mode */
|
|
9080
9172
|
handleStateChange: canvasExportService.handleStateChange,
|
|
9173
|
+
/** Pointer released (drag end): flush a state held behind an in-flight frame. Safe on every pointerup. */
|
|
9174
|
+
handleInteractionEnd: canvasExportService.handleInteractionEnd,
|
|
9081
9175
|
/** Call when entering editor mode */
|
|
9082
9176
|
onEnterEditorMode: () => canvasExportService.onEnterEditorMode(),
|
|
9083
9177
|
/** Call when exiting editor mode */
|
|
@@ -10337,6 +10431,7 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10337
10431
|
const actualMockupId = (0, import_react46.useMemo)(() => {
|
|
10338
10432
|
return (0, import_sdk10.resolveMockupId)(context, actualGvid, mockupId);
|
|
10339
10433
|
}, [mockupId, actualGvid, context]);
|
|
10434
|
+
const unavailable = actualMockupId ? realtimeContext?.unavailableMockups?.get(actualMockupId) ?? null : null;
|
|
10340
10435
|
const artworkSrc = artwork?.src;
|
|
10341
10436
|
const selectedArtworkSrc = designContext?.selectedArtwork?.src;
|
|
10342
10437
|
const effectiveArtworkSrc = artworkSrc || selectedArtworkSrc;
|
|
@@ -10785,7 +10880,17 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10785
10880
|
className: `relative overflow-hidden ${className || ""}`,
|
|
10786
10881
|
style,
|
|
10787
10882
|
"data-hero-image": "true",
|
|
10883
|
+
"data-mockup-unavailable": unavailable ? unavailable.code : void 0,
|
|
10788
10884
|
children: [
|
|
10885
|
+
unavailable && /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
|
|
10886
|
+
"div",
|
|
10887
|
+
{
|
|
10888
|
+
role: "status",
|
|
10889
|
+
title: unavailable.message,
|
|
10890
|
+
className: "absolute inset-0 z-20 flex items-center justify-center bg-white/80 text-sm text-neutral-700",
|
|
10891
|
+
children: "Mockup unavailable"
|
|
10892
|
+
}
|
|
10893
|
+
),
|
|
10789
10894
|
renderedUrl && !imgFailed && /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
|
|
10790
10895
|
"img",
|
|
10791
10896
|
{
|