@snowcone-app/sdk 0.2.1 → 0.3.1

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,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#258](https://github.com/snowcone-app/snowcone-monorepo/pull/258) [`a6d53d8`](https://github.com/snowcone-app/snowcone-monorepo/commit/a6d53d82a18d6dbbb55be4525637cd8372aed760) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Move `zod` from `peerDependencies` to `dependencies`. The SDK uses zod internally (to validate catalog responses) and never asks the consumer to supply a zod schema across the API boundary, so it should bring its own zod rather than demand one as a peer. This removes the unmet-peer warning consumers saw on npm (which is on zod 4 while the SDK targets zod 3).
8
+
9
+ ## 0.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#256](https://github.com/snowcone-app/snowcone-monorepo/pull/256) [`49f18e2`](https://github.com/snowcone-app/snowcone-monorepo/commit/49f18e221b49422e88f6aa6a06a1de153f170ff3) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Surface the renderer's required-placement contract on `getProduct()`. The catalog
14
+ product now carries a `requiredPlacements` array — `{ label, key?, type: "image" |
15
+ "color", autoFilledByVariant }` — so a developer can read exactly which placements
16
+ `renderState` will demand (and which are auto-filled from `variantId`) instead of
17
+ reverse-engineering it from a runtime timeout error. Image placements also no longer
18
+ report `type: null`; they default to `"image"`.
19
+
20
+ ## 0.2.2
21
+
22
+ ### Patch Changes
23
+
24
+ - [#239](https://github.com/snowcone-app/snowcone-monorepo/pull/239) [`0556a8f`](https://github.com/snowcone-app/snowcone-monorepo/commit/0556a8f1fbef862dadc50463657fb716517b3264) Thanks [@kevinsproles](https://github.com/kevinsproles)! - fix(realtime): `RenderSession` now surfaces a render timeout via `onError`
25
+
26
+ A render that produces no mockup used to hang forever with no signal — the
27
+ classic trigger is a product with a color/variant axis where `variantId` is
28
+ omitted, so the server waits for a color blob that never arrives and never
29
+ errors. `RenderSession` now arms a per-render watchdog: if no mockup arrives in
30
+ time it reports an actionable timeout (with the `variantId` hint) through the
31
+ same `onError` channel as other errors, instead of silently hanging.
32
+
33
+ Configurable via the new `renderTimeoutMs` option (default `30000`; set to `0`
34
+ to disable). A successful mockup cancels the watchdog, each new render re-arms
35
+ it, and `close()` clears any pending timer. `renderState` still resolves on
36
+ send — the watchdog is a side-channel and throttle behavior is unchanged.
37
+
3
38
  ## 0.2.1
4
39
 
5
40
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -217,6 +217,18 @@ var CatalogProductSchema = import_zod.z.object({
217
217
  offsetY: import_zod.z.number().optional()
218
218
  })
219
219
  ).optional(),
220
+ // The renderer's required-placement contract, surfaced by the catalog so a
221
+ // dev reads exactly which placements renderState will demand. Image
222
+ // placements + variant-auto-filled color placements. Optional — older catalog
223
+ // docs predate it.
224
+ requiredPlacements: import_zod.z.array(
225
+ import_zod.z.object({
226
+ label: import_zod.z.string(),
227
+ key: import_zod.z.string().optional(),
228
+ type: import_zod.z.enum(["image", "color"]),
229
+ autoFilledByVariant: import_zod.z.boolean()
230
+ })
231
+ ).optional(),
220
232
  defaultGvid: import_zod.z.string().optional()
221
233
  });
222
234
 
@@ -5511,6 +5523,7 @@ async function fetchRealtimeGrant(grantUrl, shop, fetchImpl) {
5511
5523
  }
5512
5524
 
5513
5525
  // src/realtime/session.ts
