@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/README.md +99 -63
- package/dist/{chunk-UJFJ7REN.js → chunk-6MV7TDTM.js} +186 -18
- package/dist/index.cjs +756 -170
- package/dist/index.d.cts +130 -29
- package/dist/index.d.ts +130 -29
- package/dist/index.js +561 -144
- package/dist/react.cjs +203 -20
- package/dist/react.d.cts +13 -1
- package/dist/react.d.ts +13 -1
- package/dist/react.js +18 -3
- package/dist/{websocket-B8_XAwWx.d.ts → websocket-Dum3OooZ.d.cts} +55 -3
- package/dist/{websocket-B8_XAwWx.d.cts → websocket-Dum3OooZ.d.ts} +55 -3
- package/package.json +21 -11
- package/dist/chunk-HOYSZQET.js +0 -476
- package/dist/chunk-IIUCW2O4.js +0 -457
- package/dist/websocket-GXMYofWp.d.cts +0 -330
- package/dist/websocket-GXMYofWp.d.ts +0 -330
|
@@ -212,8 +212,13 @@ interface WebSocketConfig {
|
|
|
212
212
|
productId: string;
|
|
213
213
|
mockupIds: string[];
|
|
214
214
|
variantId: string;
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snowcone-app/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Merch Javascript SDK for product mockups and print-on-demand",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"merch",
|
|
@@ -27,17 +27,20 @@
|
|
|
27
27
|
".": {
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
29
29
|
"import": "./dist/index.js",
|
|
30
|
-
"require": "./dist/index.cjs"
|
|
30
|
+
"require": "./dist/index.cjs",
|
|
31
|
+
"default": "./dist/index.js"
|
|
31
32
|
},
|
|
32
33
|
"./react": {
|
|
33
34
|
"types": "./dist/react.d.ts",
|
|
34
35
|
"import": "./dist/react.js",
|
|
35
|
-
"require": "./dist/react.cjs"
|
|
36
|
+
"require": "./dist/react.cjs",
|
|
37
|
+
"default": "./dist/react.js"
|
|
36
38
|
},
|
|
37
39
|
"./dev-fetcher": {
|
|
38
40
|
"types": "./dist/dev-fetcher.d.ts",
|
|
39
41
|
"import": "./dist/dev-fetcher.js",
|
|
40
|
-
"require": "./dist/dev-fetcher.cjs"
|
|
42
|
+
"require": "./dist/dev-fetcher.cjs",
|
|
43
|
+
"default": "./dist/dev-fetcher.js"
|
|
41
44
|
}
|
|
42
45
|
},
|
|
43
46
|
"files": [
|
|
@@ -45,12 +48,10 @@
|
|
|
45
48
|
"README.md",
|
|
46
49
|
"CHANGELOG.md"
|
|
47
50
|
],
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
"build": "tsup src/index.ts src/react.ts src/dev-fetcher.ts --format esm,cjs --dts --external zod --external react",
|
|
51
|
-
"clean": "rm -rf dist",
|
|
52
|
-
"typecheck": "tsc --project ./tsconfig.json --noEmit"
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
53
|
},
|
|
54
|
+
"sideEffects": false,
|
|
54
55
|
"peerDependencies": {
|
|
55
56
|
"zod": "^3.0.0",
|
|
56
57
|
"react": "^18 || ^19"
|
|
@@ -62,11 +63,20 @@
|
|
|
62
63
|
},
|
|
63
64
|
"dependencies": {},
|
|
64
65
|
"devDependencies": {
|
|
66
|
+
"@types/node": "^20.11.0",
|
|
67
|
+
"@types/react": "^18.3.0",
|
|
65
68
|
"tsup": "^8.1.0",
|
|
66
69
|
"typescript": "^5.5.4",
|
|
67
|
-
"
|
|
70
|
+
"vitest": "^2.0.5",
|
|
71
|
+
"zod": "^3.23.8",
|
|
72
|
+
"@snowcone-app/mockup-url": "0.1.0"
|
|
68
73
|
},
|
|
69
74
|
"engines": {
|
|
70
75
|
"node": ">=18"
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "tsup src/index.ts src/react.ts src/dev-fetcher.ts --format esm,cjs --dts --external zod --external react",
|
|
79
|
+
"clean": "rm -rf dist",
|
|
80
|
+
"typecheck": "tsc --project ./tsconfig.json --noEmit"
|
|
71
81
|
}
|
|
72
|
-
}
|
|
82
|
+
}
|
package/dist/chunk-HOYSZQET.js
DELETED
|
@@ -1,476 +0,0 @@
|
|
|
1
|
-
// src/realtime/websocket.ts
|
|
2
|
-
var RealtimeMockupService = class {
|
|
3
|
-
constructor(wsUrl = "wss://WS_URL_NOT_CONFIGURED.invalid/realtime") {
|
|
4
|
-
this.wsUrl = wsUrl;
|
|
5
|
-
}
|
|
6
|
-
ws = null;
|
|
7
|
-
config = null;
|
|
8
|
-
configSent = false;
|
|
9
|
-
sessionId = null;
|
|
10
|
-
isConfigured = false;
|
|
11
|
-
mockupResults = [];
|
|
12
|
-
status = "Disconnected";
|
|
13
|
-
logs = [];
|
|
14
|
-
lastError = null;
|
|
15
|
-
callbacks = {};
|
|
16
|
-
canvasBlobs = /* @__PURE__ */ new Map();
|
|
17
|
-
colors = /* @__PURE__ */ new Map();
|
|
18
|
-
lastSendTime = {};
|
|
19
|
-
throttleTimeouts = {};
|
|
20
|
-
// Request versioning to detect stale responses
|
|
21
|
-
// Increments each time we send a blob, so we can ignore old server responses
|
|
22
|
-
requestVersion = 0;
|
|
23
|
-
lastSentVersion = 0;
|
|
24
|
-
// Track latest sent version per placement to detect stale responses
|
|
25
|
-
latestSentVersionByPlacement = {};
|
|
26
|
-
// Feature flag: server now supports version in blob message
|
|
27
|
-
sendVersionInBlob = true;
|
|
28
|
-
setCallbacks(callbacks) {
|
|
29
|
-
this.callbacks = callbacks;
|
|
30
|
-
}
|
|
31
|
-
getState() {
|
|
32
|
-
return {
|
|
33
|
-
isConnected: this.ws?.readyState === WebSocket.OPEN,
|
|
34
|
-
sessionId: this.sessionId,
|
|
35
|
-
isConfigured: this.isConfigured,
|
|
36
|
-
mockupResults: [...this.mockupResults],
|
|
37
|
-
status: this.status,
|
|
38
|
-
logs: [...this.logs],
|
|
39
|
-
lastError: this.lastError
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
addLog(message, type = "info") {
|
|
43
|
-
const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
44
|
-
const prefix = type === "sent" ? "\u2192 SENT: " : type === "received" ? "\u2190 RECEIVED: " : "\u2022 ";
|
|
45
|
-
const logMessage = `[${timestamp}] ${prefix}${message}`;
|
|
46
|
-
this.logs.push(logMessage);
|
|
47
|
-
this.callbacks.onLog?.(message, type);
|
|
48
|
-
}
|
|
49
|
-
connect() {
|
|
50
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
this.addLog(`Connecting to ${this.wsUrl}...`);
|
|
54
|
-
this.ws = new WebSocket(this.wsUrl);
|
|
55
|
-
this.ws.onopen = () => {
|
|
56
|
-
this.addLog("WebSocket connection opened");
|
|
57
|
-
this.status = "Connected";
|
|
58
|
-
};
|
|
59
|
-
this.ws.onmessage = (event) => {
|
|
60
|
-
try {
|
|
61
|
-
const data = JSON.parse(event.data);
|
|
62
|
-
this.addLog(JSON.stringify(data, null, 2), "received");
|
|
63
|
-
this.handleMessage(data);
|
|
64
|
-
} catch (error) {
|
|
65
|
-
this.addLog(`Received non-JSON message: ${event.data}`, "received");
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
this.ws.onclose = (event) => {
|
|
69
|
-
this.addLog(`WebSocket connection closed (code: ${event.code})`);
|
|
70
|
-
this.status = "Disconnected";
|
|
71
|
-
this.sessionId = null;
|
|
72
|
-
this.isConfigured = false;
|
|
73
|
-
this.configSent = false;
|
|
74
|
-
this.callbacks.onDisconnected?.();
|
|
75
|
-
};
|
|
76
|
-
this.ws.onerror = (error) => {
|
|
77
|
-
this.addLog(`WebSocket error: ${error}`);
|
|
78
|
-
this.status = "Disconnected";
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
handleMessage(data) {
|
|
82
|
-
switch (data.type) {
|
|
83
|
-
case "connected":
|
|
84
|
-
this.sessionId = data.sessionId || null;
|
|
85
|
-
this.status = `Connected (Session: ${data.sessionId || "N/A"})`;
|
|
86
|
-
this.addLog(`\u2705 Session established: ${data.sessionId}`);
|
|
87
|
-
if (data.sessionId) {
|
|
88
|
-
this.callbacks.onConnected?.(data.sessionId);
|
|
89
|
-
}
|
|
90
|
-
if (this.config && !this.configSent) {
|
|
91
|
-
this.addLog("\u{1F4E4} Auto-sending cached config after connection...");
|
|
92
|
-
setTimeout(() => {
|
|
93
|
-
if (this.config && !this.configSent) {
|
|
94
|
-
this.sendConfig(this.config);
|
|
95
|
-
}
|
|
96
|
-
}, 100);
|
|
97
|
-
}
|
|
98
|
-
break;
|
|
99
|
-
case "config_received":
|
|
100
|
-
case "configured":
|
|
101
|
-
this.isConfigured = true;
|
|
102
|
-
this.status = "Configured";
|
|
103
|
-
this.addLog("\u2705 Configuration accepted! You can now send blobs.");
|
|
104
|
-
this.callbacks.onConfigReceived?.();
|
|
105
|
-
const cachedCanvasCount = this.canvasBlobs.size;
|
|
106
|
-
const cachedColorCount = this.colors.size;
|
|
107
|
-
if (cachedCanvasCount > 0 || cachedColorCount > 0) {
|
|
108
|
-
this.addLog(`\u{1F4E6} Auto-sending cached data: ${cachedCanvasCount} canvas blobs, ${cachedColorCount} colors`);
|
|
109
|
-
setTimeout(() => {
|
|
110
|
-
this.canvasBlobs.forEach((blob, placement) => {
|
|
111
|
-
this.sendCanvasBlob(placement, blob, 1, 0, false);
|
|
112
|
-
});
|
|
113
|
-
this.colors.forEach((color, placement) => {
|
|
114
|
-
this.sendColorBlob(placement, color);
|
|
115
|
-
});
|
|
116
|
-
}, 100);
|
|
117
|
-
}
|
|
118
|
-
break;
|
|
119
|
-
case "blob_received":
|
|
120
|
-
const placementName = data.placement || "unknown";
|
|
121
|
-
const missingCount = data.missingPlacements?.length || 0;
|
|
122
|
-
this.addLog(`\u2705 Blob received for "${placementName}" - ${missingCount} placements still needed`);
|
|
123
|
-
this.callbacks.onBlobReceived?.(placementName);
|
|
124
|
-
break;
|
|
125
|
-
case "have_all_blobs":
|
|
126
|
-
this.addLog("\u{1F389} All blobs received! Starting render...");
|
|
127
|
-
break;
|
|
128
|
-
case "rendering_started":
|
|
129
|
-
this.addLog("\u{1F3A8} Mockup rendering has started...");
|
|
130
|
-
break;
|
|
131
|
-
case "mockup_rendered":
|
|
132
|
-
if (data.imageUrl && data.mockupId) {
|
|
133
|
-
const responseVersion = data.requestVersion;
|
|
134
|
-
const responsePlacement = data.placement;
|
|
135
|
-
if (responseVersion !== void 0 && responsePlacement) {
|
|
136
|
-
const latestVersion = this.latestSentVersionByPlacement[responsePlacement];
|
|
137
|
-
if (latestVersion !== void 0 && responseVersion < latestVersion) {
|
|
138
|
-
this.addLog(`\u23ED\uFE0F Ignoring stale mockup v${responseVersion} for "${responsePlacement}" (latest sent: v${latestVersion})`);
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
const mockupResult = {
|
|
143
|
-
mockupId: data.mockupId,
|
|
144
|
-
imageUrl: data.imageUrl,
|
|
145
|
-
renderUrl: data.renderUrl || data.imageUrl,
|
|
146
|
-
imageSize: data.imageSize || 0,
|
|
147
|
-
requestVersion: responseVersion,
|
|
148
|
-
placement: responsePlacement
|
|
149
|
-
};
|
|
150
|
-
const existingIndex = this.mockupResults.findIndex((m) => m.mockupId === data.mockupId);
|
|
151
|
-
if (existingIndex >= 0) {
|
|
152
|
-
this.mockupResults[existingIndex] = mockupResult;
|
|
153
|
-
} else {
|
|
154
|
-
this.mockupResults.push(mockupResult);
|
|
155
|
-
}
|
|
156
|
-
this.addLog(`\u2705 Mockup rendered v${responseVersion ?? "?"} for "${responsePlacement ?? "?"}" (${data.imageSize} bytes)`);
|
|
157
|
-
this.callbacks.onMockupRendered?.(mockupResult);
|
|
158
|
-
} else {
|
|
159
|
-
const missing = [
|
|
160
|
-
!data.imageUrl && "imageUrl",
|
|
161
|
-
!data.mockupId && "mockupId"
|
|
162
|
-
].filter(Boolean);
|
|
163
|
-
this.addLog(`\u26A0\uFE0F mockup_rendered message dropped: missing required fields [${missing.join(", ")}]. Full data keys: [${Object.keys(data).join(", ")}]`);
|
|
164
|
-
}
|
|
165
|
-
break;
|
|
166
|
-
case "all_mockups_rendered":
|
|
167
|
-
if (data.mockups) {
|
|
168
|
-
const freshMockups = data.mockups.filter((mockup) => {
|
|
169
|
-
if (mockup.requestVersion !== void 0 && mockup.placement) {
|
|
170
|
-
const latestVersion = this.latestSentVersionByPlacement[mockup.placement];
|
|
171
|
-
if (latestVersion !== void 0 && mockup.requestVersion < latestVersion) {
|
|
172
|
-
this.addLog(`\u23ED\uFE0F Filtering stale mockup v${mockup.requestVersion} for "${mockup.placement}" (latest: v${latestVersion})`);
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return true;
|
|
177
|
-
});
|
|
178
|
-
this.mockupResults = freshMockups;
|
|
179
|
-
this.addLog(`\u{1F389} All mockups rendered: ${freshMockups.length} fresh (${data.mockups.length - freshMockups.length} stale filtered)`);
|
|
180
|
-
this.callbacks.onAllMockupsRendered?.(freshMockups);
|
|
181
|
-
} else {
|
|
182
|
-
this.addLog(`\u26A0\uFE0F all_mockups_rendered message dropped: missing 'mockups' array. Full data keys: [${Object.keys(data).join(", ")}]`);
|
|
183
|
-
}
|
|
184
|
-
break;
|
|
185
|
-
case "error":
|
|
186
|
-
const errorMessage = data.message || "Unknown error occurred";
|
|
187
|
-
this.lastError = errorMessage;
|
|
188
|
-
this.addLog(`\u274C Server Error: ${errorMessage}`);
|
|
189
|
-
this.callbacks.onError?.(errorMessage);
|
|
190
|
-
break;
|
|
191
|
-
default:
|
|
192
|
-
this.addLog(`\u{1F914} Unknown message type: ${data.type}`, "received");
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
disconnect() {
|
|
197
|
-
if (this.ws) {
|
|
198
|
-
this.ws.close();
|
|
199
|
-
this.ws = null;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
sendConfig(config) {
|
|
203
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
204
|
-
this.addLog("WebSocket not connected, caching config");
|
|
205
|
-
this.config = config;
|
|
206
|
-
this.configSent = false;
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
if (!this.sessionId) {
|
|
210
|
-
this.addLog("WebSocket connected but no session yet, caching config");
|
|
211
|
-
this.config = config;
|
|
212
|
-
this.configSent = false;
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
const hasConfigChanged = !this.config || JSON.stringify(this.config) !== JSON.stringify(config);
|
|
216
|
-
if (this.configSent && !hasConfigChanged) {
|
|
217
|
-
this.addLog("Config already sent and unchanged, skipping duplicate");
|
|
218
|
-
return false;
|
|
219
|
-
}
|
|
220
|
-
if (this.configSent && hasConfigChanged) {
|
|
221
|
-
const isOnlyMockupIdsChange = this.config && this.config.productId === config.productId && this.config.variantId === config.variantId;
|
|
222
|
-
if (isOnlyMockupIdsChange) {
|
|
223
|
-
this.addLog("\u{1F504} MockupIds changed, keeping cached blobs (server will reuse)");
|
|
224
|
-
this.isConfigured = false;
|
|
225
|
-
this.mockupResults = [];
|
|
226
|
-
} else {
|
|
227
|
-
this.addLog("\u{1F504} Product/variant changed, full reset");
|
|
228
|
-
this.isConfigured = false;
|
|
229
|
-
this.mockupResults = [];
|
|
230
|
-
this.canvasBlobs.clear();
|
|
231
|
-
this.colors.clear();
|
|
232
|
-
this.lastSendTime = {};
|
|
233
|
-
Object.values(this.throttleTimeouts).forEach((timeout) => clearTimeout(timeout));
|
|
234
|
-
this.throttleTimeouts = {};
|
|
235
|
-
this.requestVersion = 0;
|
|
236
|
-
this.lastSentVersion = 0;
|
|
237
|
-
this.latestSentVersionByPlacement = {};
|
|
238
|
-
this.addLog("\u{1F9F9} Cleared all cached canvas/color data for new product");
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
this.config = config;
|
|
242
|
-
this.configSent = true;
|
|
243
|
-
const message = {
|
|
244
|
-
type: "config",
|
|
245
|
-
config
|
|
246
|
-
};
|
|
247
|
-
const messageStr = JSON.stringify(message);
|
|
248
|
-
this.addLog(`\u{1F4E4} Sending config: ${JSON.stringify(message, null, 2)}`, "sent");
|
|
249
|
-
this.ws.send(messageStr);
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Update only the mockupIds without changing other config.
|
|
254
|
-
* Server will use already-cached blobs to render the requested mockups.
|
|
255
|
-
* This is the preferred method for priority-based rendering.
|
|
256
|
-
*/
|
|
257
|
-
updateMockupIds(mockupIds) {
|
|
258
|
-
if (!this.config) {
|
|
259
|
-
this.addLog("Cannot update mockupIds: no config set");
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
263
|
-
this.addLog("Cannot update mockupIds: WebSocket not connected");
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
const updatedConfig = {
|
|
267
|
-
...this.config,
|
|
268
|
-
mockupIds
|
|
269
|
-
};
|
|
270
|
-
this.addLog(`\u{1F3AF} Updating mockupIds to: [${mockupIds.join(", ")}]`);
|
|
271
|
-
return this.sendConfig(updatedConfig);
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Update placementSettings without changing other config.
|
|
275
|
-
* Used to override scaleMode when canvas editor is active.
|
|
276
|
-
*/
|
|
277
|
-
updatePlacementSettings(placementSettings) {
|
|
278
|
-
if (!this.config) {
|
|
279
|
-
this.addLog("Cannot update placementSettings: no config set");
|
|
280
|
-
return false;
|
|
281
|
-
}
|
|
282
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
283
|
-
this.addLog("Cannot update placementSettings: WebSocket not connected");
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
|
-
const updatedConfig = {
|
|
287
|
-
...this.config,
|
|
288
|
-
placementSettings
|
|
289
|
-
};
|
|
290
|
-
return this.sendConfig(updatedConfig);
|
|
291
|
-
}
|
|
292
|
-
sendCanvasBlob(placement, blob, mockupCount = 1, baseThrottleMs = 1e3, notifyCallback = true) {
|
|
293
|
-
this.canvasBlobs.set(placement, blob);
|
|
294
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
if (baseThrottleMs <= 0) {
|
|
298
|
-
this.sendBlobImmediately(placement, blob, notifyCallback);
|
|
299
|
-
this.lastSendTime[placement] = Date.now();
|
|
300
|
-
return true;
|
|
301
|
-
}
|
|
302
|
-
const throttleMs = baseThrottleMs * mockupCount;
|
|
303
|
-
const now = Date.now();
|
|
304
|
-
const lastSendTime = this.lastSendTime[placement] || 0;
|
|
305
|
-
const timeSinceLastSend = now - lastSendTime;
|
|
306
|
-
const hasNeverSent = lastSendTime === 0;
|
|
307
|
-
if (hasNeverSent || timeSinceLastSend >= throttleMs) {
|
|
308
|
-
this.sendBlobImmediately(placement, blob, notifyCallback);
|
|
309
|
-
this.lastSendTime[placement] = now;
|
|
310
|
-
} else if (!this.throttleTimeouts[placement]) {
|
|
311
|
-
const delayTime = throttleMs - timeSinceLastSend;
|
|
312
|
-
this.throttleTimeouts[placement] = setTimeout(() => {
|
|
313
|
-
const latestBlob = this.canvasBlobs.get(placement);
|
|
314
|
-
if (latestBlob && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
|
|
315
|
-
this.sendBlobImmediately(placement, latestBlob, notifyCallback);
|
|
316
|
-
this.lastSendTime[placement] = Date.now();
|
|
317
|
-
}
|
|
318
|
-
delete this.throttleTimeouts[placement];
|
|
319
|
-
}, delayTime);
|
|
320
|
-
}
|
|
321
|
-
return true;
|
|
322
|
-
}
|
|
323
|
-
sendBlobImmediately(placement, blob, notifyCallback = true) {
|
|
324
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
325
|
-
this.lastSentVersion = ++this.requestVersion;
|
|
326
|
-
this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
|
|
327
|
-
const versionToSend = this.lastSentVersion;
|
|
328
|
-
const reader = new FileReader();
|
|
329
|
-
reader.onload = (e) => {
|
|
330
|
-
if (e.target?.result && this.ws) {
|
|
331
|
-
const imageBytes = new Uint8Array(e.target.result);
|
|
332
|
-
let combined;
|
|
333
|
-
if (this.sendVersionInBlob) {
|
|
334
|
-
const headerBytes = new TextEncoder().encode(`${placement}
|
|
335
|
-
${versionToSend}
|
|
336
|
-
`);
|
|
337
|
-
combined = new Uint8Array(headerBytes.length + imageBytes.length);
|
|
338
|
-
combined.set(headerBytes, 0);
|
|
339
|
-
combined.set(imageBytes, headerBytes.length);
|
|
340
|
-
} else {
|
|
341
|
-
const headerBytes = new TextEncoder().encode(`${placement}
|
|
342
|
-
`);
|
|
343
|
-
combined = new Uint8Array(headerBytes.length + imageBytes.length);
|
|
344
|
-
combined.set(headerBytes, 0);
|
|
345
|
-
combined.set(imageBytes, headerBytes.length);
|
|
346
|
-
}
|
|
347
|
-
this.ws.send(combined.buffer);
|
|
348
|
-
this.addLog(`Sent canvas blob for placement "${placement}" (${imageBytes.length} bytes, v${versionToSend})`, "sent");
|
|
349
|
-
if (notifyCallback) {
|
|
350
|
-
this.callbacks.onBlobSent?.(placement);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
reader.readAsArrayBuffer(blob);
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Enable sending version number in blob messages.
|
|
358
|
-
* Call this once the server supports the new format: <placement>\n<version>\n<blob>
|
|
359
|
-
*/
|
|
360
|
-
enableVersionInBlob(enabled = true) {
|
|
361
|
-
this.sendVersionInBlob = enabled;
|
|
362
|
-
this.addLog(`Version in blob ${enabled ? "enabled" : "disabled"}`);
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Flush all pending throttled blobs immediately.
|
|
366
|
-
* Call this when the user finishes an action (e.g., mouse up after drag/flip)
|
|
367
|
-
* to ensure the final state is sent without waiting for throttle.
|
|
368
|
-
*/
|
|
369
|
-
flushPendingBlobs() {
|
|
370
|
-
Object.keys(this.throttleTimeouts).forEach((placement) => {
|
|
371
|
-
clearTimeout(this.throttleTimeouts[placement]);
|
|
372
|
-
delete this.throttleTimeouts[placement];
|
|
373
|
-
});
|
|
374
|
-
if (this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
|
|
375
|
-
this.canvasBlobs.forEach((blob, placement) => {
|
|
376
|
-
this.sendBlobImmediately(placement, blob);
|
|
377
|
-
this.lastSendTime[placement] = Date.now();
|
|
378
|
-
});
|
|
379
|
-
this.addLog(`\u{1F680} Flushed ${this.canvasBlobs.size} pending blob(s)`);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* Check if there are pending blobs waiting to be sent (in throttle queue)
|
|
384
|
-
*/
|
|
385
|
-
hasPendingBlobs() {
|
|
386
|
-
return Object.keys(this.throttleTimeouts).length > 0;
|
|
387
|
-
}
|
|
388
|
-
sendColorBlob(placement, hexColor) {
|
|
389
|
-
this.colors.set(placement, hexColor);
|
|
390
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
|
|
391
|
-
return false;
|
|
392
|
-
}
|
|
393
|
-
const message = placement + "\ncolor:" + hexColor;
|
|
394
|
-
const messageBytes = new TextEncoder().encode(message);
|
|
395
|
-
this.ws.send(messageBytes.buffer);
|
|
396
|
-
this.addLog(`Sent color "${hexColor}" for placement "${placement}"`, "sent");
|
|
397
|
-
return true;
|
|
398
|
-
}
|
|
399
|
-
createEmptyCanvasBlob(width = 400, height = 400) {
|
|
400
|
-
return new Promise((resolve, reject) => {
|
|
401
|
-
const canvas = document.createElement("canvas");
|
|
402
|
-
canvas.width = width;
|
|
403
|
-
canvas.height = height;
|
|
404
|
-
const ctx = canvas.getContext("2d");
|
|
405
|
-
if (!ctx) {
|
|
406
|
-
reject(new Error("Could not get canvas context"));
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
ctx.fillStyle = "white";
|
|
410
|
-
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
411
|
-
canvas.toBlob(
|
|
412
|
-
(blob) => {
|
|
413
|
-
if (blob) {
|
|
414
|
-
resolve(blob);
|
|
415
|
-
} else {
|
|
416
|
-
reject(new Error("Could not create blob from canvas"));
|
|
417
|
-
}
|
|
418
|
-
},
|
|
419
|
-
"image/png",
|
|
420
|
-
0.8
|
|
421
|
-
);
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
async sendInitialEmptyCanvases(placements) {
|
|
425
|
-
this.addLog(`\u{1F3A8} Creating initial empty canvases for ${placements.length} placements...`);
|
|
426
|
-
let emptyCanvasCount = 0;
|
|
427
|
-
let preservedCanvasCount = 0;
|
|
428
|
-
for (const placement of placements) {
|
|
429
|
-
try {
|
|
430
|
-
const existingBlob = this.canvasBlobs.get(placement.label);
|
|
431
|
-
if (existingBlob) {
|
|
432
|
-
this.addLog(`\u{1F3AF} Preserving existing canvas for "${placement.label}" (${existingBlob.size} bytes)`);
|
|
433
|
-
preservedCanvasCount++;
|
|
434
|
-
if (this.isConfigured) {
|
|
435
|
-
this.sendCanvasBlob(placement.label, existingBlob);
|
|
436
|
-
}
|
|
437
|
-
} else {
|
|
438
|
-
const emptyBlob = await this.createEmptyCanvasBlob(
|
|
439
|
-
placement.width || 400,
|
|
440
|
-
placement.height || 400
|
|
441
|
-
);
|
|
442
|
-
this.canvasBlobs.set(placement.label, emptyBlob);
|
|
443
|
-
this.addLog(`\u2705 Created empty canvas for "${placement.label}" (${emptyBlob.size} bytes)`);
|
|
444
|
-
emptyCanvasCount++;
|
|
445
|
-
if (this.isConfigured) {
|
|
446
|
-
this.sendCanvasBlob(placement.label, emptyBlob);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
} catch (error) {
|
|
450
|
-
this.addLog(`\u274C Failed to process canvas for "${placement.label}": ${error}`);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
this.addLog(`\u{1F389} Canvas processing complete: ${preservedCanvasCount} preserved, ${emptyCanvasCount} created empty`);
|
|
454
|
-
}
|
|
455
|
-
setInitialData(canvasBlobs, colors) {
|
|
456
|
-
const existingCanvasCount = this.canvasBlobs.size;
|
|
457
|
-
if (existingCanvasCount === 0) {
|
|
458
|
-
this.canvasBlobs = new Map(canvasBlobs);
|
|
459
|
-
this.addLog(`Set initial canvas blobs: ${canvasBlobs.size} new blobs`);
|
|
460
|
-
} else {
|
|
461
|
-
this.addLog(`Preserving existing canvas blobs: ${existingCanvasCount} blobs (ignoring ${canvasBlobs.size} new empty blobs)`);
|
|
462
|
-
}
|
|
463
|
-
this.colors = new Map(colors);
|
|
464
|
-
this.addLog(`Set initial colors: ${colors.size} colors`);
|
|
465
|
-
}
|
|
466
|
-
clearLogs() {
|
|
467
|
-
this.logs = [];
|
|
468
|
-
}
|
|
469
|
-
clearMockups() {
|
|
470
|
-
this.mockupResults = [];
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
export {
|
|
475
|
-
RealtimeMockupService
|
|
476
|
-
};
|