@zseven-w/dsh-openpencil-linux-arm64 0.1.0-rc.4
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/LICENSE +21 -0
- package/THIRD_PARTY_NOTICES.md +72 -0
- package/bin/op-host-web-server +0 -0
- package/openpencil-runtime.json +211 -0
- package/package.json +34 -0
- package/web/canvaskit/LICENSE +29 -0
- package/web/canvaskit/README.md +21 -0
- package/web/canvaskit/canvaskit.js +288 -0
- package/web/canvaskit/canvaskit.wasm +0 -0
- package/web/pkg/assets/iconify-catalog-core.json +1 -0
- package/web/pkg/assets/prompt_center_previews/component-data-grid.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/component-form-lab.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/dashboard-pulse.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/dashboard-sentinel.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/extreme-calendar.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/extreme-calm.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/extreme-daily-app.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/extreme-now-playing.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/extreme-weather.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-aloft.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-bloom.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-companion.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-daybreak.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-ember.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-forage.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-gallery.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-hearth.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-lingua.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-marginalia.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-marquee.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-meteo.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-nightcap.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-nocturne.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-relic.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-ritual.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-still.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-verdant.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-volt.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/freeform-wander.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-aloft.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-bloom.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-companion.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-daybreak.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-ember.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-forage.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-gallery.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-hearth.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-lingua.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-marginalia.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-marquee.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-meteo.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-nightcap.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-nocturne.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-relic.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-ritual.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-still.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-verdant.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-volt.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/gallery-wander.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/modify-complete-states.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/modify-polish-current.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/starter-barbershop.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/starter-coffee-shop.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/starter-dashboard.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/starter-travel-app.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/web-atelier.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/web-kilnform.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/web-orbit.jpg +0 -0
- package/web/pkg/assets/prompt_center_previews/web-reefwright.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/app-onboarding-triptych.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/banxin-rule-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/before-after.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/bookreview-silk-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/brand-concept-sheet.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/challenge-tracker-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/cityguide-film-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/concept-contrast-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/daily-sign-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/data-report-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/data-story-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/datareport-grid-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/diy-blueprint-guide.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/do-dont-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/dossier-linen-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/ecosystem-map-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/event-poster-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/faq-thread-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/gradient-tech.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/gridpaper-graphite-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/journal-checklist-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/knowledge-card-square.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/knowledge-card-vertical.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/knowledge-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/lecture-deck-light.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/ledger-tick-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/logo-qa-board.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/metric-single-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/milestone-timeline-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/minimal-keynote.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/myth-truth-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/notice-board-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/opinion-longform-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/photo-composition-tutorial.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/pitch-deck-dark.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/pitfall-list-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/price-tier-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/pricing-tiers-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/product-landing-light.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/punch-quote-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/qa-chalkboard-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/quote-frame-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/ranking-board-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/recipe-four-step.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/saas-landing-orange.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/scenario-guide-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/screenshot-tutorial.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/skincare-routine-cards.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/slide-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/software-step-tutorial.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/sounding-navy-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/spec-table-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/spine-culture-card.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/steps-flow-infographic.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/storage-makeover-steps.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/story-night-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/three-way-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/tidemark-slate-deck.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/time-shift-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/toolkit-notebook-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/tradeoff-scale-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/tutorial-journal-carousel.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/version-diff-comparison.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/weekly-report-lesson.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/workout-breakdown-guide.jpg +0 -0
- package/web/pkg/assets/scene_template_previews/yearreview-mineral-carousel.jpg +0 -0
- package/web/pkg/assets/scene_templates/app-onboarding-triptych.op +1087 -0
- package/web/pkg/assets/scene_templates/banxin-rule-deck.op +2702 -0
- package/web/pkg/assets/scene_templates/before-after.op +753 -0
- package/web/pkg/assets/scene_templates/bookreview-silk-carousel.op +3272 -0
- package/web/pkg/assets/scene_templates/brand-concept-sheet.op +1 -0
- package/web/pkg/assets/scene_templates/challenge-tracker-infographic.op +2448 -0
- package/web/pkg/assets/scene_templates/cityguide-film-carousel.op +13691 -0
- package/web/pkg/assets/scene_templates/concept-contrast-infographic.op +1433 -0
- package/web/pkg/assets/scene_templates/daily-sign-card.op +370 -0
- package/web/pkg/assets/scene_templates/data-report-infographic.op +1946 -0
- package/web/pkg/assets/scene_templates/data-story-infographic.op +1916 -0
- package/web/pkg/assets/scene_templates/datareport-grid-carousel.op +2799 -0
- package/web/pkg/assets/scene_templates/diy-blueprint-guide.op +2744 -0
- package/web/pkg/assets/scene_templates/do-dont-comparison.op +1170 -0
- package/web/pkg/assets/scene_templates/dossier-linen-deck.op +7045 -0
- package/web/pkg/assets/scene_templates/ecosystem-map-infographic.op +2015 -0
- package/web/pkg/assets/scene_templates/event-poster-deck.op +2756 -0
- package/web/pkg/assets/scene_templates/faq-thread-infographic.op +1698 -0
- package/web/pkg/assets/scene_templates/gradient-tech.op +3322 -0
- package/web/pkg/assets/scene_templates/gridpaper-graphite-deck.op +11996 -0
- package/web/pkg/assets/scene_templates/journal-checklist-card.op +764 -0
- package/web/pkg/assets/scene_templates/knowledge-card-square.op +560 -0
- package/web/pkg/assets/scene_templates/knowledge-card-vertical.op +719 -0
- package/web/pkg/assets/scene_templates/knowledge-carousel.op +1640 -0
- package/web/pkg/assets/scene_templates/lecture-deck-light.op +5211 -0
- package/web/pkg/assets/scene_templates/ledger-tick-deck.op +6756 -0
- package/web/pkg/assets/scene_templates/logo-qa-board.op +1 -0
- package/web/pkg/assets/scene_templates/metric-single-card.op +259 -0
- package/web/pkg/assets/scene_templates/milestone-timeline-infographic.op +2116 -0
- package/web/pkg/assets/scene_templates/minimal-keynote.op +1085 -0
- package/web/pkg/assets/scene_templates/myth-truth-comparison.op +1315 -0
- package/web/pkg/assets/scene_templates/notice-board-card.op +671 -0
- package/web/pkg/assets/scene_templates/opinion-longform-carousel.op +2492 -0
- package/web/pkg/assets/scene_templates/photo-composition-tutorial.op +2558 -0
- package/web/pkg/assets/scene_templates/pitch-deck-dark.op +3020 -0
- package/web/pkg/assets/scene_templates/pitfall-list-infographic.op +1646 -0
- package/web/pkg/assets/scene_templates/price-tier-card.op +647 -0
- package/web/pkg/assets/scene_templates/pricing-tiers-comparison.op +1762 -0
- package/web/pkg/assets/scene_templates/product-landing-light.op +10717 -0
- package/web/pkg/assets/scene_templates/punch-quote-card.op +438 -0
- package/web/pkg/assets/scene_templates/qa-chalkboard-carousel.op +2568 -0
- package/web/pkg/assets/scene_templates/quote-frame-card.op +1233 -0
- package/web/pkg/assets/scene_templates/ranking-board-infographic.op +1945 -0
- package/web/pkg/assets/scene_templates/recipe-four-step.op +1177 -0
- package/web/pkg/assets/scene_templates/saas-landing-orange.op +6616 -0
- package/web/pkg/assets/scene_templates/scenario-guide-comparison.op +1721 -0
- package/web/pkg/assets/scene_templates/screenshot-tutorial.op +1 -0
- package/web/pkg/assets/scene_templates/skincare-routine-cards.op +3240 -0
- package/web/pkg/assets/scene_templates/slide-deck.op +2475 -0
- package/web/pkg/assets/scene_templates/software-step-tutorial.op +797 -0
- package/web/pkg/assets/scene_templates/sounding-navy-deck.op +4292 -0
- package/web/pkg/assets/scene_templates/spec-table-comparison.op +2033 -0
- package/web/pkg/assets/scene_templates/spine-culture-card.op +647 -0
- package/web/pkg/assets/scene_templates/steps-flow-infographic.op +1664 -0
- package/web/pkg/assets/scene_templates/storage-makeover-steps.op +2912 -0
- package/web/pkg/assets/scene_templates/story-night-carousel.op +2814 -0
- package/web/pkg/assets/scene_templates/three-way-comparison.op +1917 -0
- package/web/pkg/assets/scene_templates/tidemark-slate-deck.op +5716 -0
- package/web/pkg/assets/scene_templates/time-shift-comparison.op +1127 -0
- package/web/pkg/assets/scene_templates/toolkit-notebook-carousel.op +5335 -0
- package/web/pkg/assets/scene_templates/tradeoff-scale-comparison.op +995 -0
- package/web/pkg/assets/scene_templates/tutorial-journal-carousel.op +4425 -0
- package/web/pkg/assets/scene_templates/version-diff-comparison.op +1001 -0
- package/web/pkg/assets/scene_templates/weekly-report-lesson.op +2188 -0
- package/web/pkg/assets/scene_templates/workout-breakdown-guide.op +2579 -0
- package/web/pkg/assets/scene_templates/yearreview-mineral-carousel.op +4367 -0
- package/web/pkg/op_host_web.js +1971 -0
- package/web/pkg/op_host_web_bg.wasm +0 -0
- package/web/pkg/snippets/op-host-web-f712ff5933a5d456/src/figma_temp_worker.js +494 -0
- package/web/pkg/snippets/op-host-web-f712ff5933a5d456/src/op_ck_bridge.js +1712 -0
- package/web/pkg/snippets/op-host-web-f712ff5933a5d456/src/op_ck_image_cache.js +404 -0
|
Binary file
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
// Browser-side module-Worker and IndexedDB adapter for `.fig` imports.
|
|
2
|
+
//
|
|
3
|
+
// The editor's WASM instance never receives the raw `.fig` bytes. A separate
|
|
4
|
+
// module Worker loads a second copy of op-host-web, converts the file through
|
|
5
|
+
// `FigmaTempWriter`, writes page shards plus a complete canonical `.op` record
|
|
6
|
+
// to IndexedDB, and then returns only a small commit receipt. The main thread
|
|
7
|
+
// terminates the Worker's large WASM instance before reading that record back
|
|
8
|
+
// for the current eager EditorState install. Lazy page residency is deliberately
|
|
9
|
+
// a later host-level change because history and document-wide commands require
|
|
10
|
+
// a stronger backing-store contract.
|
|
11
|
+
|
|
12
|
+
const DB_NAME = 'openpencil-figma-temp';
|
|
13
|
+
const DB_VERSION = 1;
|
|
14
|
+
const RECORDS_STORE = 'records';
|
|
15
|
+
const MANIFESTS_STORE = 'manifests';
|
|
16
|
+
const TEMP_TTL_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
const IMPORT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
18
|
+
const activeImports = new Map();
|
|
19
|
+
|
|
20
|
+
function mainRequestResult(request, label) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
request.onsuccess = () => resolve(request.result);
|
|
23
|
+
request.onerror = () => reject(request.error || new Error(label));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function mainTransactionDone(transaction, label) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
transaction.oncomplete = () => resolve();
|
|
30
|
+
transaction.onerror = () => reject(transaction.error || new Error(label));
|
|
31
|
+
transaction.onabort = () => reject(transaction.error || new Error(`${label} (aborted)`));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function openMainDatabase() {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
38
|
+
let blocked = false;
|
|
39
|
+
request.onupgradeneeded = () => {
|
|
40
|
+
const opened = request.result;
|
|
41
|
+
if (!opened.objectStoreNames.contains(RECORDS_STORE)) opened.createObjectStore(RECORDS_STORE);
|
|
42
|
+
if (!opened.objectStoreNames.contains(MANIFESTS_STORE)) opened.createObjectStore(MANIFESTS_STORE);
|
|
43
|
+
};
|
|
44
|
+
request.onsuccess = () => {
|
|
45
|
+
if (blocked) request.result.close();
|
|
46
|
+
else resolve(request.result);
|
|
47
|
+
};
|
|
48
|
+
request.onerror = () => reject(request.error || new Error('IndexedDB open failed'));
|
|
49
|
+
request.onblocked = () => {
|
|
50
|
+
blocked = true;
|
|
51
|
+
reject(new Error('IndexedDB cleanup was blocked by another tab'));
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function readCommittedDocument(sessionId, expectedPageCount) {
|
|
57
|
+
const db = await openMainDatabase();
|
|
58
|
+
let documentBlob = null;
|
|
59
|
+
try {
|
|
60
|
+
const transaction = db.transaction([RECORDS_STORE, MANIFESTS_STORE], 'readonly');
|
|
61
|
+
const done = mainTransactionDone(transaction, 'IndexedDB document read failed');
|
|
62
|
+
const manifestRequest = transaction.objectStore(MANIFESTS_STORE).get(sessionId);
|
|
63
|
+
const documentKey = sessionId + '/document';
|
|
64
|
+
const documentRequest = transaction.objectStore(RECORDS_STORE).get(documentKey);
|
|
65
|
+
const [manifest, storedDocument] = await Promise.all([
|
|
66
|
+
mainRequestResult(manifestRequest, 'IndexedDB manifest read failed'),
|
|
67
|
+
mainRequestResult(documentRequest, 'IndexedDB document record read failed'),
|
|
68
|
+
]);
|
|
69
|
+
await done;
|
|
70
|
+
|
|
71
|
+
if (!manifest || manifest.status !== 'committed' || manifest.sessionId !== sessionId) {
|
|
72
|
+
throw new Error('Figma temp session was not committed');
|
|
73
|
+
}
|
|
74
|
+
if (Number(manifest.pageCount || 0) !== expectedPageCount) {
|
|
75
|
+
throw new Error('Figma temp session page count changed before install');
|
|
76
|
+
}
|
|
77
|
+
if (manifest.documentKey !== documentKey) {
|
|
78
|
+
throw new Error('Figma temp session document key is invalid');
|
|
79
|
+
}
|
|
80
|
+
if (!storedDocument || typeof storedDocument.text !== 'function') {
|
|
81
|
+
throw new Error('Figma temp session is missing canonical JSON');
|
|
82
|
+
}
|
|
83
|
+
documentBlob = storedDocument;
|
|
84
|
+
} finally {
|
|
85
|
+
db.close();
|
|
86
|
+
}
|
|
87
|
+
return documentBlob.text();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function generatedWasmModuleUrl() {
|
|
91
|
+
const url = new URL(import.meta.url);
|
|
92
|
+
const marker = '/snippets/';
|
|
93
|
+
const at = url.pathname.lastIndexOf(marker);
|
|
94
|
+
if (at >= 0) {
|
|
95
|
+
// wasm-bindgen copies this file to
|
|
96
|
+
// <pkg>/snippets/<hash>/src/figma_temp_worker.js
|
|
97
|
+
// while the generated module remains at <pkg>/op_host_web.js.
|
|
98
|
+
url.pathname = `${url.pathname.slice(0, at)}/op_host_web.js`;
|
|
99
|
+
url.search = '';
|
|
100
|
+
url.hash = '';
|
|
101
|
+
return url.href;
|
|
102
|
+
}
|
|
103
|
+
// Source-tree/dev fallback. Production bundles always take the snippets
|
|
104
|
+
// branch above, but keeping this deterministic makes direct smoke harnesses
|
|
105
|
+
// easier to diagnose.
|
|
106
|
+
return new URL('../pkg-ck/op_host_web.js', import.meta.url).href;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function workerSource() {
|
|
110
|
+
// Dynamic import keeps the generated module URL a runtime value. It also
|
|
111
|
+
// means the same source works after the pkg directory is mounted below a
|
|
112
|
+
// different base path.
|
|
113
|
+
return `
|
|
114
|
+
const DB_NAME = ${JSON.stringify(DB_NAME)};
|
|
115
|
+
const DB_VERSION = ${DB_VERSION};
|
|
116
|
+
const RECORDS_STORE = ${JSON.stringify(RECORDS_STORE)};
|
|
117
|
+
const MANIFESTS_STORE = ${JSON.stringify(MANIFESTS_STORE)};
|
|
118
|
+
const TEMP_TTL_MS = ${TEMP_TTL_MS};
|
|
119
|
+
|
|
120
|
+
function requestResult(request) {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
request.onsuccess = () => resolve(request.result);
|
|
123
|
+
request.onerror = () => reject(request.error || new Error('IndexedDB request failed'));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function transactionDone(transaction) {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
transaction.oncomplete = () => resolve();
|
|
130
|
+
transaction.onerror = () => reject(transaction.error || new Error('IndexedDB transaction failed'));
|
|
131
|
+
transaction.onabort = () => reject(transaction.error || new Error('IndexedDB transaction aborted'));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function openDatabase() {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
138
|
+
let blocked = false;
|
|
139
|
+
request.onupgradeneeded = () => {
|
|
140
|
+
const db = request.result;
|
|
141
|
+
if (!db.objectStoreNames.contains(RECORDS_STORE)) db.createObjectStore(RECORDS_STORE);
|
|
142
|
+
if (!db.objectStoreNames.contains(MANIFESTS_STORE)) db.createObjectStore(MANIFESTS_STORE);
|
|
143
|
+
};
|
|
144
|
+
request.onsuccess = () => {
|
|
145
|
+
if (blocked) request.result.close();
|
|
146
|
+
else resolve(request.result);
|
|
147
|
+
};
|
|
148
|
+
request.onerror = () => reject(request.error || new Error('IndexedDB open failed'));
|
|
149
|
+
request.onblocked = () => {
|
|
150
|
+
blocked = true;
|
|
151
|
+
reject(new Error('IndexedDB open was blocked by another tab'));
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function put(db, storeName, key, value) {
|
|
157
|
+
const transaction = db.transaction(storeName, 'readwrite');
|
|
158
|
+
transaction.objectStore(storeName).put(value, key);
|
|
159
|
+
await transactionDone(transaction);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function deleteSession(db, sessionId, pageCount) {
|
|
163
|
+
const transaction = db.transaction([RECORDS_STORE, MANIFESTS_STORE], 'readwrite');
|
|
164
|
+
const records = transaction.objectStore(RECORDS_STORE);
|
|
165
|
+
for (let index = 0; index < pageCount; index += 1) {
|
|
166
|
+
records.delete(sessionId + '/page/' + index);
|
|
167
|
+
}
|
|
168
|
+
records.delete(sessionId + '/document');
|
|
169
|
+
records.delete(sessionId + '/skeleton');
|
|
170
|
+
records.delete(sessionId + '/image-tables');
|
|
171
|
+
transaction.objectStore(MANIFESTS_STORE).delete(sessionId);
|
|
172
|
+
await transactionDone(transaction).catch(() => {});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function sessionCreatedAt(sessionId) {
|
|
176
|
+
const match = /^fig-([0-9a-f]{13})-/i.exec(sessionId);
|
|
177
|
+
if (!match) return null;
|
|
178
|
+
const createdAt = Number.parseInt(match[1], 16);
|
|
179
|
+
return Number.isFinite(createdAt) ? createdAt : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function cleanupExpired(db) {
|
|
183
|
+
const manifestsTransaction = db.transaction(MANIFESTS_STORE, 'readonly');
|
|
184
|
+
const manifestsStore = manifestsTransaction.objectStore(MANIFESTS_STORE);
|
|
185
|
+
const manifests = await requestResult(manifestsStore.getAll());
|
|
186
|
+
await transactionDone(manifestsTransaction);
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
const live = new Set();
|
|
189
|
+
for (const manifest of manifests) {
|
|
190
|
+
if (!manifest || typeof manifest.sessionId !== 'string') continue;
|
|
191
|
+
if (now - Number(manifest.createdAt || 0) > TEMP_TTL_MS) {
|
|
192
|
+
await deleteSession(db, manifest.sessionId, Number(manifest.pageCount || 0));
|
|
193
|
+
} else {
|
|
194
|
+
// Both pending leases and committed sessions are live. Future readers
|
|
195
|
+
// must require status === 'committed', but cleanup must not race a Worker
|
|
196
|
+
// that is still writing the page records behind a pending lease.
|
|
197
|
+
live.add(manifest.sessionId);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// A tab/Worker crash can leave page records without a manifest. Enumerating
|
|
202
|
+
// keys (not values) is cheap; only TTL-expired session ids are removed so an
|
|
203
|
+
// older concurrently-running Worker cannot be mistaken for an orphan.
|
|
204
|
+
const keysTransaction = db.transaction(RECORDS_STORE, 'readonly');
|
|
205
|
+
const keys = await requestResult(keysTransaction.objectStore(RECORDS_STORE).getAllKeys());
|
|
206
|
+
await transactionDone(keysTransaction);
|
|
207
|
+
const orphanKeys = keys.filter((key) => {
|
|
208
|
+
const text = String(key);
|
|
209
|
+
const slash = text.indexOf('/');
|
|
210
|
+
if (slash <= 0) return false;
|
|
211
|
+
const sessionId = text.slice(0, slash);
|
|
212
|
+
if (live.has(sessionId)) return false;
|
|
213
|
+
const createdAt = sessionCreatedAt(sessionId);
|
|
214
|
+
// Never delete an uncommitted record from another active Worker. New
|
|
215
|
+
// sessions publish a pending lease before their first record; this age
|
|
216
|
+
// guard also keeps cleanup safe across one old/new-version overlap.
|
|
217
|
+
return createdAt !== null && now - createdAt > TEMP_TTL_MS;
|
|
218
|
+
});
|
|
219
|
+
if (orphanKeys.length > 0) {
|
|
220
|
+
const transaction = db.transaction(RECORDS_STORE, 'readwrite');
|
|
221
|
+
const store = transaction.objectStore(RECORDS_STORE);
|
|
222
|
+
for (const key of orphanKeys) store.delete(key);
|
|
223
|
+
await transactionDone(transaction);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
self.onmessage = async (event) => {
|
|
228
|
+
const { file, fileName, sessionId, wasmModuleUrl } = event.data;
|
|
229
|
+
let db = null;
|
|
230
|
+
let pageCount = 0;
|
|
231
|
+
let writer = null;
|
|
232
|
+
try {
|
|
233
|
+
const wasm = await import(wasmModuleUrl);
|
|
234
|
+
await wasm.default();
|
|
235
|
+
let bytes = new Uint8Array(await file.arrayBuffer());
|
|
236
|
+
writer = new wasm.FigmaTempWriter(bytes, fileName);
|
|
237
|
+
bytes = null;
|
|
238
|
+
pageCount = writer.page_count();
|
|
239
|
+
if (pageCount < 1) throw new Error('Figma import produced no pages');
|
|
240
|
+
|
|
241
|
+
db = await openDatabase();
|
|
242
|
+
await cleanupExpired(db);
|
|
243
|
+
|
|
244
|
+
const pages = [];
|
|
245
|
+
const createdAt = Date.now();
|
|
246
|
+
const pendingManifest = {
|
|
247
|
+
formatVersion: 1,
|
|
248
|
+
status: 'pending',
|
|
249
|
+
sessionId,
|
|
250
|
+
fileName,
|
|
251
|
+
pageCount,
|
|
252
|
+
pages: [],
|
|
253
|
+
skeletonKey: sessionId + '/skeleton',
|
|
254
|
+
imageTablesKey: sessionId + '/image-tables',
|
|
255
|
+
documentKey: sessionId + '/document',
|
|
256
|
+
createdAt,
|
|
257
|
+
};
|
|
258
|
+
// Publish the lease before the first page record. Cleanup in another tab
|
|
259
|
+
// preserves non-expired pending sessions while future readers ignore them.
|
|
260
|
+
await put(db, MANIFESTS_STORE, sessionId, pendingManifest);
|
|
261
|
+
|
|
262
|
+
for (let index = 0; index < pageCount; index += 1) {
|
|
263
|
+
const id = writer.page_id(index) || ('page-' + index);
|
|
264
|
+
const name = writer.page_name(index) || ('Page ' + (index + 1));
|
|
265
|
+
const pageJson = writer.page_json(index);
|
|
266
|
+
await put(
|
|
267
|
+
db,
|
|
268
|
+
RECORDS_STORE,
|
|
269
|
+
sessionId + '/page/' + index,
|
|
270
|
+
new Blob([pageJson], { type: 'application/json' }),
|
|
271
|
+
);
|
|
272
|
+
pages.push({ id, name });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const skeletonJson = writer.skeleton_json();
|
|
276
|
+
await put(
|
|
277
|
+
db,
|
|
278
|
+
RECORDS_STORE,
|
|
279
|
+
sessionId + '/skeleton',
|
|
280
|
+
new Blob([skeletonJson], { type: 'application/json' }),
|
|
281
|
+
);
|
|
282
|
+
const imageTablesJson = writer.image_tables_json();
|
|
283
|
+
await put(
|
|
284
|
+
db,
|
|
285
|
+
RECORDS_STORE,
|
|
286
|
+
sessionId + '/image-tables',
|
|
287
|
+
new Blob([imageTablesJson], { type: 'application/json' }),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const warningsJson = writer.warnings_json();
|
|
291
|
+
const fullDocumentJson = writer.full_document_json();
|
|
292
|
+
// Drop the complete serde_json::Value before Blob encoding and IndexedDB
|
|
293
|
+
// IO create more representations of the canonical JSON.
|
|
294
|
+
writer.free();
|
|
295
|
+
writer = null;
|
|
296
|
+
await put(
|
|
297
|
+
db,
|
|
298
|
+
RECORDS_STORE,
|
|
299
|
+
sessionId + '/document',
|
|
300
|
+
new Blob([fullDocumentJson], { type: 'application/json' }),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// Updating the pending lease to committed is the visibility marker. Future
|
|
304
|
+
// lazy readers must require this exact status, so quota/error paths never
|
|
305
|
+
// expose a half-written document.
|
|
306
|
+
const manifest = {
|
|
307
|
+
...pendingManifest,
|
|
308
|
+
status: 'committed',
|
|
309
|
+
pages,
|
|
310
|
+
committedAt: Date.now(),
|
|
311
|
+
};
|
|
312
|
+
await put(db, MANIFESTS_STORE, sessionId, manifest);
|
|
313
|
+
|
|
314
|
+
db.close();
|
|
315
|
+
db = null;
|
|
316
|
+
self.postMessage({
|
|
317
|
+
ok: true,
|
|
318
|
+
sessionId,
|
|
319
|
+
pageCount,
|
|
320
|
+
warningsJson,
|
|
321
|
+
});
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (writer) writer.free();
|
|
324
|
+
if (db) {
|
|
325
|
+
await deleteSession(db, sessionId, pageCount);
|
|
326
|
+
db.close();
|
|
327
|
+
}
|
|
328
|
+
self.postMessage({
|
|
329
|
+
ok: false,
|
|
330
|
+
sessionId,
|
|
331
|
+
pageCount,
|
|
332
|
+
error: error instanceof Error ? error.message : String(error),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Start one isolated `.fig` conversion. `done` is called exactly once with a
|
|
341
|
+
* result object. Worker/IndexedDB/CSP failures are reported to Rust, whose
|
|
342
|
+
* caller falls back to the existing main-thread parser.
|
|
343
|
+
*/
|
|
344
|
+
export function opStartFigmaTempImport(file, fileName, sessionId, done) {
|
|
345
|
+
// The Rust generation guard owns which result may install. Canceling here
|
|
346
|
+
// ensures direct JS callers also never retain two parser Workers at once.
|
|
347
|
+
opCancelAllFigmaTempImports();
|
|
348
|
+
let objectUrl = '';
|
|
349
|
+
let worker = null;
|
|
350
|
+
let timeout = 0;
|
|
351
|
+
let settled = false;
|
|
352
|
+
const releaseWorker = () => {
|
|
353
|
+
if (worker) {
|
|
354
|
+
worker.onmessage = null;
|
|
355
|
+
worker.onerror = null;
|
|
356
|
+
worker.onmessageerror = null;
|
|
357
|
+
worker.terminate();
|
|
358
|
+
worker = null;
|
|
359
|
+
}
|
|
360
|
+
if (objectUrl) {
|
|
361
|
+
URL.revokeObjectURL(objectUrl);
|
|
362
|
+
objectUrl = '';
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const finish = (result) => {
|
|
366
|
+
if (settled) return;
|
|
367
|
+
settled = true;
|
|
368
|
+
activeImports.delete(sessionId);
|
|
369
|
+
if (timeout) clearTimeout(timeout);
|
|
370
|
+
releaseWorker();
|
|
371
|
+
const deliver = () => {
|
|
372
|
+
// Keep one task boundary between Worker teardown/cleanup and Rust's
|
|
373
|
+
// synchronous parse of the canonical success payload or fallback.
|
|
374
|
+
setTimeout(() => done(result), 0);
|
|
375
|
+
};
|
|
376
|
+
if (!result || result.ok !== true) {
|
|
377
|
+
void opDeleteFigmaTempSession(sessionId, Number(result?.pageCount || 0)).then(deliver);
|
|
378
|
+
} else {
|
|
379
|
+
deliver();
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
const cancel = () =>
|
|
383
|
+
finish({ ok: false, canceled: true, sessionId, pageCount: 0, error: 'Figma import canceled' });
|
|
384
|
+
try {
|
|
385
|
+
const blob = new Blob([workerSource()], { type: 'text/javascript' });
|
|
386
|
+
objectUrl = URL.createObjectURL(blob);
|
|
387
|
+
worker = new Worker(objectUrl, { type: 'module', name: 'openpencil-figma-import' });
|
|
388
|
+
worker.onmessage = async (event) => {
|
|
389
|
+
const receipt = event.data;
|
|
390
|
+
if (!receipt || receipt.ok !== true) {
|
|
391
|
+
finish(receipt);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (receipt.sessionId !== sessionId || Number(receipt.pageCount || 0) < 1) {
|
|
395
|
+
finish({
|
|
396
|
+
ok: false,
|
|
397
|
+
sessionId,
|
|
398
|
+
pageCount: Number(receipt.pageCount || 0),
|
|
399
|
+
error: 'Figma Worker returned an invalid commit receipt',
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
// The receipt is intentionally tiny. Tear down the second WASM instance
|
|
404
|
+
// before materializing the canonical JSON in the main JavaScript realm.
|
|
405
|
+
releaseWorker();
|
|
406
|
+
try {
|
|
407
|
+
const fullDocumentJson = await readCommittedDocument(
|
|
408
|
+
receipt.sessionId,
|
|
409
|
+
Number(receipt.pageCount || 0),
|
|
410
|
+
);
|
|
411
|
+
finish({ ...receipt, fullDocumentJson });
|
|
412
|
+
} catch (error) {
|
|
413
|
+
finish({
|
|
414
|
+
ok: false,
|
|
415
|
+
sessionId,
|
|
416
|
+
pageCount: Number(receipt.pageCount || 0),
|
|
417
|
+
error: error instanceof Error ? error.message : String(error),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
worker.onerror = (event) => {
|
|
422
|
+
event.preventDefault();
|
|
423
|
+
finish({ ok: false, sessionId, pageCount: 0, error: event.message || 'Figma Worker failed' });
|
|
424
|
+
};
|
|
425
|
+
worker.onmessageerror = () =>
|
|
426
|
+
finish({
|
|
427
|
+
ok: false,
|
|
428
|
+
sessionId,
|
|
429
|
+
pageCount: 0,
|
|
430
|
+
error: 'Figma Worker returned an unreadable message',
|
|
431
|
+
});
|
|
432
|
+
activeImports.set(sessionId, { cancel });
|
|
433
|
+
timeout = setTimeout(
|
|
434
|
+
() =>
|
|
435
|
+
finish({
|
|
436
|
+
ok: false,
|
|
437
|
+
sessionId,
|
|
438
|
+
pageCount: 0,
|
|
439
|
+
error: 'Figma Worker timed out',
|
|
440
|
+
}),
|
|
441
|
+
IMPORT_TIMEOUT_MS,
|
|
442
|
+
);
|
|
443
|
+
worker.postMessage({
|
|
444
|
+
file,
|
|
445
|
+
fileName,
|
|
446
|
+
sessionId,
|
|
447
|
+
wasmModuleUrl: generatedWasmModuleUrl(),
|
|
448
|
+
});
|
|
449
|
+
} catch (error) {
|
|
450
|
+
finish({
|
|
451
|
+
ok: false,
|
|
452
|
+
sessionId,
|
|
453
|
+
pageCount: 0,
|
|
454
|
+
error: error instanceof Error ? error.message : String(error),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Cancel every in-flight parser Worker before a new document import starts. */
|
|
460
|
+
export function opCancelAllFigmaTempImports() {
|
|
461
|
+
for (const entry of Array.from(activeImports.values())) entry.cancel();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Delete a completed session that lost the Rust import-generation race. */
|
|
465
|
+
export async function opDeleteFigmaTempSession(sessionId, pageCount) {
|
|
466
|
+
let db = null;
|
|
467
|
+
try {
|
|
468
|
+
db = await openMainDatabase();
|
|
469
|
+
|
|
470
|
+
const keysTransaction = db.transaction(RECORDS_STORE, 'readonly');
|
|
471
|
+
const keysRequest = keysTransaction.objectStore(RECORDS_STORE).getAllKeys();
|
|
472
|
+
const keysDone = mainTransactionDone(keysTransaction, 'IndexedDB key transaction failed');
|
|
473
|
+
const keys = await mainRequestResult(keysRequest, 'IndexedDB key read failed');
|
|
474
|
+
await keysDone;
|
|
475
|
+
|
|
476
|
+
const transaction = db.transaction([RECORDS_STORE, MANIFESTS_STORE], 'readwrite');
|
|
477
|
+
const records = transaction.objectStore(RECORDS_STORE);
|
|
478
|
+
const prefix = sessionId + '/';
|
|
479
|
+
for (const key of keys) {
|
|
480
|
+
if (String(key).startsWith(prefix)) records.delete(key);
|
|
481
|
+
}
|
|
482
|
+
// Retain the count-based deletes for compatibility with a browser whose
|
|
483
|
+
// key enumeration omitted records written by an older bundle.
|
|
484
|
+
for (let index = 0; index < pageCount; index += 1) {
|
|
485
|
+
records.delete(sessionId + '/page/' + index);
|
|
486
|
+
}
|
|
487
|
+
transaction.objectStore(MANIFESTS_STORE).delete(sessionId);
|
|
488
|
+
await mainTransactionDone(transaction, 'IndexedDB cleanup failed');
|
|
489
|
+
} catch (error) {
|
|
490
|
+
console.warn('[import-figma] temp cleanup failed:', error);
|
|
491
|
+
} finally {
|
|
492
|
+
if (db) db.close();
|
|
493
|
+
}
|
|
494
|
+
}
|