@snowcone-app/sdk 0.3.3 → 0.3.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 +24 -0
- package/dist/{chunk-D5ZRGKA5.js → chunk-YTOTCNEW.js} +88 -7
- package/dist/index.cjs +158 -15
- package/dist/index.d.cts +30 -4
- package/dist/index.d.ts +30 -4
- package/dist/index.js +71 -9
- package/dist/react.cjs +88 -7
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{websocket-Poy8LZNA.d.cts → websocket-hXpNcyEn.d.cts} +14 -2
- package/dist/{websocket-Poy8LZNA.d.ts → websocket-hXpNcyEn.d.ts} +14 -2
- package/package.json +9 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createDevFetcher } from './dev-fetcher.cjs';
|
|
2
2
|
import { PublicDesign, PublicFill, PublicOptions } from '@snowcone-app/mockup-url';
|
|
3
|
-
import { O as OptionAttribute, a as OptionSelection, C as Combination, F as FrameworkAdapter, b as ComponentProps, c as ComponentDescriptor, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, g as FrameworkUtilities, P as PlacementSettings, M as MockupResult, R as RealtimeMockupService } from './websocket-
|
|
4
|
-
export { A as AdapterRegistry, h as ProductComponentContext, i as RealtimeMockupCallbacks, j as RealtimeMockupState, k as RenderResult, W as WebSocketConfig, l as WebSocketMessage, m as adapterRegistry, n as computeDisabledChoices, o as createComponent, p as defineComponent, q as deriveDefaultSelection, r as findBestCombination, s as getPricePreview, t as isOptionAvailable, u as resolveBestCombination } from './websocket-
|
|
3
|
+
import { O as OptionAttribute, a as OptionSelection, C as Combination, F as FrameworkAdapter, b as ComponentProps, c as ComponentDescriptor, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, g as FrameworkUtilities, P as PlacementSettings, M as MockupResult, R as RealtimeMockupService } from './websocket-hXpNcyEn.cjs';
|
|
4
|
+
export { A as AdapterRegistry, h as ProductComponentContext, i as RealtimeMockupCallbacks, j as RealtimeMockupState, k as RenderResult, W as WebSocketConfig, l as WebSocketMessage, m as adapterRegistry, n as computeDisabledChoices, o as createComponent, p as defineComponent, q as deriveDefaultSelection, r as findBestCombination, s as getPricePreview, t as isOptionAvailable, u as resolveBestCombination } from './websocket-hXpNcyEn.cjs';
|
|
5
5
|
|
|
6
6
|
interface CatalogProduct$2 {
|
|
7
7
|
id: string;
|
|
@@ -2584,8 +2584,7 @@ declare function fetchRealtimeGrant(grantUrl: string, shop: string, fetchImpl?:
|
|
|
2584
2584
|
* product: { productId: 'BEEB77', mockupIds: ['front'] },
|
|
2585
2585
|
* });
|
|
2586
2586
|
* session.onMockups((results) => { img.src = results[0].imageUrl; });
|
|
2587
|
-
* await session.
|
|
2588
|
-
* session.renderState('Front', serializeStateForServer(canvasState));
|
|
2587
|
+
* await session.renderState('Front', serializeStateForServer(canvasState));
|
|
2589
2588
|
*
|
|
2590
2589
|
* For the low-level escape hatch, use {@link RealtimeMockupService} directly.
|
|
2591
2590
|
*/
|
|
@@ -2656,6 +2655,16 @@ interface RenderProduct {
|
|
|
2656
2655
|
* catalog product's `options` object verbatim.
|
|
2657
2656
|
*/
|
|
2658
2657
|
options?: RenderProductOptions;
|
|
2658
|
+
/**
|
|
2659
|
+
* The product's valid placement labels (the catalog product's
|
|
2660
|
+
* `placements[].label`, e.g. `['Front']`). Optional, but passing it lets the
|
|
2661
|
+
* session WARN LOUDLY when a `renderState(placement, ...)` call uses a name
|
|
2662
|
+
* that isn't a real placement — turning the classic silent blank-mockup
|
|
2663
|
+
* (server waits on `incomplete_canvas_placements`) into an actionable
|
|
2664
|
+
* dev-time message. You can pass the catalog product's
|
|
2665
|
+
* `placements.map(p => p.label)` verbatim.
|
|
2666
|
+
*/
|
|
2667
|
+
placements?: string[];
|
|
2659
2668
|
/** Render width in px (default 1000). */
|
|
2660
2669
|
width?: number;
|
|
2661
2670
|
/** Per-placement settings (tiling, scale mode). */
|
|
@@ -2756,6 +2765,14 @@ declare class RenderSession {
|
|
|
2756
2765
|
getMockups(): MockupResult[];
|
|
2757
2766
|
/** Close the WebSocket and stop auto-renew. */
|
|
2758
2767
|
close(): void;
|
|
2768
|
+
/**
|
|
2769
|
+
* Emit loud, non-fatal warnings for the two ways a placement name silently
|
|
2770
|
+
* produces a blank mockup: (1) the name isn't one of the product's known
|
|
2771
|
+
* `placements` labels, and (2) no artboard in `state` is named after it. Each
|
|
2772
|
+
* warning goes to BOTH `console.warn` and the `onError` callback (if wired) so
|
|
2773
|
+
* a dev sees it whichever channel they watch. Never throws.
|
|
2774
|
+
*/
|
|
2775
|
+
private warnPlacement;
|
|
2759
2776
|
/**
|
|
2760
2777
|
* (Re)arm the render watchdog. Clears any prior timer and, unless disabled
|
|
2761
2778
|
* (`renderTimeoutMs <= 0`), starts a fresh one. If it fires before a mockup
|
|
@@ -2763,6 +2780,15 @@ declare class RenderSession {
|
|
|
2763
2780
|
* server/transport errors — with a hint at the most common cause.
|
|
2764
2781
|
*/
|
|
2765
2782
|
private armWatchdog;
|
|
2783
|
+
/**
|
|
2784
|
+
* Build the watchdog timeout message from the *observed* session state rather
|
|
2785
|
+
* than a single hardcoded guess. The old message always blamed a missing
|
|
2786
|
+
* `variantId` — which sent a clean-room dogfood chasing the wrong cause when
|
|
2787
|
+
* the real problem was elsewhere. Report what's actually true: is the socket
|
|
2788
|
+
* open, did config get acknowledged, and only then fall back to the
|
|
2789
|
+
* variant/catalog hypotheses, naming the exact product so it's checkable.
|
|
2790
|
+
*/
|
|
2791
|
+
private timeoutMessage;
|
|
2766
2792
|
/** Cancel the pending render watchdog, if any. */
|
|
2767
2793
|
private clearWatchdog;
|
|
2768
2794
|
/** Escape hatch: the underlying low-level service. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createDevFetcher } from './dev-fetcher.js';
|
|
2
2
|
import { PublicDesign, PublicFill, PublicOptions } from '@snowcone-app/mockup-url';
|
|
3
|
-
import { O as OptionAttribute, a as OptionSelection, C as Combination, F as FrameworkAdapter, b as ComponentProps, c as ComponentDescriptor, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, g as FrameworkUtilities, P as PlacementSettings, M as MockupResult, R as RealtimeMockupService } from './websocket-
|
|
4
|
-
export { A as AdapterRegistry, h as ProductComponentContext, i as RealtimeMockupCallbacks, j as RealtimeMockupState, k as RenderResult, W as WebSocketConfig, l as WebSocketMessage, m as adapterRegistry, n as computeDisabledChoices, o as createComponent, p as defineComponent, q as deriveDefaultSelection, r as findBestCombination, s as getPricePreview, t as isOptionAvailable, u as resolveBestCombination } from './websocket-
|
|
3
|
+
import { O as OptionAttribute, a as OptionSelection, C as Combination, F as FrameworkAdapter, b as ComponentProps, c as ComponentDescriptor, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, g as FrameworkUtilities, P as PlacementSettings, M as MockupResult, R as RealtimeMockupService } from './websocket-hXpNcyEn.js';
|
|
4
|
+
export { A as AdapterRegistry, h as ProductComponentContext, i as RealtimeMockupCallbacks, j as RealtimeMockupState, k as RenderResult, W as WebSocketConfig, l as WebSocketMessage, m as adapterRegistry, n as computeDisabledChoices, o as createComponent, p as defineComponent, q as deriveDefaultSelection, r as findBestCombination, s as getPricePreview, t as isOptionAvailable, u as resolveBestCombination } from './websocket-hXpNcyEn.js';
|
|
5
5
|
|
|
6
6
|
interface CatalogProduct$2 {
|
|
7
7
|
id: string;
|
|
@@ -2584,8 +2584,7 @@ declare function fetchRealtimeGrant(grantUrl: string, shop: string, fetchImpl?:
|
|
|
2584
2584
|
* product: { productId: 'BEEB77', mockupIds: ['front'] },
|
|
2585
2585
|
* });
|
|
2586
2586
|
* session.onMockups((results) => { img.src = results[0].imageUrl; });
|
|
2587
|
-
* await session.
|
|
2588
|
-
* session.renderState('Front', serializeStateForServer(canvasState));
|
|
2587
|
+
* await session.renderState('Front', serializeStateForServer(canvasState));
|
|
2589
2588
|
*
|
|
2590
2589
|
* For the low-level escape hatch, use {@link RealtimeMockupService} directly.
|
|
2591
2590
|
*/
|
|
@@ -2656,6 +2655,16 @@ interface RenderProduct {
|
|
|
2656
2655
|
* catalog product's `options` object verbatim.
|
|
2657
2656
|
*/
|
|
2658
2657
|
options?: RenderProductOptions;
|
|
2658
|
+
/**
|
|
2659
|
+
* The product's valid placement labels (the catalog product's
|
|
2660
|
+
* `placements[].label`, e.g. `['Front']`). Optional, but passing it lets the
|
|
2661
|
+
* session WARN LOUDLY when a `renderState(placement, ...)` call uses a name
|
|
2662
|
+
* that isn't a real placement — turning the classic silent blank-mockup
|
|
2663
|
+
* (server waits on `incomplete_canvas_placements`) into an actionable
|
|
2664
|
+
* dev-time message. You can pass the catalog product's
|
|
2665
|
+
* `placements.map(p => p.label)` verbatim.
|
|
2666
|
+
*/
|
|
2667
|
+
placements?: string[];
|
|
2659
2668
|
/** Render width in px (default 1000). */
|
|
2660
2669
|
width?: number;
|
|
2661
2670
|
/** Per-placement settings (tiling, scale mode). */
|
|
@@ -2756,6 +2765,14 @@ declare class RenderSession {
|
|
|
2756
2765
|
getMockups(): MockupResult[];
|
|
2757
2766
|
/** Close the WebSocket and stop auto-renew. */
|
|
2758
2767
|
close(): void;
|
|
2768
|
+
/**
|
|
2769
|
+
* Emit loud, non-fatal warnings for the two ways a placement name silently
|
|
2770
|
+
* produces a blank mockup: (1) the name isn't one of the product's known
|
|
2771
|
+
* `placements` labels, and (2) no artboard in `state` is named after it. Each
|
|
2772
|
+
* warning goes to BOTH `console.warn` and the `onError` callback (if wired) so
|
|
2773
|
+
* a dev sees it whichever channel they watch. Never throws.
|
|
2774
|
+
*/
|
|
2775
|
+
private warnPlacement;
|
|
2759
2776
|
/**
|
|
2760
2777
|
* (Re)arm the render watchdog. Clears any prior timer and, unless disabled
|
|
2761
2778
|
* (`renderTimeoutMs <= 0`), starts a fresh one. If it fires before a mockup
|
|
@@ -2763,6 +2780,15 @@ declare class RenderSession {
|
|
|
2763
2780
|
* server/transport errors — with a hint at the most common cause.
|
|
2764
2781
|
*/
|
|
2765
2782
|
private armWatchdog;
|
|
2783
|
+
/**
|
|
2784
|
+
* Build the watchdog timeout message from the *observed* session state rather
|
|
2785
|
+
* than a single hardcoded guess. The old message always blamed a missing
|
|
2786
|
+
* `variantId` — which sent a clean-room dogfood chasing the wrong cause when
|
|
2787
|
+
* the real problem was elsewhere. Report what's actually true: is the socket
|
|
2788
|
+
* open, did config get acknowledged, and only then fall back to the
|
|
2789
|
+
* variant/catalog hypotheses, naming the exact product so it's checkable.
|
|
2790
|
+
*/
|
|
2791
|
+
private timeoutMessage;
|
|
2766
2792
|
/** Cancel the pending render watchdog, if any. */
|
|
2767
2793
|
private clearWatchdog;
|
|
2768
2794
|
/** Escape hatch: the underlying low-level service. */
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
REALTIME_WS_URL,
|
|
6
6
|
RealtimeMockupService
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-YTOTCNEW.js";
|
|
8
8
|
|
|
9
9
|
// src/validation.ts
|
|
10
10
|
import { z } from "zod";
|
|
@@ -1642,14 +1642,12 @@ function buildMockupUrl2(options, cfg) {
|
|
|
1642
1642
|
);
|
|
1643
1643
|
}
|
|
1644
1644
|
function resolveMockupBaseUrl() {
|
|
1645
|
-
const env = typeof process !== "undefined" ? process.env : void 0;
|
|
1646
1645
|
const winConfig = typeof window !== "undefined" && window.snowcone || {};
|
|
1647
|
-
return winConfig.mockupUrl || env?.SNOWCONE_IMAGE_URL || env?.NEXT_PUBLIC_MERCH_MOCKUP_URL || "https://cdn.snowcone.app";
|
|
1646
|
+
return winConfig.mockupUrl || typeof process !== "undefined" && process.env?.SNOWCONE_IMAGE_URL || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MERCH_MOCKUP_URL || "https://cdn.snowcone.app";
|
|
1648
1647
|
}
|
|
1649
1648
|
function resolveShop() {
|
|
1650
|
-
const env = typeof process !== "undefined" ? process.env : void 0;
|
|
1651
1649
|
const winConfig = typeof window !== "undefined" && window.snowcone || {};
|
|
1652
|
-
return winConfig.shop || env?.SNOWCONE_SHOP_ID || env?.NEXT_PUBLIC_SNOWCONE_SHOP_ID || "SHOP_NOT_CONFIGURED";
|
|
1650
|
+
return winConfig.shop || typeof process !== "undefined" && process.env?.SNOWCONE_SHOP_ID || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_SNOWCONE_SHOP_ID || "SHOP_NOT_CONFIGURED";
|
|
1653
1651
|
}
|
|
1654
1652
|
function mockupUrl(options) {
|
|
1655
1653
|
const mockupBaseUrl = resolveMockupBaseUrl();
|
|
@@ -4714,6 +4712,20 @@ function assertVariantSelected(product) {
|
|
|
4714
4712
|
);
|
|
4715
4713
|
}
|
|
4716
4714
|
}
|
|
4715
|
+
function warnUnknownPlacement(product, placement) {
|
|
4716
|
+
const placements = product.placements;
|
|
4717
|
+
if (!placements?.length) return null;
|
|
4718
|
+
if (placements.includes(placement)) return null;
|
|
4719
|
+
return `RenderSession: placement "${placement}" doesn't match product ${product.productId}'s placements: [${placements.join(", ")}]. Your artboard name + renderState placement arg must equal the catalog placements[].label.`;
|
|
4720
|
+
}
|
|
4721
|
+
function warnPlacementArtboardMismatch(placement, state) {
|
|
4722
|
+
const artboards = state.artboards;
|
|
4723
|
+
if (!Array.isArray(artboards) || artboards.length === 0) return null;
|
|
4724
|
+
const names = artboards.map((a) => a.name).filter((n) => typeof n === "string");
|
|
4725
|
+
if (names.length === 0) return null;
|
|
4726
|
+
if (names.includes(placement)) return null;
|
|
4727
|
+
return `RenderSession: renderState placement "${placement}" has no matching artboard in the state (artboards: [${names.join(", ")}]). The artboard name must equal the placement arg.`;
|
|
4728
|
+
}
|
|
4717
4729
|
var DEFAULT_RENDER_TIMEOUT_MS = 3e4;
|
|
4718
4730
|
var RenderSession = class {
|
|
4719
4731
|
svc;
|
|
@@ -4823,6 +4835,7 @@ var RenderSession = class {
|
|
|
4823
4835
|
*/
|
|
4824
4836
|
async renderState(placement, state, throttleMs = 0) {
|
|
4825
4837
|
await this.connect();
|
|
4838
|
+
this.warnPlacement(placement, state);
|
|
4826
4839
|
this.svc.sendCanvasState(placement, state, this.product?.mockupIds.length ?? 1, throttleMs);
|
|
4827
4840
|
this.armWatchdog();
|
|
4828
4841
|
}
|
|
@@ -4835,6 +4848,7 @@ var RenderSession = class {
|
|
|
4835
4848
|
*/
|
|
4836
4849
|
async renderSavedState(placement, stateId) {
|
|
4837
4850
|
await this.connect();
|
|
4851
|
+
this.warnPlacement(placement);
|
|
4838
4852
|
this.svc.sendCanvasStateRef(placement, stateId);
|
|
4839
4853
|
this.armWatchdog();
|
|
4840
4854
|
}
|
|
@@ -4853,6 +4867,28 @@ var RenderSession = class {
|
|
|
4853
4867
|
this.svc.disconnect();
|
|
4854
4868
|
this.ready = null;
|
|
4855
4869
|
}
|
|
4870
|
+
/**
|
|
4871
|
+
* Emit loud, non-fatal warnings for the two ways a placement name silently
|
|
4872
|
+
* produces a blank mockup: (1) the name isn't one of the product's known
|
|
4873
|
+
* `placements` labels, and (2) no artboard in `state` is named after it. Each
|
|
4874
|
+
* warning goes to BOTH `console.warn` and the `onError` callback (if wired) so
|
|
4875
|
+
* a dev sees it whichever channel they watch. Never throws.
|
|
4876
|
+
*/
|
|
4877
|
+
warnPlacement(placement, state) {
|
|
4878
|
+
const warnings = [];
|
|
4879
|
+
if (this.product) {
|
|
4880
|
+
const w = warnUnknownPlacement(this.product, placement);
|
|
4881
|
+
if (w) warnings.push(w);
|
|
4882
|
+
}
|
|
4883
|
+
if (state) {
|
|
4884
|
+
const w = warnPlacementArtboardMismatch(placement, state);
|
|
4885
|
+
if (w) warnings.push(w);
|
|
4886
|
+
}
|
|
4887
|
+
for (const w of warnings) {
|
|
4888
|
+
console.warn(w);
|
|
4889
|
+
this.errorCb?.(w);
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4856
4892
|
/**
|
|
4857
4893
|
* (Re)arm the render watchdog. Clears any prior timer and, unless disabled
|
|
4858
4894
|
* (`renderTimeoutMs <= 0`), starts a fresh one. If it fires before a mockup
|
|
@@ -4864,11 +4900,32 @@ var RenderSession = class {
|
|
|
4864
4900
|
if (this.renderTimeoutMs <= 0) return;
|
|
4865
4901
|
this.watchdog = setTimeout(() => {
|
|
4866
4902
|
this.watchdog = null;
|
|
4867
|
-
this.errorCb?.(
|
|
4868
|
-
`realtime render timed out after ${this.renderTimeoutMs}ms with no mockup \u2014 a product with a color/variant axis needs \`variantId\` in the product config (see RenderProduct.variantId)`
|
|
4869
|
-
);
|
|
4903
|
+
this.errorCb?.(this.timeoutMessage());
|
|
4870
4904
|
}, this.renderTimeoutMs);
|
|
4871
4905
|
}
|
|
4906
|
+
/**
|
|
4907
|
+
* Build the watchdog timeout message from the *observed* session state rather
|
|
4908
|
+
* than a single hardcoded guess. The old message always blamed a missing
|
|
4909
|
+
* `variantId` — which sent a clean-room dogfood chasing the wrong cause when
|
|
4910
|
+
* the real problem was elsewhere. Report what's actually true: is the socket
|
|
4911
|
+
* open, did config get acknowledged, and only then fall back to the
|
|
4912
|
+
* variant/catalog hypotheses, naming the exact product so it's checkable.
|
|
4913
|
+
*/
|
|
4914
|
+
timeoutMessage() {
|
|
4915
|
+
const base = `realtime render timed out after ${this.renderTimeoutMs}ms with no mockup`;
|
|
4916
|
+
const st = this.svc.getState();
|
|
4917
|
+
if (!st.isConnected) {
|
|
4918
|
+
return `${base} \u2014 the WebSocket is not open (it connected, then closed). Check the grant endpoint and network; the session needs a live socket to render.`;
|
|
4919
|
+
}
|
|
4920
|
+
if (!st.isConfigured) {
|
|
4921
|
+
return `${base} \u2014 the server never acknowledged the session config. The socket is open but no \`configured\` message arrived; the render was never accepted.`;
|
|
4922
|
+
}
|
|
4923
|
+
const product = this.product;
|
|
4924
|
+
if (product && variantIdsOf(product).length >= 2 && !product.variantId) {
|
|
4925
|
+
return `${base} \u2014 product ${product.productId} has a color/variant axis but no \`variantId\` was set, so the server is waiting for a variant selection (see RenderProduct.variantId).`;
|
|
4926
|
+
}
|
|
4927
|
+
return `${base} \u2014 the server accepted the config but rendered nothing. Verify the product and mockupIds exist in the catalog (productId="${product?.productId}", mockupIds=[${product?.mockupIds?.join(", ") ?? ""}]) and that the placement name matches an artboard.`;
|
|
4928
|
+
}
|
|
4872
4929
|
/** Cancel the pending render watchdog, if any. */
|
|
4873
4930
|
clearWatchdog() {
|
|
4874
4931
|
if (this.watchdog) {
|
|
@@ -4968,7 +5025,12 @@ async function getProduct(idOrSlug, config) {
|
|
|
4968
5025
|
const url = `${meilisearchHost}/indexes/${meilisearchIndex}/documents/${idOrSlug}`;
|
|
4969
5026
|
const res = await f(url, { method: "GET", headers });
|
|
4970
5027
|
if (res.status === 404)
|
|
4971
|
-
throw Object.assign(
|
|
5028
|
+
throw Object.assign(
|
|
5029
|
+
new Error(
|
|
5030
|
+
`getProduct: no catalog product "${idOrSlug}" in index "${meilisearchIndex}". Pass a valid product id/slug (browse with listProducts()).`
|
|
5031
|
+
),
|
|
5032
|
+
{ code: "NOT_FOUND", productId: idOrSlug }
|
|
5033
|
+
);
|
|
4972
5034
|
if (!res.ok) throw new Error(`getProduct failed: ${res.status}`);
|
|
4973
5035
|
const raw = await res.json();
|
|
4974
5036
|
return validateProductLoose(raw);
|
package/dist/react.cjs
CHANGED
|
@@ -34,6 +34,16 @@ var import_react = require("react");
|
|
|
34
34
|
var REALTIME_WS_URL = "wss://cdn.snowcone.app/realtime";
|
|
35
35
|
|
|
36
36
|
// src/realtime/websocket.ts
|
|
37
|
+
function randHex(len) {
|
|
38
|
+
const arr = new Uint8Array(len);
|
|
39
|
+
globalThis.crypto.getRandomValues(arr);
|
|
40
|
+
return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
41
|
+
}
|
|
42
|
+
function newRenderCorrelation() {
|
|
43
|
+
const traceId = randHex(16);
|
|
44
|
+
const spanId = randHex(8);
|
|
45
|
+
return { requestId: traceId, traceparent: `00-${traceId}-${spanId}-01` };
|
|
46
|
+
}
|
|
37
47
|
var RealtimeMockupService = class {
|
|
38
48
|
constructor(wsUrl = REALTIME_WS_URL) {
|
|
39
49
|
this.wsUrl = wsUrl;
|
|
@@ -49,8 +59,25 @@ var RealtimeMockupService = class {
|
|
|
49
59
|
lastError = null;
|
|
50
60
|
callbacks = {};
|
|
51
61
|
lastBlobSentAt = 0;
|
|
62
|
+
// Per-request send timestamps keyed by requestId, for client-observed
|
|
63
|
+
// round-trip timing in the mockup_rendered log. Pruned to a small cap so an
|
|
64
|
+
// in-flight burst can't grow it unbounded.
|
|
65
|
+
requestSendTimes = /* @__PURE__ */ new Map();
|
|
52
66
|
canvasBlobs = /* @__PURE__ */ new Map();
|
|
53
67
|
canvasStates = /* @__PURE__ */ new Map();
|
|
68
|
+
// States sent BEFORE the session is connected+configured. The #1 recurring DX
|
|
69
|
+
// failure ("connect race"): a caller fires renderState/sendCanvasState during
|
|
70
|
+
// the WS handshake, the state was silently dropped, and the 30s watchdog fired
|
|
71
|
+
// with no mockup pointing at the wrong cause. Blobs were already buffered and
|
|
72
|
+
// auto-flushed on `configured`; states were not. We now buffer the latest
|
|
73
|
+
// state per placement and flush it on `configured`, so the first render
|
|
74
|
+
// survives the race on EVERY path through this service — the `RenderSession`
|
|
75
|
+
// facade and the `useRealtimeMockup` hook alike.
|
|
76
|
+
pendingStates = /* @__PURE__ */ new Map();
|
|
77
|
+
// Serialized JSON of the last canvas state ACTUALLY SENT per placement. Used to
|
|
78
|
+
// drop duplicate consecutive states so an idle canvas (whose onChange keeps
|
|
79
|
+
// firing identical states) doesn't stream renders and burn the realtime budget.
|
|
80
|
+
lastSentStateJson = {};
|
|
54
81
|
colors = /* @__PURE__ */ new Map();
|
|
55
82
|
lastSendTime = {};
|
|
56
83
|
throttleTimeouts = {};
|
|
@@ -71,6 +98,8 @@ var RealtimeMockupService = class {
|
|
|
71
98
|
// the WS with `?token=`, and renews ~15s before expiry via a `renew` message.
|
|
72
99
|
tokenProvider;
|
|
73
100
|
renewTimer = null;
|
|
101
|
+
isConnecting = false;
|
|
102
|
+
connectAttempt = 0;
|
|
74
103
|
setCallbacks(callbacks) {
|
|
75
104
|
this.callbacks = callbacks;
|
|
76
105
|
}
|
|
@@ -97,16 +126,21 @@ var RealtimeMockupService = class {
|
|
|
97
126
|
this.callbacks.onLog?.(message, type);
|
|
98
127
|
}
|
|
99
128
|
connect() {
|
|
100
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
129
|
+
if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING || this.isConnecting) {
|
|
101
130
|
return;
|
|
102
131
|
}
|
|
132
|
+
const attempt = ++this.connectAttempt;
|
|
133
|
+
this.isConnecting = true;
|
|
103
134
|
if (this.tokenProvider) {
|
|
104
135
|
this.tokenProvider().then((grant) => {
|
|
136
|
+
if (attempt !== this.connectAttempt || !this.isConnecting) return;
|
|
105
137
|
this.openSocket(grant.token);
|
|
106
138
|
this.scheduleRenew(grant.expiresAt);
|
|
107
139
|
}).catch((err) => {
|
|
140
|
+
if (attempt !== this.connectAttempt) return;
|
|
108
141
|
this.addLog(`Failed to obtain realtime grant: ${err}`);
|
|
109
142
|
this.status = "Disconnected";
|
|
143
|
+
this.isConnecting = false;
|
|
110
144
|
});
|
|
111
145
|
return;
|
|
112
146
|
}
|
|
@@ -120,6 +154,7 @@ var RealtimeMockupService = class {
|
|
|
120
154
|
console.log(`[WS] connection OPENED to ${this.wsUrl}`);
|
|
121
155
|
this.addLog("WebSocket connection opened");
|
|
122
156
|
this.status = "Connected";
|
|
157
|
+
this.isConnecting = false;
|
|
123
158
|
};
|
|
124
159
|
this.ws.onmessage = (event) => {
|
|
125
160
|
try {
|
|
@@ -138,11 +173,13 @@ var RealtimeMockupService = class {
|
|
|
138
173
|
this.isConfigured = false;
|
|
139
174
|
this.configSent = false;
|
|
140
175
|
this.clearRenew();
|
|
176
|
+
this.isConnecting = false;
|
|
141
177
|
this.callbacks.onDisconnected?.();
|
|
142
178
|
};
|
|
143
179
|
this.ws.onerror = (error) => {
|
|
144
180
|
this.addLog(`WebSocket error: ${error}`);
|
|
145
181
|
this.status = "Disconnected";
|
|
182
|
+
this.isConnecting = false;
|
|
146
183
|
};
|
|
147
184
|
}
|
|
148
185
|
scheduleRenew(expiresAt) {
|
|
@@ -205,6 +242,16 @@ var RealtimeMockupService = class {
|
|
|
205
242
|
});
|
|
206
243
|
}, 100);
|
|
207
244
|
}
|
|
245
|
+
if (this.pendingStates.size > 0) {
|
|
246
|
+
const pending = Array.from(this.pendingStates.entries());
|
|
247
|
+
this.pendingStates.clear();
|
|
248
|
+
this.addLog(`\u{1F4E6} Flushing ${pending.length} buffered canvas state(s)`);
|
|
249
|
+
setTimeout(() => {
|
|
250
|
+
for (const [placement, { state, mockupCount }] of pending) {
|
|
251
|
+
this.sendCanvasState(placement, state, mockupCount, 0);
|
|
252
|
+
}
|
|
253
|
+
}, 100);
|
|
254
|
+
}
|
|
208
255
|
break;
|
|
209
256
|
case "blob_received":
|
|
210
257
|
const placementName = data.placement || "unknown";
|
|
@@ -218,8 +265,10 @@ var RealtimeMockupService = class {
|
|
|
218
265
|
case "rendering_started":
|
|
219
266
|
this.addLog("\u{1F3A8} Mockup rendering has started...");
|
|
220
267
|
break;
|
|
221
|
-
case "mockup_rendered":
|
|
222
|
-
|
|
268
|
+
case "mockup_rendered": {
|
|
269
|
+
const sentAt = data.requestId ? this.requestSendTimes.get(data.requestId) : void 0;
|
|
270
|
+
const clientRoundTripMs = sentAt !== void 0 ? Date.now() - sentAt : void 0;
|
|
271
|
+
console.log(`[WS] mockup_rendered received: mockupId=${data.mockupId} hasImageUrl=${!!data.imageUrl} v=${data.requestVersion} req=${data.requestId ?? "?"} placement="${data.placement}" renderMs=${data.renderMs} blobToRenderMs=${data.blobToRenderMs} clientRoundTripMs=${clientRoundTripMs ?? "?"} at ${Date.now()}`);
|
|
223
272
|
if (data.imageUrl && data.mockupId) {
|
|
224
273
|
const responseVersion = data.requestVersion;
|
|
225
274
|
const responsePlacement = data.placement;
|
|
@@ -237,6 +286,7 @@ var RealtimeMockupService = class {
|
|
|
237
286
|
imageUrl: data.imageUrl,
|
|
238
287
|
renderUrl: data.renderUrl || data.imageUrl,
|
|
239
288
|
imageSize: data.imageSize || 0,
|
|
289
|
+
requestId: data.requestId,
|
|
240
290
|
requestVersion: responseVersion,
|
|
241
291
|
placement: responsePlacement,
|
|
242
292
|
renderMs: data.renderMs,
|
|
@@ -259,6 +309,7 @@ var RealtimeMockupService = class {
|
|
|
259
309
|
this.addLog(`\u26A0\uFE0F mockup_rendered message dropped: missing required fields [${missing.join(", ")}]. Full data keys: [${Object.keys(data).join(", ")}]`);
|
|
260
310
|
}
|
|
261
311
|
break;
|
|
312
|
+
}
|
|
262
313
|
case "all_mockups_rendered":
|
|
263
314
|
console.log(`[WS] all_mockups_rendered received: ${data.mockups?.length ?? 0} mockups`);
|
|
264
315
|
if (data.mockups) {
|
|
@@ -292,7 +343,10 @@ var RealtimeMockupService = class {
|
|
|
292
343
|
}
|
|
293
344
|
}
|
|
294
345
|
disconnect() {
|
|
346
|
+
this.connectAttempt++;
|
|
347
|
+
this.isConnecting = false;
|
|
295
348
|
this.clearRenew();
|
|
349
|
+
this.pendingStates.clear();
|
|
296
350
|
if (this.ws) {
|
|
297
351
|
this.ws.close();
|
|
298
352
|
this.ws = null;
|
|
@@ -330,6 +384,7 @@ var RealtimeMockupService = class {
|
|
|
330
384
|
this.canvasStates.clear();
|
|
331
385
|
this.colors.clear();
|
|
332
386
|
this.lastSendTime = {};
|
|
387
|
+
this.lastSentStateJson = {};
|
|
333
388
|
Object.values(this.throttleTimeouts).forEach((timeout) => clearTimeout(timeout));
|
|
334
389
|
this.throttleTimeouts = {};
|
|
335
390
|
this.requestVersion = 0;
|
|
@@ -463,9 +518,11 @@ var RealtimeMockupService = class {
|
|
|
463
518
|
sendCanvasState(placement, state, mockupCount = 1, baseThrottleMs = 1e3) {
|
|
464
519
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
|
|
465
520
|
const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
|
|
466
|
-
|
|
467
|
-
|
|
521
|
+
this.pendingStates.set(placement, { state, mockupCount, baseThrottleMs });
|
|
522
|
+
console.log(`[WS] sendCanvasState buffered for "${placement}" until configured: ${reason}`);
|
|
523
|
+
return true;
|
|
468
524
|
}
|
|
525
|
+
this.pendingStates.delete(placement);
|
|
469
526
|
this.canvasStates.set(placement, state);
|
|
470
527
|
if (baseThrottleMs <= 0) {
|
|
471
528
|
this.sendCanvasStateImmediately(placement, state);
|
|
@@ -518,34 +575,58 @@ var RealtimeMockupService = class {
|
|
|
518
575
|
}
|
|
519
576
|
this.lastSentVersion = ++this.requestVersion;
|
|
520
577
|
this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
|
|
578
|
+
const { requestId, traceparent } = newRenderCorrelation();
|
|
521
579
|
const message = JSON.stringify({
|
|
522
580
|
type: "canvas_state",
|
|
523
581
|
placement,
|
|
524
582
|
version: this.lastSentVersion,
|
|
583
|
+
requestId,
|
|
584
|
+
traceparent,
|
|
525
585
|
stateId
|
|
526
586
|
});
|
|
527
587
|
this.ws.send(message);
|
|
588
|
+
this.recordRequestSent(requestId);
|
|
528
589
|
this.lastBlobSentAt = Date.now();
|
|
529
|
-
|
|
590
|
+
console.log(`[WS] canvas_state(ref) SENT: placement="${placement}" v${this.lastSentVersion} req=${requestId} stateId=${stateId} at ${Date.now()}`);
|
|
591
|
+
this.addLog(`Sent canvas stateId "${stateId}" for "${placement}" (v${this.lastSentVersion}, req ${requestId})`, "sent");
|
|
530
592
|
this.callbacks.onBlobSent?.(placement);
|
|
531
593
|
return true;
|
|
532
594
|
}
|
|
595
|
+
/** Stamp a request's send time for client round-trip measurement (capped). */
|
|
596
|
+
recordRequestSent(requestId) {
|
|
597
|
+
this.requestSendTimes.set(requestId, Date.now());
|
|
598
|
+
if (this.requestSendTimes.size > 50) {
|
|
599
|
+
const oldest = this.requestSendTimes.keys().next().value;
|
|
600
|
+
if (oldest !== void 0) this.requestSendTimes.delete(oldest);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
533
603
|
sendCanvasStateImmediately(placement, state) {
|
|
534
604
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
535
605
|
console.log(`[WS] sendCanvasStateImmediately ABORTED: ws not open`);
|
|
536
606
|
return;
|
|
537
607
|
}
|
|
608
|
+
const stateJson = JSON.stringify(state);
|
|
609
|
+
if (this.lastSentStateJson[placement] === stateJson) {
|
|
610
|
+
this.addLog(`Skipped duplicate canvas state for "${placement}" (unchanged)`, "sent");
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
538
613
|
this.lastSentVersion = ++this.requestVersion;
|
|
539
614
|
this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
|
|
615
|
+
const { requestId, traceparent } = newRenderCorrelation();
|
|
540
616
|
const message = JSON.stringify({
|
|
541
617
|
type: "canvas_state",
|
|
542
618
|
placement,
|
|
543
619
|
version: this.lastSentVersion,
|
|
620
|
+
requestId,
|
|
621
|
+
traceparent,
|
|
544
622
|
state
|
|
545
623
|
});
|
|
546
624
|
this.ws.send(message);
|
|
625
|
+
this.recordRequestSent(requestId);
|
|
626
|
+
this.lastSentStateJson[placement] = stateJson;
|
|
547
627
|
this.lastBlobSentAt = Date.now();
|
|
548
|
-
|
|
628
|
+
console.log(`[WS] canvas_state SENT: placement="${placement}" v${this.lastSentVersion} req=${requestId} (${(message.length / 1024).toFixed(1)}KB) at ${Date.now()}`);
|
|
629
|
+
this.addLog(`Sent canvas state for "${placement}" (${(message.length / 1024).toFixed(1)}KB, v${this.lastSentVersion}, req ${requestId})`, "sent");
|
|
549
630
|
this.callbacks.onBlobSent?.(placement);
|
|
550
631
|
}
|
|
551
632
|
sendBlobImmediately(placement, blob, notifyCallback = true) {
|
package/dist/react.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { M as MockupResult, W as WebSocketConfig, P as PlacementSettings, F as FrameworkAdapter, b as ComponentProps, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, c as ComponentDescriptor, g as FrameworkUtilities } from './websocket-
|
|
1
|
+
import { M as MockupResult, W as WebSocketConfig, P as PlacementSettings, F as FrameworkAdapter, b as ComponentProps, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, c as ComponentDescriptor, g as FrameworkUtilities } from './websocket-hXpNcyEn.cjs';
|
|
2
2
|
|
|
3
3
|
interface UseRealtimeMockupOptions {
|
|
4
4
|
wsUrl?: string;
|
package/dist/react.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { M as MockupResult, W as WebSocketConfig, P as PlacementSettings, F as FrameworkAdapter, b as ComponentProps, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, c as ComponentDescriptor, g as FrameworkUtilities } from './websocket-
|
|
1
|
+
import { M as MockupResult, W as WebSocketConfig, P as PlacementSettings, F as FrameworkAdapter, b as ComponentProps, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, c as ComponentDescriptor, g as FrameworkUtilities } from './websocket-hXpNcyEn.js';
|
|
2
2
|
|
|
3
3
|
interface UseRealtimeMockupOptions {
|
|
4
4
|
wsUrl?: string;
|
package/dist/react.js
CHANGED
|
@@ -17,9 +17,10 @@ declare function resolveBestCombination(selection: OptionSelection, attributes:
|
|
|
17
17
|
*/
|
|
18
18
|
declare function computeDisabledChoices(selection: OptionSelection, attributes: Record<string, OptionAttribute>, combinations: Combination[]): Record<string, string[]>;
|
|
19
19
|
/**
|
|
20
|
-
* Derives a
|
|
20
|
+
* Derives a complete default selection based on the best combination.
|
|
21
21
|
* - For attributes that affect combinations, choose the value from the best combo.
|
|
22
|
-
* - For
|
|
22
|
+
* - For every remaining attribute, fall back to a default (color pickers get
|
|
23
|
+
* black; otherwise the first choice) so the returned selection is complete.
|
|
23
24
|
*/
|
|
24
25
|
declare function deriveDefaultSelection(attributes: Record<string, OptionAttribute>, combinations: Combination[]): OptionSelection;
|
|
25
26
|
/**
|
|
@@ -229,6 +230,8 @@ interface MockupResult {
|
|
|
229
230
|
imageUrl: string;
|
|
230
231
|
renderUrl: string;
|
|
231
232
|
imageSize: number;
|
|
233
|
+
/** Per-render correlation id (== OTel trace-id) echoed back by the server */
|
|
234
|
+
requestId?: string;
|
|
232
235
|
/** The request version this mockup corresponds to (for stale detection) */
|
|
233
236
|
requestVersion?: number;
|
|
234
237
|
/** The placement this mockup corresponds to */
|
|
@@ -251,6 +254,8 @@ interface WebSocketMessage {
|
|
|
251
254
|
message?: string;
|
|
252
255
|
placement?: string;
|
|
253
256
|
missingPlacements?: string[];
|
|
257
|
+
/** Per-render correlation id (== OTel trace-id) echoed back by the server */
|
|
258
|
+
requestId?: string;
|
|
254
259
|
/** The request version this response corresponds to (for stale detection) */
|
|
255
260
|
requestVersion?: number;
|
|
256
261
|
/** Server-side PIXI render time in ms */
|
|
@@ -304,8 +309,11 @@ declare class RealtimeMockupService {
|
|
|
304
309
|
private lastError;
|
|
305
310
|
private callbacks;
|
|
306
311
|
private lastBlobSentAt;
|
|
312
|
+
private requestSendTimes;
|
|
307
313
|
private canvasBlobs;
|
|
308
314
|
private canvasStates;
|
|
315
|
+
private pendingStates;
|
|
316
|
+
private lastSentStateJson;
|
|
309
317
|
private colors;
|
|
310
318
|
private lastSendTime;
|
|
311
319
|
private throttleTimeouts;
|
|
@@ -316,6 +324,8 @@ declare class RealtimeMockupService {
|
|
|
316
324
|
private sendVersionInBlob;
|
|
317
325
|
private tokenProvider?;
|
|
318
326
|
private renewTimer;
|
|
327
|
+
private isConnecting;
|
|
328
|
+
private connectAttempt;
|
|
319
329
|
constructor(wsUrl?: string);
|
|
320
330
|
setCallbacks(callbacks: RealtimeMockupCallbacks): void;
|
|
321
331
|
/** Provide a grant fetcher to authorize the session (per-shop, renewable). */
|
|
@@ -363,6 +373,8 @@ declare class RealtimeMockupService {
|
|
|
363
373
|
* canvas JSON. One-shot (no throttle). Results arrive like any other render.
|
|
364
374
|
*/
|
|
365
375
|
sendCanvasStateRef(placement: string, stateId: string): boolean;
|
|
376
|
+
/** Stamp a request's send time for client round-trip measurement (capped). */
|
|
377
|
+
private recordRequestSent;
|
|
366
378
|
private sendCanvasStateImmediately;
|
|
367
379
|
private sendBlobImmediately;
|
|
368
380
|
/**
|
|
@@ -17,9 +17,10 @@ declare function resolveBestCombination(selection: OptionSelection, attributes:
|
|
|
17
17
|
*/
|
|
18
18
|
declare function computeDisabledChoices(selection: OptionSelection, attributes: Record<string, OptionAttribute>, combinations: Combination[]): Record<string, string[]>;
|
|
19
19
|
/**
|
|
20
|
-
* Derives a
|
|
20
|
+
* Derives a complete default selection based on the best combination.
|
|
21
21
|
* - For attributes that affect combinations, choose the value from the best combo.
|
|
22
|
-
* - For
|
|
22
|
+
* - For every remaining attribute, fall back to a default (color pickers get
|
|
23
|
+
* black; otherwise the first choice) so the returned selection is complete.
|
|
23
24
|
*/
|
|
24
25
|
declare function deriveDefaultSelection(attributes: Record<string, OptionAttribute>, combinations: Combination[]): OptionSelection;
|
|
25
26
|
/**
|
|
@@ -229,6 +230,8 @@ interface MockupResult {
|
|
|
229
230
|
imageUrl: string;
|
|
230
231
|
renderUrl: string;
|
|
231
232
|
imageSize: number;
|
|
233
|
+
/** Per-render correlation id (== OTel trace-id) echoed back by the server */
|
|
234
|
+
requestId?: string;
|
|
232
235
|
/** The request version this mockup corresponds to (for stale detection) */
|
|
233
236
|
requestVersion?: number;
|
|
234
237
|
/** The placement this mockup corresponds to */
|
|
@@ -251,6 +254,8 @@ interface WebSocketMessage {
|
|
|
251
254
|
message?: string;
|
|
252
255
|
placement?: string;
|
|
253
256
|
missingPlacements?: string[];
|
|
257
|
+
/** Per-render correlation id (== OTel trace-id) echoed back by the server */
|
|
258
|
+
requestId?: string;
|
|
254
259
|
/** The request version this response corresponds to (for stale detection) */
|
|
255
260
|
requestVersion?: number;
|
|
256
261
|
/** Server-side PIXI render time in ms */
|
|
@@ -304,8 +309,11 @@ declare class RealtimeMockupService {
|
|
|
304
309
|
private lastError;
|
|
305
310
|
private callbacks;
|
|
306
311
|
private lastBlobSentAt;
|
|
312
|
+
private requestSendTimes;
|
|
307
313
|
private canvasBlobs;
|
|
308
314
|
private canvasStates;
|
|
315
|
+
private pendingStates;
|
|
316
|
+
private lastSentStateJson;
|
|
309
317
|
private colors;
|
|
310
318
|
private lastSendTime;
|
|
311
319
|
private throttleTimeouts;
|
|
@@ -316,6 +324,8 @@ declare class RealtimeMockupService {
|
|
|
316
324
|
private sendVersionInBlob;
|
|
317
325
|
private tokenProvider?;
|
|
318
326
|
private renewTimer;
|
|
327
|
+
private isConnecting;
|
|
328
|
+
private connectAttempt;
|
|
319
329
|
constructor(wsUrl?: string);
|
|
320
330
|
setCallbacks(callbacks: RealtimeMockupCallbacks): void;
|
|
321
331
|
/** Provide a grant fetcher to authorize the session (per-shop, renewable). */
|
|
@@ -363,6 +373,8 @@ declare class RealtimeMockupService {
|
|
|
363
373
|
* canvas JSON. One-shot (no throttle). Results arrive like any other render.
|
|
364
374
|
*/
|
|
365
375
|
sendCanvasStateRef(placement: string, stateId: string): boolean;
|
|
376
|
+
/** Stamp a request's send time for client round-trip measurement (capped). */
|
|
377
|
+
private recordRequestSent;
|
|
366
378
|
private sendCanvasStateImmediately;
|
|
367
379
|
private sendBlobImmediately;
|
|
368
380
|
/**
|