@snowcone-app/sdk 0.3.2 → 0.3.4

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 CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [#288](https://github.com/snowcone-app/snowcone-monorepo/pull/288) [`7ccfe17`](https://github.com/snowcone-app/snowcone-monorepo/commit/7ccfe1724b166719e2aac4e0f5e9a2a5c2c440a4) Thanks [@kevinsproles](https://github.com/kevinsproles)! - `getProduct()` now throws an actionable error for an unknown product id/slug (naming the id, the index, and pointing to `listProducts()`) instead of a bare "Not found", so a typo'd catalog id no longer surfaces as the raw Meilisearch `document_not_found` shape.
8
+
9
+ - [#281](https://github.com/snowcone-app/snowcone-monorepo/pull/281) [`9e3d941`](https://github.com/snowcone-app/snowcone-monorepo/commit/9e3d941419b3df004feb47369692d30a4a8a2938) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Coalesce duplicate realtime `connect()` calls while a grant is pending or the WebSocket is still connecting, preventing competing initial sockets and benign 1006 close noise during first render.
10
+
11
+ - [#293](https://github.com/snowcone-app/snowcone-monorepo/pull/293) [`64bcaf0`](https://github.com/snowcone-app/snowcone-monorepo/commit/64bcaf0b61dcd611a81d9a9cb013ab89dc8003f7) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Stop idle canvases from continuously streaming server renders (which silently burned the realtime render budget). `serializeStateForServer` now quantizes element/artboard geometry to 0.01px so the render loop's sub-pixel float drift no longer produces a new wire state every frame, and `RealtimeMockupService` drops duplicate consecutive canvas states per placement. A naive `onChange → renderState` wiring no longer bills the integrator's realtime quota while the user is doing nothing.
12
+
13
+ - [#300](https://github.com/snowcone-app/snowcone-monorepo/pull/300) [`f80f00a`](https://github.com/snowcone-app/snowcone-monorepo/commit/f80f00a0e38631aac936bfe60de14e7d47be3e6a) Thanks [@kevinsproles](https://github.com/kevinsproles)! - `mockupUrl()` now resolves the shop id and mockup base URL by reading `process.env.NEXT_PUBLIC_*` directly instead of through an aliased `const env = process.env`. The alias defeated Next/webpack's static inlining, so in browser bundles the values came back `undefined` and the live-preview URL fell through to the `SHOP_NOT_CONFIGURED` placeholder. Consumers relying on `NEXT_PUBLIC_SNOWCONE_SHOP_ID` / `NEXT_PUBLIC_MERCH_MOCKUP_URL` env fallbacks (rather than the explicit `shop` prop / `window.snowcone`) now get the configured values.
14
+
15
+ ## 0.3.3
16
+
17
+ ### Patch Changes
18
+
19
+ - [#268](https://github.com/snowcone-app/snowcone-monorepo/pull/268) [`dd90eba`](https://github.com/snowcone-app/snowcone-monorepo/commit/dd90eba8bbfbbaef82379cb997af84e21f7cad8f) Thanks [@kevinsproles](https://github.com/kevinsproles)! - fix(realtime): fail fast when a color/variant product is missing `variantId`
20
+
21
+ `RenderSession` now rejects a product that has a real variant axis (its
22
+ `options.combinations` expose 2+ distinct `variantId`s) but no valid `variantId`,
23
+ surfacing an immediate, actionable error at config/`connect()` time instead of
24
+ hanging silently for ~30s until the watchdog. Pass the catalog product's
25
+ `options` on `RenderProduct` to opt in. Single-variant / no-option products are
26
+ unaffected and still render with no `variantId`. The watchdog remains as the
27
+ backstop. New exports: `assertVariantSelected`, `RenderProductOptions`,
28
+ `RenderProductCombination`.
29
+
3
30
  ## 0.3.2
4
31
 
5
32
  ### Patch Changes
@@ -19,6 +19,10 @@ var RealtimeMockupService = class {
19
19
  lastBlobSentAt = 0;
20
20
  canvasBlobs = /* @__PURE__ */ new Map();
21
21
  canvasStates = /* @__PURE__ */ new Map();
22
+ // Serialized JSON of the last canvas state ACTUALLY SENT per placement. Used to
23
+ // drop duplicate consecutive states so an idle canvas (whose onChange keeps
24
+ // firing identical states) doesn't stream renders and burn the realtime budget.
25
+ lastSentStateJson = {};
22
26
  colors = /* @__PURE__ */ new Map();
23
27
  lastSendTime = {};
24
28
  throttleTimeouts = {};
@@ -39,6 +43,8 @@ var RealtimeMockupService = class {
39
43
  // the WS with `?token=`, and renews ~15s before expiry via a `renew` message.
40
44
  tokenProvider;
41
45
  renewTimer = null;
46
+ isConnecting = false;
47
+ connectAttempt = 0;
42
48
  setCallbacks(callbacks) {
43
49
  this.callbacks = callbacks;
44
50
  }
@@ -65,16 +71,21 @@ var RealtimeMockupService = class {
65
71
  this.callbacks.onLog?.(message, type);
66
72
  }
67
73
  connect() {
68
- if (this.ws?.readyState === WebSocket.OPEN) {
74
+ if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING || this.isConnecting) {
69
75
  return;
70
76
  }
77
+ const attempt = ++this.connectAttempt;
78
+ this.isConnecting = true;
71
79
  if (this.tokenProvider) {
72
80
  this.tokenProvider().then((grant) => {
81
+ if (attempt !== this.connectAttempt || !this.isConnecting) return;
73
82
  this.openSocket(grant.token);
74
83
  this.scheduleRenew(grant.expiresAt);
75
84
  }).catch((err) => {
85
+ if (attempt !== this.connectAttempt) return;
76
86
  this.addLog(`Failed to obtain realtime grant: ${err}`);
77
87
  this.status = "Disconnected";
88
+ this.isConnecting = false;
78
89
  });
79
90
  return;
80
91
  }
@@ -88,6 +99,7 @@ var RealtimeMockupService = class {
88
99
  console.log(`[WS] connection OPENED to ${this.wsUrl}`);
89
100
  this.addLog("WebSocket connection opened");
90
101
  this.status = "Connected";
102
+ this.isConnecting = false;
91
103
  };
92
104
  this.ws.onmessage = (event) => {
93
105
  try {
@@ -106,11 +118,13 @@ var RealtimeMockupService = class {
106
118
  this.isConfigured = false;
107
119
  this.configSent = false;
108
120
  this.clearRenew();
121
+ this.isConnecting = false;
109
122
  this.callbacks.onDisconnected?.();
110
123
  };
111
124
  this.ws.onerror = (error) => {
112
125
  this.addLog(`WebSocket error: ${error}`);
113
126
  this.status = "Disconnected";
127
+ this.isConnecting = false;
114
128
  };
115
129
  }
116
130
  scheduleRenew(expiresAt) {
@@ -260,6 +274,8 @@ var RealtimeMockupService = class {
260
274
  }
261
275
  }
262
276
  disconnect() {
277
+ this.connectAttempt++;
278
+ this.isConnecting = false;
263
279
  this.clearRenew();
264
280
  if (this.ws) {
265
281
  this.ws.close();
@@ -298,6 +314,7 @@ var RealtimeMockupService = class {
298
314
  this.canvasStates.clear();
299
315
  this.colors.clear();
300
316
  this.lastSendTime = {};
317
+ this.lastSentStateJson = {};
301
318
  Object.values(this.throttleTimeouts).forEach((timeout) => clearTimeout(timeout));
302
319
  this.throttleTimeouts = {};
303
320
  this.requestVersion = 0;
@@ -503,6 +520,11 @@ var RealtimeMockupService = class {
503
520
  console.log(`[WS] sendCanvasStateImmediately ABORTED: ws not open`);
504
521
  return;
505
522
  }
523
+ const stateJson = JSON.stringify(state);
524
+ if (this.lastSentStateJson[placement] === stateJson) {
525
+ this.addLog(`Skipped duplicate canvas state for "${placement}" (unchanged)`, "sent");
526
+ return;
527
+ }
506
528
  this.lastSentVersion = ++this.requestVersion;
507
529
  this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
508
530
  const message = JSON.stringify({
@@ -512,6 +534,7 @@ var RealtimeMockupService = class {
512
534
  state
513
535
  });
514
536
  this.ws.send(message);
537
+ this.lastSentStateJson[placement] = stateJson;
515
538
  this.lastBlobSentAt = Date.now();
516
539
  this.addLog(`Sent canvas state for "${placement}" (${(message.length / 1024).toFixed(1)}KB, v${this.lastSentVersion})`, "sent");
517
540
  this.callbacks.onBlobSent?.(placement);
package/dist/index.cjs CHANGED
@@ -65,6 +65,7 @@ __export(index_exports, {
65
65
  UniversalContextProvider: () => UniversalContextProvider,
66
66
  VueAdapter: () => VueAdapter,
67
67
  adapterRegistry: () => adapterRegistry,
68
+ assertVariantSelected: () => assertVariantSelected,
68
69
  autoRegister: () => autoRegister,
69
70
  buildMockupUrl: () => buildMockupUrl2,
70
71
  componentRegistry: () => componentRegistry,
@@ -1796,14 +1797,12 @@ function buildMockupUrl2(options, cfg) {
1796
1797
  );
1797
1798
  }
1798
1799
  function resolveMockupBaseUrl() {
1799
- const env = typeof process !== "undefined" ? process.env : void 0;
1800
1800
  const winConfig = typeof window !== "undefined" && window.snowcone || {};
1801
- return winConfig.mockupUrl || env?.SNOWCONE_IMAGE_URL || env?.NEXT_PUBLIC_MERCH_MOCKUP_URL || "https://cdn.snowcone.app";
1801
+ 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";
1802
1802
  }
1803
1803
  function resolveShop() {
1804
- const env = typeof process !== "undefined" ? process.env : void 0;
1805
1804
  const winConfig = typeof window !== "undefined" && window.snowcone || {};
1806
- return winConfig.shop || env?.SNOWCONE_SHOP_ID || env?.NEXT_PUBLIC_SNOWCONE_SHOP_ID || "SHOP_NOT_CONFIGURED";
1805
+ return winConfig.shop || typeof process !== "undefined" && process.env?.SNOWCONE_SHOP_ID || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_SNOWCONE_SHOP_ID || "SHOP_NOT_CONFIGURED";
1807
1806
  }
1808
1807
  function mockupUrl(options) {
1809
1808
  const mockupBaseUrl = resolveMockupBaseUrl();
@@ -4836,6 +4835,10 @@ var RealtimeMockupService = class {
4836
4835
  lastBlobSentAt = 0;
4837
4836
  canvasBlobs = /* @__PURE__ */ new Map();
4838
4837
  canvasStates = /* @__PURE__ */ new Map();
4838
+ // Serialized JSON of the last canvas state ACTUALLY SENT per placement. Used to
4839
+ // drop duplicate consecutive states so an idle canvas (whose onChange keeps
4840
+ // firing identical states) doesn't stream renders and burn the realtime budget.
4841
+ lastSentStateJson = {};
4839
4842
  colors = /* @__PURE__ */ new Map();
4840
4843
  lastSendTime = {};
4841
4844
  throttleTimeouts = {};
@@ -4856,6 +4859,8 @@ var RealtimeMockupService = class {
4856
4859
  // the WS with `?token=`, and renews ~15s before expiry via a `renew` message.
4857
4860
  tokenProvider;
4858
4861
  renewTimer = null;
4862
+ isConnecting = false;
4863
+ connectAttempt = 0;
4859
4864
  setCallbacks(callbacks) {
4860
4865
  this.callbacks = callbacks;
4861
4866
  }
@@ -4882,16 +4887,21 @@ var RealtimeMockupService = class {
4882
4887
  this.callbacks.onLog?.(message, type);
4883
4888
  }
4884
4889
  connect() {
4885
- if (this.ws?.readyState === WebSocket.OPEN) {
4890
+ if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING || this.isConnecting) {
4886
4891
  return;
4887
4892
  }
4893
+ const attempt = ++this.connectAttempt;
4894
+ this.isConnecting = true;
4888
4895
  if (this.tokenProvider) {
4889
4896
  this.tokenProvider().then((grant) => {
4897
+ if (attempt !== this.connectAttempt || !this.isConnecting) return;
4890
4898
  this.openSocket(grant.token);
4891
4899
  this.scheduleRenew(grant.expiresAt);
4892
4900
  }).catch((err) => {
4901
+ if (attempt !== this.connectAttempt) return;
4893
4902
  this.addLog(`Failed to obtain realtime grant: ${err}`);
4894
4903
  this.status = "Disconnected";
4904
+ this.isConnecting = false;
4895
4905
  });
4896
4906
  return;
4897
4907
  }
@@ -4905,6 +4915,7 @@ var RealtimeMockupService = class {
4905
4915
  console.log(`[WS] connection OPENED to ${this.wsUrl}`);
4906
4916
  this.addLog("WebSocket connection opened");
4907
4917
  this.status = "Connected";
4918
+ this.isConnecting = false;
4908
4919
  };
4909
4920
  this.ws.onmessage = (event) => {
4910
4921
  try {
@@ -4923,11 +4934,13 @@ var RealtimeMockupService = class {
4923
4934
  this.isConfigured = false;
4924
4935
  this.configSent = false;
4925
4936
  this.clearRenew();
4937
+ this.isConnecting = false;
4926
4938
  this.callbacks.onDisconnected?.();
4927
4939
  };
4928
4940
  this.ws.onerror = (error) => {
4929
4941
  this.addLog(`WebSocket error: ${error}`);
4930
4942
  this.status = "Disconnected";
4943
+ this.isConnecting = false;
4931
4944
  };
4932
4945
  }
4933
4946
  scheduleRenew(expiresAt) {
@@ -5077,6 +5090,8 @@ var RealtimeMockupService = class {
5077
5090
  }
5078
5091
  }
5079
5092
  disconnect() {
5093
+ this.connectAttempt++;
5094
+ this.isConnecting = false;
5080
5095
  this.clearRenew();
5081
5096
  if (this.ws) {
5082
5097
  this.ws.close();
@@ -5115,6 +5130,7 @@ var RealtimeMockupService = class {
5115
5130
  this.canvasStates.clear();
5116
5131
  this.colors.clear();
5117
5132
  this.lastSendTime = {};
5133
+ this.lastSentStateJson = {};
5118
5134
  Object.values(this.throttleTimeouts).forEach((timeout) => clearTimeout(timeout));
5119
5135
  this.throttleTimeouts = {};
5120
5136
  this.requestVersion = 0;
@@ -5320,6 +5336,11 @@ var RealtimeMockupService = class {
5320
5336
  console.log(`[WS] sendCanvasStateImmediately ABORTED: ws not open`);
5321
5337
  return;
5322
5338
  }
5339
+ const stateJson = JSON.stringify(state);
5340
+ if (this.lastSentStateJson[placement] === stateJson) {
5341
+ this.addLog(`Skipped duplicate canvas state for "${placement}" (unchanged)`, "sent");
5342
+ return;
5343
+ }
5323
5344
  this.lastSentVersion = ++this.requestVersion;
5324
5345
  this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
5325
5346
  const message = JSON.stringify({
@@ -5329,6 +5350,7 @@ var RealtimeMockupService = class {
5329
5350
  state
5330
5351
  });
5331
5352
  this.ws.send(message);
5353
+ this.lastSentStateJson[placement] = stateJson;
5332
5354
  this.lastBlobSentAt = Date.now();
5333
5355
  this.addLog(`Sent canvas state for "${placement}" (${(message.length / 1024).toFixed(1)}KB, v${this.lastSentVersion})`, "sent");
5334
5356
  this.callbacks.onBlobSent?.(placement);
@@ -5523,6 +5545,44 @@ async function fetchRealtimeGrant(grantUrl, shop, fetchImpl) {
5523
5545
  }
5524
5546
 
5525
5547
  // src/realtime/session.ts
5548
+ function variantIdsOf(product) {
5549
+ const combos = product.options?.combinations;
5550
+ if (!combos?.length) return [];
5551
+ const ids = /* @__PURE__ */ new Set();
5552
+ for (const c of combos) {
5553
+ if (typeof c.variantId === "string" && c.variantId !== "") ids.add(c.variantId);
5554
+ }
5555
+ return [...ids];
5556
+ }
5557
+ function assertVariantSelected(product) {
5558
+ const variantIds = variantIdsOf(product);
5559
+ if (variantIds.length < 2) return;
5560
+ const fix = `pass product.variantId from options.combinations[].variantId (valid ids: ${variantIds.join(", ")})`;
5561
+ if (!product.variantId) {
5562
+ throw new Error(
5563
+ `RenderSession: product ${product.productId} requires a variantId \u2014 ${fix}`
5564
+ );
5565
+ }
5566
+ if (!variantIds.includes(product.variantId)) {
5567
+ throw new Error(
5568
+ `RenderSession: product ${product.productId} has an invalid variantId "${product.variantId}" \u2014 ${fix}`
5569
+ );
5570
+ }
5571
+ }
5572
+ function warnUnknownPlacement(product, placement) {
5573
+ const placements = product.placements;
5574
+ if (!placements?.length) return null;
5575
+ if (placements.includes(placement)) return null;
5576
+ 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.`;
5577
+ }
5578
+ function warnPlacementArtboardMismatch(placement, state) {
5579
+ const artboards = state.artboards;
5580
+ if (!Array.isArray(artboards) || artboards.length === 0) return null;
5581
+ const names = artboards.map((a) => a.name).filter((n) => typeof n === "string");
5582
+ if (names.length === 0) return null;
5583
+ if (names.includes(placement)) return null;
5584
+ return `RenderSession: renderState placement "${placement}" has no matching artboard in the state (artboards: [${names.join(", ")}]). The artboard name must equal the placement arg.`;
5585
+ }
5526
5586
  var DEFAULT_RENDER_TIMEOUT_MS = 3e4;
5527
5587
  var RenderSession = class {
5528
5588
  svc;
@@ -5538,6 +5598,7 @@ var RenderSession = class {
5538
5598
  if (!opts.getToken && !opts.grantUrl) {
5539
5599
  throw new Error("RenderSession: provide `grantUrl` or `getToken` to authorize the session");
5540
5600
  }
5601
+ if (opts.product) assertVariantSelected(opts.product);
5541
5602
  this.opts = opts;
5542
5603
  this.product = opts.product ?? null;
5543
5604
  this.renderTimeoutMs = opts.renderTimeoutMs ?? DEFAULT_RENDER_TIMEOUT_MS;
@@ -5557,6 +5618,7 @@ var RenderSession = class {
5557
5618
  }
5558
5619
  /** Set or update the product. Sends config immediately if already connected. */
5559
5620
  setProduct(product) {
5621
+ assertVariantSelected(product);
5560
5622
  this.product = product;
5561
5623
  this.svc.sendConfig(this.toConfig(product));
5562
5624
  }
@@ -5572,6 +5634,13 @@ var RenderSession = class {
5572
5634
  );
5573
5635
  }
5574
5636
  const product = this.product;
5637
+ try {
5638
+ assertVariantSelected(product);
5639
+ } catch (err) {
5640
+ const message = err instanceof Error ? err.message : String(err);
5641
+ this.errorCb?.(message);
5642
+ return Promise.reject(err instanceof Error ? err : new Error(message));
5643
+ }
5575
5644
  this.ready = new Promise((resolve, reject) => {
5576
5645
  let settled = false;
5577
5646
  this.svc.setCallbacks({
@@ -5623,6 +5692,7 @@ var RenderSession = class {
5623
5692
  */
5624
5693
  async renderState(placement, state, throttleMs = 0) {
5625
5694
  await this.connect();
5695
+ this.warnPlacement(placement, state);
5626
5696
  this.svc.sendCanvasState(placement, state, this.product?.mockupIds.length ?? 1, throttleMs);
5627
5697
  this.armWatchdog();
5628
5698
  }
@@ -5635,6 +5705,7 @@ var RenderSession = class {
5635
5705
  */
5636
5706
  async renderSavedState(placement, stateId) {
5637
5707
  await this.connect();
5708
+ this.warnPlacement(placement);
5638
5709
  this.svc.sendCanvasStateRef(placement, stateId);
5639
5710
  this.armWatchdog();
5640
5711
  }
@@ -5653,6 +5724,28 @@ var RenderSession = class {
5653
5724
  this.svc.disconnect();
5654
5725
  this.ready = null;
5655
5726
  }
5727
+ /**
5728
+ * Emit loud, non-fatal warnings for the two ways a placement name silently
5729
+ * produces a blank mockup: (1) the name isn't one of the product's known
5730
+ * `placements` labels, and (2) no artboard in `state` is named after it. Each
5731
+ * warning goes to BOTH `console.warn` and the `onError` callback (if wired) so
5732
+ * a dev sees it whichever channel they watch. Never throws.
5733
+ */
5734
+ warnPlacement(placement, state) {
5735
+ const warnings = [];
5736
+ if (this.product) {
5737
+ const w = warnUnknownPlacement(this.product, placement);
5738
+ if (w) warnings.push(w);
5739
+ }
5740
+ if (state) {
5741
+ const w = warnPlacementArtboardMismatch(placement, state);
5742
+ if (w) warnings.push(w);
5743
+ }
5744
+ for (const w of warnings) {
5745
+ console.warn(w);
5746
+ this.errorCb?.(w);
5747
+ }
5748
+ }
5656
5749
  /**
5657
5750
  * (Re)arm the render watchdog. Clears any prior timer and, unless disabled
5658
5751
  * (`renderTimeoutMs <= 0`), starts a fresh one. If it fires before a mockup
@@ -5768,7 +5861,12 @@ async function getProduct(idOrSlug, config) {
5768
5861
  const url = `${meilisearchHost}/indexes/${meilisearchIndex}/documents/${idOrSlug}`;
5769
5862
  const res = await f(url, { method: "GET", headers });
5770
5863
  if (res.status === 404)
5771
- throw Object.assign(new Error("Not found"), { code: "NOT_FOUND" });
5864
+ throw Object.assign(
5865
+ new Error(
5866
+ `getProduct: no catalog product "${idOrSlug}" in index "${meilisearchIndex}". Pass a valid product id/slug (browse with listProducts()).`
5867
+ ),
5868
+ { code: "NOT_FOUND", productId: idOrSlug }
5869
+ );
5772
5870
  if (!res.ok) throw new Error(`getProduct failed: ${res.status}`);
5773
5871
  const raw = await res.json();
5774
5872
  return validateProductLoose(raw);
@@ -5831,6 +5929,7 @@ function createClient(config) {
5831
5929
  UniversalContextProvider,
5832
5930
  VueAdapter,
5833
5931
  adapterRegistry,
5932
+ assertVariantSelected,
5834
5933
  autoRegister,
5835
5934
  buildMockupUrl,
5836
5935
  componentRegistry,
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-Poy8LZNA.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-Poy8LZNA.cjs';
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-DTRStLRH.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-DTRStLRH.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.connect();
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
  */
@@ -2604,6 +2603,29 @@ interface RenderState {
2604
2603
  elements?: Array<Record<string, unknown>>;
2605
2604
  [key: string]: unknown;
2606
2605
  }
2606
+ /**
2607
+ * A single variant combination from the catalog product's `options.combinations`.
2608
+ * Each combination pins one value per variant-affecting attribute plus the
2609
+ * `variantId` (gvid) that selection resolves to. Pass `options` on
2610
+ * {@link RenderProduct} so the session can FAIL FAST when a required `variantId`
2611
+ * is missing instead of silently hanging until the watchdog.
2612
+ */
2613
+ interface RenderProductCombination {
2614
+ /** The variant (gvid) this combination resolves to. */
2615
+ variantId?: string;
2616
+ /** Attribute selections (e.g. `{ Frame: 'Walnut', Size: '5x7' }`). */
2617
+ [attribute: string]: string | number | undefined;
2618
+ }
2619
+ /**
2620
+ * The catalog product's `options` block (structurally typed). Only
2621
+ * `combinations` is consumed by the session — to detect whether the product has
2622
+ * a real variant axis (multiple distinct `variantId`s) so a missing/invalid
2623
+ * {@link RenderProduct.variantId} can be rejected up front.
2624
+ */
2625
+ interface RenderProductOptions {
2626
+ combinations?: RenderProductCombination[];
2627
+ [key: string]: unknown;
2628
+ }
2607
2629
  /** Product the session renders against (Snowcone catalog ids). */
2608
2630
  interface RenderProduct {
2609
2631
  productId: string;
@@ -2618,13 +2640,44 @@ interface RenderProduct {
2618
2640
  * Optional variant (gvid). Omit if the product has no variant axis. Required
2619
2641
  * for products with a color option — it auto-fills the color placement; without
2620
2642
  * it the render waits for a color blob that never comes.
2643
+ *
2644
+ * If you also pass {@link RenderProduct.options}, the session validates this
2645
+ * up front: a product whose `options.combinations` exposes more than one
2646
+ * variant REQUIRES a `variantId` (and it must be one of the combinations'),
2647
+ * else the session throws immediately rather than hanging until the watchdog.
2621
2648
  */
2622
2649
  variantId?: string;
2650
+ /**
2651
+ * The catalog product's `options` block (its `options.combinations[].variantId`
2652
+ * are the valid variant ids). Optional, but passing it lets the session detect
2653
+ * a missing/invalid required `variantId` and FAIL FAST — turning the classic
2654
+ * 30s silent hang into an immediate, actionable error. You can pass the whole
2655
+ * catalog product's `options` object verbatim.
2656
+ */
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[];
2623
2668
  /** Render width in px (default 1000). */
2624
2669
  width?: number;
2625
2670
  /** Per-placement settings (tiling, scale mode). */
2626
2671
  placementSettings?: Record<string, PlacementSettings>;
2627
2672
  }
2673
+ /**
2674
+ * Throws immediately if the product has a variant axis (its
2675
+ * `options.combinations` expose 2+ distinct `variantId`s) but no valid
2676
+ * `variantId` was supplied. This is the fail-fast guard for the otherwise-silent
2677
+ * 30s render hang on color/variant products. Products with no `options` (or a
2678
+ * single variant) are left untouched — they render fine with no `variantId`.
2679
+ */
2680
+ declare function assertVariantSelected(product: RenderProduct): void;
2628
2681
  interface RenderSessionOptions {
2629
2682
  /** Publishable shop id (= shop.id, like Stripe pk_). */
2630
2683
  shop: string;
@@ -2712,6 +2765,14 @@ declare class RenderSession {
2712
2765
  getMockups(): MockupResult[];
2713
2766
  /** Close the WebSocket and stop auto-renew. */
2714
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;
2715
2776
  /**
2716
2777
  * (Re)arm the render watchdog. Clears any prior timer and, unless disabled
2717
2778
  * (`renderTimeoutMs <= 0`), starts a fresh one. If it fires before a mockup
@@ -2819,4 +2880,4 @@ declare function getProduct(idOrSlug: string, config?: Partial<SdkConfig>): Prom
2819
2880
  */
2820
2881
  declare function createClient(config: SdkConfig): SnowconeClient;
2821
2882
 
2822
- export { AbstractTemplateRenderer, type AddToCartOptions, Animations, type ArtSelectorContext, type ArtSelectorDescriptor, type ArtSelectorOptions, type ArtworkData, type AspectRatio, Attributes, type BaseDescriptor, type BlendConfig, type BuildMockupUrlConfig, type CartDetail, type CatalogListResponse, type CatalogProduct$2 as CatalogProduct, ClassNames, type ColorDesignElement, ColorPickerUtils, Combination, CommonProps, ComponentContext, type ComponentDefinition, ComponentDescriptor, ComponentEventManager, ComponentFactory, ComponentLifecycle, ComponentLifecycleHooks, type ComponentMetadata, ComponentProps, ComponentRegistry, ComponentState, type ComponentTemplate, ComponentTemplates, type ContainerDescriptor, ContextBridge, type ContextComparator, type ContextConsumer, ContextInjector, type ContextProviderConfig, type ContextSubscriber, ContextSynchronizer, type CreateDesignStateInput, type CreatedDesignState, DEFAULT_ARTWORK_URL, DEFAULT_ASPECT_RATIO, DEFAULT_COLOR, DEFAULT_GRANT_BASE, DEFAULT_MOCKUP_BASE, DEFAULT_PLACEMENT_DIMENSIONS, type Descriptor, type Design, type DesignElement, type DesignGenerationContext, type Effects, Elements, type ErrorContext, type ErrorHandler, ErrorManager, type EventDefinition, EventDelegator, EventEmitter, EventHandler, type EventListener, type EventPayload, type Fill, Focus, FrameworkAdapter, FrameworkUtilities, type GetMockupUrlOptions, type ImageAlignment, type ImageDesignElement, type LifecycleHook, LifecycleManager, type LifecyclePhase, type LifecycleTransition, LitAdapter, type MaskOverride, type MockupGenerationOptions, MockupResult, type MockupService, type MockupServiceConfig, OptionAttribute, type OptionChoice, type OptionChoiceRenderData, type OptionRenderData, OptionSelection, type Placement, type PlacementDesign, PlacementSettings, type ProductArtAlignmentContext, type ProductArtAlignmentDescriptor, type ProductArtAlignmentOptions, type ProductContext, type ProductContextEvents, ProductContextManager, type ProductData, type ProductFetcher, type ProductImageDescriptor, type ProductImageOptions, ProductLoader, type ProductMockupData, type ProductOptionChoice, type ProductOptionData, type ProductOptionsDescriptor, type ProductOptionsOptions, type ProductPlacement, type ProductPriceDescriptor, type ProductPriceOptions, ProductProps, type ProductSpec, type ProductTitleDescriptor, type ProductTitleOptions, type ProductVariant, type PropDefinition, type PropSchema, type PropType, PropertyManager, REALTIME_WS_URL, type RateLimitState, type RealtimeGrant, RealtimeMockupService, type RegularArtwork, type RenderProduct, RenderSession, type RenderSessionOptions, type RenderState, type SdkConfig, type SeamlessPattern, type SignedUrlResponse, type SnowconeClient, StandardComponents, StandardEvents, StateManager, type StateManagerOptions, type StateMiddleware, type StateSelector, type StateSubscriber, type StateUpdater, Styles, SvelteAdapter, SwatchUtils, type TagName, TemplateBuilder, type TemplateNode, type TemplateNodeType, type TemplateRenderer, type TextDescriptor, type TileCount, UniversalContextProvider, type UserSelection, type ValidationError, type ValidationResult, VueAdapter, autoRegister, buildMockupUrl, componentRegistry, createAddToCartEvent, createAddToCartHandler, createCartDetail, createClient, createContextProvider, createDesignForPlacements, createDesignState, createErrorHandler, createEventManager, createLifecycle, createLitComponent, createProductContext, createProductLoader, createPropertyManager, createStateHook, createStateStore, createSvelteComponent, createUniversalProvider, createVueComponent, describeArtSelector, describeProductArtAlignment, describeProductImage, describeProductOptions, describeProductPrice, describeProductTitle, extractProductId, fetchRealtimeGrant, filterImagePlacements, findClosestSnapPoint, findVariantForSelection, formatPrice, formatValidationErrors, getDefaultVariantId, getEffectiveAlignment, getIncompleteSelectionMessage, getMissingSelections, getMockupUrl, getOptionRenderType, getProduct, getSelectionDisplayText, getSnapPoints, getVariant, handleOptionChange, isSelectionComplete, isValidAlignment, isValidTileCount, listProducts, mintRealtimeGrant, mockupUrl, normalizeChoice, prepareOptionRenderData, registerStandardComponents, resolveMockupId, resolveVariantId, retryOperation, simulateCartOperation, toCombinations, toOptionAttributes, useFrameworkAdapter as useVueAdapter, validateAlignment, validateDesignElement, validateEffects, validateImageUrl, validateMockupOptions, validateProductSelection, validateProductSpec, validateRequiredOptions, validateTileCount, withContext, withErrorHandling, withSyncErrorHandling };
2883
+ export { AbstractTemplateRenderer, type AddToCartOptions, Animations, type ArtSelectorContext, type ArtSelectorDescriptor, type ArtSelectorOptions, type ArtworkData, type AspectRatio, Attributes, type BaseDescriptor, type BlendConfig, type BuildMockupUrlConfig, type CartDetail, type CatalogListResponse, type CatalogProduct$2 as CatalogProduct, ClassNames, type ColorDesignElement, ColorPickerUtils, Combination, CommonProps, ComponentContext, type ComponentDefinition, ComponentDescriptor, ComponentEventManager, ComponentFactory, ComponentLifecycle, ComponentLifecycleHooks, type ComponentMetadata, ComponentProps, ComponentRegistry, ComponentState, type ComponentTemplate, ComponentTemplates, type ContainerDescriptor, ContextBridge, type ContextComparator, type ContextConsumer, ContextInjector, type ContextProviderConfig, type ContextSubscriber, ContextSynchronizer, type CreateDesignStateInput, type CreatedDesignState, DEFAULT_ARTWORK_URL, DEFAULT_ASPECT_RATIO, DEFAULT_COLOR, DEFAULT_GRANT_BASE, DEFAULT_MOCKUP_BASE, DEFAULT_PLACEMENT_DIMENSIONS, type Descriptor, type Design, type DesignElement, type DesignGenerationContext, type Effects, Elements, type ErrorContext, type ErrorHandler, ErrorManager, type EventDefinition, EventDelegator, EventEmitter, EventHandler, type EventListener, type EventPayload, type Fill, Focus, FrameworkAdapter, FrameworkUtilities, type GetMockupUrlOptions, type ImageAlignment, type ImageDesignElement, type LifecycleHook, LifecycleManager, type LifecyclePhase, type LifecycleTransition, LitAdapter, type MaskOverride, type MockupGenerationOptions, MockupResult, type MockupService, type MockupServiceConfig, OptionAttribute, type OptionChoice, type OptionChoiceRenderData, type OptionRenderData, OptionSelection, type Placement, type PlacementDesign, PlacementSettings, type ProductArtAlignmentContext, type ProductArtAlignmentDescriptor, type ProductArtAlignmentOptions, type ProductContext, type ProductContextEvents, ProductContextManager, type ProductData, type ProductFetcher, type ProductImageDescriptor, type ProductImageOptions, ProductLoader, type ProductMockupData, type ProductOptionChoice, type ProductOptionData, type ProductOptionsDescriptor, type ProductOptionsOptions, type ProductPlacement, type ProductPriceDescriptor, type ProductPriceOptions, ProductProps, type ProductSpec, type ProductTitleDescriptor, type ProductTitleOptions, type ProductVariant, type PropDefinition, type PropSchema, type PropType, PropertyManager, REALTIME_WS_URL, type RateLimitState, type RealtimeGrant, RealtimeMockupService, type RegularArtwork, type RenderProduct, type RenderProductCombination, type RenderProductOptions, RenderSession, type RenderSessionOptions, type RenderState, type SdkConfig, type SeamlessPattern, type SignedUrlResponse, type SnowconeClient, StandardComponents, StandardEvents, StateManager, type StateManagerOptions, type StateMiddleware, type StateSelector, type StateSubscriber, type StateUpdater, Styles, SvelteAdapter, SwatchUtils, type TagName, TemplateBuilder, type TemplateNode, type TemplateNodeType, type TemplateRenderer, type TextDescriptor, type TileCount, UniversalContextProvider, type UserSelection, type ValidationError, type ValidationResult, VueAdapter, assertVariantSelected, autoRegister, buildMockupUrl, componentRegistry, createAddToCartEvent, createAddToCartHandler, createCartDetail, createClient, createContextProvider, createDesignForPlacements, createDesignState, createErrorHandler, createEventManager, createLifecycle, createLitComponent, createProductContext, createProductLoader, createPropertyManager, createStateHook, createStateStore, createSvelteComponent, createUniversalProvider, createVueComponent, describeArtSelector, describeProductArtAlignment, describeProductImage, describeProductOptions, describeProductPrice, describeProductTitle, extractProductId, fetchRealtimeGrant, filterImagePlacements, findClosestSnapPoint, findVariantForSelection, formatPrice, formatValidationErrors, getDefaultVariantId, getEffectiveAlignment, getIncompleteSelectionMessage, getMissingSelections, getMockupUrl, getOptionRenderType, getProduct, getSelectionDisplayText, getSnapPoints, getVariant, handleOptionChange, isSelectionComplete, isValidAlignment, isValidTileCount, listProducts, mintRealtimeGrant, mockupUrl, normalizeChoice, prepareOptionRenderData, registerStandardComponents, resolveMockupId, resolveVariantId, retryOperation, simulateCartOperation, toCombinations, toOptionAttributes, useFrameworkAdapter as useVueAdapter, validateAlignment, validateDesignElement, validateEffects, validateImageUrl, validateMockupOptions, validateProductSelection, validateProductSpec, validateRequiredOptions, validateTileCount, withContext, withErrorHandling, withSyncErrorHandling };
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-Poy8LZNA.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-Poy8LZNA.js';
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-DTRStLRH.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-DTRStLRH.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.connect();
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
  */
@@ -2604,6 +2603,29 @@ interface RenderState {
2604
2603
  elements?: Array<Record<string, unknown>>;
2605
2604
  [key: string]: unknown;
2606
2605
  }
2606
+ /**
2607
+ * A single variant combination from the catalog product's `options.combinations`.
2608
+ * Each combination pins one value per variant-affecting attribute plus the
2609
+ * `variantId` (gvid) that selection resolves to. Pass `options` on
2610
+ * {@link RenderProduct} so the session can FAIL FAST when a required `variantId`
2611
+ * is missing instead of silently hanging until the watchdog.
2612
+ */
2613
+ interface RenderProductCombination {
2614
+ /** The variant (gvid) this combination resolves to. */
2615
+ variantId?: string;
2616
+ /** Attribute selections (e.g. `{ Frame: 'Walnut', Size: '5x7' }`). */
2617
+ [attribute: string]: string | number | undefined;
2618
+ }
2619
+ /**
2620
+ * The catalog product's `options` block (structurally typed). Only
2621
+ * `combinations` is consumed by the session — to detect whether the product has
2622
+ * a real variant axis (multiple distinct `variantId`s) so a missing/invalid
2623
+ * {@link RenderProduct.variantId} can be rejected up front.
2624
+ */
2625
+ interface RenderProductOptions {
2626
+ combinations?: RenderProductCombination[];
2627
+ [key: string]: unknown;
2628
+ }
2607
2629
  /** Product the session renders against (Snowcone catalog ids). */
2608
2630
  interface RenderProduct {
2609
2631
  productId: string;
@@ -2618,13 +2640,44 @@ interface RenderProduct {
2618
2640
  * Optional variant (gvid). Omit if the product has no variant axis. Required
2619
2641
  * for products with a color option — it auto-fills the color placement; without
2620
2642
  * it the render waits for a color blob that never comes.
2643
+ *
2644
+ * If you also pass {@link RenderProduct.options}, the session validates this
2645
+ * up front: a product whose `options.combinations` exposes more than one
2646
+ * variant REQUIRES a `variantId` (and it must be one of the combinations'),
2647
+ * else the session throws immediately rather than hanging until the watchdog.
2621
2648
  */
2622
2649
  variantId?: string;
2650
+ /**
2651
+ * The catalog product's `options` block (its `options.combinations[].variantId`
2652
+ * are the valid variant ids). Optional, but passing it lets the session detect
2653
+ * a missing/invalid required `variantId` and FAIL FAST — turning the classic
2654
+ * 30s silent hang into an immediate, actionable error. You can pass the whole
2655
+ * catalog product's `options` object verbatim.
2656
+ */
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[];
2623
2668
  /** Render width in px (default 1000). */
2624
2669
  width?: number;
2625
2670
  /** Per-placement settings (tiling, scale mode). */
2626
2671
  placementSettings?: Record<string, PlacementSettings>;
2627
2672
  }
2673
+ /**
2674
+ * Throws immediately if the product has a variant axis (its
2675
+ * `options.combinations` expose 2+ distinct `variantId`s) but no valid
2676
+ * `variantId` was supplied. This is the fail-fast guard for the otherwise-silent
2677
+ * 30s render hang on color/variant products. Products with no `options` (or a
2678
+ * single variant) are left untouched — they render fine with no `variantId`.
2679
+ */
2680
+ declare function assertVariantSelected(product: RenderProduct): void;
2628
2681
  interface RenderSessionOptions {
2629
2682
  /** Publishable shop id (= shop.id, like Stripe pk_). */
2630
2683
  shop: string;
@@ -2712,6 +2765,14 @@ declare class RenderSession {
2712
2765
  getMockups(): MockupResult[];
2713
2766
  /** Close the WebSocket and stop auto-renew. */
2714
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;
2715
2776
  /**
2716
2777
  * (Re)arm the render watchdog. Clears any prior timer and, unless disabled
2717
2778
  * (`renderTimeoutMs <= 0`), starts a fresh one. If it fires before a mockup
@@ -2819,4 +2880,4 @@ declare function getProduct(idOrSlug: string, config?: Partial<SdkConfig>): Prom
2819
2880
  */
2820
2881
  declare function createClient(config: SdkConfig): SnowconeClient;
2821
2882
 
2822
- export { AbstractTemplateRenderer, type AddToCartOptions, Animations, type ArtSelectorContext, type ArtSelectorDescriptor, type ArtSelectorOptions, type ArtworkData, type AspectRatio, Attributes, type BaseDescriptor, type BlendConfig, type BuildMockupUrlConfig, type CartDetail, type CatalogListResponse, type CatalogProduct$2 as CatalogProduct, ClassNames, type ColorDesignElement, ColorPickerUtils, Combination, CommonProps, ComponentContext, type ComponentDefinition, ComponentDescriptor, ComponentEventManager, ComponentFactory, ComponentLifecycle, ComponentLifecycleHooks, type ComponentMetadata, ComponentProps, ComponentRegistry, ComponentState, type ComponentTemplate, ComponentTemplates, type ContainerDescriptor, ContextBridge, type ContextComparator, type ContextConsumer, ContextInjector, type ContextProviderConfig, type ContextSubscriber, ContextSynchronizer, type CreateDesignStateInput, type CreatedDesignState, DEFAULT_ARTWORK_URL, DEFAULT_ASPECT_RATIO, DEFAULT_COLOR, DEFAULT_GRANT_BASE, DEFAULT_MOCKUP_BASE, DEFAULT_PLACEMENT_DIMENSIONS, type Descriptor, type Design, type DesignElement, type DesignGenerationContext, type Effects, Elements, type ErrorContext, type ErrorHandler, ErrorManager, type EventDefinition, EventDelegator, EventEmitter, EventHandler, type EventListener, type EventPayload, type Fill, Focus, FrameworkAdapter, FrameworkUtilities, type GetMockupUrlOptions, type ImageAlignment, type ImageDesignElement, type LifecycleHook, LifecycleManager, type LifecyclePhase, type LifecycleTransition, LitAdapter, type MaskOverride, type MockupGenerationOptions, MockupResult, type MockupService, type MockupServiceConfig, OptionAttribute, type OptionChoice, type OptionChoiceRenderData, type OptionRenderData, OptionSelection, type Placement, type PlacementDesign, PlacementSettings, type ProductArtAlignmentContext, type ProductArtAlignmentDescriptor, type ProductArtAlignmentOptions, type ProductContext, type ProductContextEvents, ProductContextManager, type ProductData, type ProductFetcher, type ProductImageDescriptor, type ProductImageOptions, ProductLoader, type ProductMockupData, type ProductOptionChoice, type ProductOptionData, type ProductOptionsDescriptor, type ProductOptionsOptions, type ProductPlacement, type ProductPriceDescriptor, type ProductPriceOptions, ProductProps, type ProductSpec, type ProductTitleDescriptor, type ProductTitleOptions, type ProductVariant, type PropDefinition, type PropSchema, type PropType, PropertyManager, REALTIME_WS_URL, type RateLimitState, type RealtimeGrant, RealtimeMockupService, type RegularArtwork, type RenderProduct, RenderSession, type RenderSessionOptions, type RenderState, type SdkConfig, type SeamlessPattern, type SignedUrlResponse, type SnowconeClient, StandardComponents, StandardEvents, StateManager, type StateManagerOptions, type StateMiddleware, type StateSelector, type StateSubscriber, type StateUpdater, Styles, SvelteAdapter, SwatchUtils, type TagName, TemplateBuilder, type TemplateNode, type TemplateNodeType, type TemplateRenderer, type TextDescriptor, type TileCount, UniversalContextProvider, type UserSelection, type ValidationError, type ValidationResult, VueAdapter, autoRegister, buildMockupUrl, componentRegistry, createAddToCartEvent, createAddToCartHandler, createCartDetail, createClient, createContextProvider, createDesignForPlacements, createDesignState, createErrorHandler, createEventManager, createLifecycle, createLitComponent, createProductContext, createProductLoader, createPropertyManager, createStateHook, createStateStore, createSvelteComponent, createUniversalProvider, createVueComponent, describeArtSelector, describeProductArtAlignment, describeProductImage, describeProductOptions, describeProductPrice, describeProductTitle, extractProductId, fetchRealtimeGrant, filterImagePlacements, findClosestSnapPoint, findVariantForSelection, formatPrice, formatValidationErrors, getDefaultVariantId, getEffectiveAlignment, getIncompleteSelectionMessage, getMissingSelections, getMockupUrl, getOptionRenderType, getProduct, getSelectionDisplayText, getSnapPoints, getVariant, handleOptionChange, isSelectionComplete, isValidAlignment, isValidTileCount, listProducts, mintRealtimeGrant, mockupUrl, normalizeChoice, prepareOptionRenderData, registerStandardComponents, resolveMockupId, resolveVariantId, retryOperation, simulateCartOperation, toCombinations, toOptionAttributes, useFrameworkAdapter as useVueAdapter, validateAlignment, validateDesignElement, validateEffects, validateImageUrl, validateMockupOptions, validateProductSelection, validateProductSpec, validateRequiredOptions, validateTileCount, withContext, withErrorHandling, withSyncErrorHandling };
2883
+ export { AbstractTemplateRenderer, type AddToCartOptions, Animations, type ArtSelectorContext, type ArtSelectorDescriptor, type ArtSelectorOptions, type ArtworkData, type AspectRatio, Attributes, type BaseDescriptor, type BlendConfig, type BuildMockupUrlConfig, type CartDetail, type CatalogListResponse, type CatalogProduct$2 as CatalogProduct, ClassNames, type ColorDesignElement, ColorPickerUtils, Combination, CommonProps, ComponentContext, type ComponentDefinition, ComponentDescriptor, ComponentEventManager, ComponentFactory, ComponentLifecycle, ComponentLifecycleHooks, type ComponentMetadata, ComponentProps, ComponentRegistry, ComponentState, type ComponentTemplate, ComponentTemplates, type ContainerDescriptor, ContextBridge, type ContextComparator, type ContextConsumer, ContextInjector, type ContextProviderConfig, type ContextSubscriber, ContextSynchronizer, type CreateDesignStateInput, type CreatedDesignState, DEFAULT_ARTWORK_URL, DEFAULT_ASPECT_RATIO, DEFAULT_COLOR, DEFAULT_GRANT_BASE, DEFAULT_MOCKUP_BASE, DEFAULT_PLACEMENT_DIMENSIONS, type Descriptor, type Design, type DesignElement, type DesignGenerationContext, type Effects, Elements, type ErrorContext, type ErrorHandler, ErrorManager, type EventDefinition, EventDelegator, EventEmitter, EventHandler, type EventListener, type EventPayload, type Fill, Focus, FrameworkAdapter, FrameworkUtilities, type GetMockupUrlOptions, type ImageAlignment, type ImageDesignElement, type LifecycleHook, LifecycleManager, type LifecyclePhase, type LifecycleTransition, LitAdapter, type MaskOverride, type MockupGenerationOptions, MockupResult, type MockupService, type MockupServiceConfig, OptionAttribute, type OptionChoice, type OptionChoiceRenderData, type OptionRenderData, OptionSelection, type Placement, type PlacementDesign, PlacementSettings, type ProductArtAlignmentContext, type ProductArtAlignmentDescriptor, type ProductArtAlignmentOptions, type ProductContext, type ProductContextEvents, ProductContextManager, type ProductData, type ProductFetcher, type ProductImageDescriptor, type ProductImageOptions, ProductLoader, type ProductMockupData, type ProductOptionChoice, type ProductOptionData, type ProductOptionsDescriptor, type ProductOptionsOptions, type ProductPlacement, type ProductPriceDescriptor, type ProductPriceOptions, ProductProps, type ProductSpec, type ProductTitleDescriptor, type ProductTitleOptions, type ProductVariant, type PropDefinition, type PropSchema, type PropType, PropertyManager, REALTIME_WS_URL, type RateLimitState, type RealtimeGrant, RealtimeMockupService, type RegularArtwork, type RenderProduct, type RenderProductCombination, type RenderProductOptions, RenderSession, type RenderSessionOptions, type RenderState, type SdkConfig, type SeamlessPattern, type SignedUrlResponse, type SnowconeClient, StandardComponents, StandardEvents, StateManager, type StateManagerOptions, type StateMiddleware, type StateSelector, type StateSubscriber, type StateUpdater, Styles, SvelteAdapter, SwatchUtils, type TagName, TemplateBuilder, type TemplateNode, type TemplateNodeType, type TemplateRenderer, type TextDescriptor, type TileCount, UniversalContextProvider, type UserSelection, type ValidationError, type ValidationResult, VueAdapter, assertVariantSelected, autoRegister, buildMockupUrl, componentRegistry, createAddToCartEvent, createAddToCartHandler, createCartDetail, createClient, createContextProvider, createDesignForPlacements, createDesignState, createErrorHandler, createEventManager, createLifecycle, createLitComponent, createProductContext, createProductLoader, createPropertyManager, createStateHook, createStateStore, createSvelteComponent, createUniversalProvider, createVueComponent, describeArtSelector, describeProductArtAlignment, describeProductImage, describeProductOptions, describeProductPrice, describeProductTitle, extractProductId, fetchRealtimeGrant, filterImagePlacements, findClosestSnapPoint, findVariantForSelection, formatPrice, formatValidationErrors, getDefaultVariantId, getEffectiveAlignment, getIncompleteSelectionMessage, getMissingSelections, getMockupUrl, getOptionRenderType, getProduct, getSelectionDisplayText, getSnapPoints, getVariant, handleOptionChange, isSelectionComplete, isValidAlignment, isValidTileCount, listProducts, mintRealtimeGrant, mockupUrl, normalizeChoice, prepareOptionRenderData, registerStandardComponents, resolveMockupId, resolveVariantId, retryOperation, simulateCartOperation, toCombinations, toOptionAttributes, useFrameworkAdapter as useVueAdapter, validateAlignment, validateDesignElement, validateEffects, validateImageUrl, validateMockupOptions, validateProductSelection, validateProductSpec, validateRequiredOptions, validateTileCount, withContext, withErrorHandling, withSyncErrorHandling };
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  REALTIME_WS_URL,
6
6
  RealtimeMockupService
7
- } from "./chunk-D5ZRGKA5.js";
7
+ } from "./chunk-IMJKV4YO.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();
@@ -4690,6 +4688,44 @@ async function fetchRealtimeGrant(grantUrl, shop, fetchImpl) {
4690
4688
  }
4691
4689
 
4692
4690
  // src/realtime/session.ts
4691
+ function variantIdsOf(product) {
4692
+ const combos = product.options?.combinations;
4693
+ if (!combos?.length) return [];
4694
+ const ids = /* @__PURE__ */ new Set();
4695
+ for (const c of combos) {
4696
+ if (typeof c.variantId === "string" && c.variantId !== "") ids.add(c.variantId);
4697
+ }
4698
+ return [...ids];
4699
+ }
4700
+ function assertVariantSelected(product) {
4701
+ const variantIds = variantIdsOf(product);
4702
+ if (variantIds.length < 2) return;
4703
+ const fix = `pass product.variantId from options.combinations[].variantId (valid ids: ${variantIds.join(", ")})`;
4704
+ if (!product.variantId) {
4705
+ throw new Error(
4706
+ `RenderSession: product ${product.productId} requires a variantId \u2014 ${fix}`
4707
+ );
4708
+ }
4709
+ if (!variantIds.includes(product.variantId)) {
4710
+ throw new Error(
4711
+ `RenderSession: product ${product.productId} has an invalid variantId "${product.variantId}" \u2014 ${fix}`
4712
+ );
4713
+ }
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
+ }
4693
4729
  var DEFAULT_RENDER_TIMEOUT_MS = 3e4;
4694
4730
  var RenderSession = class {
4695
4731
  svc;
@@ -4705,6 +4741,7 @@ var RenderSession = class {
4705
4741
  if (!opts.getToken && !opts.grantUrl) {
4706
4742
  throw new Error("RenderSession: provide `grantUrl` or `getToken` to authorize the session");
4707
4743
  }
4744
+ if (opts.product) assertVariantSelected(opts.product);
4708
4745
  this.opts = opts;
4709
4746
  this.product = opts.product ?? null;
4710
4747
  this.renderTimeoutMs = opts.renderTimeoutMs ?? DEFAULT_RENDER_TIMEOUT_MS;
@@ -4724,6 +4761,7 @@ var RenderSession = class {
4724
4761
  }
4725
4762
  /** Set or update the product. Sends config immediately if already connected. */
4726
4763
  setProduct(product) {
4764
+ assertVariantSelected(product);
4727
4765
  this.product = product;
4728
4766
  this.svc.sendConfig(this.toConfig(product));
4729
4767
  }
@@ -4739,6 +4777,13 @@ var RenderSession = class {
4739
4777
  );
4740
4778
  }
4741
4779
  const product = this.product;
4780
+ try {
4781
+ assertVariantSelected(product);
4782
+ } catch (err) {
4783
+ const message = err instanceof Error ? err.message : String(err);
4784
+ this.errorCb?.(message);
4785
+ return Promise.reject(err instanceof Error ? err : new Error(message));
4786
+ }
4742
4787
  this.ready = new Promise((resolve, reject) => {
4743
4788
  let settled = false;
4744
4789
  this.svc.setCallbacks({
@@ -4790,6 +4835,7 @@ var RenderSession = class {
4790
4835
  */
4791
4836
  async renderState(placement, state, throttleMs = 0) {
4792
4837
  await this.connect();
4838
+ this.warnPlacement(placement, state);
4793
4839
  this.svc.sendCanvasState(placement, state, this.product?.mockupIds.length ?? 1, throttleMs);
4794
4840
  this.armWatchdog();
4795
4841
  }
@@ -4802,6 +4848,7 @@ var RenderSession = class {
4802
4848
  */
4803
4849
  async renderSavedState(placement, stateId) {
4804
4850
  await this.connect();
4851
+ this.warnPlacement(placement);
4805
4852
  this.svc.sendCanvasStateRef(placement, stateId);
4806
4853
  this.armWatchdog();
4807
4854
  }
@@ -4820,6 +4867,28 @@ var RenderSession = class {
4820
4867
  this.svc.disconnect();
4821
4868
  this.ready = null;
4822
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
+ }
4823
4892
  /**
4824
4893
  * (Re)arm the render watchdog. Clears any prior timer and, unless disabled
4825
4894
  * (`renderTimeoutMs <= 0`), starts a fresh one. If it fires before a mockup
@@ -4935,7 +5004,12 @@ async function getProduct(idOrSlug, config) {
4935
5004
  const url = `${meilisearchHost}/indexes/${meilisearchIndex}/documents/${idOrSlug}`;
4936
5005
  const res = await f(url, { method: "GET", headers });
4937
5006
  if (res.status === 404)
4938
- throw Object.assign(new Error("Not found"), { code: "NOT_FOUND" });
5007
+ throw Object.assign(
5008
+ new Error(
5009
+ `getProduct: no catalog product "${idOrSlug}" in index "${meilisearchIndex}". Pass a valid product id/slug (browse with listProducts()).`
5010
+ ),
5011
+ { code: "NOT_FOUND", productId: idOrSlug }
5012
+ );
4939
5013
  if (!res.ok) throw new Error(`getProduct failed: ${res.status}`);
4940
5014
  const raw = await res.json();
4941
5015
  return validateProductLoose(raw);
@@ -4997,6 +5071,7 @@ export {
4997
5071
  UniversalContextProvider,
4998
5072
  VueAdapter,
4999
5073
  adapterRegistry,
5074
+ assertVariantSelected,
5000
5075
  autoRegister,
5001
5076
  buildMockupUrl2 as buildMockupUrl,
5002
5077
  componentRegistry,
package/dist/react.cjs CHANGED
@@ -51,6 +51,10 @@ var RealtimeMockupService = class {
51
51
  lastBlobSentAt = 0;
52
52
  canvasBlobs = /* @__PURE__ */ new Map();
53
53
  canvasStates = /* @__PURE__ */ new Map();
54
+ // Serialized JSON of the last canvas state ACTUALLY SENT per placement. Used to
55
+ // drop duplicate consecutive states so an idle canvas (whose onChange keeps
56
+ // firing identical states) doesn't stream renders and burn the realtime budget.
57
+ lastSentStateJson = {};
54
58
  colors = /* @__PURE__ */ new Map();
55
59
  lastSendTime = {};
56
60
  throttleTimeouts = {};
@@ -71,6 +75,8 @@ var RealtimeMockupService = class {
71
75
  // the WS with `?token=`, and renews ~15s before expiry via a `renew` message.
72
76
  tokenProvider;
73
77
  renewTimer = null;
78
+ isConnecting = false;
79
+ connectAttempt = 0;
74
80
  setCallbacks(callbacks) {
75
81
  this.callbacks = callbacks;
76
82
  }
@@ -97,16 +103,21 @@ var RealtimeMockupService = class {
97
103
  this.callbacks.onLog?.(message, type);
98
104
  }
99
105
  connect() {
100
- if (this.ws?.readyState === WebSocket.OPEN) {
106
+ if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING || this.isConnecting) {
101
107
  return;
102
108
  }
109
+ const attempt = ++this.connectAttempt;
110
+ this.isConnecting = true;
103
111
  if (this.tokenProvider) {
104
112
  this.tokenProvider().then((grant) => {
113
+ if (attempt !== this.connectAttempt || !this.isConnecting) return;
105
114
  this.openSocket(grant.token);
106
115
  this.scheduleRenew(grant.expiresAt);
107
116
  }).catch((err) => {
117
+ if (attempt !== this.connectAttempt) return;
108
118
  this.addLog(`Failed to obtain realtime grant: ${err}`);
109
119
  this.status = "Disconnected";
120
+ this.isConnecting = false;
110
121
  });
111
122
  return;
112
123
  }
@@ -120,6 +131,7 @@ var RealtimeMockupService = class {
120
131
  console.log(`[WS] connection OPENED to ${this.wsUrl}`);
121
132
  this.addLog("WebSocket connection opened");
122
133
  this.status = "Connected";
134
+ this.isConnecting = false;
123
135
  };
124
136
  this.ws.onmessage = (event) => {
125
137
  try {
@@ -138,11 +150,13 @@ var RealtimeMockupService = class {
138
150
  this.isConfigured = false;
139
151
  this.configSent = false;
140
152
  this.clearRenew();
153
+ this.isConnecting = false;
141
154
  this.callbacks.onDisconnected?.();
142
155
  };
143
156
  this.ws.onerror = (error) => {
144
157
  this.addLog(`WebSocket error: ${error}`);
145
158
  this.status = "Disconnected";
159
+ this.isConnecting = false;
146
160
  };
147
161
  }
148
162
  scheduleRenew(expiresAt) {
@@ -292,6 +306,8 @@ var RealtimeMockupService = class {
292
306
  }
293
307
  }
294
308
  disconnect() {
309
+ this.connectAttempt++;
310
+ this.isConnecting = false;
295
311
  this.clearRenew();
296
312
  if (this.ws) {
297
313
  this.ws.close();
@@ -330,6 +346,7 @@ var RealtimeMockupService = class {
330
346
  this.canvasStates.clear();
331
347
  this.colors.clear();
332
348
  this.lastSendTime = {};
349
+ this.lastSentStateJson = {};
333
350
  Object.values(this.throttleTimeouts).forEach((timeout) => clearTimeout(timeout));
334
351
  this.throttleTimeouts = {};
335
352
  this.requestVersion = 0;
@@ -535,6 +552,11 @@ var RealtimeMockupService = class {
535
552
  console.log(`[WS] sendCanvasStateImmediately ABORTED: ws not open`);
536
553
  return;
537
554
  }
555
+ const stateJson = JSON.stringify(state);
556
+ if (this.lastSentStateJson[placement] === stateJson) {
557
+ this.addLog(`Skipped duplicate canvas state for "${placement}" (unchanged)`, "sent");
558
+ return;
559
+ }
538
560
  this.lastSentVersion = ++this.requestVersion;
539
561
  this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
540
562
  const message = JSON.stringify({
@@ -544,6 +566,7 @@ var RealtimeMockupService = class {
544
566
  state
545
567
  });
546
568
  this.ws.send(message);
569
+ this.lastSentStateJson[placement] = stateJson;
547
570
  this.lastBlobSentAt = Date.now();
548
571
  this.addLog(`Sent canvas state for "${placement}" (${(message.length / 1024).toFixed(1)}KB, v${this.lastSentVersion})`, "sent");
549
572
  this.callbacks.onBlobSent?.(placement);
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-Poy8LZNA.cjs';
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-DTRStLRH.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-Poy8LZNA.js';
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-DTRStLRH.js';
2
2
 
3
3
  interface UseRealtimeMockupOptions {
4
4
  wsUrl?: string;
package/dist/react.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RealtimeMockupService
3
- } from "./chunk-D5ZRGKA5.js";
3
+ } from "./chunk-IMJKV4YO.js";
4
4
 
5
5
  // src/realtime/react.ts
6
6
  import { useCallback, useEffect, useRef, useState } from "react";
@@ -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 reasonable default selection based on the best combination.
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 non-affecting attributes, leave unset (caller may choose first choice if desired).
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
  /**
@@ -306,6 +307,7 @@ declare class RealtimeMockupService {
306
307
  private lastBlobSentAt;
307
308
  private canvasBlobs;
308
309
  private canvasStates;
310
+ private lastSentStateJson;
309
311
  private colors;
310
312
  private lastSendTime;
311
313
  private throttleTimeouts;
@@ -316,6 +318,8 @@ declare class RealtimeMockupService {
316
318
  private sendVersionInBlob;
317
319
  private tokenProvider?;
318
320
  private renewTimer;
321
+ private isConnecting;
322
+ private connectAttempt;
319
323
  constructor(wsUrl?: string);
320
324
  setCallbacks(callbacks: RealtimeMockupCallbacks): void;
321
325
  /** Provide a grant fetcher to authorize the session (per-shop, renewable). */
@@ -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 reasonable default selection based on the best combination.
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 non-affecting attributes, leave unset (caller may choose first choice if desired).
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
  /**
@@ -306,6 +307,7 @@ declare class RealtimeMockupService {
306
307
  private lastBlobSentAt;
307
308
  private canvasBlobs;
308
309
  private canvasStates;
310
+ private lastSentStateJson;
309
311
  private colors;
310
312
  private lastSendTime;
311
313
  private throttleTimeouts;
@@ -316,6 +318,8 @@ declare class RealtimeMockupService {
316
318
  private sendVersionInBlob;
317
319
  private tokenProvider?;
318
320
  private renewTimer;
321
+ private isConnecting;
322
+ private connectAttempt;
319
323
  constructor(wsUrl?: string);
320
324
  setCallbacks(callbacks: RealtimeMockupCallbacks): void;
321
325
  /** Provide a grant fetcher to authorize the session (per-shop, renewable). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowcone-app/sdk",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Snowcone SDK for product mockups and print-on-demand",
5
5
  "keywords": [
6
6
  "merch",
@@ -81,6 +81,7 @@
81
81
  "scripts": {
82
82
  "build": "tsup src/index.ts src/react.ts src/dev-fetcher.ts --format esm,cjs --dts --external zod --external react",
83
83
  "clean": "rm -rf dist",
84
- "typecheck": "tsc --project ./tsconfig.json --noEmit"
84
+ "typecheck": "tsc --project ./tsconfig.json --noEmit",
85
+ "test": "vitest run"
85
86
  }
86
87
  }