5526
+ var DEFAULT_RENDER_TIMEOUT_MS = 3e4;
5514
5527
  var RenderSession = class {
5515
5528
  svc;
5516
5529
  opts;
@@ -5518,6 +5531,8 @@ var RenderSession = class {
5518
5531
  mockupCb = null;
5519
5532
  errorCb = null;
5520
5533
  ready = null;
5534
+ renderTimeoutMs;
5535
+ watchdog = null;
5521
5536
  constructor(opts) {
5522
5537
  if (!opts.shop) throw new Error("RenderSession: `shop` is required");
5523
5538
  if (!opts.getToken && !opts.grantUrl) {
@@ -5525,6 +5540,7 @@ var RenderSession = class {
5525
5540
  }
5526
5541
  this.opts = opts;
5527
5542
  this.product = opts.product ?? null;
5543
+ this.renderTimeoutMs = opts.renderTimeoutMs ?? DEFAULT_RENDER_TIMEOUT_MS;
5528
5544
  this.svc = new RealtimeMockupService(opts.wsUrl ?? REALTIME_WS_URL);
5529
5545
  const getToken = opts.getToken ?? (() => fetchRealtimeGrant(opts.grantUrl, opts.shop, opts.fetch));
5530
5546
  this.svc.setTokenProvider(getToken);
@@ -5569,9 +5585,11 @@ var RenderSession = class {
5569
5585
  }
5570
5586
  },
5571
5587
  onMockupRendered: () => {
5588
+ this.clearWatchdog();
5572
5589
  this.mockupCb?.(this.svc.getState().mockupResults);
5573
5590
  },
5574
5591
  onAllMockupsRendered: (results) => {
5592
+ this.clearWatchdog();
5575
5593
  this.mockupCb?.(results);
5576
5594
  },
5577
5595
  onError: (error) => {
@@ -5606,6 +5624,7 @@ var RenderSession = class {
5606
5624
  async renderState(placement, state, throttleMs = 0) {
5607
5625
  await this.connect();
5608
5626
  this.svc.sendCanvasState(placement, state, this.product?.mockupIds.length ?? 1, throttleMs);
5627
+ this.armWatchdog();
5609
5628
  }
5610
5629
  /**
5611
5630
  * Render a SAVED canvas by reference (ADR-0079 Phase 4). The server resolves
@@ -5617,6 +5636,7 @@ var RenderSession = class {
5617
5636
  async renderSavedState(placement, stateId) {
5618
5637
  await this.connect();
5619
5638
  this.svc.sendCanvasStateRef(placement, stateId);
5639
+ this.armWatchdog();
5620
5640
  }
5621
5641
  /** Update only the mockup ids to render (reuses the current state). */
5622
5642
  updateMockupIds(mockupIds) {
@@ -5629,9 +5649,33 @@ var RenderSession = class {
5629
5649
  }
5630
5650
  /** Close the WebSocket and stop auto-renew. */
5631
5651
  close() {
5652
+ this.clearWatchdog();
5632
5653
  this.svc.disconnect();
5633
5654
  this.ready = null;
5634
5655
  }
5656
+ /**
5657
+ * (Re)arm the render watchdog. Clears any prior timer and, unless disabled
5658
+ * (`renderTimeoutMs <= 0`), starts a fresh one. If it fires before a mockup
5659
+ * arrives, it surfaces a timeout via the error callback — the same channel as
5660
+ * server/transport errors — with a hint at the most common cause.
5661
+ */
5662
+ armWatchdog() {
5663
+ this.clearWatchdog();
5664
+ if (this.renderTimeoutMs <= 0) return;
5665
+ this.watchdog = setTimeout(() => {
5666
+ this.watchdog = null;
5667
+ this.errorCb?.(
5668
+ `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)`
5669
+ );
5670
+ }, this.renderTimeoutMs);
5671
+ }
5672
+ /** Cancel the pending render watchdog, if any. */
5673
+ clearWatchdog() {
5674
+ if (this.watchdog) {
5675
+ clearTimeout(this.watchdog);
5676
+ this.watchdog = null;
5677
+ }
5678
+ }
5635
5679
  /** Escape hatch: the underlying low-level service. */
5636
5680
  get service() {
5637
5681
  return this.svc;
package/dist/index.d.cts CHANGED
@@ -80,6 +80,21 @@ interface CatalogProduct$2 {
80
80
  offsetX?: number;
81
81
  offsetY?: number;
82
82
  }[];
83
+ /**
84
+ * The renderer's required-placement contract, surfaced so a developer reads exactly which placements `renderState` will demand. Image placements are always sent by the caller; color placements (autoFilledByVariant: true) are filled from `variantId` — omit them when you pass a variantId, send them otherwise.
85
+ */
86
+ requiredPlacements?: {
87
+ label: string;
88
+ /**
89
+ * URL-safe slug derived from `label`. Present for image placements.
90
+ */
91
+ key?: string;
92
+ type: 'image' | 'color';
93
+ /**
94
+ * When true, this (color) placement is auto-filled from the variant's option choice when a `variantId` is sent; otherwise the caller must send it explicitly.
95
+ */
96
+ autoFilledByVariant: boolean;
97
+ }[];
83
98
  defaultGvid?: string | null;
84
99
  }
85
100
 
@@ -2627,6 +2642,23 @@ interface RenderSessionOptions {
2627
2642
  getToken?: () => Promise<RealtimeGrant>;
2628
2643
  /** Override fetch (used with `grantUrl`). */
2629
2644
  fetch?: typeof fetch;
2645
+ /**
2646
+ * Watchdog for silent render hangs. After a render is dispatched (via
2647
+ * {@link RenderSession.renderState} or {@link RenderSession.renderSavedState}),
2648
+ * if NO mockup arrives within this many milliseconds the session surfaces a
2649
+ * timeout through {@link RenderSession.onError} — turning an otherwise-silent
2650
+ * infinite hang into an actionable error.
2651
+ *
2652
+ * The classic trigger is a product with a color/variant axis where
2653
+ * {@link RenderProduct.variantId} is omitted: the server waits for a color
2654
+ * blob that never arrives, so it neither renders nor errors. The watchdog
2655
+ * makes that visible.
2656
+ *
2657
+ * Defaults to `30000` (30s). A value of `0` (or negative) DISABLES the
2658
+ * watchdog entirely. A successful mockup always cancels the pending watchdog,
2659
+ * and each new render re-arms it.
2660
+ */
2661
+ renderTimeoutMs?: number;
2630
2662
  }
2631
2663
  declare class RenderSession {
2632
2664
  private readonly svc;
@@ -2635,6 +2667,8 @@ declare class RenderSession {
2635
2667
  private mockupCb;
2636
2668
  private errorCb;
2637
2669
  private ready;
2670
+ private readonly renderTimeoutMs;
2671
+ private watchdog;
2638
2672
  constructor(opts: RenderSessionOptions);
2639
2673
  /** Register a callback fired whenever rendered mockup URLs update. */
2640
2674
  onMockups(cb: (results: MockupResult[]) => void): this;
@@ -2678,6 +2712,15 @@ declare class RenderSession {
2678
2712
  getMockups(): MockupResult[];
2679
2713
  /** Close the WebSocket and stop auto-renew. */
2680
2714
  close(): void;
2715
+ /**
2716
+ * (Re)arm the render watchdog. Clears any prior timer and, unless disabled
2717
+ * (`renderTimeoutMs <= 0`), starts a fresh one. If it fires before a mockup
2718
+ * arrives, it surfaces a timeout via the error callback — the same channel as
2719
+ * server/transport errors — with a hint at the most common cause.
2720
+ */
2721
+ private armWatchdog;
2722
+ /** Cancel the pending render watchdog, if any. */
2723
+ private clearWatchdog;
2681
2724
  /** Escape hatch: the underlying low-level service. */
2682
2725
  get service(): RealtimeMockupService;
2683
2726
  private toConfig;
package/dist/index.d.ts CHANGED
@@ -80,6 +80,21 @@ interface CatalogProduct$2 {
80
80
  offsetX?: number;
81
81
  offsetY?: number;
82
82
  }[];
83
+ /**
84
+ * The renderer's required-placement contract, surfaced so a developer reads exactly which placements `renderState` will demand. Image placements are always sent by the caller; color placements (autoFilledByVariant: true) are filled from `variantId` — omit them when you pass a variantId, send them otherwise.
85
+ */
86
+ requiredPlacements?: {
87
+ label: string;
88
+ /**
89
+ * URL-safe slug derived from `label`. Present for image placements.
90
+ */
91
+ key?: string;
92
+ type: 'image' | 'color';
93
+ /**
94
+ * When true, this (color) placement is auto-filled from the variant's option choice when a `variantId` is sent; otherwise the caller must send it explicitly.
95
+ */
96
+ autoFilledByVariant: boolean;
97
+ }[];
83
98
  defaultGvid?: string | null;
84
99
  }
85
100
 
@@ -2627,6 +2642,23 @@ interface RenderSessionOptions {
2627
2642
  getToken?: () => Promise<RealtimeGrant>;
2628
2643
  /** Override fetch (used with `grantUrl`). */
2629
2644
  fetch?: typeof fetch;
2645
+ /**
2646
+ * Watchdog for silent render hangs. After a render is dispatched (via
2647
+ * {@link RenderSession.renderState} or {@link RenderSession.renderSavedState}),
2648
+ * if NO mockup arrives within this many milliseconds the session surfaces a
2649
+ * timeout through {@link RenderSession.onError} — turning an otherwise-silent
2650
+ * infinite hang into an actionable error.
2651
+ *
2652
+ * The classic trigger is a product with a color/variant axis where
2653
+ * {@link RenderProduct.variantId} is omitted: the server waits for a color
2654
+ * blob that never arrives, so it neither renders nor errors. The watchdog
2655
+ * makes that visible.
2656
+ *
2657
+ * Defaults to `30000` (30s). A value of `0` (or negative) DISABLES the
2658
+ * watchdog entirely. A successful mockup always cancels the pending watchdog,
2659
+ * and each new render re-arms it.
2660
+ */
2661
+ renderTimeoutMs?: number;
2630
2662
  }
2631
2663
  declare class RenderSession {
2632
2664
  private readonly svc;
@@ -2635,6 +2667,8 @@ declare class RenderSession {
2635
2667
  private mockupCb;
2636
2668
  private errorCb;
2637
2669
  private ready;
2670
+ private readonly renderTimeoutMs;
2671
+ private watchdog;
2638
2672
  constructor(opts: RenderSessionOptions);
2639
2673
  /** Register a callback fired whenever rendered mockup URLs update. */
2640
2674
  onMockups(cb: (results: MockupResult[]) => void): this;
@@ -2678,6 +2712,15 @@ declare class RenderSession {
2678
2712
  getMockups(): MockupResult[];
2679
2713
  /** Close the WebSocket and stop auto-renew. */
2680
2714
  close(): void;
2715
+ /**
2716
+ * (Re)arm the render watchdog. Clears any prior timer and, unless disabled
2717
+ * (`renderTimeoutMs <= 0`), starts a fresh one. If it fires before a mockup
2718
+ * arrives, it surfaces a timeout via the error callback — the same channel as
2719
+ * server/transport errors — with a hint at the most common cause.
2720
+ */
2721
+ private armWatchdog;
2722
+ /** Cancel the pending render watchdog, if any. */
2723
+ private clearWatchdog;
2681
2724
  /** Escape hatch: the underlying low-level service. */
2682
2725
  get service(): RealtimeMockupService;
2683
2726
  private toConfig;
package/dist/index.js CHANGED
@@ -72,6 +72,18 @@ var CatalogProductSchema = z.object({
72
72
  offsetY: z.number().optional()
73
73
  })
74
74
  ).optional(),
75
+ // The renderer's required-placement contract, surfaced by the catalog so a
76
+ // dev reads exactly which placements renderState will demand. Image
77
+ // placements + variant-auto-filled color placements. Optional — older catalog
78
+ // docs predate it.
79
+ requiredPlacements: z.array(
80
+ z.object({
81
+ label: z.string(),
82
+ key: z.string().optional(),
83
+ type: z.enum(["image", "color"]),
84
+ autoFilledByVariant: z.boolean()
85
+ })
86
+ ).optional(),
75
87
  defaultGvid: z.string().optional()
76
88
  });
77
89
 
@@ -4678,6 +4690,7 @@ async function fetchRealtimeGrant(grantUrl, shop, fetchImpl) {
4678
4690
  }
4679
4691
 
4680
4692
  // src/realtime/session.ts
4693
+ var DEFAULT_RENDER_TIMEOUT_MS = 3e4;
4681
4694
  var RenderSession = class {
4682
4695
  svc;
4683
4696
  opts;
@@ -4685,6 +4698,8 @@ var RenderSession = class {
4685
4698
  mockupCb = null;
4686
4699
  errorCb = null;
4687
4700
  ready = null;
4701
+ renderTimeoutMs;
4702
+ watchdog = null;
4688
4703
  constructor(opts) {
4689
4704
  if (!opts.shop) throw new Error("RenderSession: `shop` is required");
4690
4705
  if (!opts.getToken && !opts.grantUrl) {
@@ -4692,6 +4707,7 @@ var RenderSession = class {
4692
4707
  }
4693
4708
  this.opts = opts;
4694
4709
  this.product = opts.product ?? null;
4710
+ this.renderTimeoutMs = opts.renderTimeoutMs ?? DEFAULT_RENDER_TIMEOUT_MS;
4695
4711
  this.svc = new RealtimeMockupService(opts.wsUrl ?? REALTIME_WS_URL);
4696
4712
  const getToken = opts.getToken ?? (() => fetchRealtimeGrant(opts.grantUrl, opts.shop, opts.fetch));
4697
4713
  this.svc.setTokenProvider(getToken);
@@ -4736,9 +4752,11 @@ var RenderSession = class {
4736
4752
  }
4737
4753
  },
4738
4754
  onMockupRendered: () => {
4755
+ this.clearWatchdog();
4739
4756
  this.mockupCb?.(this.svc.getState().mockupResults);
4740
4757
  },
4741
4758
  onAllMockupsRendered: (results) => {
4759
+ this.clearWatchdog();
4742
4760
  this.mockupCb?.(results);
4743
4761
  },
4744
4762
  onError: (error) => {
@@ -4773,6 +4791,7 @@ var RenderSession = class {
4773
4791
  async renderState(placement, state, throttleMs = 0) {
4774
4792
  await this.connect();
4775
4793
  this.svc.sendCanvasState(placement, state, this.product?.mockupIds.length ?? 1, throttleMs);
4794
+ this.armWatchdog();
4776
4795
  }
4777
4796
  /**
4778
4797
  * Render a SAVED canvas by reference (ADR-0079 Phase 4). The server resolves
@@ -4784,6 +4803,7 @@ var RenderSession = class {
4784
4803
  async renderSavedState(placement, stateId) {
4785
4804
  await this.connect();
4786
4805
  this.svc.sendCanvasStateRef(placement, stateId);
4806
+ this.armWatchdog();
4787
4807
  }
4788
4808
  /** Update only the mockup ids to render (reuses the current state). */
4789
4809
  updateMockupIds(mockupIds) {
@@ -4796,9 +4816,33 @@ var RenderSession = class {
4796
4816
  }
4797
4817
  /** Close the WebSocket and stop auto-renew. */
4798
4818
  close() {
4819
+ this.clearWatchdog();
4799
4820
  this.svc.disconnect();
4800
4821
  this.ready = null;
4801
4822
  }
4823
+ /**
4824
+ * (Re)arm the render watchdog. Clears any prior timer and, unless disabled
4825
+ * (`renderTimeoutMs <= 0`), starts a fresh one. If it fires before a mockup
4826
+ * arrives, it surfaces a timeout via the error callback — the same channel as
4827
+ * server/transport errors — with a hint at the most common cause.
4828
+ */
4829
+ armWatchdog() {
4830
+ this.clearWatchdog();
4831
+ if (this.renderTimeoutMs <= 0) return;
4832
+ this.watchdog = setTimeout(() => {
4833
+ this.watchdog = null;
4834
+ this.errorCb?.(
4835
+ `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)`
4836
+ );
4837
+ }, this.renderTimeoutMs);
4838
+ }
4839
+ /** Cancel the pending render watchdog, if any. */
4840
+ clearWatchdog() {
4841
+ if (this.watchdog) {
4842
+ clearTimeout(this.watchdog);
4843
+ this.watchdog = null;
4844
+ }
4845
+ }
4802
4846
  /** Escape hatch: the underlying low-level service. */
4803
4847
  get service() {
4804
4848
  return this.svc;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowcone-app/sdk",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Snowcone SDK for product mockups and print-on-demand",
5
5
  "keywords": [
6
6
  "merch",
@@ -56,7 +56,6 @@
56
56
  },
57
57
  "sideEffects": false,
58
58
  "peerDependencies": {
59
- "zod": "^3.0.0",
60
59
  "react": "^18 || ^19"
61
60
  },
62
61
  "peerDependenciesMeta": {
@@ -64,14 +63,15 @@
64
63
  "optional": true
65
64
  }
66
65
  },
67
- "dependencies": {},
66
+ "dependencies": {
67
+ "zod": "^3.23.8"
68
+ },
68
69
  "devDependencies": {
69
70
  "@types/node": "^20.11.0",
70
71
  "@types/react": "^18.3.0",
71
72
  "tsup": "^8.1.0",
72
73
  "typescript": "^5.5.4",
73
74
  "vitest": "^2.0.5",
74
- "zod": "^3.23.8",
75
75
  "@snowcone-app/mockup-url": "0.1.0"
76
76
  },
77
77
  "engines": {