@snowcone-app/sdk 0.1.12 → 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
package/README.md
CHANGED
|
@@ -17,50 +17,25 @@ pnpm add @snowcone-app/sdk
|
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
19
19
|
```typescript
|
|
20
|
-
import { getProduct, listProducts,
|
|
20
|
+
import { getProduct, listProducts, getMockupUrl } from '@snowcone-app/sdk';
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
const product = await getProduct('BEEB77');
|
|
24
|
-
console.log(product.name); // "Bella + Canvas 3001 Unisex Jersey Short Sleeve Tee"
|
|
25
|
-
|
|
26
|
-
// List all products
|
|
22
|
+
// 1. Find a product (public catalog read — no key needed)
|
|
27
23
|
const products = await listProducts({ limit: 10 });
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
alignment: 'center'
|
|
37
|
-
});
|
|
38
|
-
console.log(mockupUrl); // URL to mockup image
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Configuration
|
|
42
|
-
|
|
43
|
-
Configure the SDK to point to your Merch endpoint:
|
|
44
|
-
|
|
45
|
-
```typescript
|
|
46
|
-
import { config } from '@snowcone-app/sdk';
|
|
47
|
-
|
|
48
|
-
config({
|
|
49
|
-
endpoint: 'https://api.snowcone.app',
|
|
50
|
-
mockupUrl: 'https://i.snowcone.app',
|
|
51
|
-
accountId: 'your-account-id',
|
|
52
|
-
mode: 'live' // or 'mock' for development
|
|
24
|
+
const product = await getProduct('BEEB77'); // by id or slug
|
|
25
|
+
|
|
26
|
+
// 2. Build a mockup URL. A mockup is a PUBLIC image URL — getMockupUrl is a
|
|
27
|
+
// pure, synchronous builder (no await, no fetch, no secret). The result
|
|
28
|
+
// drops straight into an <img>. Code-first: productCode, then options.
|
|
29
|
+
const url = getMockupUrl('hoodie-black', {
|
|
30
|
+
shop: shop.id, // your Shop ID (publishable, = shop.id)
|
|
31
|
+
asset: 'https://example.com/art.png' // your artwork
|
|
53
32
|
});
|
|
33
|
+
// <img src={url} />
|
|
54
34
|
```
|
|
55
35
|
|
|
56
|
-
**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
NEXT_PUBLIC_MERCH_MOCKUP_URL=https://i.snowcone.app
|
|
60
|
-
NEXT_PUBLIC_MERCH_ACCOUNT_ID=your-account-id
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
The SDK automatically reads these environment variables if not configured explicitly.
|
|
36
|
+
> The `key` is your **publishable** token — public and safe to expose (like
|
|
37
|
+
> Cloudinary's cloud name). It defaults to your `shop.id`. See the security
|
|
38
|
+
> ladder below if you want to lock things down.
|
|
64
39
|
|
|
65
40
|
## Core Functions
|
|
66
41
|
|
|
@@ -97,23 +72,95 @@ const products = await listProducts(options?: {
|
|
|
97
72
|
}
|
|
98
73
|
```
|
|
99
74
|
|
|
100
|
-
### Mockup
|
|
75
|
+
### Mockup URLs
|
|
76
|
+
|
|
77
|
+
A mockup is a public image URL on `img.snowcone.app`:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
https://img.snowcone.app/{productCode}?asset={assetUrl}&key={publishableKey}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
You can hand-write it, or use **`getMockupUrl`** — a pure, synchronous,
|
|
84
|
+
isomorphic builder (browser + server, no `await`). It never hand-rolls the query
|
|
85
|
+
string; it delegates to `@snowcone-app/mockup-url`'s `buildPublicMockupUrl`
|
|
86
|
+
(the single source of truth shared byte-for-byte with the edge resolver).
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { getMockupUrl } from '@snowcone-app/sdk';
|
|
90
|
+
|
|
91
|
+
// Code-first form: productCode, then options.
|
|
92
|
+
const url = getMockupUrl(productCode: string, opts: {
|
|
93
|
+
shop: string; // Required: your Shop ID (publishable, = shop.id)
|
|
94
|
+
asset?: string; // Single default-placement image (get-started shorthand)
|
|
95
|
+
design?: Design; // Multi-placement: { [placementKey]: Fill } — takes precedence over `asset`
|
|
96
|
+
options?: Record<string, string>; // Variant picks: { size: "m" } → opt.size=m
|
|
97
|
+
secret?: string; // Optional: per-shop secret → appends an L3 &signature (server only)
|
|
98
|
+
base?: string; // Optional: override the host (default img.snowcone.app)
|
|
99
|
+
width?: number; // Optional: display width
|
|
100
|
+
view?: string; // Optional: camera view / mockup scene (alias: `mockup`)
|
|
101
|
+
variant?: string; // Optional: specific resolved variant (gvid)
|
|
102
|
+
placement?: string; // Optional: specific print area (single-asset only)
|
|
103
|
+
aspect?: '16:9' | '2:3';// Optional: canvas aspect ratio
|
|
104
|
+
}): string;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`Fill` (re-exported from the SDK) is what fills one placement:
|
|
101
108
|
|
|
102
109
|
```typescript
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
type Fill =
|
|
111
|
+
| string // image URL (shorthand for { src })
|
|
112
|
+
| { src: string; align?: ImageAlignment; tile?: 0.25 | 0.5 | 1 | 2 | 4 }
|
|
113
|
+
| { color: string }; // a color placement (e.g. a cap's Crown)
|
|
114
|
+
type Design = Record<string, Fill>; // placement key → fill
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### Multi-placement + variant options
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// A front + back tee, size M.
|
|
121
|
+
const url = getMockupUrl('KMYKUK', {
|
|
122
|
+
shop: shop.id,
|
|
123
|
+
options: { size: 'm' },
|
|
124
|
+
design: {
|
|
125
|
+
front: 'https://cdn.example.com/front.png',
|
|
126
|
+
back: { src: 'https://cdn.example.com/back.png', tile: 2, align: 'top' },
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
// → …/KMYKUK?shop=…&asset.back=…&tile.back=2&align.back=top&asset.front=…&opt.size=m
|
|
130
|
+
|
|
131
|
+
// A cap with a printed front + chosen Crown/Strap colors (color placements).
|
|
132
|
+
const cap = getMockupUrl('RQNU68', {
|
|
133
|
+
shop: shop.id,
|
|
134
|
+
design: { front: 'https://cdn.example.com/logo.png', crown: { color: '#001f3f' } },
|
|
112
135
|
});
|
|
136
|
+
// → …/RQNU68?shop=…&color.crown=%23001f3f&asset.front=…
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
> A **legacy positional form** `getMockupUrl(assetUrl, productCode, opts)` is also
|
|
140
|
+
> accepted (it builds the same URL as `getMockupUrl(productCode, { asset, … })`).
|
|
141
|
+
> Prefer the code-first form above for new code.
|
|
142
|
+
|
|
143
|
+
### Security ladder
|
|
144
|
+
|
|
145
|
+
The default is open and frictionless; climb only as far as you need:
|
|
113
146
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
147
|
+
| Level | Control | Effect |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| **L0** (default) | none | public token, any domain, any asset |
|
|
150
|
+
| **L1** | domain allowlist | token only renders from your domains |
|
|
151
|
+
| **L2** | asset allowlist | restrict which asset sources composite |
|
|
152
|
+
| **L3** | signed URLs | URL must carry a valid `&s` (HMAC of a per-shop secret) |
|
|
153
|
+
|
|
154
|
+
For **L3**, pass `secret` (server-side only) and `getMockupUrl` appends the
|
|
155
|
+
verified `&signature`:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// On your server/BFF — the secret never ships to the browser.
|
|
159
|
+
const url = getMockupUrl('hoodie-black', {
|
|
160
|
+
shop: shop.id,
|
|
161
|
+
asset: assetUrl,
|
|
162
|
+
secret: process.env.SNOWCONE_SHOP_SECRET,
|
|
163
|
+
});
|
|
117
164
|
```
|
|
118
165
|
|
|
119
166
|
### React Hooks (Optional)
|
|
@@ -148,17 +195,6 @@ import type {
|
|
|
148
195
|
} from '@snowcone-app/sdk';
|
|
149
196
|
```
|
|
150
197
|
|
|
151
|
-
## Development Mode
|
|
152
|
-
|
|
153
|
-
Use mock mode for development without API calls:
|
|
154
|
-
|
|
155
|
-
```typescript
|
|
156
|
-
config({ mode: 'mock' });
|
|
157
|
-
|
|
158
|
-
// Returns mock product data
|
|
159
|
-
const product = await getProduct('BEEB77');
|
|
160
|
-
```
|
|
161
|
-
|
|
162
198
|
## API Reference
|
|
163
199
|
|
|
164
200
|
See full API documentation at: [https://developers.snowcone.app/sdk](https://developers.snowcone.app/sdk)
|
|
@@ -13,7 +13,9 @@ var RealtimeMockupService = class {
|
|
|
13
13
|
logs = [];
|
|
14
14
|
lastError = null;
|
|
15
15
|
callbacks = {};
|
|
16
|
+
lastBlobSentAt = 0;
|
|
16
17
|
canvasBlobs = /* @__PURE__ */ new Map();
|
|
18
|
+
canvasStates = /* @__PURE__ */ new Map();
|
|
17
19
|
colors = /* @__PURE__ */ new Map();
|
|
18
20
|
lastSendTime = {};
|
|
19
21
|
throttleTimeouts = {};
|
|
@@ -23,11 +25,24 @@ var RealtimeMockupService = class {
|
|
|
23
25
|
lastSentVersion = 0;
|
|
24
26
|
// Track latest sent version per placement to detect stale responses
|
|
25
27
|
latestSentVersionByPlacement = {};
|
|
28
|
+
// Track latest accepted (displayed) version per placement — only drop results
|
|
29
|
+
// older than what we've already shown, not older than what we've sent.
|
|
30
|
+
// This prevents the "version racing" problem during drag where sent versions
|
|
31
|
+
// advance faster than the server can render.
|
|
32
|
+
latestAcceptedVersionByPlacement = {};
|
|
26
33
|
// Feature flag: server now supports version in blob message
|
|
27
34
|
sendVersionInBlob = true;
|
|
35
|
+
// Session-grant auth: when set, connect() fetches a short-lived token, opens
|
|
36
|
+
// the WS with `?token=`, and renews ~15s before expiry via a `renew` message.
|
|
37
|
+
tokenProvider;
|
|
38
|
+
renewTimer = null;
|
|
28
39
|
setCallbacks(callbacks) {
|
|
29
40
|
this.callbacks = callbacks;
|
|
30
41
|
}
|
|
42
|
+
/** Provide a grant fetcher to authorize the session (per-shop, renewable). */
|
|
43
|
+
setTokenProvider(fn) {
|
|
44
|
+
this.tokenProvider = fn;
|
|
45
|
+
}
|
|
31
46
|
getState() {
|
|
32
47
|
return {
|
|
33
48
|
isConnected: this.ws?.readyState === WebSocket.OPEN,
|
|
@@ -50,9 +65,24 @@ var RealtimeMockupService = class {
|
|
|
50
65
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
51
66
|
return;
|
|
52
67
|
}
|
|
68
|
+
if (this.tokenProvider) {
|
|
69
|
+
this.tokenProvider().then((grant) => {
|
|
70
|
+
this.openSocket(grant.token);
|
|
71
|
+
this.scheduleRenew(grant.expiresAt);
|
|
72
|
+
}).catch((err) => {
|
|
73
|
+
this.addLog(`Failed to obtain realtime grant: ${err}`);
|
|
74
|
+
this.status = "Disconnected";
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.openSocket();
|
|
79
|
+
}
|
|
80
|
+
openSocket(token) {
|
|
81
|
+
const url = token ? `${this.wsUrl}${this.wsUrl.includes("?") ? "&" : "?"}token=${encodeURIComponent(token)}` : this.wsUrl;
|
|
53
82
|
this.addLog(`Connecting to ${this.wsUrl}...`);
|
|
54
|
-
this.ws = new WebSocket(
|
|
83
|
+
this.ws = new WebSocket(url);
|
|
55
84
|
this.ws.onopen = () => {
|
|
85
|
+
console.log(`[WS] connection OPENED to ${this.wsUrl}`);
|
|
56
86
|
this.addLog("WebSocket connection opened");
|
|
57
87
|
this.status = "Connected";
|
|
58
88
|
};
|
|
@@ -66,11 +96,13 @@ var RealtimeMockupService = class {
|
|
|
66
96
|
}
|
|
67
97
|
};
|
|
68
98
|
this.ws.onclose = (event) => {
|
|
99
|
+
console.log(`[WS] connection CLOSED (code: ${event.code}, reason: "${event.reason}", wasClean: ${event.wasClean})`);
|
|
69
100
|
this.addLog(`WebSocket connection closed (code: ${event.code})`);
|
|
70
101
|
this.status = "Disconnected";
|
|
71
102
|
this.sessionId = null;
|
|
72
103
|
this.isConfigured = false;
|
|
73
104
|
this.configSent = false;
|
|
105
|
+
this.clearRenew();
|
|
74
106
|
this.callbacks.onDisconnected?.();
|
|
75
107
|
};
|
|
76
108
|
this.ws.onerror = (error) => {
|
|
@@ -78,6 +110,29 @@ var RealtimeMockupService = class {
|
|
|
78
110
|
this.status = "Disconnected";
|
|
79
111
|
};
|
|
80
112
|
}
|
|
113
|
+
scheduleRenew(expiresAt) {
|
|
114
|
+
this.clearRenew();
|
|
115
|
+
if (!this.tokenProvider) return;
|
|
116
|
+
const ms = Math.max(1e3, expiresAt * 1e3 - Date.now() - 15e3);
|
|
117
|
+
this.renewTimer = setTimeout(() => void this.renew(), ms);
|
|
118
|
+
}
|
|
119
|
+
clearRenew() {
|
|
120
|
+
if (this.renewTimer) {
|
|
121
|
+
clearTimeout(this.renewTimer);
|
|
122
|
+
this.renewTimer = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async renew() {
|
|
126
|
+
if (!this.tokenProvider || this.ws?.readyState !== WebSocket.OPEN) return;
|
|
127
|
+
try {
|
|
128
|
+
const grant = await this.tokenProvider();
|
|
129
|
+
this.ws.send(JSON.stringify({ type: "renew", token: grant.token }));
|
|
130
|
+
this.addLog("\u{1F504} Renewed realtime session token", "sent");
|
|
131
|
+
this.scheduleRenew(grant.expiresAt);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
this.addLog(`Failed to renew realtime grant: ${err}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
81
136
|
handleMessage(data) {
|
|
82
137
|
switch (data.type) {
|
|
83
138
|
case "connected":
|
|
@@ -129,15 +184,18 @@ var RealtimeMockupService = class {
|
|
|
129
184
|
this.addLog("\u{1F3A8} Mockup rendering has started...");
|
|
130
185
|
break;
|
|
131
186
|
case "mockup_rendered":
|
|
187
|
+
console.log(`[WS] mockup_rendered received: mockupId=${data.mockupId} hasImageUrl=${!!data.imageUrl} v=${data.requestVersion} placement="${data.placement}" renderMs=${data.renderMs}`);
|
|
132
188
|
if (data.imageUrl && data.mockupId) {
|
|
133
189
|
const responseVersion = data.requestVersion;
|
|
134
190
|
const responsePlacement = data.placement;
|
|
135
191
|
if (responseVersion !== void 0 && responsePlacement) {
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
138
|
-
|
|
192
|
+
const lastAccepted = this.latestAcceptedVersionByPlacement[responsePlacement];
|
|
193
|
+
if (lastAccepted !== void 0 && responseVersion < lastAccepted) {
|
|
194
|
+
console.log(`[WS] STALE mockup dropped: v${responseVersion} for "${responsePlacement}" (already displayed: v${lastAccepted}, latest sent: v${this.latestSentVersionByPlacement[responsePlacement]})`);
|
|
195
|
+
this.addLog(`\u23ED\uFE0F Ignoring stale mockup v${responseVersion} for "${responsePlacement}" (displayed: v${lastAccepted})`);
|
|
139
196
|
break;
|
|
140
197
|
}
|
|
198
|
+
this.latestAcceptedVersionByPlacement[responsePlacement] = responseVersion;
|
|
141
199
|
}
|
|
142
200
|
const mockupResult = {
|
|
143
201
|
mockupId: data.mockupId,
|
|
@@ -145,7 +203,10 @@ var RealtimeMockupService = class {
|
|
|
145
203
|
renderUrl: data.renderUrl || data.imageUrl,
|
|
146
204
|
imageSize: data.imageSize || 0,
|
|
147
205
|
requestVersion: responseVersion,
|
|
148
|
-
placement: responsePlacement
|
|
206
|
+
placement: responsePlacement,
|
|
207
|
+
renderMs: data.renderMs,
|
|
208
|
+
blobToRenderMs: data.blobToRenderMs,
|
|
209
|
+
canvasRenderTiming: data.canvasRenderTiming
|
|
149
210
|
};
|
|
150
211
|
const existingIndex = this.mockupResults.findIndex((m) => m.mockupId === data.mockupId);
|
|
151
212
|
if (existingIndex >= 0) {
|
|
@@ -164,14 +225,16 @@ var RealtimeMockupService = class {
|
|
|
164
225
|
}
|
|
165
226
|
break;
|
|
166
227
|
case "all_mockups_rendered":
|
|
228
|
+
console.log(`[WS] all_mockups_rendered received: ${data.mockups?.length ?? 0} mockups`);
|
|
167
229
|
if (data.mockups) {
|
|
168
230
|
const freshMockups = data.mockups.filter((mockup) => {
|
|
169
231
|
if (mockup.requestVersion !== void 0 && mockup.placement) {
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
this.addLog(`\u23ED\uFE0F Filtering stale mockup v${mockup.requestVersion} for "${mockup.placement}" (
|
|
232
|
+
const lastAccepted = this.latestAcceptedVersionByPlacement[mockup.placement];
|
|
233
|
+
if (lastAccepted !== void 0 && mockup.requestVersion < lastAccepted) {
|
|
234
|
+
this.addLog(`\u23ED\uFE0F Filtering stale mockup v${mockup.requestVersion} for "${mockup.placement}" (displayed: v${lastAccepted})`);
|
|
173
235
|
return false;
|
|
174
236
|
}
|
|
237
|
+
this.latestAcceptedVersionByPlacement[mockup.placement] = mockup.requestVersion;
|
|
175
238
|
}
|
|
176
239
|
return true;
|
|
177
240
|
});
|
|
@@ -194,6 +257,7 @@ var RealtimeMockupService = class {
|
|
|
194
257
|
}
|
|
195
258
|
}
|
|
196
259
|
disconnect() {
|
|
260
|
+
this.clearRenew();
|
|
197
261
|
if (this.ws) {
|
|
198
262
|
this.ws.close();
|
|
199
263
|
this.ws = null;
|
|
@@ -228,6 +292,7 @@ var RealtimeMockupService = class {
|
|
|
228
292
|
this.isConfigured = false;
|
|
229
293
|
this.mockupResults = [];
|
|
230
294
|
this.canvasBlobs.clear();
|
|
295
|
+
this.canvasStates.clear();
|
|
231
296
|
this.colors.clear();
|
|
232
297
|
this.lastSendTime = {};
|
|
233
298
|
Object.values(this.throttleTimeouts).forEach((timeout) => clearTimeout(timeout));
|
|
@@ -235,6 +300,7 @@ var RealtimeMockupService = class {
|
|
|
235
300
|
this.requestVersion = 0;
|
|
236
301
|
this.lastSentVersion = 0;
|
|
237
302
|
this.latestSentVersionByPlacement = {};
|
|
303
|
+
this.latestAcceptedVersionByPlacement = {};
|
|
238
304
|
this.addLog("\u{1F9F9} Cleared all cached canvas/color data for new product");
|
|
239
305
|
}
|
|
240
306
|
}
|
|
@@ -270,6 +336,30 @@ var RealtimeMockupService = class {
|
|
|
270
336
|
this.addLog(`\u{1F3AF} Updating mockupIds to: [${mockupIds.join(", ")}]`);
|
|
271
337
|
return this.sendConfig(updatedConfig);
|
|
272
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Update render width without changing other config.
|
|
341
|
+
* Used for low-res preview during rapid edits (e.g., 600 while dragging, 1200 on release).
|
|
342
|
+
* Preserves blobs since product/variant don't change.
|
|
343
|
+
*/
|
|
344
|
+
updateWidth(width) {
|
|
345
|
+
if (!this.config) {
|
|
346
|
+
this.addLog("Cannot update width: no config set");
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
350
|
+
this.addLog("Cannot update width: WebSocket not connected");
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
if (this.config.width === width) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
const updatedConfig = {
|
|
357
|
+
...this.config,
|
|
358
|
+
width
|
|
359
|
+
};
|
|
360
|
+
this.addLog(`\u{1F4D0} Updating render width: ${this.config.width} \u2192 ${width}`);
|
|
361
|
+
return this.sendConfig(updatedConfig);
|
|
362
|
+
}
|
|
273
363
|
/**
|
|
274
364
|
* Update placementSettings without changing other config.
|
|
275
365
|
* Used to override scaleMode when canvas editor is active.
|
|
@@ -292,6 +382,8 @@ var RealtimeMockupService = class {
|
|
|
292
382
|
sendCanvasBlob(placement, blob, mockupCount = 1, baseThrottleMs = 1e3, notifyCallback = true) {
|
|
293
383
|
this.canvasBlobs.set(placement, blob);
|
|
294
384
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
|
|
385
|
+
const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
|
|
386
|
+
console.log(`[WS] sendCanvasBlob BLOCKED for "${placement}" (${blob.size}B): ${reason} (cached for later)`);
|
|
295
387
|
return false;
|
|
296
388
|
}
|
|
297
389
|
if (baseThrottleMs <= 0) {
|
|
@@ -299,16 +391,25 @@ var RealtimeMockupService = class {
|
|
|
299
391
|
this.lastSendTime[placement] = Date.now();
|
|
300
392
|
return true;
|
|
301
393
|
}
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
const
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const
|
|
394
|
+
const debounceMs = baseThrottleMs * mockupCount;
|
|
395
|
+
const lastSendTime = this.lastSendTime[placement];
|
|
396
|
+
const timeSinceLastSend = lastSendTime ? Date.now() - lastSendTime : 0;
|
|
397
|
+
const isActiveInteraction = lastSendTime && timeSinceLastSend < debounceMs * 3;
|
|
398
|
+
if (isActiveInteraction && timeSinceLastSend >= debounceMs) {
|
|
399
|
+
if (this.throttleTimeouts[placement]) {
|
|
400
|
+
clearTimeout(this.throttleTimeouts[placement]);
|
|
401
|
+
delete this.throttleTimeouts[placement];
|
|
402
|
+
}
|
|
403
|
+
const latestBlob = this.canvasBlobs.get(placement);
|
|
404
|
+
if (latestBlob && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
|
|
405
|
+
this.sendBlobImmediately(placement, latestBlob, notifyCallback);
|
|
406
|
+
this.lastSendTime[placement] = Date.now();
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
if (this.throttleTimeouts[placement]) {
|
|
410
|
+
clearTimeout(this.throttleTimeouts[placement]);
|
|
411
|
+
}
|
|
412
|
+
const delayTime = isActiveInteraction ? debounceMs - timeSinceLastSend : debounceMs;
|
|
312
413
|
this.throttleTimeouts[placement] = setTimeout(() => {
|
|
313
414
|
const latestBlob = this.canvasBlobs.get(placement);
|
|
314
415
|
if (latestBlob && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
|
|
@@ -320,6 +421,72 @@ var RealtimeMockupService = class {
|
|
|
320
421
|
}
|
|
321
422
|
return true;
|
|
322
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* Send canvas state JSON for server-side rendering.
|
|
426
|
+
* Alternative to sendCanvasBlob — the server renders the PNG instead of the client.
|
|
427
|
+
*/
|
|
428
|
+
sendCanvasState(placement, state, mockupCount = 1, baseThrottleMs = 1e3) {
|
|
429
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
|
|
430
|
+
const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
|
|
431
|
+
console.log(`[WS] sendCanvasState BLOCKED for "${placement}": ${reason}`);
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
this.canvasStates.set(placement, state);
|
|
435
|
+
if (baseThrottleMs <= 0) {
|
|
436
|
+
this.sendCanvasStateImmediately(placement, state);
|
|
437
|
+
this.lastSendTime[placement] = Date.now();
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
const debounceMs = baseThrottleMs * mockupCount;
|
|
441
|
+
const lastSendTime = this.lastSendTime[placement];
|
|
442
|
+
const timeSinceLastSend = lastSendTime ? Date.now() - lastSendTime : 0;
|
|
443
|
+
const isActiveInteraction = lastSendTime && timeSinceLastSend < debounceMs * 3;
|
|
444
|
+
if (isActiveInteraction && timeSinceLastSend >= debounceMs) {
|
|
445
|
+
if (this.throttleTimeouts[placement]) {
|
|
446
|
+
clearTimeout(this.throttleTimeouts[placement]);
|
|
447
|
+
delete this.throttleTimeouts[placement];
|
|
448
|
+
}
|
|
449
|
+
const latestState = this.canvasStates.get(placement);
|
|
450
|
+
if (latestState && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
|
|
451
|
+
console.log(`[WS] sendCanvasState "${placement}": max-wait flush (${timeSinceLastSend}ms since last)`);
|
|
452
|
+
this.sendCanvasStateImmediately(placement, latestState);
|
|
453
|
+
this.lastSendTime[placement] = Date.now();
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
if (this.throttleTimeouts[placement]) {
|
|
457
|
+
clearTimeout(this.throttleTimeouts[placement]);
|
|
458
|
+
}
|
|
459
|
+
const delayTime = isActiveInteraction ? debounceMs - timeSinceLastSend : debounceMs;
|
|
460
|
+
this.throttleTimeouts[placement] = setTimeout(() => {
|
|
461
|
+
const latestState = this.canvasStates.get(placement);
|
|
462
|
+
if (latestState && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
|
|
463
|
+
console.log(`[WS] sendCanvasState "${placement}": debounce firing (${debounceMs}ms)`);
|
|
464
|
+
this.sendCanvasStateImmediately(placement, latestState);
|
|
465
|
+
this.lastSendTime[placement] = Date.now();
|
|
466
|
+
}
|
|
467
|
+
delete this.throttleTimeouts[placement];
|
|
468
|
+
}, delayTime);
|
|
469
|
+
}
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
sendCanvasStateImmediately(placement, state) {
|
|
473
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
474
|
+
console.log(`[WS] sendCanvasStateImmediately ABORTED: ws not open`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
this.lastSentVersion = ++this.requestVersion;
|
|
478
|
+
this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
|
|
479
|
+
const message = JSON.stringify({
|
|
480
|
+
type: "canvas_state",
|
|
481
|
+
placement,
|
|
482
|
+
version: this.lastSentVersion,
|
|
483
|
+
state
|
|
484
|
+
});
|
|
485
|
+
this.ws.send(message);
|
|
486
|
+
this.lastBlobSentAt = Date.now();
|
|
487
|
+
this.addLog(`Sent canvas state for "${placement}" (${(message.length / 1024).toFixed(1)}KB, v${this.lastSentVersion})`, "sent");
|
|
488
|
+
this.callbacks.onBlobSent?.(placement);
|
|
489
|
+
}
|
|
323
490
|
sendBlobImmediately(placement, blob, notifyCallback = true) {
|
|
324
491
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
325
492
|
this.lastSentVersion = ++this.requestVersion;
|
|
@@ -345,6 +512,7 @@ ${versionToSend}
|
|
|
345
512
|
combined.set(imageBytes, headerBytes.length);
|
|
346
513
|
}
|
|
347
514
|
this.ws.send(combined.buffer);
|
|
515
|
+
this.lastBlobSentAt = Date.now();
|
|
348
516
|
this.addLog(`Sent canvas blob for placement "${placement}" (${imageBytes.length} bytes, v${versionToSend})`, "sent");
|
|
349
517
|
if (notifyCallback) {
|
|
350
518
|
this.callbacks.onBlobSent?.(placement);
|