@snowcone-app/ui 0.9.3 → 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/CHANGELOG.md +15 -0
- package/dist/index.cjs +90 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +90 -24
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/composed/HeroProductImage.tsx +14 -3
- package/src/patterns/RealtimeProvider.tsx +127 -37
- package/src/patterns/__tests__/RealtimeProvider.cropDelivery.test.tsx +97 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#4351](https://github.com/snowcone-app/snowcone-monorepo/pull/4351) [`f9910a8`](https://github.com/snowcone-app/snowcone-monorepo/commit/f9910a86ec7d3ee48d9d4fbd5099f4790ce91e98) Thanks [@kevinsproles](https://github.com/kevinsproles)! - A cropped frame whose blob: URL was already revoked (its newer frame reached the SDK before React state delivered it) no longer switches the session to full-frame delivery: it waits up to 2 s for the newer frame, and only counts as a real failure if none arrives.
|
|
8
|
+
|
|
9
|
+
## 0.9.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#4344](https://github.com/snowcone-app/snowcone-monorepo/pull/4344) [`379a042`](https://github.com/snowcone-app/snowcone-monorepo/commit/379a042eb69a26f98fe0050f694c4ccf342e55b8) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Per-placement render versions and an explicit re-render. SDK: `MockupResult.versions` (every placement folded into a frame, from boxes that stamp it), `getLatestSentVersions()`, and `rerender(mockupIds)` (answered by `rerender_queued`; refusals are typed `E_RERENDER_*` errors). UI: a picture's `data-render-state` is `current` only when every placement is at its latest sent version, and a picture whose cropped frame failed is re-rendered with `rerender` once the box stamps versions (falling back to forcing it with configs on an `E_RERENDER_*` refusal or an older box).
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [[`379a042`](https://github.com/snowcone-app/snowcone-monorepo/commit/379a042eb69a26f98fe0050f694c4ccf342e55b8)]:
|
|
16
|
+
- @snowcone-app/sdk@0.25.0
|
|
17
|
+
|
|
3
18
|
## 0.9.3
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -6062,6 +6062,7 @@ async function compositeCroppedFrame(args) {
|
|
|
6062
6062
|
var import_jsx_runtime52 = require("react/jsx-runtime");
|
|
6063
6063
|
var DEFAULT_ARTBOARD_SIZE = 2e3;
|
|
6064
6064
|
var MAX_CONCURRENT_BLOBS = 5;
|
|
6065
|
+
var FRAME_SUPERSEDE_GRACE_MS = 2e3;
|
|
6065
6066
|
function mergeMockupIds(current, extra) {
|
|
6066
6067
|
const out = [...current];
|
|
6067
6068
|
for (const id of extra) if (!out.includes(id)) out.push(id);
|
|
@@ -6145,6 +6146,12 @@ function RealtimeProvider({
|
|
|
6145
6146
|
},
|
|
6146
6147
|
onError: (error, detail) => {
|
|
6147
6148
|
console.error(`[RealtimeProvider] Render error [${detail.code}]:`, error);
|
|
6149
|
+
if (detail.code.startsWith("E_RERENDER_") && pendingRerenderRef.current.length > 0) {
|
|
6150
|
+
const ids = pendingRerenderRef.current;
|
|
6151
|
+
pendingRerenderRef.current = [];
|
|
6152
|
+
forceRerenderByConfigRef.current?.(ids);
|
|
6153
|
+
return;
|
|
6154
|
+
}
|
|
6148
6155
|
if (detail.mockupId) {
|
|
6149
6156
|
const id = detail.mockupId;
|
|
6150
6157
|
setUnavailableMockups((prev) => prev.get(id)?.code === detail.code ? prev : new Map(prev).set(id, detail));
|
|
@@ -6170,7 +6177,9 @@ function RealtimeProvider({
|
|
|
6170
6177
|
disconnect,
|
|
6171
6178
|
flushPendingBlobs,
|
|
6172
6179
|
hasPendingBlobs,
|
|
6173
|
-
getLastSentVersion
|
|
6180
|
+
getLastSentVersion,
|
|
6181
|
+
getLatestSentVersions,
|
|
6182
|
+
rerender: rerenderRaw
|
|
6174
6183
|
} = realtimeMockup;
|
|
6175
6184
|
const mockupResultsRef = (0, import_react25.useRef)([]);
|
|
6176
6185
|
const mockupSubscribersRef = (0, import_react25.useRef)(
|
|
@@ -6208,6 +6217,9 @@ function RealtimeProvider({
|
|
|
6208
6217
|
const compositeBusyRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
|
|
6209
6218
|
const compositeCanvasRef = (0, import_react25.useRef)(null);
|
|
6210
6219
|
const requestedMockupIdsRef = (0, import_react25.useRef)([]);
|
|
6220
|
+
const serverStampsVersionsRef = (0, import_react25.useRef)(false);
|
|
6221
|
+
const pendingRerenderRef = (0, import_react25.useRef)([]);
|
|
6222
|
+
const forceRerenderByConfigRef = (0, import_react25.useRef)(null);
|
|
6211
6223
|
const fullFrameRerenderIdsRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
|
|
6212
6224
|
const fullFrameRerenderTimerRef = (0, import_react25.useRef)(null);
|
|
6213
6225
|
(0, import_react25.useEffect)(
|
|
@@ -6260,6 +6272,36 @@ function RealtimeProvider({
|
|
|
6260
6272
|
if (!croppedActiveRef.current) return;
|
|
6261
6273
|
sendCropConfig(false);
|
|
6262
6274
|
}, [sendCropConfig]);
|
|
6275
|
+
const sendFullFrameConfig = (0, import_react25.useCallback)(
|
|
6276
|
+
(mockupIds) => {
|
|
6277
|
+
const base = lastConfigBaseRef.current;
|
|
6278
|
+
if (!base) return;
|
|
6279
|
+
sendConfigRaw({
|
|
6280
|
+
...base,
|
|
6281
|
+
mockupIds,
|
|
6282
|
+
placementSettings: { ...placementSettingsOverridesRef.current }
|
|
6283
|
+
});
|
|
6284
|
+
},
|
|
6285
|
+
[sendConfigRaw]
|
|
6286
|
+
);
|
|
6287
|
+
const forceRerenderByConfig = (0, import_react25.useCallback)(
|
|
6288
|
+
(failed) => {
|
|
6289
|
+
const inForce = requestedMockupIdsRef.current;
|
|
6290
|
+
const fp = (list) => [...list].sort().join(",");
|
|
6291
|
+
const step = fp(failed) !== fp(inForce) ? failed : inForce.slice(0, -1);
|
|
6292
|
+
if (step.length === 0) {
|
|
6293
|
+
console.error(
|
|
6294
|
+
`[RealtimeProvider] E_OVERLAY_RERENDER_UNAVAILABLE: cannot force a full-frame re-render of ${failed.join(", ")} (single-mockup session on a box without \`rerender\`); it updates on the next edit.`
|
|
6295
|
+
);
|
|
6296
|
+
sendFullFrameConfig(inForce);
|
|
6297
|
+
return;
|
|
6298
|
+
}
|
|
6299
|
+
sendFullFrameConfig(step);
|
|
6300
|
+
sendFullFrameConfig(inForce);
|
|
6301
|
+
},
|
|
6302
|
+
[sendFullFrameConfig]
|
|
6303
|
+
);
|
|
6304
|
+
forceRerenderByConfigRef.current = forceRerenderByConfig;
|
|
6263
6305
|
const rerenderFullFrame = (0, import_react25.useCallback)(
|
|
6264
6306
|
(mockupId) => {
|
|
6265
6307
|
fullFrameRerenderIdsRef.current.add(mockupId);
|
|
@@ -6268,35 +6310,26 @@ function RealtimeProvider({
|
|
|
6268
6310
|
fullFrameRerenderTimerRef.current = null;
|
|
6269
6311
|
const failed = [...fullFrameRerenderIdsRef.current];
|
|
6270
6312
|
fullFrameRerenderIdsRef.current.clear();
|
|
6271
|
-
|
|
6272
|
-
if (!base || failed.length === 0) return;
|
|
6313
|
+
if (!lastConfigBaseRef.current || failed.length === 0) return;
|
|
6273
6314
|
const previous = requestedMockupIdsRef.current;
|
|
6274
6315
|
const ids = mergeMockupIds(previous, failed);
|
|
6275
6316
|
croppedActiveRef.current = false;
|
|
6276
6317
|
requestedMockupIdsRef.current = ids;
|
|
6277
|
-
const
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
placementSettings: { ...placementSettingsOverridesRef.current }
|
|
6281
|
-
});
|
|
6282
|
-
const fingerprint = (list) => [...list].sort().join(",");
|
|
6283
|
-
if (fingerprint(ids) !== fingerprint(previous)) {
|
|
6284
|
-
send(ids);
|
|
6318
|
+
const fp = (list) => [...list].sort().join(",");
|
|
6319
|
+
if (fp(ids) !== fp(previous)) {
|
|
6320
|
+
sendFullFrameConfig(ids);
|
|
6285
6321
|
return;
|
|
6286
6322
|
}
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
send(ids);
|
|
6293
|
-
return;
|
|
6323
|
+
if (serverStampsVersionsRef.current) {
|
|
6324
|
+
sendFullFrameConfig(ids);
|
|
6325
|
+
pendingRerenderRef.current = failed;
|
|
6326
|
+
if (rerenderRaw(failed)) return;
|
|
6327
|
+
pendingRerenderRef.current = [];
|
|
6294
6328
|
}
|
|
6295
|
-
|
|
6296
|
-
send(ids);
|
|
6329
|
+
forceRerenderByConfig(failed);
|
|
6297
6330
|
}, 0);
|
|
6298
6331
|
},
|
|
6299
|
-
[
|
|
6332
|
+
[sendFullFrameConfig, forceRerenderByConfig, rerenderRaw]
|
|
6300
6333
|
);
|
|
6301
6334
|
const maybeFlipCrop = (0, import_react25.useCallback)(() => {
|
|
6302
6335
|
if (!croppedDeliveryEnabled || !cropCapableRef.current) return;
|
|
@@ -6331,6 +6364,10 @@ function RealtimeProvider({
|
|
|
6331
6364
|
else merged.push(result);
|
|
6332
6365
|
});
|
|
6333
6366
|
mockupResultsRef.current = merged;
|
|
6367
|
+
if (pendingRerenderRef.current.length > 0) {
|
|
6368
|
+
const delivered = new Set(results.map((r) => r.mockupId));
|
|
6369
|
+
pendingRerenderRef.current = pendingRerenderRef.current.filter((id) => !delivered.has(id));
|
|
6370
|
+
}
|
|
6334
6371
|
const clearedPlacements = /* @__PURE__ */ new Set();
|
|
6335
6372
|
results.forEach((result) => {
|
|
6336
6373
|
const wasPending = pendingMockupIdsRef.current.has(result.mockupId);
|
|
@@ -6418,6 +6455,22 @@ function RealtimeProvider({
|
|
|
6418
6455
|
if (gen !== sceneGenRef.current || compositeQueueRef.current.has(result.mockupId) || processedFrameRef.current.get(result.mockupId) !== result.imageUrl) {
|
|
6419
6456
|
return;
|
|
6420
6457
|
}
|
|
6458
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6459
|
+
if (result.imageUrl.startsWith("blob:") && message.startsWith("E_OVERLAY_FRAME_LOAD") && message.includes(result.imageUrl.slice(0, 160))) {
|
|
6460
|
+
const failedUrl = result.imageUrl;
|
|
6461
|
+
setTimeout(() => {
|
|
6462
|
+
if (gen !== sceneGenRef.current) return;
|
|
6463
|
+
if (compositeQueueRef.current.has(result.mockupId)) return;
|
|
6464
|
+
if (processedFrameRef.current.get(result.mockupId) !== failedUrl) return;
|
|
6465
|
+
console.error(
|
|
6466
|
+
`[RealtimeProvider] E_OVERLAY_COMPOSITE_FAILED: frame for ${result.mockupId} failed to load and no newer frame arrived within ${FRAME_SUPERSEDE_GRACE_MS} ms \u2014 full-frame delivery for the rest of this session.`,
|
|
6467
|
+
err
|
|
6468
|
+
);
|
|
6469
|
+
cropCapableRef.current = false;
|
|
6470
|
+
rerenderFullFrame(result.mockupId);
|
|
6471
|
+
}, FRAME_SUPERSEDE_GRACE_MS);
|
|
6472
|
+
return;
|
|
6473
|
+
}
|
|
6421
6474
|
console.error(
|
|
6422
6475
|
"[RealtimeProvider] E_OVERLAY_COMPOSITE_FAILED: cropped-frame composite failed \u2014 full-frame delivery for the rest of this session.",
|
|
6423
6476
|
err
|
|
@@ -6452,6 +6505,9 @@ function RealtimeProvider({
|
|
|
6452
6505
|
}
|
|
6453
6506
|
console.log(`[RTP] rawMockupResults changed: ${rawMockupResults.length} result(s)`, rawMockupResults.map((r) => `${r.mockupId}${r.imageUrl ? " \u2713url" : " \u2717NO-URL"}${r.cropRect ? " [crop]" : ""}`));
|
|
6454
6507
|
const displayable = [];
|
|
6508
|
+
if (!serverStampsVersionsRef.current && rawMockupResults.some((r) => r.versions && typeof r.versions === "object")) {
|
|
6509
|
+
serverStampsVersionsRef.current = true;
|
|
6510
|
+
}
|
|
6455
6511
|
rawMockupResults.forEach((result) => {
|
|
6456
6512
|
if (!result.imageUrl) {
|
|
6457
6513
|
console.log(`[RTP] Result for ${result.mockupId} has NO imageUrl \u2014 not caching, pending NOT cleared`);
|
|
@@ -6934,6 +6990,9 @@ function RealtimeProvider({
|
|
|
6934
6990
|
const getLastSentVersionRef = (0, import_react25.useRef)(getLastSentVersion);
|
|
6935
6991
|
getLastSentVersionRef.current = getLastSentVersion;
|
|
6936
6992
|
const getCanvasVersion = (0, import_react25.useCallback)(() => getLastSentVersionRef.current?.() ?? 0, []);
|
|
6993
|
+
const getLatestSentVersionsRef = (0, import_react25.useRef)(getLatestSentVersions);
|
|
6994
|
+
getLatestSentVersionsRef.current = getLatestSentVersions;
|
|
6995
|
+
const getLatestSentVersionsStable = (0, import_react25.useCallback)(() => getLatestSentVersionsRef.current?.() ?? {}, []);
|
|
6937
6996
|
const stableSubscribePipelineSettled = (0, import_react25.useCallback)(
|
|
6938
6997
|
(callback) => {
|
|
6939
6998
|
return subscribePipelineSettledRef.current(callback);
|
|
@@ -6983,6 +7042,7 @@ function RealtimeProvider({
|
|
|
6983
7042
|
getMockupUrl: getMockupUrl3,
|
|
6984
7043
|
getMockupUrls,
|
|
6985
7044
|
getCanvasVersion,
|
|
7045
|
+
getLatestSentVersions: getLatestSentVersionsStable,
|
|
6986
7046
|
isMockupLoading,
|
|
6987
7047
|
registerBlobUrl,
|
|
6988
7048
|
// WebRTC streaming
|
|
@@ -7015,6 +7075,7 @@ function RealtimeProvider({
|
|
|
7015
7075
|
getMockupUrl3,
|
|
7016
7076
|
getMockupUrls,
|
|
7017
7077
|
getCanvasVersion,
|
|
7078
|
+
getLatestSentVersionsStable,
|
|
7018
7079
|
isMockupLoading,
|
|
7019
7080
|
registerBlobUrl
|
|
7020
7081
|
]
|
|
@@ -10769,6 +10830,8 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10769
10830
|
};
|
|
10770
10831
|
}, [mode, realtimeContext?.subscribePendingBlob, placement]);
|
|
10771
10832
|
const renderVersionRef = (0, import_react46.useRef)(0);
|
|
10833
|
+
const renderVersionsRef = (0, import_react46.useRef)(null);
|
|
10834
|
+
const getLatestSentVersions = realtimeContext?.getLatestSentVersions;
|
|
10772
10835
|
const refusalCodeRef = (0, import_react46.useRef)(null);
|
|
10773
10836
|
refusalCodeRef.current = unavailable?.code ?? null;
|
|
10774
10837
|
const sessionErrorCodeRef = (0, import_react46.useRef)(null);
|
|
@@ -10785,11 +10848,13 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10785
10848
|
return;
|
|
10786
10849
|
}
|
|
10787
10850
|
const rendered = renderVersionRef.current;
|
|
10788
|
-
const
|
|
10851
|
+
const versions = renderVersionsRef.current;
|
|
10852
|
+
const sent = versions ? getLatestSentVersions?.() : void 0;
|
|
10853
|
+
const behind = versions && sent ? Object.entries(sent).some(([placement2, v]) => (versions[placement2] ?? -1) < v) : rendered < canvas;
|
|
10789
10854
|
node.dataset.canvasVersion = String(canvas);
|
|
10790
10855
|
node.dataset.renderVersion = String(rendered);
|
|
10791
10856
|
node.dataset.renderState = refusalCodeRef.current || behind && sessionErrorCodeRef.current ? "refused" : behind ? "stale" : "current";
|
|
10792
|
-
}, [getCanvasVersion]);
|
|
10857
|
+
}, [getCanvasVersion, getLatestSentVersions]);
|
|
10793
10858
|
(0, import_react46.useEffect)(() => {
|
|
10794
10859
|
applyRenderStamp();
|
|
10795
10860
|
});
|
|
@@ -10799,8 +10864,9 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10799
10864
|
if (!actualMockupId || !subscribeSent || !subscribeResult) return;
|
|
10800
10865
|
const offSent = subscribeSent(() => applyRenderStamp());
|
|
10801
10866
|
const offResult = subscribeResult(actualMockupId, (result) => {
|
|
10802
|
-
if (typeof result?.requestVersion === "number" && result.requestVersion
|
|
10867
|
+
if (typeof result?.requestVersion === "number" && result.requestVersion >= renderVersionRef.current) {
|
|
10803
10868
|
renderVersionRef.current = result.requestVersion;
|
|
10869
|
+
renderVersionsRef.current = result.versions ?? null;
|
|
10804
10870
|
}
|
|
10805
10871
|
applyRenderStamp();
|
|
10806
10872
|
});
|