@snowcone-app/sdk 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/{chunk-IMJKV4YO.js → chunk-YTOTCNEW.js} +64 -6
- package/dist/index.cjs +88 -9
- package/dist/index.d.cts +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +25 -4
- package/dist/react.cjs +64 -6
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{websocket-DTRStLRH.d.cts → websocket-hXpNcyEn.d.cts} +8 -0
- package/dist/{websocket-DTRStLRH.d.ts → websocket-hXpNcyEn.d.ts} +8 -0
- package/package.json +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#304](https://github.com/snowcone-app/snowcone-monorepo/pull/304) [`2700cd9`](https://github.com/snowcone-app/snowcone-monorepo/commit/2700cd93831c50a1a5d496b04d40fd0c1a89b641) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Realtime: the first render no longer races the WebSocket handshake. `sendCanvasState` (and therefore both `RenderSession.renderState` and the `useRealtimeMockup` hook) now **buffers** a canvas state sent before the session is connected+configured and flushes it automatically on `configured` — exactly as canvas blobs already did. Previously the state was silently dropped (`ws.readyState=0 BLOCKED`) and dead-ended in the 30s render watchdog with no mockup. This was the most-reported clean-room DX failure; it is now impossible on every code path through `RealtimeMockupService`.
|
|
8
|
+
|
|
9
|
+
The render-timeout message is also now **cause-aware**: instead of always blaming a missing `variantId`, it reports what was actually observed — socket not open, config never acknowledged, a genuine missing variant selection, or "the server accepted config but rendered nothing" (naming the product/mockupIds to check). The old message sent dogfooders chasing the wrong cause.
|
|
10
|
+
|
|
11
|
+
Pinned by regression tests: pre-connect buffering + flush at the service level, and a React **StrictMode** mount of `useRealtimeMockup` proving the first render survives the mount→cleanup→mount double-invoke that App-Router projects trigger.
|
|
12
|
+
|
|
13
|
+
- [#309](https://github.com/snowcone-app/snowcone-monorepo/pull/309) [`df2793c`](https://github.com/snowcone-app/snowcone-monorepo/commit/df2793c915bc8dc43eb0948a5f74912b333a43d2) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Realtime: every `canvas_state` render now carries a per-request **`requestId`** (a 32-hex W3C trace-id) and a **`traceparent`** header so a single edit can be followed end-to-end. The client logs `req=<id>` on send and on `mockup_rendered` (alongside `renderMs`, `blobToRenderMs`, and a client-observed `clientRoundTripMs`), the server tags the queue → dequeue → composite-fetch start/end log lines with the same `req=<id>`, and the server forwards the `traceparent` onto the internal `/mockups` composite render so it becomes a child span in OTel. Grep one id across client + server logs, or paste it as a trace-id in the tracing backend, to localize where realtime render latency is spent. `requestId` is now also surfaced on `MockupResult`.
|
|
14
|
+
|
|
3
15
|
## 0.3.4
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
var REALTIME_WS_URL = "wss://cdn.snowcone.app/realtime";
|
|
3
3
|
|
|
4
4
|
// src/realtime/websocket.ts
|
|
5
|
+
function randHex(len) {
|
|
6
|
+
const arr = new Uint8Array(len);
|
|
7
|
+
globalThis.crypto.getRandomValues(arr);
|
|
8
|
+
return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
9
|
+
}
|
|
10
|
+
function newRenderCorrelation() {
|
|
11
|
+
const traceId = randHex(16);
|
|
12
|
+
const spanId = randHex(8);
|
|
13
|
+
return { requestId: traceId, traceparent: `00-${traceId}-${spanId}-01` };
|
|
14
|
+
}
|
|
5
15
|
var RealtimeMockupService = class {
|
|
6
16
|
constructor(wsUrl = REALTIME_WS_URL) {
|
|
7
17
|
this.wsUrl = wsUrl;
|
|
@@ -17,8 +27,21 @@ var RealtimeMockupService = class {
|
|
|
17
27
|
lastError = null;
|
|
18
28
|
callbacks = {};
|
|
19
29
|
lastBlobSentAt = 0;
|
|
30
|
+
// Per-request send timestamps keyed by requestId, for client-observed
|
|
31
|
+
// round-trip timing in the mockup_rendered log. Pruned to a small cap so an
|
|
32
|
+
// in-flight burst can't grow it unbounded.
|
|
33
|
+
requestSendTimes = /* @__PURE__ */ new Map();
|
|
20
34
|
canvasBlobs = /* @__PURE__ */ new Map();
|
|
21
35
|
canvasStates = /* @__PURE__ */ new Map();
|
|
36
|
+
// States sent BEFORE the session is connected+configured. The #1 recurring DX
|
|
37
|
+
// failure ("connect race"): a caller fires renderState/sendCanvasState during
|
|
38
|
+
// the WS handshake, the state was silently dropped, and the 30s watchdog fired
|
|
39
|
+
// with no mockup pointing at the wrong cause. Blobs were already buffered and
|
|
40
|
+
// auto-flushed on `configured`; states were not. We now buffer the latest
|
|
41
|
+
// state per placement and flush it on `configured`, so the first render
|
|
42
|
+
// survives the race on EVERY path through this service — the `RenderSession`
|
|
43
|
+
// facade and the `useRealtimeMockup` hook alike.
|
|
44
|
+
pendingStates = /* @__PURE__ */ new Map();
|
|
22
45
|
// Serialized JSON of the last canvas state ACTUALLY SENT per placement. Used to
|
|
23
46
|
// drop duplicate consecutive states so an idle canvas (whose onChange keeps
|
|
24
47
|
// firing identical states) doesn't stream renders and burn the realtime budget.
|
|
@@ -187,6 +210,16 @@ var RealtimeMockupService = class {
|
|
|
187
210
|
});
|
|
188
211
|
}, 100);
|
|
189
212
|
}
|
|
213
|
+
if (this.pendingStates.size > 0) {
|
|
214
|
+
const pending = Array.from(this.pendingStates.entries());
|
|
215
|
+
this.pendingStates.clear();
|
|
216
|
+
this.addLog(`\u{1F4E6} Flushing ${pending.length} buffered canvas state(s)`);
|
|
217
|
+
setTimeout(() => {
|
|
218
|
+
for (const [placement, { state, mockupCount }] of pending) {
|
|
219
|
+
this.sendCanvasState(placement, state, mockupCount, 0);
|
|
220
|
+
}
|
|
221
|
+
}, 100);
|
|
222
|
+
}
|
|
190
223
|
break;
|
|
191
224
|
case "blob_received":
|
|
192
225
|
const placementName = data.placement || "unknown";
|
|
@@ -200,8 +233,10 @@ var RealtimeMockupService = class {
|
|
|
200
233
|
case "rendering_started":
|
|
201
234
|
this.addLog("\u{1F3A8} Mockup rendering has started...");
|
|
202
235
|
break;
|
|
203
|
-
case "mockup_rendered":
|
|
204
|
-
|
|
236
|
+
case "mockup_rendered": {
|
|
237
|
+
const sentAt = data.requestId ? this.requestSendTimes.get(data.requestId) : void 0;
|
|
238
|
+
const clientRoundTripMs = sentAt !== void 0 ? Date.now() - sentAt : void 0;
|
|
239
|
+
console.log(`[WS] mockup_rendered received: mockupId=${data.mockupId} hasImageUrl=${!!data.imageUrl} v=${data.requestVersion} req=${data.requestId ?? "?"} placement="${data.placement}" renderMs=${data.renderMs} blobToRenderMs=${data.blobToRenderMs} clientRoundTripMs=${clientRoundTripMs ?? "?"} at ${Date.now()}`);
|
|
205
240
|
if (data.imageUrl && data.mockupId) {
|
|
206
241
|
const responseVersion = data.requestVersion;
|
|
207
242
|
const responsePlacement = data.placement;
|
|
@@ -219,6 +254,7 @@ var RealtimeMockupService = class {
|
|
|
219
254
|
imageUrl: data.imageUrl,
|
|
220
255
|
renderUrl: data.renderUrl || data.imageUrl,
|
|
221
256
|
imageSize: data.imageSize || 0,
|
|
257
|
+
requestId: data.requestId,
|
|
222
258
|
requestVersion: responseVersion,
|
|
223
259
|
placement: responsePlacement,
|
|
224
260
|
renderMs: data.renderMs,
|
|
@@ -241,6 +277,7 @@ var RealtimeMockupService = class {
|
|
|
241
277
|
this.addLog(`\u26A0\uFE0F mockup_rendered message dropped: missing required fields [${missing.join(", ")}]. Full data keys: [${Object.keys(data).join(", ")}]`);
|
|
242
278
|
}
|
|
243
279
|
break;
|
|
280
|
+
}
|
|
244
281
|
case "all_mockups_rendered":
|
|
245
282
|
console.log(`[WS] all_mockups_rendered received: ${data.mockups?.length ?? 0} mockups`);
|
|
246
283
|
if (data.mockups) {
|
|
@@ -277,6 +314,7 @@ var RealtimeMockupService = class {
|
|
|
277
314
|
this.connectAttempt++;
|
|
278
315
|
this.isConnecting = false;
|
|
279
316
|
this.clearRenew();
|
|
317
|
+
this.pendingStates.clear();
|
|
280
318
|
if (this.ws) {
|
|
281
319
|
this.ws.close();
|
|
282
320
|
this.ws = null;
|
|
@@ -448,9 +486,11 @@ var RealtimeMockupService = class {
|
|
|
448
486
|
sendCanvasState(placement, state, mockupCount = 1, baseThrottleMs = 1e3) {
|
|
449
487
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
|
|
450
488
|
const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
|
|
451
|
-
|
|
452
|
-
|
|
489
|
+
this.pendingStates.set(placement, { state, mockupCount, baseThrottleMs });
|
|
490
|
+
console.log(`[WS] sendCanvasState buffered for "${placement}" until configured: ${reason}`);
|
|
491
|
+
return true;
|
|
453
492
|
}
|
|
493
|
+
this.pendingStates.delete(placement);
|
|
454
494
|
this.canvasStates.set(placement, state);
|
|
455
495
|
if (baseThrottleMs <= 0) {
|
|
456
496
|
this.sendCanvasStateImmediately(placement, state);
|
|
@@ -503,18 +543,31 @@ var RealtimeMockupService = class {
|
|
|
503
543
|
}
|
|
504
544
|
this.lastSentVersion = ++this.requestVersion;
|
|
505
545
|
this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
|
|
546
|
+
const { requestId, traceparent } = newRenderCorrelation();
|
|
506
547
|
const message = JSON.stringify({
|
|
507
548
|
type: "canvas_state",
|
|
508
549
|
placement,
|
|
509
550
|
version: this.lastSentVersion,
|
|
551
|
+
requestId,
|
|
552
|
+
traceparent,
|
|
510
553
|
stateId
|
|
511
554
|
});
|
|
512
555
|
this.ws.send(message);
|
|
556
|
+
this.recordRequestSent(requestId);
|
|
513
557
|
this.lastBlobSentAt = Date.now();
|
|
514
|
-
|
|
558
|
+
console.log(`[WS] canvas_state(ref) SENT: placement="${placement}" v${this.lastSentVersion} req=${requestId} stateId=${stateId} at ${Date.now()}`);
|
|
559
|
+
this.addLog(`Sent canvas stateId "${stateId}" for "${placement}" (v${this.lastSentVersion}, req ${requestId})`, "sent");
|
|
515
560
|
this.callbacks.onBlobSent?.(placement);
|
|
516
561
|
return true;
|
|
517
562
|
}
|
|
563
|
+
/** Stamp a request's send time for client round-trip measurement (capped). */
|
|
564
|
+
recordRequestSent(requestId) {
|
|
565
|
+
this.requestSendTimes.set(requestId, Date.now());
|
|
566
|
+
if (this.requestSendTimes.size > 50) {
|
|
567
|
+
const oldest = this.requestSendTimes.keys().next().value;
|
|
568
|
+
if (oldest !== void 0) this.requestSendTimes.delete(oldest);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
518
571
|
sendCanvasStateImmediately(placement, state) {
|
|
519
572
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
520
573
|
console.log(`[WS] sendCanvasStateImmediately ABORTED: ws not open`);
|
|
@@ -527,16 +580,21 @@ var RealtimeMockupService = class {
|
|
|
527
580
|
}
|
|
528
581
|
this.lastSentVersion = ++this.requestVersion;
|
|
529
582
|
this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
|
|
583
|
+
const { requestId, traceparent } = newRenderCorrelation();
|
|
530
584
|
const message = JSON.stringify({
|
|
531
585
|
type: "canvas_state",
|
|
532
586
|
placement,
|
|
533
587
|
version: this.lastSentVersion,
|
|
588
|
+
requestId,
|
|
589
|
+
traceparent,
|
|
534
590
|
state
|
|
535
591
|
});
|
|
536
592
|
this.ws.send(message);
|
|
593
|
+
this.recordRequestSent(requestId);
|
|
537
594
|
this.lastSentStateJson[placement] = stateJson;
|
|
538
595
|
this.lastBlobSentAt = Date.now();
|
|
539
|
-
|
|
596
|
+
console.log(`[WS] canvas_state SENT: placement="${placement}" v${this.lastSentVersion} req=${requestId} (${(message.length / 1024).toFixed(1)}KB) at ${Date.now()}`);
|
|
597
|
+
this.addLog(`Sent canvas state for "${placement}" (${(message.length / 1024).toFixed(1)}KB, v${this.lastSentVersion}, req ${requestId})`, "sent");
|
|
540
598
|
this.callbacks.onBlobSent?.(placement);
|
|
541
599
|
}
|
|
542
600
|
sendBlobImmediately(placement, blob, notifyCallback = true) {
|
package/dist/index.cjs
CHANGED
|
@@ -4818,6 +4818,16 @@ function createSvelteComponent(descriptor, svelte) {
|
|
|
4818
4818
|
var REALTIME_WS_URL = "wss://cdn.snowcone.app/realtime";
|
|
4819
4819
|
|
|
4820
4820
|
// src/realtime/websocket.ts
|
|
4821
|
+
function randHex(len) {
|
|
4822
|
+
const arr = new Uint8Array(len);
|
|
4823
|
+
globalThis.crypto.getRandomValues(arr);
|
|
4824
|
+
return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
4825
|
+
}
|
|
4826
|
+
function newRenderCorrelation() {
|
|
4827
|
+
const traceId = randHex(16);
|
|
4828
|
+
const spanId = randHex(8);
|
|
4829
|
+
return { requestId: traceId, traceparent: `00-${traceId}-${spanId}-01` };
|
|
4830
|
+
}
|
|
4821
4831
|
var RealtimeMockupService = class {
|
|
4822
4832
|
constructor(wsUrl = REALTIME_WS_URL) {
|
|
4823
4833
|
this.wsUrl = wsUrl;
|
|
@@ -4833,8 +4843,21 @@ var RealtimeMockupService = class {
|
|
|
4833
4843
|
lastError = null;
|
|
4834
4844
|
callbacks = {};
|
|
4835
4845
|
lastBlobSentAt = 0;
|
|
4846
|
+
// Per-request send timestamps keyed by requestId, for client-observed
|
|
4847
|
+
// round-trip timing in the mockup_rendered log. Pruned to a small cap so an
|
|
4848
|
+
// in-flight burst can't grow it unbounded.
|
|
4849
|
+
requestSendTimes = /* @__PURE__ */ new Map();
|
|
4836
4850
|
canvasBlobs = /* @__PURE__ */ new Map();
|
|
4837
4851
|
canvasStates = /* @__PURE__ */ new Map();
|
|
4852
|
+
// States sent BEFORE the session is connected+configured. The #1 recurring DX
|
|
4853
|
+
// failure ("connect race"): a caller fires renderState/sendCanvasState during
|
|
4854
|
+
// the WS handshake, the state was silently dropped, and the 30s watchdog fired
|
|
4855
|
+
// with no mockup pointing at the wrong cause. Blobs were already buffered and
|
|
4856
|
+
// auto-flushed on `configured`; states were not. We now buffer the latest
|
|
4857
|
+
// state per placement and flush it on `configured`, so the first render
|
|
4858
|
+
// survives the race on EVERY path through this service — the `RenderSession`
|
|
4859
|
+
// facade and the `useRealtimeMockup` hook alike.
|
|
4860
|
+
pendingStates = /* @__PURE__ */ new Map();
|
|
4838
4861
|
// Serialized JSON of the last canvas state ACTUALLY SENT per placement. Used to
|
|
4839
4862
|
// drop duplicate consecutive states so an idle canvas (whose onChange keeps
|
|
4840
4863
|
// firing identical states) doesn't stream renders and burn the realtime budget.
|
|
@@ -5003,6 +5026,16 @@ var RealtimeMockupService = class {
|
|
|
5003
5026
|
});
|
|
5004
5027
|
}, 100);
|
|
5005
5028
|
}
|
|
5029
|
+
if (this.pendingStates.size > 0) {
|
|
5030
|
+
const pending = Array.from(this.pendingStates.entries());
|
|
5031
|
+
this.pendingStates.clear();
|
|
5032
|
+
this.addLog(`\u{1F4E6} Flushing ${pending.length} buffered canvas state(s)`);
|
|
5033
|
+
setTimeout(() => {
|
|
5034
|
+
for (const [placement, { state, mockupCount }] of pending) {
|
|
5035
|
+
this.sendCanvasState(placement, state, mockupCount, 0);
|
|
5036
|
+
}
|
|
5037
|
+
}, 100);
|
|
5038
|
+
}
|
|
5006
5039
|
break;
|
|
5007
5040
|
case "blob_received":
|
|
5008
5041
|
const placementName = data.placement || "unknown";
|
|
@@ -5016,8 +5049,10 @@ var RealtimeMockupService = class {
|
|
|
5016
5049
|
case "rendering_started":
|
|
5017
5050
|
this.addLog("\u{1F3A8} Mockup rendering has started...");
|
|
5018
5051
|
break;
|
|
5019
|
-
case "mockup_rendered":
|
|
5020
|
-
|
|
5052
|
+
case "mockup_rendered": {
|
|
5053
|
+
const sentAt = data.requestId ? this.requestSendTimes.get(data.requestId) : void 0;
|
|
5054
|
+
const clientRoundTripMs = sentAt !== void 0 ? Date.now() - sentAt : void 0;
|
|
5055
|
+
console.log(`[WS] mockup_rendered received: mockupId=${data.mockupId} hasImageUrl=${!!data.imageUrl} v=${data.requestVersion} req=${data.requestId ?? "?"} placement="${data.placement}" renderMs=${data.renderMs} blobToRenderMs=${data.blobToRenderMs} clientRoundTripMs=${clientRoundTripMs ?? "?"} at ${Date.now()}`);
|
|
5021
5056
|
if (data.imageUrl && data.mockupId) {
|
|
5022
5057
|
const responseVersion = data.requestVersion;
|
|
5023
5058
|
const responsePlacement = data.placement;
|
|
@@ -5035,6 +5070,7 @@ var RealtimeMockupService = class {
|
|
|
5035
5070
|
imageUrl: data.imageUrl,
|
|
5036
5071
|
renderUrl: data.renderUrl || data.imageUrl,
|
|
5037
5072
|
imageSize: data.imageSize || 0,
|
|
5073
|
+
requestId: data.requestId,
|
|
5038
5074
|
requestVersion: responseVersion,
|
|
5039
5075
|
placement: responsePlacement,
|
|
5040
5076
|
renderMs: data.renderMs,
|
|
@@ -5057,6 +5093,7 @@ var RealtimeMockupService = class {
|
|
|
5057
5093
|
this.addLog(`\u26A0\uFE0F mockup_rendered message dropped: missing required fields [${missing.join(", ")}]. Full data keys: [${Object.keys(data).join(", ")}]`);
|
|
5058
5094
|
}
|
|
5059
5095
|
break;
|
|
5096
|
+
}
|
|
5060
5097
|
case "all_mockups_rendered":
|
|
5061
5098
|
console.log(`[WS] all_mockups_rendered received: ${data.mockups?.length ?? 0} mockups`);
|
|
5062
5099
|
if (data.mockups) {
|
|
@@ -5093,6 +5130,7 @@ var RealtimeMockupService = class {
|
|
|
5093
5130
|
this.connectAttempt++;
|
|
5094
5131
|
this.isConnecting = false;
|
|
5095
5132
|
this.clearRenew();
|
|
5133
|
+
this.pendingStates.clear();
|
|
5096
5134
|
if (this.ws) {
|
|
5097
5135
|
this.ws.close();
|
|
5098
5136
|
this.ws = null;
|
|
@@ -5264,9 +5302,11 @@ var RealtimeMockupService = class {
|
|
|
5264
5302
|
sendCanvasState(placement, state, mockupCount = 1, baseThrottleMs = 1e3) {
|
|
5265
5303
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
|
|
5266
5304
|
const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
|
|
5267
|
-
|
|
5268
|
-
|
|
5305
|
+
this.pendingStates.set(placement, { state, mockupCount, baseThrottleMs });
|
|
5306
|
+
console.log(`[WS] sendCanvasState buffered for "${placement}" until configured: ${reason}`);
|
|
5307
|
+
return true;
|
|
5269
5308
|
}
|
|
5309
|
+
this.pendingStates.delete(placement);
|
|
5270
5310
|
this.canvasStates.set(placement, state);
|
|
5271
5311
|
if (baseThrottleMs <= 0) {
|
|
5272
5312
|
this.sendCanvasStateImmediately(placement, state);
|
|
@@ -5319,18 +5359,31 @@ var RealtimeMockupService = class {
|
|
|
5319
5359
|
}
|
|
5320
5360
|
this.lastSentVersion = ++this.requestVersion;
|
|
5321
5361
|
this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
|
|
5362
|
+
const { requestId, traceparent } = newRenderCorrelation();
|
|
5322
5363
|
const message = JSON.stringify({
|
|
5323
5364
|
type: "canvas_state",
|
|
5324
5365
|
placement,
|
|
5325
5366
|
version: this.lastSentVersion,
|
|
5367
|
+
requestId,
|
|
5368
|
+
traceparent,
|
|
5326
5369
|
stateId
|
|
5327
5370
|
});
|
|
5328
5371
|
this.ws.send(message);
|
|
5372
|
+
this.recordRequestSent(requestId);
|
|
5329
5373
|
this.lastBlobSentAt = Date.now();
|
|
5330
|
-
|
|
5374
|
+
console.log(`[WS] canvas_state(ref) SENT: placement="${placement}" v${this.lastSentVersion} req=${requestId} stateId=${stateId} at ${Date.now()}`);
|
|
5375
|
+
this.addLog(`Sent canvas stateId "${stateId}" for "${placement}" (v${this.lastSentVersion}, req ${requestId})`, "sent");
|
|
5331
5376
|
this.callbacks.onBlobSent?.(placement);
|
|
5332
5377
|
return true;
|
|
5333
5378
|
}
|
|
5379
|
+
/** Stamp a request's send time for client round-trip measurement (capped). */
|
|
5380
|
+
recordRequestSent(requestId) {
|
|
5381
|
+
this.requestSendTimes.set(requestId, Date.now());
|
|
5382
|
+
if (this.requestSendTimes.size > 50) {
|
|
5383
|
+
const oldest = this.requestSendTimes.keys().next().value;
|
|
5384
|
+
if (oldest !== void 0) this.requestSendTimes.delete(oldest);
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5334
5387
|
sendCanvasStateImmediately(placement, state) {
|
|
5335
5388
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
5336
5389
|
console.log(`[WS] sendCanvasStateImmediately ABORTED: ws not open`);
|
|
@@ -5343,16 +5396,21 @@ var RealtimeMockupService = class {
|
|
|
5343
5396
|
}
|
|
5344
5397
|
this.lastSentVersion = ++this.requestVersion;
|
|
5345
5398
|
this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
|
|
5399
|
+
const { requestId, traceparent } = newRenderCorrelation();
|
|
5346
5400
|
const message = JSON.stringify({
|
|
5347
5401
|
type: "canvas_state",
|
|
5348
5402
|
placement,
|
|
5349
5403
|
version: this.lastSentVersion,
|
|
5404
|
+
requestId,
|
|
5405
|
+
traceparent,
|
|
5350
5406
|
state
|
|
5351
5407
|
});
|
|
5352
5408
|
this.ws.send(message);
|
|
5409
|
+
this.recordRequestSent(requestId);
|
|
5353
5410
|
this.lastSentStateJson[placement] = stateJson;
|
|
5354
5411
|
this.lastBlobSentAt = Date.now();
|
|
5355
|
-
|
|
5412
|
+
console.log(`[WS] canvas_state SENT: placement="${placement}" v${this.lastSentVersion} req=${requestId} (${(message.length / 1024).toFixed(1)}KB) at ${Date.now()}`);
|
|
5413
|
+
this.addLog(`Sent canvas state for "${placement}" (${(message.length / 1024).toFixed(1)}KB, v${this.lastSentVersion}, req ${requestId})`, "sent");
|
|
5356
5414
|
this.callbacks.onBlobSent?.(placement);
|
|
5357
5415
|
}
|
|
5358
5416
|
sendBlobImmediately(placement, blob, notifyCallback = true) {
|
|
@@ -5757,11 +5815,32 @@ var RenderSession = class {
|
|
|
5757
5815
|
if (this.renderTimeoutMs <= 0) return;
|
|
5758
5816
|
this.watchdog = setTimeout(() => {
|
|
5759
5817
|
this.watchdog = null;
|
|
5760
|
-
this.errorCb?.(
|
|
5761
|
-
`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)`
|
|
5762
|
-
);
|
|
5818
|
+
this.errorCb?.(this.timeoutMessage());
|
|
5763
5819
|
}, this.renderTimeoutMs);
|
|
5764
5820
|
}
|
|
5821
|
+
/**
|
|
5822
|
+
* Build the watchdog timeout message from the *observed* session state rather
|
|
5823
|
+
* than a single hardcoded guess. The old message always blamed a missing
|
|
5824
|
+
* `variantId` — which sent a clean-room dogfood chasing the wrong cause when
|
|
5825
|
+
* the real problem was elsewhere. Report what's actually true: is the socket
|
|
5826
|
+
* open, did config get acknowledged, and only then fall back to the
|
|
5827
|
+
* variant/catalog hypotheses, naming the exact product so it's checkable.
|
|
5828
|
+
*/
|
|
5829
|
+
timeoutMessage() {
|
|
5830
|
+
const base = `realtime render timed out after ${this.renderTimeoutMs}ms with no mockup`;
|
|
5831
|
+
const st = this.svc.getState();
|
|
5832
|
+
if (!st.isConnected) {
|
|
5833
|
+
return `${base} \u2014 the WebSocket is not open (it connected, then closed). Check the grant endpoint and network; the session needs a live socket to render.`;
|
|
5834
|
+
}
|
|
5835
|
+
if (!st.isConfigured) {
|
|
5836
|
+
return `${base} \u2014 the server never acknowledged the session config. The socket is open but no \`configured\` message arrived; the render was never accepted.`;
|
|
5837
|
+
}
|
|
5838
|
+
const product = this.product;
|
|
5839
|
+
if (product && variantIdsOf(product).length >= 2 && !product.variantId) {
|
|
5840
|
+
return `${base} \u2014 product ${product.productId} has a color/variant axis but no \`variantId\` was set, so the server is waiting for a variant selection (see RenderProduct.variantId).`;
|
|
5841
|
+
}
|
|
5842
|
+
return `${base} \u2014 the server accepted the config but rendered nothing. Verify the product and mockupIds exist in the catalog (productId="${product?.productId}", mockupIds=[${product?.mockupIds?.join(", ") ?? ""}]) and that the placement name matches an artboard.`;
|
|
5843
|
+
}
|
|
5765
5844
|
/** Cancel the pending render watchdog, if any. */
|
|
5766
5845
|
clearWatchdog() {
|
|
5767
5846
|
if (this.watchdog) {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createDevFetcher } from './dev-fetcher.cjs';
|
|
2
2
|
import { PublicDesign, PublicFill, PublicOptions } from '@snowcone-app/mockup-url';
|
|
3
|
-
import { O as OptionAttribute, a as OptionSelection, C as Combination, F as FrameworkAdapter, b as ComponentProps, c as ComponentDescriptor, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, g as FrameworkUtilities, P as PlacementSettings, M as MockupResult, R as RealtimeMockupService } from './websocket-
|
|
4
|
-
export { A as AdapterRegistry, h as ProductComponentContext, i as RealtimeMockupCallbacks, j as RealtimeMockupState, k as RenderResult, W as WebSocketConfig, l as WebSocketMessage, m as adapterRegistry, n as computeDisabledChoices, o as createComponent, p as defineComponent, q as deriveDefaultSelection, r as findBestCombination, s as getPricePreview, t as isOptionAvailable, u as resolveBestCombination } from './websocket-
|
|
3
|
+
import { O as OptionAttribute, a as OptionSelection, C as Combination, F as FrameworkAdapter, b as ComponentProps, c as ComponentDescriptor, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, g as FrameworkUtilities, P as PlacementSettings, M as MockupResult, R as RealtimeMockupService } from './websocket-hXpNcyEn.cjs';
|
|
4
|
+
export { A as AdapterRegistry, h as ProductComponentContext, i as RealtimeMockupCallbacks, j as RealtimeMockupState, k as RenderResult, W as WebSocketConfig, l as WebSocketMessage, m as adapterRegistry, n as computeDisabledChoices, o as createComponent, p as defineComponent, q as deriveDefaultSelection, r as findBestCombination, s as getPricePreview, t as isOptionAvailable, u as resolveBestCombination } from './websocket-hXpNcyEn.cjs';
|
|
5
5
|
|
|
6
6
|
interface CatalogProduct$2 {
|
|
7
7
|
id: string;
|
|
@@ -2780,6 +2780,15 @@ declare class RenderSession {
|
|
|
2780
2780
|
* server/transport errors — with a hint at the most common cause.
|
|
2781
2781
|
*/
|
|
2782
2782
|
private armWatchdog;
|
|
2783
|
+
/**
|
|
2784
|
+
* Build the watchdog timeout message from the *observed* session state rather
|
|
2785
|
+
* than a single hardcoded guess. The old message always blamed a missing
|
|
2786
|
+
* `variantId` — which sent a clean-room dogfood chasing the wrong cause when
|
|
2787
|
+
* the real problem was elsewhere. Report what's actually true: is the socket
|
|
2788
|
+
* open, did config get acknowledged, and only then fall back to the
|
|
2789
|
+
* variant/catalog hypotheses, naming the exact product so it's checkable.
|
|
2790
|
+
*/
|
|
2791
|
+
private timeoutMessage;
|
|
2783
2792
|
/** Cancel the pending render watchdog, if any. */
|
|
2784
2793
|
private clearWatchdog;
|
|
2785
2794
|
/** Escape hatch: the underlying low-level service. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createDevFetcher } from './dev-fetcher.js';
|
|
2
2
|
import { PublicDesign, PublicFill, PublicOptions } from '@snowcone-app/mockup-url';
|
|
3
|
-
import { O as OptionAttribute, a as OptionSelection, C as Combination, F as FrameworkAdapter, b as ComponentProps, c as ComponentDescriptor, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, g as FrameworkUtilities, P as PlacementSettings, M as MockupResult, R as RealtimeMockupService } from './websocket-
|
|
4
|
-
export { A as AdapterRegistry, h as ProductComponentContext, i as RealtimeMockupCallbacks, j as RealtimeMockupState, k as RenderResult, W as WebSocketConfig, l as WebSocketMessage, m as adapterRegistry, n as computeDisabledChoices, o as createComponent, p as defineComponent, q as deriveDefaultSelection, r as findBestCombination, s as getPricePreview, t as isOptionAvailable, u as resolveBestCombination } from './websocket-
|
|
3
|
+
import { O as OptionAttribute, a as OptionSelection, C as Combination, F as FrameworkAdapter, b as ComponentProps, c as ComponentDescriptor, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, g as FrameworkUtilities, P as PlacementSettings, M as MockupResult, R as RealtimeMockupService } from './websocket-hXpNcyEn.js';
|
|
4
|
+
export { A as AdapterRegistry, h as ProductComponentContext, i as RealtimeMockupCallbacks, j as RealtimeMockupState, k as RenderResult, W as WebSocketConfig, l as WebSocketMessage, m as adapterRegistry, n as computeDisabledChoices, o as createComponent, p as defineComponent, q as deriveDefaultSelection, r as findBestCombination, s as getPricePreview, t as isOptionAvailable, u as resolveBestCombination } from './websocket-hXpNcyEn.js';
|
|
5
5
|
|
|
6
6
|
interface CatalogProduct$2 {
|
|
7
7
|
id: string;
|
|
@@ -2780,6 +2780,15 @@ declare class RenderSession {
|
|
|
2780
2780
|
* server/transport errors — with a hint at the most common cause.
|
|
2781
2781
|
*/
|
|
2782
2782
|
private armWatchdog;
|
|
2783
|
+
/**
|
|
2784
|
+
* Build the watchdog timeout message from the *observed* session state rather
|
|
2785
|
+
* than a single hardcoded guess. The old message always blamed a missing
|
|
2786
|
+
* `variantId` — which sent a clean-room dogfood chasing the wrong cause when
|
|
2787
|
+
* the real problem was elsewhere. Report what's actually true: is the socket
|
|
2788
|
+
* open, did config get acknowledged, and only then fall back to the
|
|
2789
|
+
* variant/catalog hypotheses, naming the exact product so it's checkable.
|
|
2790
|
+
*/
|
|
2791
|
+
private timeoutMessage;
|
|
2783
2792
|
/** Cancel the pending render watchdog, if any. */
|
|
2784
2793
|
private clearWatchdog;
|
|
2785
2794
|
/** Escape hatch: the underlying low-level service. */
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
REALTIME_WS_URL,
|
|
6
6
|
RealtimeMockupService
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-YTOTCNEW.js";
|
|
8
8
|
|
|
9
9
|
// src/validation.ts
|
|
10
10
|
import { z } from "zod";
|
|
@@ -4900,11 +4900,32 @@ var RenderSession = class {
|
|
|
4900
4900
|
if (this.renderTimeoutMs <= 0) return;
|
|
4901
4901
|
this.watchdog = setTimeout(() => {
|
|
4902
4902
|
this.watchdog = null;
|
|
4903
|
-
this.errorCb?.(
|
|
4904
|
-
`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)`
|
|
4905
|
-
);
|
|
4903
|
+
this.errorCb?.(this.timeoutMessage());
|
|
4906
4904
|
}, this.renderTimeoutMs);
|
|
4907
4905
|
}
|
|
4906
|
+
/**
|
|
4907
|
+
* Build the watchdog timeout message from the *observed* session state rather
|
|
4908
|
+
* than a single hardcoded guess. The old message always blamed a missing
|
|
4909
|
+
* `variantId` — which sent a clean-room dogfood chasing the wrong cause when
|
|
4910
|
+
* the real problem was elsewhere. Report what's actually true: is the socket
|
|
4911
|
+
* open, did config get acknowledged, and only then fall back to the
|
|
4912
|
+
* variant/catalog hypotheses, naming the exact product so it's checkable.
|
|
4913
|
+
*/
|
|
4914
|
+
timeoutMessage() {
|
|
4915
|
+
const base = `realtime render timed out after ${this.renderTimeoutMs}ms with no mockup`;
|
|
4916
|
+
const st = this.svc.getState();
|
|
4917
|
+
if (!st.isConnected) {
|
|
4918
|
+
return `${base} \u2014 the WebSocket is not open (it connected, then closed). Check the grant endpoint and network; the session needs a live socket to render.`;
|
|
4919
|
+
}
|
|
4920
|
+
if (!st.isConfigured) {
|
|
4921
|
+
return `${base} \u2014 the server never acknowledged the session config. The socket is open but no \`configured\` message arrived; the render was never accepted.`;
|
|
4922
|
+
}
|
|
4923
|
+
const product = this.product;
|
|
4924
|
+
if (product && variantIdsOf(product).length >= 2 && !product.variantId) {
|
|
4925
|
+
return `${base} \u2014 product ${product.productId} has a color/variant axis but no \`variantId\` was set, so the server is waiting for a variant selection (see RenderProduct.variantId).`;
|
|
4926
|
+
}
|
|
4927
|
+
return `${base} \u2014 the server accepted the config but rendered nothing. Verify the product and mockupIds exist in the catalog (productId="${product?.productId}", mockupIds=[${product?.mockupIds?.join(", ") ?? ""}]) and that the placement name matches an artboard.`;
|
|
4928
|
+
}
|
|
4908
4929
|
/** Cancel the pending render watchdog, if any. */
|
|
4909
4930
|
clearWatchdog() {
|
|
4910
4931
|
if (this.watchdog) {
|
package/dist/react.cjs
CHANGED
|
@@ -34,6 +34,16 @@ var import_react = require("react");
|
|
|
34
34
|
var REALTIME_WS_URL = "wss://cdn.snowcone.app/realtime";
|
|
35
35
|
|
|
36
36
|
// src/realtime/websocket.ts
|
|
37
|
+
function randHex(len) {
|
|
38
|
+
const arr = new Uint8Array(len);
|
|
39
|
+
globalThis.crypto.getRandomValues(arr);
|
|
40
|
+
return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
41
|
+
}
|
|
42
|
+
function newRenderCorrelation() {
|
|
43
|
+
const traceId = randHex(16);
|
|
44
|
+
const spanId = randHex(8);
|
|
45
|
+
return { requestId: traceId, traceparent: `00-${traceId}-${spanId}-01` };
|
|
46
|
+
}
|
|
37
47
|
var RealtimeMockupService = class {
|
|
38
48
|
constructor(wsUrl = REALTIME_WS_URL) {
|
|
39
49
|
this.wsUrl = wsUrl;
|
|
@@ -49,8 +59,21 @@ var RealtimeMockupService = class {
|
|
|
49
59
|
lastError = null;
|
|
50
60
|
callbacks = {};
|
|
51
61
|
lastBlobSentAt = 0;
|
|
62
|
+
// Per-request send timestamps keyed by requestId, for client-observed
|
|
63
|
+
// round-trip timing in the mockup_rendered log. Pruned to a small cap so an
|
|
64
|
+
// in-flight burst can't grow it unbounded.
|
|
65
|
+
requestSendTimes = /* @__PURE__ */ new Map();
|
|
52
66
|
canvasBlobs = /* @__PURE__ */ new Map();
|
|
53
67
|
canvasStates = /* @__PURE__ */ new Map();
|
|
68
|
+
// States sent BEFORE the session is connected+configured. The #1 recurring DX
|
|
69
|
+
// failure ("connect race"): a caller fires renderState/sendCanvasState during
|
|
70
|
+
// the WS handshake, the state was silently dropped, and the 30s watchdog fired
|
|
71
|
+
// with no mockup pointing at the wrong cause. Blobs were already buffered and
|
|
72
|
+
// auto-flushed on `configured`; states were not. We now buffer the latest
|
|
73
|
+
// state per placement and flush it on `configured`, so the first render
|
|
74
|
+
// survives the race on EVERY path through this service — the `RenderSession`
|
|
75
|
+
// facade and the `useRealtimeMockup` hook alike.
|
|
76
|
+
pendingStates = /* @__PURE__ */ new Map();
|
|
54
77
|
// Serialized JSON of the last canvas state ACTUALLY SENT per placement. Used to
|
|
55
78
|
// drop duplicate consecutive states so an idle canvas (whose onChange keeps
|
|
56
79
|
// firing identical states) doesn't stream renders and burn the realtime budget.
|
|
@@ -219,6 +242,16 @@ var RealtimeMockupService = class {
|
|
|
219
242
|
});
|
|
220
243
|
}, 100);
|
|
221
244
|
}
|
|
245
|
+
if (this.pendingStates.size > 0) {
|
|
246
|
+
const pending = Array.from(this.pendingStates.entries());
|
|
247
|
+
this.pendingStates.clear();
|
|
248
|
+
this.addLog(`\u{1F4E6} Flushing ${pending.length} buffered canvas state(s)`);
|
|
249
|
+
setTimeout(() => {
|
|
250
|
+
for (const [placement, { state, mockupCount }] of pending) {
|
|
251
|
+
this.sendCanvasState(placement, state, mockupCount, 0);
|
|
252
|
+
}
|
|
253
|
+
}, 100);
|
|
254
|
+
}
|
|
222
255
|
break;
|
|
223
256
|
case "blob_received":
|
|
224
257
|
const placementName = data.placement || "unknown";
|
|
@@ -232,8 +265,10 @@ var RealtimeMockupService = class {
|
|
|
232
265
|
case "rendering_started":
|
|
233
266
|
this.addLog("\u{1F3A8} Mockup rendering has started...");
|
|
234
267
|
break;
|
|
235
|
-
case "mockup_rendered":
|
|
236
|
-
|
|
268
|
+
case "mockup_rendered": {
|
|
269
|
+
const sentAt = data.requestId ? this.requestSendTimes.get(data.requestId) : void 0;
|
|
270
|
+
const clientRoundTripMs = sentAt !== void 0 ? Date.now() - sentAt : void 0;
|
|
271
|
+
console.log(`[WS] mockup_rendered received: mockupId=${data.mockupId} hasImageUrl=${!!data.imageUrl} v=${data.requestVersion} req=${data.requestId ?? "?"} placement="${data.placement}" renderMs=${data.renderMs} blobToRenderMs=${data.blobToRenderMs} clientRoundTripMs=${clientRoundTripMs ?? "?"} at ${Date.now()}`);
|
|
237
272
|
if (data.imageUrl && data.mockupId) {
|
|
238
273
|
const responseVersion = data.requestVersion;
|
|
239
274
|
const responsePlacement = data.placement;
|
|
@@ -251,6 +286,7 @@ var RealtimeMockupService = class {
|
|
|
251
286
|
imageUrl: data.imageUrl,
|
|
252
287
|
renderUrl: data.renderUrl || data.imageUrl,
|
|
253
288
|
imageSize: data.imageSize || 0,
|
|
289
|
+
requestId: data.requestId,
|
|
254
290
|
requestVersion: responseVersion,
|
|
255
291
|
placement: responsePlacement,
|
|
256
292
|
renderMs: data.renderMs,
|
|
@@ -273,6 +309,7 @@ var RealtimeMockupService = class {
|
|
|
273
309
|
this.addLog(`\u26A0\uFE0F mockup_rendered message dropped: missing required fields [${missing.join(", ")}]. Full data keys: [${Object.keys(data).join(", ")}]`);
|
|
274
310
|
}
|
|
275
311
|
break;
|
|
312
|
+
}
|
|
276
313
|
case "all_mockups_rendered":
|
|
277
314
|
console.log(`[WS] all_mockups_rendered received: ${data.mockups?.length ?? 0} mockups`);
|
|
278
315
|
if (data.mockups) {
|
|
@@ -309,6 +346,7 @@ var RealtimeMockupService = class {
|
|
|
309
346
|
this.connectAttempt++;
|
|
310
347
|
this.isConnecting = false;
|
|
311
348
|
this.clearRenew();
|
|
349
|
+
this.pendingStates.clear();
|
|
312
350
|
if (this.ws) {
|
|
313
351
|
this.ws.close();
|
|
314
352
|
this.ws = null;
|
|
@@ -480,9 +518,11 @@ var RealtimeMockupService = class {
|
|
|
480
518
|
sendCanvasState(placement, state, mockupCount = 1, baseThrottleMs = 1e3) {
|
|
481
519
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
|
|
482
520
|
const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
|
|
483
|
-
|
|
484
|
-
|
|
521
|
+
this.pendingStates.set(placement, { state, mockupCount, baseThrottleMs });
|
|
522
|
+
console.log(`[WS] sendCanvasState buffered for "${placement}" until configured: ${reason}`);
|
|
523
|
+
return true;
|
|
485
524
|
}
|
|
525
|
+
this.pendingStates.delete(placement);
|
|
486
526
|
this.canvasStates.set(placement, state);
|
|
487
527
|
if (baseThrottleMs <= 0) {
|
|
488
528
|
this.sendCanvasStateImmediately(placement, state);
|
|
@@ -535,18 +575,31 @@ var RealtimeMockupService = class {
|
|
|
535
575
|
}
|
|
536
576
|
this.lastSentVersion = ++this.requestVersion;
|
|
537
577
|
this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
|
|
578
|
+
const { requestId, traceparent } = newRenderCorrelation();
|
|
538
579
|
const message = JSON.stringify({
|
|
539
580
|
type: "canvas_state",
|
|
540
581
|
placement,
|
|
541
582
|
version: this.lastSentVersion,
|
|
583
|
+
requestId,
|
|
584
|
+
traceparent,
|
|
542
585
|
stateId
|
|
543
586
|
});
|
|
544
587
|
this.ws.send(message);
|
|
588
|
+
this.recordRequestSent(requestId);
|
|
545
589
|
this.lastBlobSentAt = Date.now();
|
|
546
|
-
|
|
590
|
+
console.log(`[WS] canvas_state(ref) SENT: placement="${placement}" v${this.lastSentVersion} req=${requestId} stateId=${stateId} at ${Date.now()}`);
|
|
591
|
+
this.addLog(`Sent canvas stateId "${stateId}" for "${placement}" (v${this.lastSentVersion}, req ${requestId})`, "sent");
|
|
547
592
|
this.callbacks.onBlobSent?.(placement);
|
|
548
593
|
return true;
|
|
549
594
|
}
|
|
595
|
+
/** Stamp a request's send time for client round-trip measurement (capped). */
|
|
596
|
+
recordRequestSent(requestId) {
|
|
597
|
+
this.requestSendTimes.set(requestId, Date.now());
|
|
598
|
+
if (this.requestSendTimes.size > 50) {
|
|
599
|
+
const oldest = this.requestSendTimes.keys().next().value;
|
|
600
|
+
if (oldest !== void 0) this.requestSendTimes.delete(oldest);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
550
603
|
sendCanvasStateImmediately(placement, state) {
|
|
551
604
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
552
605
|
console.log(`[WS] sendCanvasStateImmediately ABORTED: ws not open`);
|
|
@@ -559,16 +612,21 @@ var RealtimeMockupService = class {
|
|
|
559
612
|
}
|
|
560
613
|
this.lastSentVersion = ++this.requestVersion;
|
|
561
614
|
this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
|
|
615
|
+
const { requestId, traceparent } = newRenderCorrelation();
|
|
562
616
|
const message = JSON.stringify({
|
|
563
617
|
type: "canvas_state",
|
|
564
618
|
placement,
|
|
565
619
|
version: this.lastSentVersion,
|
|
620
|
+
requestId,
|
|
621
|
+
traceparent,
|
|
566
622
|
state
|
|
567
623
|
});
|
|
568
624
|
this.ws.send(message);
|
|
625
|
+
this.recordRequestSent(requestId);
|
|
569
626
|
this.lastSentStateJson[placement] = stateJson;
|
|
570
627
|
this.lastBlobSentAt = Date.now();
|
|
571
|
-
|
|
628
|
+
console.log(`[WS] canvas_state SENT: placement="${placement}" v${this.lastSentVersion} req=${requestId} (${(message.length / 1024).toFixed(1)}KB) at ${Date.now()}`);
|
|
629
|
+
this.addLog(`Sent canvas state for "${placement}" (${(message.length / 1024).toFixed(1)}KB, v${this.lastSentVersion}, req ${requestId})`, "sent");
|
|
572
630
|
this.callbacks.onBlobSent?.(placement);
|
|
573
631
|
}
|
|
574
632
|
sendBlobImmediately(placement, blob, notifyCallback = true) {
|
package/dist/react.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { M as MockupResult, W as WebSocketConfig, P as PlacementSettings, F as FrameworkAdapter, b as ComponentProps, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, c as ComponentDescriptor, g as FrameworkUtilities } from './websocket-
|
|
1
|
+
import { M as MockupResult, W as WebSocketConfig, P as PlacementSettings, F as FrameworkAdapter, b as ComponentProps, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, c as ComponentDescriptor, g as FrameworkUtilities } from './websocket-hXpNcyEn.cjs';
|
|
2
2
|
|
|
3
3
|
interface UseRealtimeMockupOptions {
|
|
4
4
|
wsUrl?: string;
|
package/dist/react.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { M as MockupResult, W as WebSocketConfig, P as PlacementSettings, F as FrameworkAdapter, b as ComponentProps, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, c as ComponentDescriptor, g as FrameworkUtilities } from './websocket-
|
|
1
|
+
import { M as MockupResult, W as WebSocketConfig, P as PlacementSettings, F as FrameworkAdapter, b as ComponentProps, d as ComponentState, e as ComponentContext, f as ComponentLifecycleHooks, E as EventHandler, c as ComponentDescriptor, g as FrameworkUtilities } from './websocket-hXpNcyEn.js';
|
|
2
2
|
|
|
3
3
|
interface UseRealtimeMockupOptions {
|
|
4
4
|
wsUrl?: string;
|
package/dist/react.js
CHANGED
|
@@ -230,6 +230,8 @@ interface MockupResult {
|
|
|
230
230
|
imageUrl: string;
|
|
231
231
|
renderUrl: string;
|
|
232
232
|
imageSize: number;
|
|
233
|
+
/** Per-render correlation id (== OTel trace-id) echoed back by the server */
|
|
234
|
+
requestId?: string;
|
|
233
235
|
/** The request version this mockup corresponds to (for stale detection) */
|
|
234
236
|
requestVersion?: number;
|
|
235
237
|
/** The placement this mockup corresponds to */
|
|
@@ -252,6 +254,8 @@ interface WebSocketMessage {
|
|
|
252
254
|
message?: string;
|
|
253
255
|
placement?: string;
|
|
254
256
|
missingPlacements?: string[];
|
|
257
|
+
/** Per-render correlation id (== OTel trace-id) echoed back by the server */
|
|
258
|
+
requestId?: string;
|
|
255
259
|
/** The request version this response corresponds to (for stale detection) */
|
|
256
260
|
requestVersion?: number;
|
|
257
261
|
/** Server-side PIXI render time in ms */
|
|
@@ -305,8 +309,10 @@ declare class RealtimeMockupService {
|
|
|
305
309
|
private lastError;
|
|
306
310
|
private callbacks;
|
|
307
311
|
private lastBlobSentAt;
|
|
312
|
+
private requestSendTimes;
|
|
308
313
|
private canvasBlobs;
|
|
309
314
|
private canvasStates;
|
|
315
|
+
private pendingStates;
|
|
310
316
|
private lastSentStateJson;
|
|
311
317
|
private colors;
|
|
312
318
|
private lastSendTime;
|
|
@@ -367,6 +373,8 @@ declare class RealtimeMockupService {
|
|
|
367
373
|
* canvas JSON. One-shot (no throttle). Results arrive like any other render.
|
|
368
374
|
*/
|
|
369
375
|
sendCanvasStateRef(placement: string, stateId: string): boolean;
|
|
376
|
+
/** Stamp a request's send time for client round-trip measurement (capped). */
|
|
377
|
+
private recordRequestSent;
|
|
370
378
|
private sendCanvasStateImmediately;
|
|
371
379
|
private sendBlobImmediately;
|
|
372
380
|
/**
|
|
@@ -230,6 +230,8 @@ interface MockupResult {
|
|
|
230
230
|
imageUrl: string;
|
|
231
231
|
renderUrl: string;
|
|
232
232
|
imageSize: number;
|
|
233
|
+
/** Per-render correlation id (== OTel trace-id) echoed back by the server */
|
|
234
|
+
requestId?: string;
|
|
233
235
|
/** The request version this mockup corresponds to (for stale detection) */
|
|
234
236
|
requestVersion?: number;
|
|
235
237
|
/** The placement this mockup corresponds to */
|
|
@@ -252,6 +254,8 @@ interface WebSocketMessage {
|
|
|
252
254
|
message?: string;
|
|
253
255
|
placement?: string;
|
|
254
256
|
missingPlacements?: string[];
|
|
257
|
+
/** Per-render correlation id (== OTel trace-id) echoed back by the server */
|
|
258
|
+
requestId?: string;
|
|
255
259
|
/** The request version this response corresponds to (for stale detection) */
|
|
256
260
|
requestVersion?: number;
|
|
257
261
|
/** Server-side PIXI render time in ms */
|
|
@@ -305,8 +309,10 @@ declare class RealtimeMockupService {
|
|
|
305
309
|
private lastError;
|
|
306
310
|
private callbacks;
|
|
307
311
|
private lastBlobSentAt;
|
|
312
|
+
private requestSendTimes;
|
|
308
313
|
private canvasBlobs;
|
|
309
314
|
private canvasStates;
|
|
315
|
+
private pendingStates;
|
|
310
316
|
private lastSentStateJson;
|
|
311
317
|
private colors;
|
|
312
318
|
private lastSendTime;
|
|
@@ -367,6 +373,8 @@ declare class RealtimeMockupService {
|
|
|
367
373
|
* canvas JSON. One-shot (no throttle). Results arrive like any other render.
|
|
368
374
|
*/
|
|
369
375
|
sendCanvasStateRef(placement: string, stateId: string): boolean;
|
|
376
|
+
/** Stamp a request's send time for client round-trip measurement (capped). */
|
|
377
|
+
private recordRequestSent;
|
|
370
378
|
private sendCanvasStateImmediately;
|
|
371
379
|
private sendBlobImmediately;
|
|
372
380
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snowcone-app/sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Snowcone SDK for product mockups and print-on-demand",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"merch",
|
|
@@ -68,8 +68,14 @@
|
|
|
68
68
|
"zod": "^3.23.8"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
+
"@testing-library/dom": "^10.4.0",
|
|
72
|
+
"@testing-library/react": "^16.3.0",
|
|
71
73
|
"@types/node": "^20.11.0",
|
|
72
74
|
"@types/react": "^18.3.0",
|
|
75
|
+
"@types/react-dom": "^18.3.0",
|
|
76
|
+
"happy-dom": "^15.11.7",
|
|
77
|
+
"react": "^18.3.1",
|
|
78
|
+
"react-dom": "^18.3.1",
|
|
73
79
|
"tsup": "^8.1.0",
|
|
74
80
|
"typescript": "^5.5.4",
|
|
75
81
|
"vitest": "^2.0.5",
|