@snowcone-app/sdk 0.1.11 → 0.1.13

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/dist/react.cjs CHANGED
@@ -45,7 +45,9 @@ var RealtimeMockupService = class {
45
45
  logs = [];
46
46
  lastError = null;
47
47
  callbacks = {};
48
+ lastBlobSentAt = 0;
48
49
  canvasBlobs = /* @__PURE__ */ new Map();
50
+ canvasStates = /* @__PURE__ */ new Map();
49
51
  colors = /* @__PURE__ */ new Map();
50
52
  lastSendTime = {};
51
53
  throttleTimeouts = {};
@@ -55,11 +57,24 @@ var RealtimeMockupService = class {
55
57
  lastSentVersion = 0;
56
58
  // Track latest sent version per placement to detect stale responses
57
59
  latestSentVersionByPlacement = {};
60
+ // Track latest accepted (displayed) version per placement — only drop results
61
+ // older than what we've already shown, not older than what we've sent.
62
+ // This prevents the "version racing" problem during drag where sent versions
63
+ // advance faster than the server can render.
64
+ latestAcceptedVersionByPlacement = {};
58
65
  // Feature flag: server now supports version in blob message
59
66
  sendVersionInBlob = true;
67
+ // Session-grant auth: when set, connect() fetches a short-lived token, opens
68
+ // the WS with `?token=`, and renews ~15s before expiry via a `renew` message.
69
+ tokenProvider;
70
+ renewTimer = null;
60
71
  setCallbacks(callbacks) {
61
72
  this.callbacks = callbacks;
62
73
  }
74
+ /** Provide a grant fetcher to authorize the session (per-shop, renewable). */
75
+ setTokenProvider(fn) {
76
+ this.tokenProvider = fn;
77
+ }
63
78
  getState() {
64
79
  return {
65
80
  isConnected: this.ws?.readyState === WebSocket.OPEN,
@@ -82,9 +97,24 @@ var RealtimeMockupService = class {
82
97
  if (this.ws?.readyState === WebSocket.OPEN) {
83
98
  return;
84
99
  }
100
+ if (this.tokenProvider) {
101
+ this.tokenProvider().then((grant) => {
102
+ this.openSocket(grant.token);
103
+ this.scheduleRenew(grant.expiresAt);
104
+ }).catch((err) => {
105
+ this.addLog(`Failed to obtain realtime grant: ${err}`);
106
+ this.status = "Disconnected";
107
+ });
108
+ return;
109
+ }
110
+ this.openSocket();
111
+ }
112
+ openSocket(token) {
113
+ const url = token ? `${this.wsUrl}${this.wsUrl.includes("?") ? "&" : "?"}token=${encodeURIComponent(token)}` : this.wsUrl;
85
114
  this.addLog(`Connecting to ${this.wsUrl}...`);
86
- this.ws = new WebSocket(this.wsUrl);
115
+ this.ws = new WebSocket(url);
87
116
  this.ws.onopen = () => {
117
+ console.log(`[WS] connection OPENED to ${this.wsUrl}`);
88
118
  this.addLog("WebSocket connection opened");
89
119
  this.status = "Connected";
90
120
  };
@@ -98,11 +128,13 @@ var RealtimeMockupService = class {
98
128
  }
99
129
  };
100
130
  this.ws.onclose = (event) => {
131
+ console.log(`[WS] connection CLOSED (code: ${event.code}, reason: "${event.reason}", wasClean: ${event.wasClean})`);
101
132
  this.addLog(`WebSocket connection closed (code: ${event.code})`);
102
133
  this.status = "Disconnected";
103
134
  this.sessionId = null;
104
135
  this.isConfigured = false;
105
136
  this.configSent = false;
137
+ this.clearRenew();
106
138
  this.callbacks.onDisconnected?.();
107
139
  };
108
140
  this.ws.onerror = (error) => {
@@ -110,6 +142,29 @@ var RealtimeMockupService = class {
110
142
  this.status = "Disconnected";
111
143
  };
112
144
  }
145
+ scheduleRenew(expiresAt) {
146
+ this.clearRenew();
147
+ if (!this.tokenProvider) return;
148
+ const ms = Math.max(1e3, expiresAt * 1e3 - Date.now() - 15e3);
149
+ this.renewTimer = setTimeout(() => void this.renew(), ms);
150
+ }
151
+ clearRenew() {
152
+ if (this.renewTimer) {
153
+ clearTimeout(this.renewTimer);
154
+ this.renewTimer = null;
155
+ }
156
+ }
157
+ async renew() {
158
+ if (!this.tokenProvider || this.ws?.readyState !== WebSocket.OPEN) return;
159
+ try {
160
+ const grant = await this.tokenProvider();
161
+ this.ws.send(JSON.stringify({ type: "renew", token: grant.token }));
162
+ this.addLog("\u{1F504} Renewed realtime session token", "sent");
163
+ this.scheduleRenew(grant.expiresAt);
164
+ } catch (err) {
165
+ this.addLog(`Failed to renew realtime grant: ${err}`);
166
+ }
167
+ }
113
168
  handleMessage(data) {
114
169
  switch (data.type) {
115
170
  case "connected":
@@ -161,15 +216,18 @@ var RealtimeMockupService = class {
161
216
  this.addLog("\u{1F3A8} Mockup rendering has started...");
162
217
  break;
163
218
  case "mockup_rendered":
219
+ console.log(`[WS] mockup_rendered received: mockupId=${data.mockupId} hasImageUrl=${!!data.imageUrl} v=${data.requestVersion} placement="${data.placement}" renderMs=${data.renderMs}`);
164
220
  if (data.imageUrl && data.mockupId) {
165
221
  const responseVersion = data.requestVersion;
166
222
  const responsePlacement = data.placement;
167
223
  if (responseVersion !== void 0 && responsePlacement) {
168
- const latestVersion = this.latestSentVersionByPlacement[responsePlacement];
169
- if (latestVersion !== void 0 && responseVersion < latestVersion) {
170
- this.addLog(`\u23ED\uFE0F Ignoring stale mockup v${responseVersion} for "${responsePlacement}" (latest sent: v${latestVersion})`);
224
+ const lastAccepted = this.latestAcceptedVersionByPlacement[responsePlacement];
225
+ if (lastAccepted !== void 0 && responseVersion < lastAccepted) {
226
+ console.log(`[WS] STALE mockup dropped: v${responseVersion} for "${responsePlacement}" (already displayed: v${lastAccepted}, latest sent: v${this.latestSentVersionByPlacement[responsePlacement]})`);
227
+ this.addLog(`\u23ED\uFE0F Ignoring stale mockup v${responseVersion} for "${responsePlacement}" (displayed: v${lastAccepted})`);
171
228
  break;
172
229
  }
230
+ this.latestAcceptedVersionByPlacement[responsePlacement] = responseVersion;
173
231
  }
174
232
  const mockupResult = {
175
233
  mockupId: data.mockupId,
@@ -177,7 +235,10 @@ var RealtimeMockupService = class {
177
235
  renderUrl: data.renderUrl || data.imageUrl,
178
236
  imageSize: data.imageSize || 0,
179
237
  requestVersion: responseVersion,
180
- placement: responsePlacement
238
+ placement: responsePlacement,
239
+ renderMs: data.renderMs,
240
+ blobToRenderMs: data.blobToRenderMs,
241
+ canvasRenderTiming: data.canvasRenderTiming
181
242
  };
182
243
  const existingIndex = this.mockupResults.findIndex((m) => m.mockupId === data.mockupId);
183
244
  if (existingIndex >= 0) {
@@ -196,14 +257,16 @@ var RealtimeMockupService = class {
196
257
  }
197
258
  break;
198
259
  case "all_mockups_rendered":
260
+ console.log(`[WS] all_mockups_rendered received: ${data.mockups?.length ?? 0} mockups`);
199
261
  if (data.mockups) {
200
262
  const freshMockups = data.mockups.filter((mockup) => {
201
263
  if (mockup.requestVersion !== void 0 && mockup.placement) {
202
- const latestVersion = this.latestSentVersionByPlacement[mockup.placement];
203
- if (latestVersion !== void 0 && mockup.requestVersion < latestVersion) {
204
- this.addLog(`\u23ED\uFE0F Filtering stale mockup v${mockup.requestVersion} for "${mockup.placement}" (latest: v${latestVersion})`);
264
+ const lastAccepted = this.latestAcceptedVersionByPlacement[mockup.placement];
265
+ if (lastAccepted !== void 0 && mockup.requestVersion < lastAccepted) {
266
+ this.addLog(`\u23ED\uFE0F Filtering stale mockup v${mockup.requestVersion} for "${mockup.placement}" (displayed: v${lastAccepted})`);
205
267
  return false;
206
268
  }
269
+ this.latestAcceptedVersionByPlacement[mockup.placement] = mockup.requestVersion;
207
270
  }
208
271
  return true;
209
272
  });
@@ -226,6 +289,7 @@ var RealtimeMockupService = class {
226
289
  }
227
290
  }
228
291
  disconnect() {
292
+ this.clearRenew();
229
293
  if (this.ws) {
230
294
  this.ws.close();
231
295
  this.ws = null;
@@ -260,6 +324,7 @@ var RealtimeMockupService = class {
260
324
  this.isConfigured = false;
261
325
  this.mockupResults = [];
262
326
  this.canvasBlobs.clear();
327
+ this.canvasStates.clear();
263
328
  this.colors.clear();
264
329
  this.lastSendTime = {};
265
330
  Object.values(this.throttleTimeouts).forEach((timeout) => clearTimeout(timeout));
@@ -267,6 +332,7 @@ var RealtimeMockupService = class {
267
332
  this.requestVersion = 0;
268
333
  this.lastSentVersion = 0;
269
334
  this.latestSentVersionByPlacement = {};
335
+ this.latestAcceptedVersionByPlacement = {};
270
336
  this.addLog("\u{1F9F9} Cleared all cached canvas/color data for new product");
271
337
  }
272
338
  }
@@ -302,6 +368,30 @@ var RealtimeMockupService = class {
302
368
  this.addLog(`\u{1F3AF} Updating mockupIds to: [${mockupIds.join(", ")}]`);
303
369
  return this.sendConfig(updatedConfig);
304
370
  }
371
+ /**
372
+ * Update render width without changing other config.
373
+ * Used for low-res preview during rapid edits (e.g., 600 while dragging, 1200 on release).
374
+ * Preserves blobs since product/variant don't change.
375
+ */
376
+ updateWidth(width) {
377
+ if (!this.config) {
378
+ this.addLog("Cannot update width: no config set");
379
+ return false;
380
+ }
381
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
382
+ this.addLog("Cannot update width: WebSocket not connected");
383
+ return false;
384
+ }
385
+ if (this.config.width === width) {
386
+ return false;
387
+ }
388
+ const updatedConfig = {
389
+ ...this.config,
390
+ width
391
+ };
392
+ this.addLog(`\u{1F4D0} Updating render width: ${this.config.width} \u2192 ${width}`);
393
+ return this.sendConfig(updatedConfig);
394
+ }
305
395
  /**
306
396
  * Update placementSettings without changing other config.
307
397
  * Used to override scaleMode when canvas editor is active.
@@ -324,6 +414,8 @@ var RealtimeMockupService = class {
324
414
  sendCanvasBlob(placement, blob, mockupCount = 1, baseThrottleMs = 1e3, notifyCallback = true) {
325
415
  this.canvasBlobs.set(placement, blob);
326
416
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
417
+ const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
418
+ console.log(`[WS] sendCanvasBlob BLOCKED for "${placement}" (${blob.size}B): ${reason} (cached for later)`);
327
419
  return false;
328
420
  }
329
421
  if (baseThrottleMs <= 0) {
@@ -331,16 +423,25 @@ var RealtimeMockupService = class {
331
423
  this.lastSendTime[placement] = Date.now();
332
424
  return true;
333
425
  }
334
- const throttleMs = baseThrottleMs * mockupCount;
335
- const now = Date.now();
336
- const lastSendTime = this.lastSendTime[placement] || 0;
337
- const timeSinceLastSend = now - lastSendTime;
338
- const hasNeverSent = lastSendTime === 0;
339
- if (hasNeverSent || timeSinceLastSend >= throttleMs) {
340
- this.sendBlobImmediately(placement, blob, notifyCallback);
341
- this.lastSendTime[placement] = now;
342
- } else if (!this.throttleTimeouts[placement]) {
343
- const delayTime = throttleMs - timeSinceLastSend;
426
+ const debounceMs = baseThrottleMs * mockupCount;
427
+ const lastSendTime = this.lastSendTime[placement];
428
+ const timeSinceLastSend = lastSendTime ? Date.now() - lastSendTime : 0;
429
+ const isActiveInteraction = lastSendTime && timeSinceLastSend < debounceMs * 3;
430
+ if (isActiveInteraction && timeSinceLastSend >= debounceMs) {
431
+ if (this.throttleTimeouts[placement]) {
432
+ clearTimeout(this.throttleTimeouts[placement]);
433
+ delete this.throttleTimeouts[placement];
434
+ }
435
+ const latestBlob = this.canvasBlobs.get(placement);
436
+ if (latestBlob && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
437
+ this.sendBlobImmediately(placement, latestBlob, notifyCallback);
438
+ this.lastSendTime[placement] = Date.now();
439
+ }
440
+ } else {
441
+ if (this.throttleTimeouts[placement]) {
442
+ clearTimeout(this.throttleTimeouts[placement]);
443
+ }
444
+ const delayTime = isActiveInteraction ? debounceMs - timeSinceLastSend : debounceMs;
344
445
  this.throttleTimeouts[placement] = setTimeout(() => {
345
446
  const latestBlob = this.canvasBlobs.get(placement);
346
447
  if (latestBlob && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
@@ -352,6 +453,72 @@ var RealtimeMockupService = class {
352
453
  }
353
454
  return true;
354
455
  }
456
+ /**
457
+ * Send canvas state JSON for server-side rendering.
458
+ * Alternative to sendCanvasBlob — the server renders the PNG instead of the client.
459
+ */
460
+ sendCanvasState(placement, state, mockupCount = 1, baseThrottleMs = 1e3) {
461
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
462
+ const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
463
+ console.log(`[WS] sendCanvasState BLOCKED for "${placement}": ${reason}`);
464
+ return false;
465
+ }
466
+ this.canvasStates.set(placement, state);
467
+ if (baseThrottleMs <= 0) {
468
+ this.sendCanvasStateImmediately(placement, state);
469
+ this.lastSendTime[placement] = Date.now();
470
+ return true;
471
+ }
472
+ const debounceMs = baseThrottleMs * mockupCount;
473
+ const lastSendTime = this.lastSendTime[placement];
474
+ const timeSinceLastSend = lastSendTime ? Date.now() - lastSendTime : 0;
475
+ const isActiveInteraction = lastSendTime && timeSinceLastSend < debounceMs * 3;
476
+ if (isActiveInteraction && timeSinceLastSend >= debounceMs) {
477
+ if (this.throttleTimeouts[placement]) {
478
+ clearTimeout(this.throttleTimeouts[placement]);
479
+ delete this.throttleTimeouts[placement];
480
+ }
481
+ const latestState = this.canvasStates.get(placement);
482
+ if (latestState && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
483
+ console.log(`[WS] sendCanvasState "${placement}": max-wait flush (${timeSinceLastSend}ms since last)`);
484
+ this.sendCanvasStateImmediately(placement, latestState);
485
+ this.lastSendTime[placement] = Date.now();
486
+ }
487
+ } else {
488
+ if (this.throttleTimeouts[placement]) {
489
+ clearTimeout(this.throttleTimeouts[placement]);
490
+ }
491
+ const delayTime = isActiveInteraction ? debounceMs - timeSinceLastSend : debounceMs;
492
+ this.throttleTimeouts[placement] = setTimeout(() => {
493
+ const latestState = this.canvasStates.get(placement);
494
+ if (latestState && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
495
+ console.log(`[WS] sendCanvasState "${placement}": debounce firing (${debounceMs}ms)`);
496
+ this.sendCanvasStateImmediately(placement, latestState);
497
+ this.lastSendTime[placement] = Date.now();
498
+ }
499
+ delete this.throttleTimeouts[placement];
500
+ }, delayTime);
501
+ }
502
+ return true;
503
+ }
504
+ sendCanvasStateImmediately(placement, state) {
505
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
506
+ console.log(`[WS] sendCanvasStateImmediately ABORTED: ws not open`);
507
+ return;
508
+ }
509
+ this.lastSentVersion = ++this.requestVersion;
510
+ this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
511
+ const message = JSON.stringify({
512
+ type: "canvas_state",
513
+ placement,
514
+ version: this.lastSentVersion,
515
+ state
516
+ });
517
+ this.ws.send(message);
518
+ this.lastBlobSentAt = Date.now();
519
+ this.addLog(`Sent canvas state for "${placement}" (${(message.length / 1024).toFixed(1)}KB, v${this.lastSentVersion})`, "sent");
520
+ this.callbacks.onBlobSent?.(placement);
521
+ }
355
522
  sendBlobImmediately(placement, blob, notifyCallback = true) {
356
523
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
357
524
  this.lastSentVersion = ++this.requestVersion;
@@ -377,6 +544,7 @@ ${versionToSend}
377
544
  combined.set(imageBytes, headerBytes.length);
378
545
  }
379
546
  this.ws.send(combined.buffer);
547
+ this.lastBlobSentAt = Date.now();
380
548
  this.addLog(`Sent canvas blob for placement "${placement}" (${imageBytes.length} bytes, v${versionToSend})`, "sent");
381
549
  if (notifyCallback) {
382
550
  this.callbacks.onBlobSent?.(placement);
@@ -515,6 +683,8 @@ ${versionToSend}
515
683
  // src/realtime/react.ts
516
684
  function useRealtimeMockup(options = {}) {
517
685
  const serviceRef = (0, import_react.useRef)(null);
686
+ const getTokenRef = (0, import_react.useRef)(options.getToken);
687
+ getTokenRef.current = options.getToken;
518
688
  const [state, setState] = (0, import_react.useState)({
519
689
  isConnected: false,
520
690
  sessionId: null,
@@ -547,11 +717,13 @@ function useRealtimeMockup(options = {}) {
547
717
  options.onBlobSent?.(placement);
548
718
  },
549
719
  onMockupRendered: (result) => {
550
- setState(service.getState());
720
+ console.log(`[useRealtimeMockup] onMockupRendered: mockupId=${result.mockupId} hasImageUrl=${!!result.imageUrl}`);
551
721
  options.onMockupRendered?.(result);
552
722
  },
553
723
  onAllMockupsRendered: (results) => {
554
- setState(service.getState());
724
+ const newState = service.getState();
725
+ console.log(`[useRealtimeMockup] onAllMockupsRendered: ${results.length} results, state.mockupResults=${newState.mockupResults.length}`);
726
+ setState(newState);
555
727
  options.onAllMockupsRendered?.(results);
556
728
  },
557
729
  onError: (error) => {
@@ -561,6 +733,9 @@ function useRealtimeMockup(options = {}) {
561
733
  // REMOVED: onLog callback was causing setState on every log message,
562
734
  // which triggered re-renders during canvas drag operations
563
735
  });
736
+ if (getTokenRef.current) {
737
+ service.setTokenProvider(() => getTokenRef.current());
738
+ }
564
739
  return () => {
565
740
  service.disconnect();
566
741
  };
@@ -580,9 +755,15 @@ function useRealtimeMockup(options = {}) {
580
755
  const updatePlacementSettings = (0, import_react.useCallback)((settings) => {
581
756
  return serviceRef.current?.updatePlacementSettings(settings) || false;
582
757
  }, []);
758
+ const updateWidth = (0, import_react.useCallback)((width) => {
759
+ return serviceRef.current?.updateWidth(width) || false;
760
+ }, []);
583
761
  const sendCanvasBlob = (0, import_react.useCallback)((placement, blob, mockupCount, baseThrottleMs) => {
584
762
  return serviceRef.current?.sendCanvasBlob(placement, blob, mockupCount, baseThrottleMs) || false;
585
763
  }, []);
764
+ const sendCanvasState = (0, import_react.useCallback)((placement, state2, mockupCount, baseThrottleMs) => {
765
+ return serviceRef.current?.sendCanvasState(placement, state2, mockupCount, baseThrottleMs) || false;
766
+ }, []);
586
767
  const sendColorBlob = (0, import_react.useCallback)((placement, hexColor) => {
587
768
  return serviceRef.current?.sendColorBlob(placement, hexColor) || false;
588
769
  }, []);
@@ -616,7 +797,9 @@ function useRealtimeMockup(options = {}) {
616
797
  sendConfig,
617
798
  updateMockupIds,
618
799
  updatePlacementSettings,
800
+ updateWidth,
619
801
  sendCanvasBlob,
802
+ sendCanvasState,
620
803
  sendColorBlob,
621
804
  createEmptyCanvasBlob,
622
805
  sendInitialEmptyCanvases,
package/dist/react.d.cts CHANGED
@@ -1,7 +1,17 @@
1
- import { M as MockupResult, W as WebSocketConfig, u 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-B8_XAwWx.cjs';
1
+ import { M as MockupResult, W as WebSocketConfig, u 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-Dum3OooZ.cjs';
2
2
 
3
3
  interface UseRealtimeMockupOptions {
4
4
  wsUrl?: string;
5
+ /**
6
+ * Optional grant fetcher. When provided, the session authorizes via a
7
+ * short-lived token (opened on the WS URL, renewed before expiry) instead of
8
+ * a static seal. Identity may change between renders — it's read via a ref,
9
+ * so it never re-opens the socket.
10
+ */
11
+ getToken?: () => Promise<{
12
+ token: string;
13
+ expiresAt: number;
14
+ }>;
5
15
  onConnected?: (sessionId: string) => void;
6
16
  onDisconnected?: () => void;
7
17
  onConfigReceived?: () => void;
@@ -19,7 +29,9 @@ declare function useRealtimeMockup(options?: UseRealtimeMockupOptions): {
19
29
  sendConfig: (config: WebSocketConfig) => boolean;
20
30
  updateMockupIds: (mockupIds: string[]) => boolean;
21
31
  updatePlacementSettings: (settings: Record<string, PlacementSettings>) => boolean;
32
+ updateWidth: (width: number) => boolean;
22
33
  sendCanvasBlob: (placement: string, blob: Blob, mockupCount?: number, baseThrottleMs?: number) => boolean;
34
+ sendCanvasState: (placement: string, state: object, mockupCount?: number, baseThrottleMs?: number) => boolean;
23
35
  sendColorBlob: (placement: string, hexColor: string) => boolean;
24
36
  createEmptyCanvasBlob: (width?: number, height?: number) => Promise<Blob>;
25
37
  sendInitialEmptyCanvases: (placements: Array<{
package/dist/react.d.ts CHANGED
@@ -1,7 +1,17 @@
1
- import { M as MockupResult, W as WebSocketConfig, u 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-B8_XAwWx.js';
1
+ import { M as MockupResult, W as WebSocketConfig, u 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-Dum3OooZ.js';
2
2
 
3
3
  interface UseRealtimeMockupOptions {
4
4
  wsUrl?: string;
5
+ /**
6
+ * Optional grant fetcher. When provided, the session authorizes via a
7
+ * short-lived token (opened on the WS URL, renewed before expiry) instead of
8
+ * a static seal. Identity may change between renders — it's read via a ref,
9
+ * so it never re-opens the socket.
10
+ */
11
+ getToken?: () => Promise<{
12
+ token: string;
13
+ expiresAt: number;
14
+ }>;
5
15
  onConnected?: (sessionId: string) => void;
6
16
  onDisconnected?: () => void;
7
17
  onConfigReceived?: () => void;
@@ -19,7 +29,9 @@ declare function useRealtimeMockup(options?: UseRealtimeMockupOptions): {
19
29
  sendConfig: (config: WebSocketConfig) => boolean;
20
30
  updateMockupIds: (mockupIds: string[]) => boolean;
21
31
  updatePlacementSettings: (settings: Record<string, PlacementSettings>) => boolean;
32
+ updateWidth: (width: number) => boolean;
22
33
  sendCanvasBlob: (placement: string, blob: Blob, mockupCount?: number, baseThrottleMs?: number) => boolean;
34
+ sendCanvasState: (placement: string, state: object, mockupCount?: number, baseThrottleMs?: number) => boolean;
23
35
  sendColorBlob: (placement: string, hexColor: string) => boolean;
24
36
  createEmptyCanvasBlob: (width?: number, height?: number) => Promise<Blob>;
25
37
  sendInitialEmptyCanvases: (placements: Array<{
package/dist/react.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  RealtimeMockupService
3
- } from "./chunk-UJFJ7REN.js";
3
+ } from "./chunk-6MV7TDTM.js";
4
4
 
5
5
  // src/realtime/react.ts
6
6
  import { useCallback, useEffect, useRef, useState } from "react";
7
7
  function useRealtimeMockup(options = {}) {
8
8
  const serviceRef = useRef(null);
9
+ const getTokenRef = useRef(options.getToken);
10
+ getTokenRef.current = options.getToken;
9
11
  const [state, setState] = useState({
10
12
  isConnected: false,
11
13
  sessionId: null,
@@ -38,11 +40,13 @@ function useRealtimeMockup(options = {}) {
38
40
  options.onBlobSent?.(placement);
39
41
  },
40
42
  onMockupRendered: (result) => {
41
- setState(service.getState());
43
+ console.log(`[useRealtimeMockup] onMockupRendered: mockupId=${result.mockupId} hasImageUrl=${!!result.imageUrl}`);
42
44
  options.onMockupRendered?.(result);
43
45
  },
44
46
  onAllMockupsRendered: (results) => {
45
- setState(service.getState());
47
+ const newState = service.getState();
48
+ console.log(`[useRealtimeMockup] onAllMockupsRendered: ${results.length} results, state.mockupResults=${newState.mockupResults.length}`);
49
+ setState(newState);
46
50
  options.onAllMockupsRendered?.(results);
47
51
  },
48
52
  onError: (error) => {
@@ -52,6 +56,9 @@ function useRealtimeMockup(options = {}) {
52
56
  // REMOVED: onLog callback was causing setState on every log message,
53
57
  // which triggered re-renders during canvas drag operations
54
58
  });
59
+ if (getTokenRef.current) {
60
+ service.setTokenProvider(() => getTokenRef.current());
61
+ }
55
62
  return () => {
56
63
  service.disconnect();
57
64
  };
@@ -71,9 +78,15 @@ function useRealtimeMockup(options = {}) {
71
78
  const updatePlacementSettings = useCallback((settings) => {
72
79
  return serviceRef.current?.updatePlacementSettings(settings) || false;
73
80
  }, []);
81
+ const updateWidth = useCallback((width) => {
82
+ return serviceRef.current?.updateWidth(width) || false;
83
+ }, []);
74
84
  const sendCanvasBlob = useCallback((placement, blob, mockupCount, baseThrottleMs) => {
75
85
  return serviceRef.current?.sendCanvasBlob(placement, blob, mockupCount, baseThrottleMs) || false;
76
86
  }, []);
87
+ const sendCanvasState = useCallback((placement, state2, mockupCount, baseThrottleMs) => {
88
+ return serviceRef.current?.sendCanvasState(placement, state2, mockupCount, baseThrottleMs) || false;
89
+ }, []);
77
90
  const sendColorBlob = useCallback((placement, hexColor) => {
78
91
  return serviceRef.current?.sendColorBlob(placement, hexColor) || false;
79
92
  }, []);
@@ -107,7 +120,9 @@ function useRealtimeMockup(options = {}) {
107
120
  sendConfig,
108
121
  updateMockupIds,
109
122
  updatePlacementSettings,
123
+ updateWidth,
110
124
  sendCanvasBlob,
125
+ sendCanvasState,
111
126
  sendColorBlob,
112
127
  createEmptyCanvasBlob,
113
128
  sendInitialEmptyCanvases,
@@ -212,8 +212,13 @@ interface WebSocketConfig {
212
212
  productId: string;
213
213
  mockupIds: string[];
214
214
  variantId: string;
215
- accountId: string;
216
- sig: string;
215
+ shop: string;
216
+ /**
217
+ * Legacy URL seal. Only sent on the unauthenticated (shim) path. When a
218
+ * tokenProvider is set, the session is authorized by the grant token on the
219
+ * WS URL and the server self-signs the render, so no seal is needed.
220
+ */
221
+ seal?: string;
217
222
  width: number;
218
223
  /** Aspect ratio for mockup rendering (e.g. '2:3' for portrait). Server default: '16:9' */
219
224
  ar?: string;
@@ -228,6 +233,12 @@ interface MockupResult {
228
233
  requestVersion?: number;
229
234
  /** The placement this mockup corresponds to */
230
235
  placement?: string;
236
+ /** Server-side PIXI render time in ms */
237
+ renderMs?: number;
238
+ /** Time from blob received to render start in ms */
239
+ blobToRenderMs?: number;
240
+ /** Server-side canvas render timing breakdown (present when canvas_state was used) */
241
+ canvasRenderTiming?: any;
231
242
  }
232
243
  interface WebSocketMessage {
233
244
  type: string;
@@ -242,6 +253,21 @@ interface WebSocketMessage {
242
253
  missingPlacements?: string[];
243
254
  /** The request version this response corresponds to (for stale detection) */
244
255
  requestVersion?: number;
256
+ /** Server-side PIXI render time in ms */
257
+ renderMs?: number;
258
+ /** Time from blob received to render start in ms */
259
+ blobToRenderMs?: number;
260
+ /** Server-side canvas render timing (only present when canvas_state was used) */
261
+ canvasRenderTiming?: {
262
+ poolAcquireMs: number;
263
+ imageFetchMs: number;
264
+ fontLoadMs: number;
265
+ canvasRenderMs: number;
266
+ blobConvertMs: number;
267
+ base64Ms: number;
268
+ totalWorkerMs: number;
269
+ totalMs: number;
270
+ };
245
271
  }
246
272
  interface RealtimeMockupState {
247
273
  isConnected: boolean;
@@ -277,19 +303,33 @@ declare class RealtimeMockupService {
277
303
  private logs;
278
304
  private lastError;
279
305
  private callbacks;
306
+ private lastBlobSentAt;
280
307
  private canvasBlobs;
308
+ private canvasStates;
281
309
  private colors;
282
310
  private lastSendTime;
283
311
  private throttleTimeouts;
284
312
  private requestVersion;
285
313
  private lastSentVersion;
286
314
  private latestSentVersionByPlacement;
315
+ private latestAcceptedVersionByPlacement;
287
316
  private sendVersionInBlob;
317
+ private tokenProvider?;
318
+ private renewTimer;
288
319
  constructor(wsUrl?: string);
289
320
  setCallbacks(callbacks: RealtimeMockupCallbacks): void;
321
+ /** Provide a grant fetcher to authorize the session (per-shop, renewable). */
322
+ setTokenProvider(fn: () => Promise<{
323
+ token: string;
324
+ expiresAt: number;
325
+ }>): void;
290
326
  getState(): RealtimeMockupState;
291
327
  private addLog;
292
328
  connect(): void;
329
+ private openSocket;
330
+ private scheduleRenew;
331
+ private clearRenew;
332
+ private renew;
293
333
  private handleMessage;
294
334
  disconnect(): void;
295
335
  sendConfig(config: WebSocketConfig): boolean;
@@ -299,12 +339,24 @@ declare class RealtimeMockupService {
299
339
  * This is the preferred method for priority-based rendering.
300
340
  */
301
341
  updateMockupIds(mockupIds: string[]): boolean;
342
+ /**
343
+ * Update render width without changing other config.
344
+ * Used for low-res preview during rapid edits (e.g., 600 while dragging, 1200 on release).
345
+ * Preserves blobs since product/variant don't change.
346
+ */
347
+ updateWidth(width: number): boolean;
302
348
  /**
303
349
  * Update placementSettings without changing other config.
304
350
  * Used to override scaleMode when canvas editor is active.
305
351
  */
306
352
  updatePlacementSettings(placementSettings: Record<string, PlacementSettings>): boolean;
307
353
  sendCanvasBlob(placement: string, blob: Blob, mockupCount?: number, baseThrottleMs?: number, notifyCallback?: boolean): boolean;
354
+ /**
355
+ * Send canvas state JSON for server-side rendering.
356
+ * Alternative to sendCanvasBlob — the server renders the PNG instead of the client.
357
+ */
358
+ sendCanvasState(placement: string, state: object, mockupCount?: number, baseThrottleMs?: number): boolean;
359
+ private sendCanvasStateImmediately;
308
360
  private sendBlobImmediately;
309
361
  /**
310
362
  * Enable sending version number in blob messages.
@@ -333,4 +385,4 @@ declare class RealtimeMockupService {
333
385
  clearMockups(): void;
334
386
  }
335
387
 
336
- export { AdapterRegistry as A, type Combination as C, type EventHandler as E, type FrameworkAdapter as F, type MockupResult as M, type OptionAttribute as O, type ProductComponentContext as P, type RenderResult as R, type WebSocketConfig as W, type OptionSelection as a, type ComponentProps as b, type ComponentDescriptor as c, type ComponentState as d, type ComponentContext as e, type ComponentLifecycleHooks as f, type FrameworkUtilities as g, createComponent as h, defineComponent as i, adapterRegistry as j, RealtimeMockupService as k, type WebSocketMessage as l, type RealtimeMockupState as m, type RealtimeMockupCallbacks as n, computeDisabledChoices as o, deriveDefaultSelection as p, getPricePreview as q, resolveBestCombination as r, findBestCombination as s, isOptionAvailable as t, type PlacementSettings as u };
388
+ export { AdapterRegistry as A, type Combination as C, type EventHandler as E, type FrameworkAdapter as F, type MockupResult as M, type OptionAttribute as O, type ProductComponentContext as P, type RealtimeMockupCallbacks as R, type WebSocketConfig as W, type OptionSelection as a, type ComponentProps as b, type ComponentDescriptor as c, type ComponentState as d, type ComponentContext as e, type ComponentLifecycleHooks as f, type FrameworkUtilities as g, RealtimeMockupService as h, type RealtimeMockupState as i, type RenderResult as j, type WebSocketMessage as k, adapterRegistry as l, computeDisabledChoices as m, createComponent as n, defineComponent as o, deriveDefaultSelection as p, findBestCombination as q, getPricePreview as r, isOptionAvailable as s, resolveBestCombination as t, type PlacementSettings as u };