@snowcone-app/sdk 0.1.10
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 +136 -0
- package/README.md +195 -0
- package/dist/chunk-7VO4EL2V.js +12 -0
- package/dist/chunk-HOYSZQET.js +476 -0
- package/dist/chunk-IIUCW2O4.js +457 -0
- package/dist/chunk-UJFJ7REN.js +485 -0
- package/dist/dev-fetcher.cjs +36 -0
- package/dist/dev-fetcher.d.cts +3 -0
- package/dist/dev-fetcher.d.ts +3 -0
- package/dist/dev-fetcher.js +6 -0
- package/dist/index.cjs +5055 -0
- package/dist/index.d.cts +2437 -0
- package/dist/index.d.ts +2437 -0
- package/dist/index.js +4424 -0
- package/dist/react.cjs +755 -0
- package/dist/react.d.cts +96 -0
- package/dist/react.d.ts +96 -0
- package/dist/react.js +245 -0
- package/dist/websocket-B8_XAwWx.d.cts +336 -0
- package/dist/websocket-B8_XAwWx.d.ts +336 -0
- package/dist/websocket-GXMYofWp.d.cts +330 -0
- package/dist/websocket-GXMYofWp.d.ts +330 -0
- package/package.json +72 -0
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/react.ts
|
|
21
|
+
var react_exports = {};
|
|
22
|
+
__export(react_exports, {
|
|
23
|
+
ReactAdapter: () => ReactAdapter,
|
|
24
|
+
createReactComponent: () => createReactComponent,
|
|
25
|
+
useReactAdapter: () => useFrameworkAdapter,
|
|
26
|
+
useRealtimeMockup: () => useRealtimeMockup
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(react_exports);
|
|
29
|
+
|
|
30
|
+
// src/realtime/react.ts
|
|
31
|
+
var import_react = require("react");
|
|
32
|
+
|
|
33
|
+
// src/realtime/websocket.ts
|
|
34
|
+
var RealtimeMockupService = class {
|
|
35
|
+
constructor(wsUrl = "wss://WS_URL_NOT_CONFIGURED.invalid/realtime") {
|
|
36
|
+
this.wsUrl = wsUrl;
|
|
37
|
+
}
|
|
38
|
+
ws = null;
|
|
39
|
+
config = null;
|
|
40
|
+
configSent = false;
|
|
41
|
+
sessionId = null;
|
|
42
|
+
isConfigured = false;
|
|
43
|
+
mockupResults = [];
|
|
44
|
+
status = "Disconnected";
|
|
45
|
+
logs = [];
|
|
46
|
+
lastError = null;
|
|
47
|
+
callbacks = {};
|
|
48
|
+
canvasBlobs = /* @__PURE__ */ new Map();
|
|
49
|
+
colors = /* @__PURE__ */ new Map();
|
|
50
|
+
lastSendTime = {};
|
|
51
|
+
throttleTimeouts = {};
|
|
52
|
+
// Request versioning to detect stale responses
|
|
53
|
+
// Increments each time we send a blob, so we can ignore old server responses
|
|
54
|
+
requestVersion = 0;
|
|
55
|
+
lastSentVersion = 0;
|
|
56
|
+
// Track latest sent version per placement to detect stale responses
|
|
57
|
+
latestSentVersionByPlacement = {};
|
|
58
|
+
// Feature flag: server now supports version in blob message
|
|
59
|
+
sendVersionInBlob = true;
|
|
60
|
+
setCallbacks(callbacks) {
|
|
61
|
+
this.callbacks = callbacks;
|
|
62
|
+
}
|
|
63
|
+
getState() {
|
|
64
|
+
return {
|
|
65
|
+
isConnected: this.ws?.readyState === WebSocket.OPEN,
|
|
66
|
+
sessionId: this.sessionId,
|
|
67
|
+
isConfigured: this.isConfigured,
|
|
68
|
+
mockupResults: [...this.mockupResults],
|
|
69
|
+
status: this.status,
|
|
70
|
+
logs: [...this.logs],
|
|
71
|
+
lastError: this.lastError
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
addLog(message, type = "info") {
|
|
75
|
+
const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
76
|
+
const prefix = type === "sent" ? "\u2192 SENT: " : type === "received" ? "\u2190 RECEIVED: " : "\u2022 ";
|
|
77
|
+
const logMessage = `[${timestamp}] ${prefix}${message}`;
|
|
78
|
+
this.logs.push(logMessage);
|
|
79
|
+
this.callbacks.onLog?.(message, type);
|
|
80
|
+
}
|
|
81
|
+
connect() {
|
|
82
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.addLog(`Connecting to ${this.wsUrl}...`);
|
|
86
|
+
this.ws = new WebSocket(this.wsUrl);
|
|
87
|
+
this.ws.onopen = () => {
|
|
88
|
+
this.addLog("WebSocket connection opened");
|
|
89
|
+
this.status = "Connected";
|
|
90
|
+
};
|
|
91
|
+
this.ws.onmessage = (event) => {
|
|
92
|
+
try {
|
|
93
|
+
const data = JSON.parse(event.data);
|
|
94
|
+
this.addLog(JSON.stringify(data, null, 2), "received");
|
|
95
|
+
this.handleMessage(data);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
this.addLog(`Received non-JSON message: ${event.data}`, "received");
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
this.ws.onclose = (event) => {
|
|
101
|
+
this.addLog(`WebSocket connection closed (code: ${event.code})`);
|
|
102
|
+
this.status = "Disconnected";
|
|
103
|
+
this.sessionId = null;
|
|
104
|
+
this.isConfigured = false;
|
|
105
|
+
this.configSent = false;
|
|
106
|
+
this.callbacks.onDisconnected?.();
|
|
107
|
+
};
|
|
108
|
+
this.ws.onerror = (error) => {
|
|
109
|
+
this.addLog(`WebSocket error: ${error}`);
|
|
110
|
+
this.status = "Disconnected";
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
handleMessage(data) {
|
|
114
|
+
switch (data.type) {
|
|
115
|
+
case "connected":
|
|
116
|
+
this.sessionId = data.sessionId || null;
|
|
117
|
+
this.status = `Connected (Session: ${data.sessionId || "N/A"})`;
|
|
118
|
+
this.addLog(`\u2705 Session established: ${data.sessionId}`);
|
|
119
|
+
if (data.sessionId) {
|
|
120
|
+
this.callbacks.onConnected?.(data.sessionId);
|
|
121
|
+
}
|
|
122
|
+
if (this.config && !this.configSent) {
|
|
123
|
+
this.addLog("\u{1F4E4} Auto-sending cached config after connection...");
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
if (this.config && !this.configSent) {
|
|
126
|
+
this.sendConfig(this.config);
|
|
127
|
+
}
|
|
128
|
+
}, 100);
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
case "config_received":
|
|
132
|
+
case "configured":
|
|
133
|
+
this.isConfigured = true;
|
|
134
|
+
this.status = "Configured";
|
|
135
|
+
this.addLog("\u2705 Configuration accepted! You can now send blobs.");
|
|
136
|
+
this.callbacks.onConfigReceived?.();
|
|
137
|
+
const cachedCanvasCount = this.canvasBlobs.size;
|
|
138
|
+
const cachedColorCount = this.colors.size;
|
|
139
|
+
if (cachedCanvasCount > 0 || cachedColorCount > 0) {
|
|
140
|
+
this.addLog(`\u{1F4E6} Auto-sending cached data: ${cachedCanvasCount} canvas blobs, ${cachedColorCount} colors`);
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
this.canvasBlobs.forEach((blob, placement) => {
|
|
143
|
+
this.sendCanvasBlob(placement, blob, 1, 0, false);
|
|
144
|
+
});
|
|
145
|
+
this.colors.forEach((color, placement) => {
|
|
146
|
+
this.sendColorBlob(placement, color);
|
|
147
|
+
});
|
|
148
|
+
}, 100);
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
case "blob_received":
|
|
152
|
+
const placementName = data.placement || "unknown";
|
|
153
|
+
const missingCount = data.missingPlacements?.length || 0;
|
|
154
|
+
this.addLog(`\u2705 Blob received for "${placementName}" - ${missingCount} placements still needed`);
|
|
155
|
+
this.callbacks.onBlobReceived?.(placementName);
|
|
156
|
+
break;
|
|
157
|
+
case "have_all_blobs":
|
|
158
|
+
this.addLog("\u{1F389} All blobs received! Starting render...");
|
|
159
|
+
break;
|
|
160
|
+
case "rendering_started":
|
|
161
|
+
this.addLog("\u{1F3A8} Mockup rendering has started...");
|
|
162
|
+
break;
|
|
163
|
+
case "mockup_rendered":
|
|
164
|
+
if (data.imageUrl && data.mockupId) {
|
|
165
|
+
const responseVersion = data.requestVersion;
|
|
166
|
+
const responsePlacement = data.placement;
|
|
167
|
+
if (responseVersion !== void 0 && responsePlacement) {
|
|
168
|
+
const latestVersion = this.latestSentVersionByPlacement[responsePlacement];
|
|
169
|
+
if (latestVersion !== void 0 && responseVersion < latestVersion) {
|
|
170
|
+
this.addLog(`\u23ED\uFE0F Ignoring stale mockup v${responseVersion} for "${responsePlacement}" (latest sent: v${latestVersion})`);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const mockupResult = {
|
|
175
|
+
mockupId: data.mockupId,
|
|
176
|
+
imageUrl: data.imageUrl,
|
|
177
|
+
renderUrl: data.renderUrl || data.imageUrl,
|
|
178
|
+
imageSize: data.imageSize || 0,
|
|
179
|
+
requestVersion: responseVersion,
|
|
180
|
+
placement: responsePlacement
|
|
181
|
+
};
|
|
182
|
+
const existingIndex = this.mockupResults.findIndex((m) => m.mockupId === data.mockupId);
|
|
183
|
+
if (existingIndex >= 0) {
|
|
184
|
+
this.mockupResults[existingIndex] = mockupResult;
|
|
185
|
+
} else {
|
|
186
|
+
this.mockupResults.push(mockupResult);
|
|
187
|
+
}
|
|
188
|
+
this.addLog(`\u2705 Mockup rendered v${responseVersion ?? "?"} for "${responsePlacement ?? "?"}" (${data.imageSize} bytes)`);
|
|
189
|
+
this.callbacks.onMockupRendered?.(mockupResult);
|
|
190
|
+
} else {
|
|
191
|
+
const missing = [
|
|
192
|
+
!data.imageUrl && "imageUrl",
|
|
193
|
+
!data.mockupId && "mockupId"
|
|
194
|
+
].filter(Boolean);
|
|
195
|
+
this.addLog(`\u26A0\uFE0F mockup_rendered message dropped: missing required fields [${missing.join(", ")}]. Full data keys: [${Object.keys(data).join(", ")}]`);
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
case "all_mockups_rendered":
|
|
199
|
+
if (data.mockups) {
|
|
200
|
+
const freshMockups = data.mockups.filter((mockup) => {
|
|
201
|
+
if (mockup.requestVersion !== void 0 && mockup.placement) {
|
|
202
|
+
const latestVersion = this.latestSentVersionByPlacement[mockup.placement];
|
|
203
|
+
if (latestVersion !== void 0 && mockup.requestVersion < latestVersion) {
|
|
204
|
+
this.addLog(`\u23ED\uFE0F Filtering stale mockup v${mockup.requestVersion} for "${mockup.placement}" (latest: v${latestVersion})`);
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
});
|
|
210
|
+
this.mockupResults = freshMockups;
|
|
211
|
+
this.addLog(`\u{1F389} All mockups rendered: ${freshMockups.length} fresh (${data.mockups.length - freshMockups.length} stale filtered)`);
|
|
212
|
+
this.callbacks.onAllMockupsRendered?.(freshMockups);
|
|
213
|
+
} else {
|
|
214
|
+
this.addLog(`\u26A0\uFE0F all_mockups_rendered message dropped: missing 'mockups' array. Full data keys: [${Object.keys(data).join(", ")}]`);
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
case "error":
|
|
218
|
+
const errorMessage = data.message || "Unknown error occurred";
|
|
219
|
+
this.lastError = errorMessage;
|
|
220
|
+
this.addLog(`\u274C Server Error: ${errorMessage}`);
|
|
221
|
+
this.callbacks.onError?.(errorMessage);
|
|
222
|
+
break;
|
|
223
|
+
default:
|
|
224
|
+
this.addLog(`\u{1F914} Unknown message type: ${data.type}`, "received");
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
disconnect() {
|
|
229
|
+
if (this.ws) {
|
|
230
|
+
this.ws.close();
|
|
231
|
+
this.ws = null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
sendConfig(config) {
|
|
235
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
236
|
+
this.addLog("WebSocket not connected, caching config");
|
|
237
|
+
this.config = config;
|
|
238
|
+
this.configSent = false;
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if (!this.sessionId) {
|
|
242
|
+
this.addLog("WebSocket connected but no session yet, caching config");
|
|
243
|
+
this.config = config;
|
|
244
|
+
this.configSent = false;
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
const hasConfigChanged = !this.config || JSON.stringify(this.config) !== JSON.stringify(config);
|
|
248
|
+
if (this.configSent && !hasConfigChanged) {
|
|
249
|
+
this.addLog("Config already sent and unchanged, skipping duplicate");
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
if (this.configSent && hasConfigChanged) {
|
|
253
|
+
const isOnlyMockupIdsChange = this.config && this.config.productId === config.productId && this.config.variantId === config.variantId;
|
|
254
|
+
if (isOnlyMockupIdsChange) {
|
|
255
|
+
this.addLog("\u{1F504} MockupIds changed, keeping cached blobs (server will reuse)");
|
|
256
|
+
this.isConfigured = false;
|
|
257
|
+
this.mockupResults = [];
|
|
258
|
+
} else {
|
|
259
|
+
this.addLog("\u{1F504} Product/variant changed, full reset");
|
|
260
|
+
this.isConfigured = false;
|
|
261
|
+
this.mockupResults = [];
|
|
262
|
+
this.canvasBlobs.clear();
|
|
263
|
+
this.colors.clear();
|
|
264
|
+
this.lastSendTime = {};
|
|
265
|
+
Object.values(this.throttleTimeouts).forEach((timeout) => clearTimeout(timeout));
|
|
266
|
+
this.throttleTimeouts = {};
|
|
267
|
+
this.requestVersion = 0;
|
|
268
|
+
this.lastSentVersion = 0;
|
|
269
|
+
this.latestSentVersionByPlacement = {};
|
|
270
|
+
this.addLog("\u{1F9F9} Cleared all cached canvas/color data for new product");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
this.config = config;
|
|
274
|
+
this.configSent = true;
|
|
275
|
+
const message = {
|
|
276
|
+
type: "config",
|
|
277
|
+
config
|
|
278
|
+
};
|
|
279
|
+
const messageStr = JSON.stringify(message);
|
|
280
|
+
this.addLog(`\u{1F4E4} Sending config: ${JSON.stringify(message, null, 2)}`, "sent");
|
|
281
|
+
this.ws.send(messageStr);
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Update only the mockupIds without changing other config.
|
|
286
|
+
* Server will use already-cached blobs to render the requested mockups.
|
|
287
|
+
* This is the preferred method for priority-based rendering.
|
|
288
|
+
*/
|
|
289
|
+
updateMockupIds(mockupIds) {
|
|
290
|
+
if (!this.config) {
|
|
291
|
+
this.addLog("Cannot update mockupIds: no config set");
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
295
|
+
this.addLog("Cannot update mockupIds: WebSocket not connected");
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
const updatedConfig = {
|
|
299
|
+
...this.config,
|
|
300
|
+
mockupIds
|
|
301
|
+
};
|
|
302
|
+
this.addLog(`\u{1F3AF} Updating mockupIds to: [${mockupIds.join(", ")}]`);
|
|
303
|
+
return this.sendConfig(updatedConfig);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Update placementSettings without changing other config.
|
|
307
|
+
* Used to override scaleMode when canvas editor is active.
|
|
308
|
+
*/
|
|
309
|
+
updatePlacementSettings(placementSettings) {
|
|
310
|
+
if (!this.config) {
|
|
311
|
+
this.addLog("Cannot update placementSettings: no config set");
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
315
|
+
this.addLog("Cannot update placementSettings: WebSocket not connected");
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
const updatedConfig = {
|
|
319
|
+
...this.config,
|
|
320
|
+
placementSettings
|
|
321
|
+
};
|
|
322
|
+
return this.sendConfig(updatedConfig);
|
|
323
|
+
}
|
|
324
|
+
sendCanvasBlob(placement, blob, mockupCount = 1, baseThrottleMs = 1e3, notifyCallback = true) {
|
|
325
|
+
this.canvasBlobs.set(placement, blob);
|
|
326
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
if (baseThrottleMs <= 0) {
|
|
330
|
+
this.sendBlobImmediately(placement, blob, notifyCallback);
|
|
331
|
+
this.lastSendTime[placement] = Date.now();
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
const throttleMs = baseThrottleMs * mockupCount;
|
|
335
|
+
const now = Date.now();
|
|
336
|
+
const lastSendTime = this.lastSendTime[placement] || 0;
|
|
337
|
+
const timeSinceLastSend = now - lastSendTime;
|
|
338
|
+
const hasNeverSent = lastSendTime === 0;
|
|
339
|
+
if (hasNeverSent || timeSinceLastSend >= throttleMs) {
|
|
340
|
+
this.sendBlobImmediately(placement, blob, notifyCallback);
|
|
341
|
+
this.lastSendTime[placement] = now;
|
|
342
|
+
} else if (!this.throttleTimeouts[placement]) {
|
|
343
|
+
const delayTime = throttleMs - timeSinceLastSend;
|
|
344
|
+
this.throttleTimeouts[placement] = setTimeout(() => {
|
|
345
|
+
const latestBlob = this.canvasBlobs.get(placement);
|
|
346
|
+
if (latestBlob && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
|
|
347
|
+
this.sendBlobImmediately(placement, latestBlob, notifyCallback);
|
|
348
|
+
this.lastSendTime[placement] = Date.now();
|
|
349
|
+
}
|
|
350
|
+
delete this.throttleTimeouts[placement];
|
|
351
|
+
}, delayTime);
|
|
352
|
+
}
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
sendBlobImmediately(placement, blob, notifyCallback = true) {
|
|
356
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
357
|
+
this.lastSentVersion = ++this.requestVersion;
|
|
358
|
+
this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
|
|
359
|
+
const versionToSend = this.lastSentVersion;
|
|
360
|
+
const reader = new FileReader();
|
|
361
|
+
reader.onload = (e) => {
|
|
362
|
+
if (e.target?.result && this.ws) {
|
|
363
|
+
const imageBytes = new Uint8Array(e.target.result);
|
|
364
|
+
let combined;
|
|
365
|
+
if (this.sendVersionInBlob) {
|
|
366
|
+
const headerBytes = new TextEncoder().encode(`${placement}
|
|
367
|
+
${versionToSend}
|
|
368
|
+
`);
|
|
369
|
+
combined = new Uint8Array(headerBytes.length + imageBytes.length);
|
|
370
|
+
combined.set(headerBytes, 0);
|
|
371
|
+
combined.set(imageBytes, headerBytes.length);
|
|
372
|
+
} else {
|
|
373
|
+
const headerBytes = new TextEncoder().encode(`${placement}
|
|
374
|
+
`);
|
|
375
|
+
combined = new Uint8Array(headerBytes.length + imageBytes.length);
|
|
376
|
+
combined.set(headerBytes, 0);
|
|
377
|
+
combined.set(imageBytes, headerBytes.length);
|
|
378
|
+
}
|
|
379
|
+
this.ws.send(combined.buffer);
|
|
380
|
+
this.addLog(`Sent canvas blob for placement "${placement}" (${imageBytes.length} bytes, v${versionToSend})`, "sent");
|
|
381
|
+
if (notifyCallback) {
|
|
382
|
+
this.callbacks.onBlobSent?.(placement);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
reader.readAsArrayBuffer(blob);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Enable sending version number in blob messages.
|
|
390
|
+
* Call this once the server supports the new format: <placement>\n<version>\n<blob>
|
|
391
|
+
*/
|
|
392
|
+
enableVersionInBlob(enabled = true) {
|
|
393
|
+
this.sendVersionInBlob = enabled;
|
|
394
|
+
this.addLog(`Version in blob ${enabled ? "enabled" : "disabled"}`);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Flush all pending throttled blobs immediately.
|
|
398
|
+
* Call this when the user finishes an action (e.g., mouse up after drag/flip)
|
|
399
|
+
* to ensure the final state is sent without waiting for throttle.
|
|
400
|
+
*/
|
|
401
|
+
flushPendingBlobs() {
|
|
402
|
+
Object.keys(this.throttleTimeouts).forEach((placement) => {
|
|
403
|
+
clearTimeout(this.throttleTimeouts[placement]);
|
|
404
|
+
delete this.throttleTimeouts[placement];
|
|
405
|
+
});
|
|
406
|
+
if (this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
|
|
407
|
+
this.canvasBlobs.forEach((blob, placement) => {
|
|
408
|
+
this.sendBlobImmediately(placement, blob);
|
|
409
|
+
this.lastSendTime[placement] = Date.now();
|
|
410
|
+
});
|
|
411
|
+
this.addLog(`\u{1F680} Flushed ${this.canvasBlobs.size} pending blob(s)`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Check if there are pending blobs waiting to be sent (in throttle queue)
|
|
416
|
+
*/
|
|
417
|
+
hasPendingBlobs() {
|
|
418
|
+
return Object.keys(this.throttleTimeouts).length > 0;
|
|
419
|
+
}
|
|
420
|
+
sendColorBlob(placement, hexColor) {
|
|
421
|
+
this.colors.set(placement, hexColor);
|
|
422
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
const message = placement + "\ncolor:" + hexColor;
|
|
426
|
+
const messageBytes = new TextEncoder().encode(message);
|
|
427
|
+
this.ws.send(messageBytes.buffer);
|
|
428
|
+
this.addLog(`Sent color "${hexColor}" for placement "${placement}"`, "sent");
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
createEmptyCanvasBlob(width = 400, height = 400) {
|
|
432
|
+
return new Promise((resolve, reject) => {
|
|
433
|
+
const canvas = document.createElement("canvas");
|
|
434
|
+
canvas.width = width;
|
|
435
|
+
canvas.height = height;
|
|
436
|
+
const ctx = canvas.getContext("2d");
|
|
437
|
+
if (!ctx) {
|
|
438
|
+
reject(new Error("Could not get canvas context"));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
ctx.fillStyle = "white";
|
|
442
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
443
|
+
canvas.toBlob(
|
|
444
|
+
(blob) => {
|
|
445
|
+
if (blob) {
|
|
446
|
+
resolve(blob);
|
|
447
|
+
} else {
|
|
448
|
+
reject(new Error("Could not create blob from canvas"));
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
"image/png",
|
|
452
|
+
0.8
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
async sendInitialEmptyCanvases(placements) {
|
|
457
|
+
this.addLog(`\u{1F3A8} Creating initial empty canvases for ${placements.length} placements...`);
|
|
458
|
+
let emptyCanvasCount = 0;
|
|
459
|
+
let preservedCanvasCount = 0;
|
|
460
|
+
for (const placement of placements) {
|
|
461
|
+
try {
|
|
462
|
+
const existingBlob = this.canvasBlobs.get(placement.label);
|
|
463
|
+
if (existingBlob) {
|
|
464
|
+
this.addLog(`\u{1F3AF} Preserving existing canvas for "${placement.label}" (${existingBlob.size} bytes)`);
|
|
465
|
+
preservedCanvasCount++;
|
|
466
|
+
if (this.isConfigured) {
|
|
467
|
+
this.sendCanvasBlob(placement.label, existingBlob);
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
const emptyBlob = await this.createEmptyCanvasBlob(
|
|
471
|
+
placement.width || 400,
|
|
472
|
+
placement.height || 400
|
|
473
|
+
);
|
|
474
|
+
const blobCachedDuringAwait = this.canvasBlobs.get(placement.label);
|
|
475
|
+
if (blobCachedDuringAwait) {
|
|
476
|
+
this.addLog(`\u{1F3AF} Skipping empty canvas for "${placement.label}" \u2014 real blob was cached during creation`);
|
|
477
|
+
preservedCanvasCount++;
|
|
478
|
+
if (this.isConfigured) {
|
|
479
|
+
this.sendCanvasBlob(placement.label, blobCachedDuringAwait);
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
this.canvasBlobs.set(placement.label, emptyBlob);
|
|
483
|
+
this.addLog(`\u2705 Created empty canvas for "${placement.label}" (${emptyBlob.size} bytes)`);
|
|
484
|
+
emptyCanvasCount++;
|
|
485
|
+
if (this.isConfigured) {
|
|
486
|
+
this.sendCanvasBlob(placement.label, emptyBlob);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} catch (error) {
|
|
491
|
+
this.addLog(`\u274C Failed to process canvas for "${placement.label}": ${error}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
this.addLog(`\u{1F389} Canvas processing complete: ${preservedCanvasCount} preserved, ${emptyCanvasCount} created empty`);
|
|
495
|
+
}
|
|
496
|
+
setInitialData(canvasBlobs, colors) {
|
|
497
|
+
const existingCanvasCount = this.canvasBlobs.size;
|
|
498
|
+
if (existingCanvasCount === 0) {
|
|
499
|
+
this.canvasBlobs = new Map(canvasBlobs);
|
|
500
|
+
this.addLog(`Set initial canvas blobs: ${canvasBlobs.size} new blobs`);
|
|
501
|
+
} else {
|
|
502
|
+
this.addLog(`Preserving existing canvas blobs: ${existingCanvasCount} blobs (ignoring ${canvasBlobs.size} new empty blobs)`);
|
|
503
|
+
}
|
|
504
|
+
this.colors = new Map(colors);
|
|
505
|
+
this.addLog(`Set initial colors: ${colors.size} colors`);
|
|
506
|
+
}
|
|
507
|
+
clearLogs() {
|
|
508
|
+
this.logs = [];
|
|
509
|
+
}
|
|
510
|
+
clearMockups() {
|
|
511
|
+
this.mockupResults = [];
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// src/realtime/react.ts
|
|
516
|
+
function useRealtimeMockup(options = {}) {
|
|
517
|
+
const serviceRef = (0, import_react.useRef)(null);
|
|
518
|
+
const [state, setState] = (0, import_react.useState)({
|
|
519
|
+
isConnected: false,
|
|
520
|
+
sessionId: null,
|
|
521
|
+
isConfigured: false,
|
|
522
|
+
mockupResults: [],
|
|
523
|
+
status: "Disconnected",
|
|
524
|
+
logs: [],
|
|
525
|
+
lastError: null
|
|
526
|
+
});
|
|
527
|
+
(0, import_react.useEffect)(() => {
|
|
528
|
+
const service = new RealtimeMockupService(options.wsUrl);
|
|
529
|
+
serviceRef.current = service;
|
|
530
|
+
service.setCallbacks({
|
|
531
|
+
onConnected: (sessionId) => {
|
|
532
|
+
setState(service.getState());
|
|
533
|
+
options.onConnected?.(sessionId);
|
|
534
|
+
},
|
|
535
|
+
onDisconnected: () => {
|
|
536
|
+
setState(service.getState());
|
|
537
|
+
options.onDisconnected?.();
|
|
538
|
+
},
|
|
539
|
+
onConfigReceived: () => {
|
|
540
|
+
setState(service.getState());
|
|
541
|
+
options.onConfigReceived?.();
|
|
542
|
+
},
|
|
543
|
+
onBlobReceived: (placement) => {
|
|
544
|
+
options.onBlobReceived?.(placement);
|
|
545
|
+
},
|
|
546
|
+
onBlobSent: (placement) => {
|
|
547
|
+
options.onBlobSent?.(placement);
|
|
548
|
+
},
|
|
549
|
+
onMockupRendered: (result) => {
|
|
550
|
+
setState(service.getState());
|
|
551
|
+
options.onMockupRendered?.(result);
|
|
552
|
+
},
|
|
553
|
+
onAllMockupsRendered: (results) => {
|
|
554
|
+
setState(service.getState());
|
|
555
|
+
options.onAllMockupsRendered?.(results);
|
|
556
|
+
},
|
|
557
|
+
onError: (error) => {
|
|
558
|
+
setState(service.getState());
|
|
559
|
+
options.onError?.(error);
|
|
560
|
+
}
|
|
561
|
+
// REMOVED: onLog callback was causing setState on every log message,
|
|
562
|
+
// which triggered re-renders during canvas drag operations
|
|
563
|
+
});
|
|
564
|
+
return () => {
|
|
565
|
+
service.disconnect();
|
|
566
|
+
};
|
|
567
|
+
}, [options.wsUrl]);
|
|
568
|
+
const connect = (0, import_react.useCallback)(() => {
|
|
569
|
+
serviceRef.current?.connect();
|
|
570
|
+
}, []);
|
|
571
|
+
const disconnect = (0, import_react.useCallback)(() => {
|
|
572
|
+
serviceRef.current?.disconnect();
|
|
573
|
+
}, []);
|
|
574
|
+
const sendConfig = (0, import_react.useCallback)((config) => {
|
|
575
|
+
return serviceRef.current?.sendConfig(config) || false;
|
|
576
|
+
}, []);
|
|
577
|
+
const updateMockupIds = (0, import_react.useCallback)((mockupIds) => {
|
|
578
|
+
return serviceRef.current?.updateMockupIds(mockupIds) || false;
|
|
579
|
+
}, []);
|
|
580
|
+
const updatePlacementSettings = (0, import_react.useCallback)((settings) => {
|
|
581
|
+
return serviceRef.current?.updatePlacementSettings(settings) || false;
|
|
582
|
+
}, []);
|
|
583
|
+
const sendCanvasBlob = (0, import_react.useCallback)((placement, blob, mockupCount, baseThrottleMs) => {
|
|
584
|
+
return serviceRef.current?.sendCanvasBlob(placement, blob, mockupCount, baseThrottleMs) || false;
|
|
585
|
+
}, []);
|
|
586
|
+
const sendColorBlob = (0, import_react.useCallback)((placement, hexColor) => {
|
|
587
|
+
return serviceRef.current?.sendColorBlob(placement, hexColor) || false;
|
|
588
|
+
}, []);
|
|
589
|
+
const createEmptyCanvasBlob = (0, import_react.useCallback)((width, height) => {
|
|
590
|
+
return serviceRef.current?.createEmptyCanvasBlob(width, height) || Promise.reject(new Error("Service not initialized"));
|
|
591
|
+
}, []);
|
|
592
|
+
const sendInitialEmptyCanvases = (0, import_react.useCallback)(async (placements) => {
|
|
593
|
+
await serviceRef.current?.sendInitialEmptyCanvases(placements);
|
|
594
|
+
}, []);
|
|
595
|
+
const setInitialData = (0, import_react.useCallback)((canvasBlobs, colors) => {
|
|
596
|
+
serviceRef.current?.setInitialData(canvasBlobs, colors);
|
|
597
|
+
}, []);
|
|
598
|
+
const clearLogs = (0, import_react.useCallback)(() => {
|
|
599
|
+
serviceRef.current?.clearLogs();
|
|
600
|
+
setState(serviceRef.current?.getState() || state);
|
|
601
|
+
}, [state]);
|
|
602
|
+
const clearMockups = (0, import_react.useCallback)(() => {
|
|
603
|
+
serviceRef.current?.clearMockups();
|
|
604
|
+
setState(serviceRef.current?.getState() || state);
|
|
605
|
+
}, [state]);
|
|
606
|
+
const flushPendingBlobs = (0, import_react.useCallback)(() => {
|
|
607
|
+
serviceRef.current?.flushPendingBlobs();
|
|
608
|
+
}, []);
|
|
609
|
+
const hasPendingBlobs = (0, import_react.useCallback)(() => {
|
|
610
|
+
return serviceRef.current?.hasPendingBlobs() || false;
|
|
611
|
+
}, []);
|
|
612
|
+
return {
|
|
613
|
+
...state,
|
|
614
|
+
connect,
|
|
615
|
+
disconnect,
|
|
616
|
+
sendConfig,
|
|
617
|
+
updateMockupIds,
|
|
618
|
+
updatePlacementSettings,
|
|
619
|
+
sendCanvasBlob,
|
|
620
|
+
sendColorBlob,
|
|
621
|
+
createEmptyCanvasBlob,
|
|
622
|
+
sendInitialEmptyCanvases,
|
|
623
|
+
setInitialData,
|
|
624
|
+
clearLogs,
|
|
625
|
+
clearMockups,
|
|
626
|
+
flushPendingBlobs,
|
|
627
|
+
hasPendingBlobs
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/framework/adapters/react.ts
|
|
632
|
+
var ReactAdapter = class {
|
|
633
|
+
name = "react";
|
|
634
|
+
React;
|
|
635
|
+
hooks;
|
|
636
|
+
constructor(React, hooks) {
|
|
637
|
+
this.React = React;
|
|
638
|
+
this.hooks = hooks || React;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Create a stateful value using useState
|
|
642
|
+
*/
|
|
643
|
+
createState(initialValue) {
|
|
644
|
+
const [value, setValue] = this.hooks.useState(initialValue);
|
|
645
|
+
return {
|
|
646
|
+
get: () => value,
|
|
647
|
+
set: (newValue) => {
|
|
648
|
+
setValue(newValue);
|
|
649
|
+
},
|
|
650
|
+
subscribe: (callback) => {
|
|
651
|
+
this.hooks.useEffect(() => {
|
|
652
|
+
callback(value);
|
|
653
|
+
}, [value]);
|
|
654
|
+
return () => {
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Create a context provider/consumer
|
|
661
|
+
*/
|
|
662
|
+
createContext(name) {
|
|
663
|
+
const Context = this.React.createContext(void 0);
|
|
664
|
+
return {
|
|
665
|
+
provide: (value) => {
|
|
666
|
+
return this.React.createElement(Context.Provider, { value });
|
|
667
|
+
},
|
|
668
|
+
consume: () => {
|
|
669
|
+
return this.hooks.useContext(Context);
|
|
670
|
+
},
|
|
671
|
+
subscribe: (callback) => {
|
|
672
|
+
const value = this.hooks.useContext(Context);
|
|
673
|
+
this.hooks.useEffect(() => {
|
|
674
|
+
if (value !== void 0) {
|
|
675
|
+
callback(value);
|
|
676
|
+
}
|
|
677
|
+
}, [value]);
|
|
678
|
+
return () => {
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Register lifecycle hooks
|
|
685
|
+
*/
|
|
686
|
+
useLifecycle(lifecycle) {
|
|
687
|
+
this.hooks.useEffect(() => {
|
|
688
|
+
lifecycle.onMount?.();
|
|
689
|
+
return () => {
|
|
690
|
+
lifecycle.onUnmount?.();
|
|
691
|
+
};
|
|
692
|
+
}, []);
|
|
693
|
+
this.hooks.useEffect(() => {
|
|
694
|
+
lifecycle.onUpdate?.({});
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Register event handlers
|
|
699
|
+
*/
|
|
700
|
+
useEvents(handlers) {
|
|
701
|
+
handlers.forEach(({ name, handler }) => {
|
|
702
|
+
this.hooks.useCallback(handler, []);
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Create a ref to a DOM element
|
|
707
|
+
*/
|
|
708
|
+
createRef() {
|
|
709
|
+
return this.hooks.useRef(null);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Render a component
|
|
713
|
+
*/
|
|
714
|
+
render(component, props) {
|
|
715
|
+
return component.render(this);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Get React-specific utilities
|
|
719
|
+
*/
|
|
720
|
+
getUtilities() {
|
|
721
|
+
return {
|
|
722
|
+
batchUpdates: (callback) => {
|
|
723
|
+
callback();
|
|
724
|
+
},
|
|
725
|
+
nextTick: (callback) => {
|
|
726
|
+
Promise.resolve().then(callback);
|
|
727
|
+
},
|
|
728
|
+
computed: (deps, compute) => {
|
|
729
|
+
return this.hooks.useMemo(compute, deps);
|
|
730
|
+
},
|
|
731
|
+
memo: (value, deps) => {
|
|
732
|
+
return this.hooks.useMemo(() => value, deps);
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
function createReactComponent(descriptor, React) {
|
|
738
|
+
return function Component(props) {
|
|
739
|
+
const adapter = new ReactAdapter(React, React);
|
|
740
|
+
if (descriptor.lifecycle) {
|
|
741
|
+
adapter.useLifecycle(descriptor.lifecycle);
|
|
742
|
+
}
|
|
743
|
+
return adapter.render(descriptor, props);
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function useFrameworkAdapter(React) {
|
|
747
|
+
return new ReactAdapter(React, React);
|
|
748
|
+
}
|
|
749
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
750
|
+
0 && (module.exports = {
|
|
751
|
+
ReactAdapter,
|
|
752
|
+
createReactComponent,
|
|
753
|
+
useReactAdapter,
|
|
754
|
+
useRealtimeMockup
|
|
755
|
+
});
|