@snowcone-app/sdk 0.2.0 → 0.2.2

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,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#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`
8
+
9
+ A render that produces no mockup used to hang forever with no signal — the
10
+ classic trigger is a product with a color/variant axis where `variantId` is
11
+ omitted, so the server waits for a color blob that never arrives and never
12
+ errors. `RenderSession` now arms a per-render watchdog: if no mockup arrives in
13
+ time it reports an actionable timeout (with the `variantId` hint) through the
14
+ same `onError` channel as other errors, instead of silently hanging.
15
+
16
+ Configurable via the new `renderTimeoutMs` option (default `30000`; set to `0`
17
+ to disable). A successful mockup cancels the watchdog, each new render re-arms
18
+ it, and `close()` clears any pending timer. `renderState` still resolves on
19
+ send — the watchdog is a side-channel and throttle behavior is unchanged.
20
+
21
+ ## 0.2.1
22
+
23
+ ### Patch Changes
24
+
25
+ - [#233](https://github.com/snowcone-app/snowcone-monorepo/pull/233) [`8b08283`](https://github.com/snowcone-app/snowcone-monorepo/commit/8b0828340ea0d2852655bffe8436f0cb71bde3aa) Thanks [@kevinsproles](https://github.com/kevinsproles)! - fix(realtime): `mintRealtimeGrant` now honors the keyless publishable path
26
+
27
+ Omitting `apiKey` (or passing `undefined`/`null`/`''`) no longer sends a
28
+ broken `Authorization: Bearer undefined` header that 401s. The header is now
29
+ sent only when a key is actually provided, so `mintRealtimeGrant({ shop })`
30
+ uses the publishable shop-id path exactly as documented. `apiKey` is now typed
31
+ as optional.
32
+
3
33
  ## 0.2.0
4
34
 
5
35
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -5486,12 +5486,11 @@ ${versionToSend}
5486
5486
  var DEFAULT_GRANT_BASE = "https://api.snowcone.app";
5487
5487
  async function mintRealtimeGrant(opts) {
5488
5488
  const f = opts.fetch ?? globalThis.fetch;
5489
+ const headers = { "Content-Type": "application/json" };
5490
+ if (opts.apiKey) headers.Authorization = `Bearer ${opts.apiKey}`;
5489
5491
  const res = await f(`${opts.base ?? DEFAULT_GRANT_BASE}/realtime/grant`, {
5490
5492
  method: "POST",
5491
- headers: {
5492
- "Content-Type": "application/json",
5493
- Authorization: `Bearer ${opts.apiKey}`
5494
- },
5493
+ headers,
5495
5494
  body: JSON.stringify({ shop: opts.shop })
5496
5495
  });
5497
5496
  if (!res.ok) {
@@ -5512,6 +5511,7 @@ async function fetchRealtimeGrant(grantUrl, shop, fetchImpl) {
5512
5511
  }
5513
5512
 
5514
5513
  // src/realtime/session.ts
5514
+ var DEFAULT_RENDER_TIMEOUT_MS = 3e4;
5515
5515
  var RenderSession = class {
5516
5516
  svc;
5517
5517
  opts;
@@ -5519,6 +5519,8 @@ var RenderSession = class {
5519
5519
  mockupCb = null;
5520
5520
  errorCb = null;
5521
5521
  ready = null;
5522
+ renderTimeoutMs;
5523
+ watchdog = null;
5522
5524
  constructor(opts) {
5523
5525
  if (!opts.shop) throw new Error("RenderSession: `shop` is required");
5524
5526
  if (!opts.getToken && !opts.grantUrl) {
@@ -5526,6 +5528,7 @@ var RenderSession = class {
5526
5528
  }
5527
5529
  this.opts = opts;
5528
5530
  this.product = opts.product ?? null;
5531
+ this.renderTimeoutMs = opts.renderTimeoutMs ?? DEFAULT_RENDER_TIMEOUT_MS;
5529
5532
  this.svc = new RealtimeMockupService(opts.wsUrl ?? REALTIME_WS_URL);
5530
5533
  const getToken = opts.getToken ?? (() => fetchRealtimeGrant(opts.grantUrl, opts.shop, opts.fetch));
5531
5534
  this.svc.setTokenProvider(getToken);
@@ -5570,9 +5573,11 @@ var RenderSession = class {
5570
5573
  }
5571
5574
  },
5572
5575
  onMockupRendered: () => {
5576
+ this.clearWatchdog();
5573
5577
  this.mockupCb?.(this.svc.getState().mockupResults);
5574
5578
  },
5575
5579
  onAllMockupsRendered: (results) => {
5580
+ this.clearWatchdog();
5576
5581
  this.mockupCb?.(results);
5577
5582
  },
5578
5583
  onError: (error) => {
@@ -5607,6 +5612,7 @@ var RenderSession = class {
5607
5612
  async renderState(placement, state, throttleMs = 0) {
5608
5613
  await this.connect();
5609
5614
  this.svc.sendCanvasState(placement, state, this.product?.mockupIds.length ?? 1, throttleMs);
5615
+ this.armWatchdog();
5610
5616
  }
5611
5617
  /**
5612
5618
  * Render a SAVED canvas by reference (ADR-0079 Phase 4). The server resolves
@@ -5618,6 +5624,7 @@ var RenderSession = class {
5618
5624
  async renderSavedState(placement, stateId) {
5619
5625
  await this.connect();
5620
5626
  this.svc.sendCanvasStateRef(placement, stateId);
5627
+ this.armWatchdog();
5621
5628
  }
5622
5629
  /** Update only the mockup ids to render (reuses the current state). */
5623
5630
  updateMockupIds(mockupIds) {
@@ -5630,9 +5637,33 @@ var RenderSession = class {
5630
5637
  }
5631
5638
  /** Close the WebSocket and stop auto-renew. */
5632
5639
  close() {
5640
+ this.clearWatchdog();
5633
5641
  this.svc.disconnect();
5634
5642
  this.ready = null;
5635
5643
  }
5644
+ /**
5645
+ * (Re)arm the render watchdog. Clears any prior timer and, unless disabled
5646
+ * (`renderTimeoutMs <= 0`), starts a fresh one. If it fires before a mockup
5647
+ * arrives, it surfaces a timeout via the error callback — the same channel as
5648
+ * server/transport errors — with a hint at the most common cause.
5649
+ */
5650
+ armWatchdog() {
5651
+ this.clearWatchdog();
5652
+ if (this.renderTimeoutMs <= 0) return;
5653
+ this.watchdog = setTimeout(() => {
5654
+ this.watchdog = null;
5655
+ this.errorCb?.(
5656
+ `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)`
5657
+ );
5658
+ }, this.renderTimeoutMs);
5659
+ }
5660
+ /** Cancel the pending render watchdog, if any. */
5661
+ clearWatchdog() {
5662
+ if (this.watchdog) {
5663
+ clearTimeout(this.watchdog);
5664
+ this.watchdog = null;
5665
+ }
5666
+ }
5636
5667
  /** Escape hatch: the underlying low-level service. */
5637
5668
  get service() {
5638
5669
  return this.svc;
package/dist/index.d.cts CHANGED
@@ -2518,14 +2518,26 @@ declare const DEFAULT_GRANT_BASE = "https://api.snowcone.app";
2518
2518
  * own the target `shop`. Returns a 60s grant your browser code opens the WS
2519
2519
  * with (hand it down via your own endpoint + {@link fetchRealtimeGrant}).
2520
2520
  *
2521
+ * Omit `apiKey` (or pass `undefined`/`null`/`''`) to use the **keyless
2522
+ * publishable path**: no `Authorization` header is sent and the grant is
2523
+ * authorized by the publishable `shop` id alone. Passing a key uses the
2524
+ * secret (sk_) path.
2525
+ *
2521
2526
  * @example
2522
2527
  * // your backend route: POST /api/realtime/grant
2523
2528
  * const grant = await mintRealtimeGrant({ apiKey: process.env.SNOWCONE_API_KEY!, shop });
2524
2529
  * return Response.json(grant);
2530
+ *
2531
+ * @example
2532
+ * // keyless publishable path — quick trials, no API key:
2533
+ * const grant = await mintRealtimeGrant({ shop });
2525
2534
  */
2526
2535
  declare function mintRealtimeGrant(opts: {
2527
- /** Secret API key (sk_) with the `mockups:realtime` or `mockups` scope. */
2528
- apiKey: string;
2536
+ /**
2537
+ * Secret API key (sk_) with the `mockups:realtime` or `mockups` scope.
2538
+ * Omit (or pass `undefined`/`null`/`''`) for the keyless publishable path.
2539
+ */
2540
+ apiKey?: string | null;
2529
2541
  /** Target shop id (= shop.id). Must belong to the key's organization. */
2530
2542
  shop: string;
2531
2543
  /** Backend base URL. Defaults to {@link DEFAULT_GRANT_BASE}. */
@@ -2615,6 +2627,23 @@ interface RenderSessionOptions {
2615
2627
  getToken?: () => Promise<RealtimeGrant>;
2616
2628
  /** Override fetch (used with `grantUrl`). */
2617
2629
  fetch?: typeof fetch;
2630
+ /**
2631
+ * Watchdog for silent render hangs. After a render is dispatched (via
2632
+ * {@link RenderSession.renderState} or {@link RenderSession.renderSavedState}),
2633
+ * if NO mockup arrives within this many milliseconds the session surfaces a
2634
+ * timeout through {@link RenderSession.onError} — turning an otherwise-silent
2635
+ * infinite hang into an actionable error.
2636
+ *
2637
+ * The classic trigger is a product with a color/variant axis where
2638
+ * {@link RenderProduct.variantId} is omitted: the server waits for a color
2639
+ * blob that never arrives, so it neither renders nor errors. The watchdog
2640
+ * makes that visible.
2641
+ *
2642
+ * Defaults to `30000` (30s). A value of `0` (or negative) DISABLES the
2643
+ * watchdog entirely. A successful mockup always cancels the pending watchdog,
2644
+ * and each new render re-arms it.
2645
+ */
2646
+ renderTimeoutMs?: number;
2618
2647
  }
2619
2648
  declare class RenderSession {
2620
2649
  private readonly svc;
@@ -2623,6 +2652,8 @@ declare class RenderSession {
2623
2652
  private mockupCb;
2624
2653
  private errorCb;
2625
2654
  private ready;
2655
+ private readonly renderTimeoutMs;
2656
+ private watchdog;
2626
2657
  constructor(opts: RenderSessionOptions);
2627
2658
  /** Register a callback fired whenever rendered mockup URLs update. */
2628
2659
  onMockups(cb: (results: MockupResult[]) => void): this;
@@ -2666,6 +2697,15 @@ declare class RenderSession {
2666
2697
  getMockups(): MockupResult[];
2667
2698
  /** Close the WebSocket and stop auto-renew. */
2668
2699
  close(): void;
2700
+ /**
2701
+ * (Re)arm the render watchdog. Clears any prior timer and, unless disabled
2702
+ * (`renderTimeoutMs <= 0`), starts a fresh one. If it fires before a mockup
2703
+ * arrives, it surfaces a timeout via the error callback — the same channel as
2704
+ * server/transport errors — with a hint at the most common cause.
2705
+ */
2706
+ private armWatchdog;
2707
+ /** Cancel the pending render watchdog, if any. */
2708
+ private clearWatchdog;
2669
2709
  /** Escape hatch: the underlying low-level service. */
2670
2710
  get service(): RealtimeMockupService;
2671
2711
  private toConfig;
package/dist/index.d.ts CHANGED
@@ -2518,14 +2518,26 @@ declare const DEFAULT_GRANT_BASE = "https://api.snowcone.app";
2518
2518
  * own the target `shop`. Returns a 60s grant your browser code opens the WS
2519
2519
  * with (hand it down via your own endpoint + {@link fetchRealtimeGrant}).
2520
2520
  *
2521
+ * Omit `apiKey` (or pass `undefined`/`null`/`''`) to use the **keyless
2522
+ * publishable path**: no `Authorization` header is sent and the grant is
2523
+ * authorized by the publishable `shop` id alone. Passing a key uses the
2524
+ * secret (sk_) path.
2525
+ *
2521
2526
  * @example
2522
2527
  * // your backend route: POST /api/realtime/grant
2523
2528
  * const grant = await mintRealtimeGrant({ apiKey: process.env.SNOWCONE_API_KEY!, shop });
2524
2529
  * return Response.json(grant);
2530
+ *
2531
+ * @example
2532
+ * // keyless publishable path — quick trials, no API key:
2533
+ * const grant = await mintRealtimeGrant({ shop });
2525
2534
  */
2526
2535
  declare function mintRealtimeGrant(opts: {
2527
- /** Secret API key (sk_) with the `mockups:realtime` or `mockups` scope. */
2528
- apiKey: string;
2536
+ /**
2537
+ * Secret API key (sk_) with the `mockups:realtime` or `mockups` scope.
2538
+ * Omit (or pass `undefined`/`null`/`''`) for the keyless publishable path.
2539
+ */
2540
+ apiKey?: string | null;
2529
2541
  /** Target shop id (= shop.id). Must belong to the key's organization. */
2530
2542
  shop: string;
2531
2543
  /** Backend base URL. Defaults to {@link DEFAULT_GRANT_BASE}. */
@@ -2615,6 +2627,23 @@ interface RenderSessionOptions {
2615
2627
  getToken?: () => Promise<RealtimeGrant>;
2616
2628
  /** Override fetch (used with `grantUrl`). */
2617
2629
  fetch?: typeof fetch;
2630
+ /**
2631
+ * Watchdog for silent render hangs. After a render is dispatched (via
2632
+ * {@link RenderSession.renderState} or {@link RenderSession.renderSavedState}),
2633
+ * if NO mockup arrives within this many milliseconds the session surfaces a
2634
+ * timeout through {@link RenderSession.onError} — turning an otherwise-silent
2635
+ * infinite hang into an actionable error.
2636
+ *
2637
+ * The classic trigger is a product with a color/variant axis where
2638
+ * {@link RenderProduct.variantId} is omitted: the server waits for a color
2639
+ * blob that never arrives, so it neither renders nor errors. The watchdog
2640
+ * makes that visible.
2641
+ *
2642
+ * Defaults to `30000` (30s). A value of `0` (or negative) DISABLES the
2643
+ * watchdog entirely. A successful mockup always cancels the pending watchdog,
2644
+ * and each new render re-arms it.
2645
+ */
2646
+ renderTimeoutMs?: number;
2618
2647
  }
2619
2648
  declare class RenderSession {
2620
2649
  private readonly svc;
@@ -2623,6 +2652,8 @@ declare class RenderSession {
2623
2652
  private mockupCb;
2624
2653
  private errorCb;
2625
2654
  private ready;
2655
+ private readonly renderTimeoutMs;
2656
+ private watchdog;
2626
2657
  constructor(opts: RenderSessionOptions);
2627
2658
  /** Register a callback fired whenever rendered mockup URLs update. */
2628
2659
  onMockups(cb: (results: MockupResult[]) => void): this;
@@ -2666,6 +2697,15 @@ declare class RenderSession {
2666
2697
  getMockups(): MockupResult[];
2667
2698
  /** Close the WebSocket and stop auto-renew. */
2668
2699
  close(): void;
2700
+ /**
2701
+ * (Re)arm the render watchdog. Clears any prior timer and, unless disabled
2702
+ * (`renderTimeoutMs <= 0`), starts a fresh one. If it fires before a mockup
2703
+ * arrives, it surfaces a timeout via the error callback — the same channel as
2704
+ * server/transport errors — with a hint at the most common cause.
2705
+ */
2706
+ private armWatchdog;
2707
+ /** Cancel the pending render watchdog, if any. */
2708
+ private clearWatchdog;
2669
2709
  /** Escape hatch: the underlying low-level service. */
2670
2710
  get service(): RealtimeMockupService;
2671
2711
  private toConfig;
package/dist/index.js CHANGED
@@ -4653,12 +4653,11 @@ function createSvelteComponent(descriptor, svelte) {
4653
4653
  var DEFAULT_GRANT_BASE = "https://api.snowcone.app";
4654
4654
  async function mintRealtimeGrant(opts) {
4655
4655
  const f = opts.fetch ?? globalThis.fetch;
4656
+ const headers = { "Content-Type": "application/json" };
4657
+ if (opts.apiKey) headers.Authorization = `Bearer ${opts.apiKey}`;
4656
4658
  const res = await f(`${opts.base ?? DEFAULT_GRANT_BASE}/realtime/grant`, {
4657
4659
  method: "POST",
4658
- headers: {
4659
- "Content-Type": "application/json",
4660
- Authorization: `Bearer ${opts.apiKey}`
4661
- },
4660
+ headers,
4662
4661
  body: JSON.stringify({ shop: opts.shop })
4663
4662
  });
4664
4663
  if (!res.ok) {
@@ -4679,6 +4678,7 @@ async function fetchRealtimeGrant(grantUrl, shop, fetchImpl) {
4679
4678
  }
4680
4679
 
4681
4680
  // src/realtime/session.ts
4681
+ var DEFAULT_RENDER_TIMEOUT_MS = 3e4;
4682
4682
  var RenderSession = class {
4683
4683
  svc;
4684
4684
  opts;
@@ -4686,6 +4686,8 @@ var RenderSession = class {
4686
4686
  mockupCb = null;
4687
4687
  errorCb = null;
4688
4688
  ready = null;
4689
+ renderTimeoutMs;
4690
+ watchdog = null;
4689
4691
  constructor(opts) {
4690
4692
  if (!opts.shop) throw new Error("RenderSession: `shop` is required");
4691
4693
  if (!opts.getToken && !opts.grantUrl) {
@@ -4693,6 +4695,7 @@ var RenderSession = class {
4693
4695
  }
4694
4696
  this.opts = opts;
4695
4697
  this.product = opts.product ?? null;
4698
+ this.renderTimeoutMs = opts.renderTimeoutMs ?? DEFAULT_RENDER_TIMEOUT_MS;
4696
4699
  this.svc = new RealtimeMockupService(opts.wsUrl ?? REALTIME_WS_URL);
4697
4700
  const getToken = opts.getToken ?? (() => fetchRealtimeGrant(opts.grantUrl, opts.shop, opts.fetch));
4698
4701
  this.svc.setTokenProvider(getToken);
@@ -4737,9 +4740,11 @@ var RenderSession = class {
4737
4740
  }
4738
4741
  },
4739
4742
  onMockupRendered: () => {
4743
+ this.clearWatchdog();
4740
4744
  this.mockupCb?.(this.svc.getState().mockupResults);
4741
4745
  },
4742
4746
  onAllMockupsRendered: (results) => {
4747
+ this.clearWatchdog();
4743
4748
  this.mockupCb?.(results);
4744
4749
  },
4745
4750
  onError: (error) => {
@@ -4774,6 +4779,7 @@ var RenderSession = class {
4774
4779
  async renderState(placement, state, throttleMs = 0) {
4775
4780
  await this.connect();
4776
4781
  this.svc.sendCanvasState(placement, state, this.product?.mockupIds.length ?? 1, throttleMs);
4782
+ this.armWatchdog();
4777
4783
  }
4778
4784
  /**
4779
4785
  * Render a SAVED canvas by reference (ADR-0079 Phase 4). The server resolves
@@ -4785,6 +4791,7 @@ var RenderSession = class {
4785
4791
  async renderSavedState(placement, stateId) {
4786
4792
  await this.connect();
4787
4793
  this.svc.sendCanvasStateRef(placement, stateId);
4794
+ this.armWatchdog();
4788
4795
  }
4789
4796
  /** Update only the mockup ids to render (reuses the current state). */
4790
4797
  updateMockupIds(mockupIds) {
@@ -4797,9 +4804,33 @@ var RenderSession = class {
4797
4804
  }
4798
4805
  /** Close the WebSocket and stop auto-renew. */
4799
4806
  close() {
4807
+ this.clearWatchdog();
4800
4808
  this.svc.disconnect();
4801
4809
  this.ready = null;
4802
4810
  }
4811
+ /**
4812
+ * (Re)arm the render watchdog. Clears any prior timer and, unless disabled
4813
+ * (`renderTimeoutMs <= 0`), starts a fresh one. If it fires before a mockup
4814
+ * arrives, it surfaces a timeout via the error callback — the same channel as
4815
+ * server/transport errors — with a hint at the most common cause.
4816
+ */
4817
+ armWatchdog() {
4818
+ this.clearWatchdog();
4819
+ if (this.renderTimeoutMs <= 0) return;
4820
+ this.watchdog = setTimeout(() => {
4821
+ this.watchdog = null;
4822
+ this.errorCb?.(
4823
+ `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)`
4824
+ );
4825
+ }, this.renderTimeoutMs);
4826
+ }
4827
+ /** Cancel the pending render watchdog, if any. */
4828
+ clearWatchdog() {
4829
+ if (this.watchdog) {
4830
+ clearTimeout(this.watchdog);
4831
+ this.watchdog = null;
4832
+ }
4833
+ }
4803
4834
  /** Escape hatch: the underlying low-level service. */
4804
4835
  get service() {
4805
4836
  return this.svc;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowcone-app/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Snowcone SDK for product mockups and print-on-demand",
5
5
  "keywords": [
6
6
  "merch",