@snowcone-app/sdk 0.1.12 → 0.1.14

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.
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowcone-app/sdk",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
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
- "sideEffects": false,
49
- "scripts": {
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
- "zod": "^3.23.8"
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
+ }
@@ -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
- };