ذخیرهسازی و پوستهٔ برنامه
کجا چه چیزی ذخیره میشود، کار چطور گم نمیشود، پروندهٔ
.darzچیست، و پوستهٔ برنامه چطور چیده شده. ماندگاری در نسل پنجم (فاز ۱) از نو نوشته شد و پایگاهِ مرورگر در فاز ۹ لایهٔ دادهٔapps/web/src/data/شد.
پایگاه مرورگر (Dexie)
Section titled “پایگاه مرورگر (Dexie)”لایهٔ داده در apps/web/src/data/ است — جدولها، مخزنها، پاکتِ { schemaVersion, data }، ارتقای پایگاه،
versionchange/blocked، پشتیبانِ کامل و دفترِ رویداد: data-layer.md. Dexie بیرون
از آن پوشه وارد نمیشود (لینت).
مرز در نوشتن هم هست. putProject پیش از نوشتن checkProject ِ هسته را میزند؛
پروژهٔ نامعتبرِ حافظه دیگر نوشته نمیشود تا روزی باز نشود. listProjects سلامتِ هر
ردیف را با loadProject میسنجد: ردیفِ خراب یا ساختهشده با نسخهٔ تازهتر روی خانه
کارتِ آسیبدیده («آسیب دیده — باز نمیشود» یا «ساختهشده با نسخهٔ تازهترِ درزساز») با علت («جزئیات») و
دانلود خام (downloadRawProject ← getRawProject) میگیرد.
کاری که گم نمیشود — lib/persistence/
Section titled “کاری که گم نمیشود — lib/persistence/”| پرونده | کار |
|---|---|
autosave-coordinator.ts |
صفِ ذخیره: فقط آخرین حالت، یکییکی، تلاشِ دوباره با پسروی (۱، ۴، ۱۵ ثانیه) برای خطای گذرا |
autosave.ts |
سیمکشی: هر ویرایش پس از ۸۰۰ms؛ پیاده شدن، پنهان شدنِ زبانه و pagehide همان لحظه تخلیه میکنند |
workspace.ts |
کانالِ BroadcastChannel و قفلِ هر پروژه با navigator.locks |
file-link.ts |
پروندهٔ متصل: دستگیره در پایگاه، اجازه فقط با کلیک |
import-project.ts |
آوردنِ پروندهای که همان شناسه در مرورگر دارد: نسخهٔ تازه / جایگزین / لغو |
home-data.ts |
دادهٔ خانه در تکهٔ تنبل، با مهاجرتِ یکبارهٔ نسل قبل |
rescue.ts |
نجات در صفحهٔ خطا: پروژهٔ مسیرِ جاری، به .darz |
backup-actions.ts |
«پرونده ▸ پشتیبان کامل / بازگرداندن از پشتیبان…»: دانلود، انتخابِ پرونده (تأیید در RestoreConfirm)، بارگذاریِ دوباره پس از بازگردانی |
restore-request.ts |
پروندهای که برای بازگردانی برگزیده شده و هنوز تأیید نشده (useRestoreRequest، بی Dexie) |
assets.ts |
تصویرِ پروژه در رابط: openStoredProject، keepImage، useAssetUrl، projectAssets — هر تصویر یک data URL ِ یکباره در حافظه؛ «تصویرهای بیاستفاده» (countUnusedImages، freeUnusedImages) |
raw-download.ts |
دانلود خام (downloadRawProject): ردیف همانطور که نشسته، بی سنجش |
- بارگذاری با
loadIntoEditor(state/store.ts) — تنها راهِ نشاندنِ پروژه: کاتالوگ، دیوار، انتخاب و تاریخچه با هم.project === loadedیعنی چیزی برای ذخیره نیست؛ ذخیرهٔ خودکار بارگذاری را از ویرایش با همین تشخیص میدهد. meta.updatedAtبا هر ویرایشِ رو به جلو (edit،editLive،replaceProject) زمانِ همان لحظه (ISO ِ محلی با منطقه،nowIso؛ تا دقیقه) میگیرد، پس.darzو گزارش همان را میخوانند. واگرد دقیقاً حالتِ پیشین را برمیگرداند.- ذخیره نشد با علت: سهمیهٔ پر («ذخیره در پرونده»، «پاک کردن رندرهای ذخیرهشده» — که رندر را از سند
برمیدارد؛ بایتش فقط با «تنظیمات ▸ تصویرهای بیاستفاده» آزاد میشود، چون تصویر خودکار پاک نمیشود)،
پروژهٔ نامعتبر («واگردِ آخرین تغییر»)، یا خطای ناشناخته («دوباره») — در پاپاورِ نشانِ
ذخیره در نوار بالا (
app/SaveBadge.tsx). پروندهٔ متصلِ بیاجازه ذخیره را «نشد» نمیکند؛ «پرونده: X — نیاز به اجازه» جدا گفته میشود.
دو زبانه، یک پروژه
Section titled “دو زبانه، یک پروژه”ProjectSession (ویرایشگر، هزینه و کارگاه زیرِ #/p/:id…) قفلِ darzsaz:project:<id> را میخواهد.
زبانهای که قفل را دارد ویرایش و ذخیره میکند؛ بقیه readOnly اند، نوارِ «این پروژه در
زبانهٔ دیگری باز است» میبینند و با هر «ذخیره شد» ِ کانال از پایگاه از نو میخوانند.
«همینجا ادامه بده» اول از دارندهٔ قفل میخواهد صفِ ذخیرهاش را تخلیه و قفل را رها
کند (handover)؛ اگر در ۱٫۵ ثانیه جواب نداد، قفل با steal گرفته میشود. زبانهای که
قفل را از دست داد در صف میماند و پیش از ویرایشپذیر شدن از پایگاه میخواند.
مرورگرِ بی navigator.locks همان رفتارِ پیشین را دارد.
پنجرهٔ دوم — کانالِ جدا روی همان قفل
Section titled “پنجرهٔ دوم — کانالِ جدا روی همان قفل”پنجرهٔ پیرو (#/p/:id/view، فاز ۸) قفل نمیخواهد و در پایگاه نه مینویسد نه میخواند: از
ProjectGate/ProjectSession نمیگذرد، ذخیرهٔ خودکار و Worker ِ چیدمان ندارد، و هر چه نشان میدهد
از کانال آمده است. نویسندهٔ کانال دارندهٔ همین قفل است (useWorkspaceWriter در ProjectSession، فقط
در حالتِ held). کانال BroadcastChannel ِ darzsaz-view:<id> است، نه darzsaz-workspace:
عکسِ پروژه در هر فریم نباید به هر زبانهٔ باز ِ برنامه برسد. پیرو پیش از «ویرایشگر بسته شد» میپرسد
قفلِ projectLockName(id) هنوز گرفته است یا نه (navigator.locks.query) — زبانهٔ پنهان ضربانِ کُند
دارد ولی قفلش را نگه داشته. جزئیاتِ پیامها: docs/reference/editor.md «پنجرهٔ دوم».
نسخههای خودکار
Section titled “نسخههای خودکار”هر ۲۰ ذخیره یا ۵ دقیقه، و پیش از جایگزینیِ پروژه با پرونده (before-replace) و پیش از
برگرداندنِ نسخه (before-restore). پنجرهٔ تاریخچه زیرِ قدمهای واگرد فهرستشان را با
علت دارد؛ «برگرداندن» خودش یک قدم واگرد است.
ماندگاری — چرا لازم است
Section titled “ماندگاری — چرا لازم است”پس از اولین ذخیرهٔ موفق و در هر شکستِ سهمیه checkStorageHealth (lib/storage-health.ts)
اجرا میشود و navigator.storage.persist() را میخواهد — در هر بارگذاریِ صفحه حداکثر یک بار، چون
requestPersistence پاسخ را نگه میدارد. بی آن، مبدأ در حالت best-effort است:
- زیر فشار فضا، مرورگر IndexedDB را بیاطلاع پاک میکند.
- سافاری (ITP) ذخیرهسازیِ نوشتنیِ اسکریپت را پس از هفت روز بیتعامل با سایت پاک میکند. یعنی کاربری که آشپزخانهای طراحی کرد و یک هفته برنگشت، اگر «ذخیره در پرونده» نزده باشد همهچیز را از دست میدهد.
چرا در اولین ذخیره و نه در بار اول صفحه: کروم persist() را بدون نشانهٔ
تعامل رد میکند و چیزی هم نمیگوید. اولین ذخیرهٔ خودکار یعنی کاربر واقعاً
چیزی ساخته.
اگر مرورگر رد کرد، یک بار (در هر مرورگر، با نشان در settings) گفته
میشود، با دکمهٔ «ذخیره در پرونده». سهمیهٔ بالای ۸۰٪ هم هشدار میگیرد — آن
یکی هر بار، چون فوری است و با پاککردن چند پروژه برطرف میشود.
اعلانها بیست ثانیه میمانند، نه برای همیشه: اعلانِ دائمی روی نوار تبِ پایینِ گوشی مینشیند و کلیک را میگیرد — هشداری که خودش کار را میبندد، از خطری که دربارهٔ آن هشدار میدهد بدتر است. اعلانِ «نسخهٔ تازه آماده است» هم بیست ثانیه است و ردیفِ ماندگارش در منوی «کمک».
- پایگاه نسل قبل (
darzsaz/ خانهٔcurrent) یک بار باmigrateLegacy(data/legacy.ts) در بارگذاریِ خانه بهprojectsمیآید و نشانlegacyMigratedمیخورد. شکستِ نوشتن نشان نمیگذارد (بارِ بعد دوباره)، و نبودنِ پایگاهِ قدیمی پایگاهی خالی به جایش نمیسازد. - مهاجرت اسکیمای پایگاه:
version(n)خودِ Dexie درdata/database.ts؛ نسخهٔ فعلی ۵ (data-layer.md).
پروندهٔ .darz
Section titled “پروندهٔ .darz”در هسته: packages/core/src/io/darz.ts (packDarz(project, { app, thumbnail?, assets? })، unpackDarz،
unpackDarzWithAssets، checkDarzSize، DARZ_LIMITS، darzFileName) — وب و خط فرمان هر دو از همین (فاز ۹٫۴). هسته
نسخهٔ برنامه را نمیداند و بستهبند app را میدهد.
zip (fflate) با project.json (همان saveProject — که پیش از نوشتن میسنجد)،
manifest.json (format: 'darz', formatVersion: 2, نسخهٔ برنامه، زمان)،
thumbnail.svg (فقط وقتی بندانگشتی داده شود؛ نجاتِ صفحهٔ خطا بی آن میبندد) و assets/<id>.<png|jpg|webp> — عکس و رندری که سند فقط شناسهشان را دارد (۹.۳؛ بی
فشردهسازیِ دوباره). بستهٔ قالبِ ۱ (تصویر درونِ project.json ِ نسخهٔ ۲) هنوز باز میشود.
تصویرهای بسته از projectAssets (lib/persistence/assets.ts) میآیند — «ذخیره در پرونده»، پروندهٔ متصل و نجات هر
سه — یعنی حافظهٔ نوشتهنشده و جدولِ assets. حذفِ پروژه تصویری از آن جدول برنمیدارد (تصمیمِ مالک؛
data-layer.md)، پس .darz ِ پروژهٔ دیگری با همان تصویرها پس از
حذف هم کامل است؛ تصویرِ بیاستفاده فقط با «تنظیمات ▸ تصویرهای بیاستفاده ▸ آزاد کردن…» و تأیید میرود.
باز کردن مرز است (unpackDarz):
- پرونده بیش از ۱۲۸ مگابایت پیش از خواندن رد میشود؛ هر عضوی که بیش از ۲۵۶ مگابایت اعلام کند پیش از تخصیص (zip ِ چند کیلوبایتی که «دو گیگابایت» بگوید، زبانه را میکُشت).
- فقط
project.json،manifest.jsonوassets/*باز میشوند؛ عضوِ دیگر خوانده نمیشود. عضوِ تصویری که محتوایش با شناسهٔ نامش نمیخواند کنار میرود: شناسه از محتواست و یک مرورگر تصویرِ همهٔ پروژهها را با همان شناسه نگه میدارد (unpackDarzWithAssets). manifest.jsonباformatVersionِ تازهترdarz/newer-formatاست، نه پروژهٔ خراب.- عکسِ دیوار و رندرِ پروندهٔ ۱ و ۲ فقط data URL ِ PNG، JPEG یا WebP (سندِ ۳ فقط
assetId)؛ رشتهها و شمارِ رندر سقف دارند (LIMITSدرpackages/core/src/schema/common.ts)؛catalogOverridesاسکیمای کامل دارد و کاتالوگش در باز کردن ساخته میشود — پروندهای که باز شد، ویرایششدنی است.
⚠️ unpackDarz عمداً thumbnail.svg را نمیخواند؛ بندانگشتی از پروژه از
نو ساخته میشود. خواندنش یعنی XSS: پروندهٔ .darz از واتساپ میآید، SVG
میتواند اسکریپت داشته باشد، و ProjectCards.tsx آن را با
dangerouslySetInnerHTML نشان میدهد.
File System Access (کروم/اِج دسکتاپ): «ذخیره در پرونده» یک بار مسیر میگیرد
(pickDarzFile)، دستگیره در fileHandles مینشیند و ذخیرهٔ خودکار همانجا هم
مینویسد. مرورگر پس از بارگذاریِ دوباره اجازه را پس میگیرد؛ نوار بالا «نیاز به اجازه»
میگوید و همان دکمه اجازه میخواهد. بدون File System Access، دانلود معمولی.
- CSP در
index.htmlِ بیلد (vite.config.ts، پس از<meta charset>): اسکریپت فقط از خودِ برنامه و اسکریپتِ درونخطیِ پوسته با هشِ sha256 ِ خودش (نهunsafe-inline)، شبکه فقط به خودش و آینههای نرخ دلار، بیunsafe-eval(zod بی JIT).vite devبی آن. - قابِ چاپ
sandbox="allow-same-origin allow-modals"— بی اسکریپت؛ پیشنمای مرکز چاپsandbox="". window.__darzsazOpen(خانه ← ویرایشگر؛darzsaz render، آزمون —app/automation.ts)،window.__darzsazLoad(ویرایشگر؛ ابزارِ عکسبرداریscripts/orbit.mjs، بنچمارک) وwindow.__darzsazRender(darzsaz render، آزمون) فقط وقتیnavigator.webdriver— هیچ اسکریپتی در صفحه نباید پروژه را جایگزین کند یا کارت گرافیک را به کار بگیرد.unhandledrejectionوerrorِ بیصاحب اعلان میشوند (lib/global-errors.ts).
مسیرها و پوسته
Section titled “مسیرها و پوسته”#/ خانه: پروژههای اخیر، تازه، باز کردن، الگوها، نمونه#/p/:id ویرایشگر#/p/:id/cost داشبورد هزینه#/p/:id/workshop حالت کارگاه#/p/:id/view?pane=3d|2d|plan پنجرهٔ پیرو (فقطدیدنی، از کانال؛ بی پایگاه)#/share پروندهٔ فرستادهشده از واتساپ#/help راهنما، نقشهٔ کلیدها، دربارهٔهر چیز دیگر «این نشانی در درزساز نیست» — نه رفتنِ بیصدا به خانهسه مسیرِ پروژه (#/p/:id، …/cost، …/workshop؛ نه …/view) از useProject میخوانند و ProjectGate حالتها را از کدِ
خطا نشان میدهد: اسکلتِ بارگذاری، آماده (با ProjectSession)، «پروژه در این مرورگر
نیست»، «با نسخهٔ تازهترِ درزساز ساخته شده» و «آسیب دیده» (هر دو با دانلود خام)، و
«پایگاه مرورگر باز نشد» با متن خطا. خانه هم بارگذاری، خالی و خطای پایگاه را جدا دارد.
App.tsxمسیرها را با wouter روی hash میسازد؛ اگر همان پروژه در فروشگاه باشد دست نمیخورد تا تاریخچه بماند.- باز کردن پرونده یک مسیر دارد (
app/useOpenProjectFile.tsx) برای خانه، صفحهٔ «فرستادهشده» و منوی ویرایشگر. - فرمانها یک رجیستریاند (
REGISTRYدرapp/registry.ts: برچسب، گروهِ منو، لایه، کلید)؛app/useCommands.tsباcommandOfفهرستِ فرمانِ ویرایشگر را از آن میسازد و منوها (TopBar)، پالت فرمان (CommandPalette،CommandListِ کیت) و میانبرها (useShortcuts، فقط لایهٔapp) همه از همان میخوانند؛ میانبرِ تازه = فیلدِshortcutِ مدخلِ رجیستری، و نقشهٔ کلیدهای راهنما (keymap.ts) از همان رجیستری ساخته میشود. میانبری که در رجیستری هست ولی درuseCommandsفرمانی ندارد، کار نمیکند — Ctrl+K از فاز ۳ تا گرفتنِ فرمانِpaletteهمینطور بود. - کلید با
shortcutKey(lib/keys.ts) خوانده میشود، نهe.key. با چیدمانِ فارسیe.keyِ کلید K «ن» است و رقم ردیف بالا «۱»؛shortcutKeyحرف/رقمِ لاتینِ چیدمان را باور میکند و وگرنه جای فیزیکی (e.code) را. Shift برای حرف، رقم و کلیدِ نامدار (Enter، جهتنما) سنجیده میشود (matchesChord)؛ نشانه Shift را در خودش دارد: «?» خودش Shift+/ است — مگر نشانهای که رجیستری با Shift نوشته (Shift+\). - وضعیت رابط (
state/ui.ts: نما، چیدمانِ پنلها برای هر اندازهٔ صفحه، پوسته، زبان، روکشهای بوم) جدا از سند است و درlocalStorageمیماند؛ وضعیت ذخیره درstate/save-status.ts. - واگرد تراکنشی:
edit(label, recipe, { coalesce })— ویرایشهای پیاپی با یک کلید در پنجرهٔ لغزندهٔ ۸۰۰ms یک قدم میشوند (COALESCE_MS)؛ هر قدم نام و زمان دارد و پنل تاریخچه (HistoryPanel) باjump(n)به آن میرود. - خط لولهٔ چیدمان (
lib/derive-client.ts): مهلت ← Worker ِ تازه در درخواستِ بعد (نه نخ اصلی برای همیشه)؛ سه افتادنِ پیاپیِ Worker ← «کندتر» در نوار پایین با «دوباره».
- واحد و DOM:
db.test.tsوdata-layer.test.ts(fake-indexeddb: ارتقا، اتصال، پشتیبان، دفترِ رویداد)،db-connection-bar.dom.test.tsx،core/test/darz.test.ts،thumbnail.test.ts،history.test.ts(زمان تزریقیclock.now)،load-into-editor.test.ts،autosave-coordinator.test.ts(ساعتِ دستی)،project-lock.test.ts(Web Locks ِ ساختگی)،file-link.test.ts،import-project.test.ts،save-problem.dom.test.tsx،app-crash.dom.test.tsx،history-panel.dom.test.tsx،derive-client.test.ts،home.dom.test.tsx،project-gate.dom.test.tsx،workspace-channel.test.ts(پیامِ نامعتبر، وصله در فریم، بسته پس از ۳ ثانیه، نویسنده و پیرو روی فروشگاه)،viewer-page.dom.test.tsx،assets-repo.test.tsوexport-images.test.ts(تصویر پس از حذفِ پروژه در.darz، نجات، پشتیبان و جلدِ چاپ)،print-share.dom.test.tsx،unused-images.dom.test.tsx. - Playwright:
e2e/work-kept.spec.ts(ویرایش و رفتن در میانهٔ مکث، دو زبانه، پایگاهِ مسدود)،e2e/security.spec.ts(CSP روی همهٔ مسیرها، قابِ چاپ)،e2e/persist.spec.ts،e2e/second-window.spec.ts(پیرو: صفر تراکنشِ نوشتنی، «بسته شد» پس از بستنِ ویرایشگر). نگهبانِe2e/fixtures.tsدر هر آزمونpageerrorو نقضِ CSP را صفر میخواهد.
PWA و تقسیم کد
Section titled “PWA و تقسیم کد”vite-plugin-pwa(Workbox) درapps/web/vite.config.ts:registerType: 'prompt'— نسخهٔ تازه تا کاربر «بارگذاری دوباره» را نزند فعال نمیشود (src/lib/pwa.ts،src/lib/app-update.ts). خروجیِ build (js، css، html، svg، png، woff2، webmanifest؛ هر پرونده تا ۶ مگابایت) پیشکش میشود؛tgju.orgهمیشه از شبکه. مانیفستِ هر زبان (manifest.<زبان>.webmanifest): بخشِ مشترک (APP_MANIFESTباid: './'، آیکون،share_target) درvite.config.tsو نام، توضیح،langوdirاز کاتالوگِ همان زبان (vite/manifest.ts)؛ مانیفستِ خودِ افزونه خاموش است. پیوندِ پیشفرض راdocumentHeadمینشاند وapplyDocumentLocaleبا تعویضِ زبان نامِ پرونده را عوض میکند (src/lib/manifest.tsیک منبع برای هر دو). آیکونها درpublic/icons/باnode scripts/pwa-icons.mjs(کرومِ نصبشده) ازicon.svg.- آزمون:
e2e/pwa.spec.ts(مانیفست با آیکونهای PNG و maskable، سرویسورکر فعال، بارگذاری آفلاین). Lighthouse ۱۲ دستهٔ PWA ندارد؛ همین آزمون جای آن است. - تقسیم کد: مسیرهای ویرایشگر، هزینه، کارگاه، «پروندهٔ فرستادهشده» و پنجرهٔ پیرو، تأییدِ بازگردانی، نمای
سهبعدی، پنل رندر، عکس، دادهٔ خانه،
پنجرهها و بستهٔ گزارش (
@darzsaz/reportبا قلمهای چاپ) باlazy/import()میآیند.scripts/size-budget.mjsعلاوه بر بودجهٔ هر نوع، تکهٔ اولیه (ورودی + importهای ایستا + CSS) را میسنجد: سقف ۱۶۵ کیلوبایت gzip.