cyclecad 3.0.0 → 3.1.0
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/BILLING-IMPLEMENTATION-SUMMARY.md +425 -0
- package/BILLING-INDEX.md +293 -0
- package/BILLING-INTEGRATION-GUIDE.md +414 -0
- package/COLLABORATION-INDEX.md +440 -0
- package/COLLABORATION-SYSTEM-SUMMARY.md +548 -0
- package/DOCKER-BUILD-MANIFEST.txt +483 -0
- package/DOCKER-FILES-REFERENCE.md +440 -0
- package/DOCKER-INFRASTRUCTURE.md +475 -0
- package/DOCKER-README.md +435 -0
- package/Dockerfile +33 -55
- package/PWA-FILES-CREATED.txt +350 -0
- package/QUICK-START-TESTING.md +126 -0
- package/STEP-IMPORT-QUICKSTART.md +347 -0
- package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
- package/app/css/mobile.css +1074 -0
- package/app/icons/generate-icons.js +203 -0
- package/app/js/billing-ui.js +990 -0
- package/app/js/brep-kernel.js +933 -981
- package/app/js/collab-client.js +750 -0
- package/app/js/mobile-nav.js +623 -0
- package/app/js/mobile-toolbar.js +476 -0
- package/app/js/modules/billing-module.js +724 -0
- package/app/js/modules/step-module-enhanced.js +938 -0
- package/app/js/offline-manager.js +705 -0
- package/app/js/responsive-init.js +360 -0
- package/app/js/touch-handler.js +429 -0
- package/app/manifest.json +211 -0
- package/app/offline.html +508 -0
- package/app/sw.js +571 -0
- package/app/tests/billing-tests.html +779 -0
- package/app/tests/brep-tests.html +980 -0
- package/app/tests/collab-tests.html +743 -0
- package/app/tests/mobile-tests.html +1299 -0
- package/app/tests/pwa-tests.html +1134 -0
- package/app/tests/step-tests.html +1042 -0
- package/app/tests/test-agent-v3.html +719 -0
- package/docker-compose.yml +225 -0
- package/docs/BILLING-HELP.json +260 -0
- package/docs/BILLING-README.md +639 -0
- package/docs/BILLING-TUTORIAL.md +736 -0
- package/docs/BREP-HELP.json +326 -0
- package/docs/BREP-TUTORIAL.md +802 -0
- package/docs/COLLABORATION-HELP.json +228 -0
- package/docs/COLLABORATION-TUTORIAL.md +818 -0
- package/docs/DOCKER-HELP.json +224 -0
- package/docs/DOCKER-TUTORIAL.md +974 -0
- package/docs/MOBILE-HELP.json +243 -0
- package/docs/MOBILE-RESPONSIVE-README.md +378 -0
- package/docs/MOBILE-TUTORIAL.md +747 -0
- package/docs/PWA-HELP.json +228 -0
- package/docs/PWA-README.md +662 -0
- package/docs/PWA-TUTORIAL.md +757 -0
- package/docs/STEP-HELP.json +481 -0
- package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
- package/docs/TESTING-GUIDE.md +528 -0
- package/docs/TESTING-HELP.json +182 -0
- package/fusion-vs-cyclecad.html +1771 -0
- package/nginx.conf +237 -0
- package/package.json +1 -1
- package/server/Dockerfile.converter +51 -0
- package/server/Dockerfile.signaling +28 -0
- package/server/billing-server.js +487 -0
- package/server/converter-enhanced.py +528 -0
- package/server/requirements-converter.txt +29 -0
- package/server/signaling-server.js +801 -0
- package/tests/docker-tests.sh +389 -0
package/app/sw.js
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cycleCAD Service Worker
|
|
3
|
+
* Enables offline mode, caching strategies, and background sync
|
|
4
|
+
* v3.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const CACHE_VERSION = 'cyclecad-v3.0.0';
|
|
8
|
+
const STATIC_CACHE = 'cyclecad-static-v3';
|
|
9
|
+
const DYNAMIC_CACHE = 'cyclecad-dynamic-v3';
|
|
10
|
+
const MODEL_CACHE = 'cyclecad-models-v3';
|
|
11
|
+
const API_CACHE = 'cyclecad-api-v3';
|
|
12
|
+
|
|
13
|
+
// Essential files to precache on install
|
|
14
|
+
const PRECACHE_URLS = [
|
|
15
|
+
'/app/',
|
|
16
|
+
'/app/index.html',
|
|
17
|
+
'/app/offline.html',
|
|
18
|
+
'/app/manifest.json',
|
|
19
|
+
'/app/js/app.js',
|
|
20
|
+
'/app/js/viewport.js',
|
|
21
|
+
'/app/js/sketch.js',
|
|
22
|
+
'/app/js/operations.js',
|
|
23
|
+
'/app/js/constraint-solver.js',
|
|
24
|
+
'/app/js/advanced-ops.js',
|
|
25
|
+
'/app/js/assembly.js',
|
|
26
|
+
'/app/js/dxf-export.js',
|
|
27
|
+
'/app/js/export.js',
|
|
28
|
+
'/app/js/params.js',
|
|
29
|
+
'/app/js/tree.js',
|
|
30
|
+
'/app/js/shortcuts.js',
|
|
31
|
+
'/app/css/style.css',
|
|
32
|
+
'/app/offline.html'
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// CDN resources to cache (long TTL)
|
|
36
|
+
const CDN_PATTERNS = [
|
|
37
|
+
'cdn.jsdelivr.net',
|
|
38
|
+
'unpkg.com',
|
|
39
|
+
'cdnjs.cloudflare.com'
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// API endpoints — network-first with fallback
|
|
43
|
+
const API_PATTERNS = [
|
|
44
|
+
'/api/',
|
|
45
|
+
'/convert',
|
|
46
|
+
'/health'
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Model files — cache with size limit
|
|
50
|
+
const MODEL_PATTERNS = [
|
|
51
|
+
/\.glb$/i,
|
|
52
|
+
/\.gltf$/i,
|
|
53
|
+
/\.step$/i,
|
|
54
|
+
/\.stp$/i,
|
|
55
|
+
/\.stl$/i,
|
|
56
|
+
/\.obj$/i
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Max size for model cache (500MB)
|
|
60
|
+
const MODEL_CACHE_MAX_SIZE = 500 * 1024 * 1024;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* INSTALL EVENT
|
|
64
|
+
* Precache all essential files
|
|
65
|
+
*/
|
|
66
|
+
self.addEventListener('install', (event) => {
|
|
67
|
+
console.log('[SW] Install event, precaching essential files...');
|
|
68
|
+
|
|
69
|
+
event.waitUntil(
|
|
70
|
+
caches.open(STATIC_CACHE)
|
|
71
|
+
.then((cache) => {
|
|
72
|
+
console.log('[SW] Precaching', PRECACHE_URLS.length, 'files');
|
|
73
|
+
return cache.addAll(PRECACHE_URLS);
|
|
74
|
+
})
|
|
75
|
+
.then(() => {
|
|
76
|
+
console.log('[SW] Precache complete, skipping wait...');
|
|
77
|
+
return self.skipWaiting();
|
|
78
|
+
})
|
|
79
|
+
.catch((err) => {
|
|
80
|
+
console.error('[SW] Precache failed:', err);
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* ACTIVATE EVENT
|
|
87
|
+
* Clean up old caches
|
|
88
|
+
*/
|
|
89
|
+
self.addEventListener('activate', (event) => {
|
|
90
|
+
console.log('[SW] Activate event, cleaning old caches...');
|
|
91
|
+
|
|
92
|
+
event.waitUntil(
|
|
93
|
+
caches.keys()
|
|
94
|
+
.then((cacheNames) => {
|
|
95
|
+
return Promise.all(
|
|
96
|
+
cacheNames.map((cacheName) => {
|
|
97
|
+
if (cacheName !== STATIC_CACHE &&
|
|
98
|
+
cacheName !== DYNAMIC_CACHE &&
|
|
99
|
+
cacheName !== MODEL_CACHE &&
|
|
100
|
+
cacheName !== API_CACHE &&
|
|
101
|
+
!cacheName.startsWith('cyclecad-')) {
|
|
102
|
+
console.log('[SW] Deleting old cache:', cacheName);
|
|
103
|
+
return caches.delete(cacheName);
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
})
|
|
108
|
+
.then(() => {
|
|
109
|
+
console.log('[SW] Old caches cleaned, claiming clients...');
|
|
110
|
+
return self.clients.claim();
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* FETCH EVENT
|
|
117
|
+
* Routing based on URL pattern
|
|
118
|
+
*/
|
|
119
|
+
self.addEventListener('fetch', (event) => {
|
|
120
|
+
const { request } = event;
|
|
121
|
+
const url = new URL(request.url);
|
|
122
|
+
|
|
123
|
+
// Skip non-GET requests
|
|
124
|
+
if (request.method !== 'GET') {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Skip chrome extensions and internal protocols
|
|
129
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// API calls — network-first with cache fallback
|
|
134
|
+
if (isApiCall(url.pathname)) {
|
|
135
|
+
event.respondWith(networkFirstApiCall(request));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Model files — cache-first with size management
|
|
140
|
+
if (isModelFile(url.pathname)) {
|
|
141
|
+
event.respondWith(cacheFirstModel(request));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// CDN resources — cache-first with long TTL
|
|
146
|
+
if (isCdnResource(url.hostname)) {
|
|
147
|
+
event.respondWith(cacheFirstCdn(request));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Static assets (HTML, JS, CSS) — cache-first, update in background
|
|
152
|
+
if (isStaticAsset(url.pathname)) {
|
|
153
|
+
event.respondWith(cacheFirstStatic(request));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Default — network-first with offline fallback
|
|
158
|
+
event.respondWith(networkFirstWithFallback(request));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* BACKGROUND SYNC
|
|
163
|
+
* Sync queued operations when back online
|
|
164
|
+
*/
|
|
165
|
+
self.addEventListener('sync', (event) => {
|
|
166
|
+
console.log('[SW] Background sync event:', event.tag);
|
|
167
|
+
|
|
168
|
+
if (event.tag === 'sync-operations') {
|
|
169
|
+
event.waitUntil(syncOfflineOperations());
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* HELPER: API Call — Network-first
|
|
175
|
+
*/
|
|
176
|
+
async function networkFirstApiCall(request) {
|
|
177
|
+
try {
|
|
178
|
+
const response = await fetch(request);
|
|
179
|
+
|
|
180
|
+
// Cache successful responses
|
|
181
|
+
if (response.ok) {
|
|
182
|
+
const cache = await caches.open(API_CACHE);
|
|
183
|
+
cache.put(request, response.clone());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return response;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.log('[SW] Network failed, falling back to cache for:', request.url);
|
|
189
|
+
const cached = await caches.match(request);
|
|
190
|
+
|
|
191
|
+
if (cached) {
|
|
192
|
+
return cached;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Return offline response
|
|
196
|
+
return new Response(
|
|
197
|
+
JSON.stringify({
|
|
198
|
+
error: 'offline',
|
|
199
|
+
message: 'API unavailable. Changes will sync when online.'
|
|
200
|
+
}),
|
|
201
|
+
{
|
|
202
|
+
status: 503,
|
|
203
|
+
statusText: 'Service Unavailable',
|
|
204
|
+
headers: { 'Content-Type': 'application/json' }
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* HELPER: Model Files — Cache-first with LRU eviction
|
|
212
|
+
*/
|
|
213
|
+
async function cacheFirstModel(request) {
|
|
214
|
+
const cache = await caches.open(MODEL_CACHE);
|
|
215
|
+
const cached = await cache.match(request);
|
|
216
|
+
|
|
217
|
+
if (cached) {
|
|
218
|
+
console.log('[SW] Model cache hit:', request.url);
|
|
219
|
+
return cached;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const response = await fetch(request);
|
|
224
|
+
|
|
225
|
+
if (response.ok) {
|
|
226
|
+
// Check cache size before adding
|
|
227
|
+
const size = parseInt(response.headers.get('content-length') || 0);
|
|
228
|
+
|
|
229
|
+
if (size > 0) {
|
|
230
|
+
await enforceModelCacheSize(size);
|
|
231
|
+
cache.put(request, response.clone());
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return response;
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.log('[SW] Model fetch failed:', request.url);
|
|
238
|
+
|
|
239
|
+
// Try to return from cache anyway
|
|
240
|
+
const cached = await cache.match(request);
|
|
241
|
+
if (cached) return cached;
|
|
242
|
+
|
|
243
|
+
// Return offline response
|
|
244
|
+
return new Response(
|
|
245
|
+
'Model file not available offline',
|
|
246
|
+
{ status: 503 }
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* HELPER: CDN Resources — Cache-first with long TTL
|
|
253
|
+
*/
|
|
254
|
+
async function cacheFirstCdn(request) {
|
|
255
|
+
const cache = await caches.open(STATIC_CACHE);
|
|
256
|
+
const cached = await cache.match(request);
|
|
257
|
+
|
|
258
|
+
if (cached) {
|
|
259
|
+
console.log('[SW] CDN cache hit:', request.url);
|
|
260
|
+
return cached;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const response = await fetch(request);
|
|
265
|
+
|
|
266
|
+
if (response.ok) {
|
|
267
|
+
cache.put(request, response.clone());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return response;
|
|
271
|
+
} catch (err) {
|
|
272
|
+
console.log('[SW] CDN fetch failed:', request.url);
|
|
273
|
+
|
|
274
|
+
const cached = await cache.match(request);
|
|
275
|
+
if (cached) return cached;
|
|
276
|
+
|
|
277
|
+
return new Response('CDN resource unavailable', { status: 503 });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* HELPER: Static Assets — Cache-first with background update
|
|
283
|
+
*/
|
|
284
|
+
async function cacheFirstStatic(request) {
|
|
285
|
+
const cache = await caches.open(STATIC_CACHE);
|
|
286
|
+
const cached = await cache.match(request);
|
|
287
|
+
|
|
288
|
+
if (cached) {
|
|
289
|
+
console.log('[SW] Static cache hit:', request.url);
|
|
290
|
+
|
|
291
|
+
// Update in background (don't block response)
|
|
292
|
+
fetch(request)
|
|
293
|
+
.then((response) => {
|
|
294
|
+
if (response.ok) {
|
|
295
|
+
cache.put(request, response.clone());
|
|
296
|
+
console.log('[SW] Updated static file:', request.url);
|
|
297
|
+
|
|
298
|
+
// Notify clients of update
|
|
299
|
+
notifyClientsOfUpdate();
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
.catch((err) => console.log('[SW] Background update failed:', err));
|
|
303
|
+
|
|
304
|
+
return cached;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const response = await fetch(request);
|
|
309
|
+
|
|
310
|
+
if (response.ok) {
|
|
311
|
+
cache.put(request, response.clone());
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return response;
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.log('[SW] Static fetch failed:', request.url);
|
|
317
|
+
|
|
318
|
+
// Return offline page for HTML requests
|
|
319
|
+
if (request.destination === 'document' || request.headers.get('accept')?.includes('text/html')) {
|
|
320
|
+
const offline = await cache.match('/app/offline.html');
|
|
321
|
+
if (offline) return offline;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return new Response('Offline', { status: 503 });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* HELPER: Network-first with offline fallback
|
|
330
|
+
*/
|
|
331
|
+
async function networkFirstWithFallback(request) {
|
|
332
|
+
try {
|
|
333
|
+
const response = await fetch(request);
|
|
334
|
+
|
|
335
|
+
if (response.ok) {
|
|
336
|
+
const cache = await caches.open(DYNAMIC_CACHE);
|
|
337
|
+
cache.put(request, response.clone());
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return response;
|
|
341
|
+
} catch (err) {
|
|
342
|
+
console.log('[SW] Network failed, checking cache:', request.url);
|
|
343
|
+
|
|
344
|
+
// Try dynamic cache
|
|
345
|
+
const cached = await caches.match(request);
|
|
346
|
+
if (cached) return cached;
|
|
347
|
+
|
|
348
|
+
// Try static cache as last resort
|
|
349
|
+
const staticCached = await caches.match(request, { cacheName: STATIC_CACHE });
|
|
350
|
+
if (staticCached) return staticCached;
|
|
351
|
+
|
|
352
|
+
// Return offline page for documents
|
|
353
|
+
if (request.destination === 'document') {
|
|
354
|
+
return caches.match('/app/offline.html');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return new Response('Offline', { status: 503 });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* HELPER: Check if URL is an API call
|
|
363
|
+
*/
|
|
364
|
+
function isApiCall(pathname) {
|
|
365
|
+
return API_PATTERNS.some((pattern) => {
|
|
366
|
+
if (typeof pattern === 'string') {
|
|
367
|
+
return pathname.includes(pattern);
|
|
368
|
+
}
|
|
369
|
+
return pattern.test(pathname);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* HELPER: Check if URL is a model file
|
|
375
|
+
*/
|
|
376
|
+
function isModelFile(pathname) {
|
|
377
|
+
return MODEL_PATTERNS.some((pattern) => pattern.test(pathname));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* HELPER: Check if hostname is CDN
|
|
382
|
+
*/
|
|
383
|
+
function isCdnResource(hostname) {
|
|
384
|
+
return CDN_PATTERNS.some((cdn) => hostname.includes(cdn));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* HELPER: Check if pathname is static asset
|
|
389
|
+
*/
|
|
390
|
+
function isStaticAsset(pathname) {
|
|
391
|
+
return /\.(js|css|svg|png|jpg|jpeg|gif|woff|woff2|ttf|eot)$/i.test(pathname) ||
|
|
392
|
+
pathname.endsWith('/app/') ||
|
|
393
|
+
pathname.endsWith('/app/index.html');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* HELPER: Enforce model cache size limit (LRU eviction)
|
|
398
|
+
*/
|
|
399
|
+
async function enforceModelCacheSize(newSize) {
|
|
400
|
+
const cache = await caches.open(MODEL_CACHE);
|
|
401
|
+
const keys = await cache.keys();
|
|
402
|
+
|
|
403
|
+
let totalSize = newSize;
|
|
404
|
+
|
|
405
|
+
for (const request of keys) {
|
|
406
|
+
const response = await cache.match(request);
|
|
407
|
+
const size = parseInt(response.headers.get('content-length') || 0);
|
|
408
|
+
totalSize += size;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (totalSize > MODEL_CACHE_MAX_SIZE) {
|
|
412
|
+
console.log('[SW] Model cache exceeds limit, evicting oldest files...');
|
|
413
|
+
|
|
414
|
+
for (const request of keys) {
|
|
415
|
+
const response = await cache.match(request);
|
|
416
|
+
const size = parseInt(response.headers.get('content-length') || 0);
|
|
417
|
+
|
|
418
|
+
await cache.delete(request);
|
|
419
|
+
totalSize -= size;
|
|
420
|
+
|
|
421
|
+
if (totalSize <= MODEL_CACHE_MAX_SIZE * 0.8) {
|
|
422
|
+
console.log('[SW] Cache size reduced to', Math.round(totalSize / 1024 / 1024), 'MB');
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* HELPER: Sync offline operations when back online
|
|
431
|
+
*/
|
|
432
|
+
async function syncOfflineOperations() {
|
|
433
|
+
try {
|
|
434
|
+
// Get queued operations from IndexedDB
|
|
435
|
+
const db = await openDatabase();
|
|
436
|
+
const tx = db.transaction('operationQueue', 'readonly');
|
|
437
|
+
const store = tx.objectStore('operationQueue');
|
|
438
|
+
const operations = await store.getAll();
|
|
439
|
+
|
|
440
|
+
console.log('[SW] Syncing', operations.length, 'queued operations...');
|
|
441
|
+
|
|
442
|
+
for (const op of operations) {
|
|
443
|
+
try {
|
|
444
|
+
const response = await fetch('/api/operations', {
|
|
445
|
+
method: 'POST',
|
|
446
|
+
headers: { 'Content-Type': 'application/json' },
|
|
447
|
+
body: JSON.stringify(op.data)
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
if (response.ok) {
|
|
451
|
+
// Remove from queue
|
|
452
|
+
const txW = db.transaction('operationQueue', 'readwrite');
|
|
453
|
+
await txW.objectStore('operationQueue').delete(op.id);
|
|
454
|
+
console.log('[SW] Synced operation:', op.id);
|
|
455
|
+
}
|
|
456
|
+
} catch (err) {
|
|
457
|
+
console.error('[SW] Sync failed for operation:', op.id, err);
|
|
458
|
+
// Leave in queue for next sync
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Notify clients of sync completion
|
|
463
|
+
notifyClientsOfSync();
|
|
464
|
+
|
|
465
|
+
} catch (err) {
|
|
466
|
+
console.error('[SW] Sync failed:', err);
|
|
467
|
+
throw err;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* HELPER: Open IndexedDB
|
|
473
|
+
*/
|
|
474
|
+
function openDatabase() {
|
|
475
|
+
return new Promise((resolve, reject) => {
|
|
476
|
+
const request = indexedDB.open('cyclecad', 1);
|
|
477
|
+
|
|
478
|
+
request.onerror = () => reject(request.error);
|
|
479
|
+
request.onsuccess = () => resolve(request.result);
|
|
480
|
+
|
|
481
|
+
request.onupgradeneeded = (event) => {
|
|
482
|
+
const db = event.target.result;
|
|
483
|
+
if (!db.objectStoreNames.contains('operationQueue')) {
|
|
484
|
+
db.createObjectStore('operationQueue', { keyPath: 'id' });
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* HELPER: Notify all clients of update available
|
|
492
|
+
*/
|
|
493
|
+
function notifyClientsOfUpdate() {
|
|
494
|
+
self.clients.matchAll().then((clients) => {
|
|
495
|
+
clients.forEach((client) => {
|
|
496
|
+
client.postMessage({
|
|
497
|
+
type: 'UPDATE_AVAILABLE',
|
|
498
|
+
message: 'A new version of cycleCAD is available. Reload to update.'
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* HELPER: Notify all clients of sync completion
|
|
506
|
+
*/
|
|
507
|
+
function notifyClientsOfSync() {
|
|
508
|
+
self.clients.matchAll().then((clients) => {
|
|
509
|
+
clients.forEach((client) => {
|
|
510
|
+
client.postMessage({
|
|
511
|
+
type: 'SYNC_COMPLETE',
|
|
512
|
+
message: 'Offline changes have been synced.'
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* MESSAGE EVENT
|
|
520
|
+
* Handle messages from clients
|
|
521
|
+
*/
|
|
522
|
+
self.addEventListener('message', (event) => {
|
|
523
|
+
console.log('[SW] Message received:', event.data);
|
|
524
|
+
|
|
525
|
+
if (event.data.type === 'SKIP_WAITING') {
|
|
526
|
+
self.skipWaiting();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (event.data.type === 'CLEAR_CACHE') {
|
|
530
|
+
clearAllCaches().then(() => {
|
|
531
|
+
event.ports[0].postMessage({ success: true });
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (event.data.type === 'GET_CACHE_SIZE') {
|
|
536
|
+
getCacheSize().then((size) => {
|
|
537
|
+
event.ports[0].postMessage({ size });
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* HELPER: Clear all caches
|
|
544
|
+
*/
|
|
545
|
+
async function clearAllCaches() {
|
|
546
|
+
const cacheNames = await caches.keys();
|
|
547
|
+
return Promise.all(cacheNames.map((name) => caches.delete(name)));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* HELPER: Get total cache size
|
|
552
|
+
*/
|
|
553
|
+
async function getCacheSize() {
|
|
554
|
+
const cacheNames = await caches.keys();
|
|
555
|
+
let totalSize = 0;
|
|
556
|
+
|
|
557
|
+
for (const name of cacheNames) {
|
|
558
|
+
const cache = await caches.open(name);
|
|
559
|
+
const keys = await cache.keys();
|
|
560
|
+
|
|
561
|
+
for (const request of keys) {
|
|
562
|
+
const response = await cache.match(request);
|
|
563
|
+
const size = parseInt(response.headers.get('content-length') || 0);
|
|
564
|
+
totalSize += size;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return totalSize;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
console.log('[SW] Service Worker loaded and ready');
|