@panphora/clayjs 1.24.0 → 1.25.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/dist/all.js +1 -1
- package/dist/clay-dom.js +1 -1
- package/dist/clay-events.js +1 -1
- package/dist/clay-internals.js +1 -1
- package/dist/clay-options.js +1 -1
- package/dist/clay-router.js +1 -1
- package/dist/clay-ui.js +1 -1
- package/dist/clay-utils.js +1 -1
- package/dist/clay.js +1 -1
- package/dist/clay.standalone.js +16610 -14400
- package/dist/m/attrs/{refetch-on-save-DPGJJMGN.js → refetch-on-save-5CBFUJUF.js} +1 -1
- package/dist/m/attrs/save-freeze-MH3AOD56.js +1 -0
- package/dist/m/c-277QP5ET.js +1 -0
- package/dist/m/{c-SXGEWPFC.js → c-3E2IK5HU.js} +1 -1
- package/dist/m/{c-A4L7APIU.js → c-4AE2A2LK.js} +1 -1
- package/dist/m/{c-DZPO22O3.js → c-4Q6QIODW.js} +1 -1
- package/dist/m/{c-RO73MKKR.js → c-52OC3OH3.js} +1 -1
- package/dist/m/{c-VNAYHV2J.js → c-5PZYDWTW.js} +1 -1
- package/dist/m/c-5Q5XBN56.js +1 -0
- package/dist/m/c-5SZ6KFR5.js +1 -0
- package/dist/m/{c-ZRGEN6QQ.js → c-AI3H6ETU.js} +2 -2
- package/dist/m/{c-7V6MGMX2.js → c-BCWYEG6P.js} +1 -1
- package/dist/m/{c-A7N5TNSL.js → c-BJ2RRQBQ.js} +1 -1
- package/dist/m/{c-CTGRDT6E.js → c-C6YZNU6E.js} +1 -1
- package/dist/m/{c-DLBX4JCJ.js → c-C7SUMNMI.js} +1 -1
- package/dist/m/c-CITLTVQ4.js +4 -0
- package/dist/m/{c-TWOXQQKV.js → c-CL5LSATX.js} +1 -1
- package/dist/m/c-CU33V6RW.js +4 -0
- package/dist/m/{c-2R4OBWIP.js → c-D62HRSZK.js} +1 -1
- package/dist/m/{c-DD26VXAP.js → c-DT7E76Z2.js} +1 -1
- package/dist/m/{c-54ZZZYUN.js → c-EAV7A6BT.js} +1 -1
- package/dist/m/{c-RBPZNU2D.js → c-EQ5NBOGA.js} +1 -1
- package/dist/m/{c-KQKKIVLW.js → c-FBOO2UY3.js} +1 -1
- package/dist/m/{c-RTSRGGTP.js → c-FJDTDGDA.js} +1 -1
- package/dist/m/{c-MQKSKQHG.js → c-FKTALXNV.js} +1 -1
- package/dist/m/{c-GNIVHLEJ.js → c-GDNZEOOZ.js} +1 -1
- package/dist/m/{c-SUXHUL7U.js → c-GEAG5XAK.js} +1 -1
- package/dist/m/{c-UM7VZDTW.js → c-GY4BREDT.js} +1 -1
- package/dist/m/{c-4PO3YGDC.js → c-HGG7M2S3.js} +1 -1
- package/dist/m/c-HKSLOVU4.js +1 -0
- package/dist/m/{c-KJHVB2KG.js → c-HXGNH6GY.js} +1 -1
- package/dist/m/{c-FLEIU6CG.js → c-IESDKFO6.js} +1 -1
- package/dist/m/{c-SNVDWMM3.js → c-IJHF4IO6.js} +1 -1
- package/dist/m/{c-SGGGCIVD.js → c-IL5IVTOG.js} +1 -1
- package/dist/m/c-JB5YU57S.js +1 -0
- package/dist/m/{c-MP6S4AZU.js → c-JD2VFAYG.js} +2 -2
- package/dist/m/{c-CV4T65JE.js → c-JWW6BHWD.js} +3 -3
- package/dist/m/{c-GM7OA6EJ.js → c-K4M76VPM.js} +1 -1
- package/dist/m/{c-RXTPKCMC.js → c-KU3PX7LL.js} +1 -1
- package/dist/m/{c-2PVGJFI5.js → c-KYTVZT4D.js} +1 -1
- package/dist/m/{c-RJKKRWHW.js → c-M7ITYKMF.js} +1 -1
- package/dist/m/c-MFB74XVN.js +3 -0
- package/dist/m/c-MOP5KBWZ.js +1 -0
- package/dist/m/{c-YB3CDTDZ.js → c-MUM53J5J.js} +1 -1
- package/dist/m/{c-YK2L436K.js → c-MV7TEUTD.js} +1 -1
- package/dist/m/{c-LA2BW7UY.js → c-MZCOGNZL.js} +1 -1
- package/dist/m/{c-YGGO2G2L.js → c-N4GEJMXW.js} +1 -1
- package/dist/m/{c-NQAPWM7O.js → c-NADLYVAH.js} +1 -1
- package/dist/m/{c-RS77ODLY.js → c-NL4I4KNG.js} +1 -1
- package/dist/m/{c-6UPEKW6O.js → c-O2QZDVZ2.js} +1 -1
- package/dist/m/{c-LTSPHVAK.js → c-OBMZASYQ.js} +1 -1
- package/dist/m/{c-LUXAB2XI.js → c-ORLPQFNU.js} +1 -1
- package/dist/m/c-P73XPAOA.js +1 -0
- package/dist/m/{c-PWJSJEJU.js → c-PNZIJFG7.js} +1 -1
- package/dist/m/{c-ZKKAF4UT.js → c-PRZNJUYZ.js} +2 -2
- package/dist/m/{c-Q6VNQDJE.js → c-Q2CB57UG.js} +1 -1
- package/dist/m/{c-TWCXBWS3.js → c-QCDVDZIR.js} +1 -1
- package/dist/m/{c-P5NKFRL4.js → c-QDVG4NAZ.js} +1 -1
- package/dist/m/c-QWB53KRW.js +1 -0
- package/dist/m/{c-GNI7WHKF.js → c-RGAALCDU.js} +1 -1
- package/dist/m/c-RHPHMTF2.js +2 -0
- package/dist/m/{c-BULK5J4M.js → c-RWJ3OSCP.js} +1 -1
- package/dist/m/{c-5FXEKU4Y.js → c-SEVOS4UE.js} +1 -1
- package/dist/m/{c-PMRRMT72.js → c-SFYTKRU7.js} +1 -1
- package/dist/m/{c-47IBJGPQ.js → c-SFZRY6RT.js} +1 -1
- package/dist/m/{c-QENKN7QI.js → c-SGG6MUXW.js} +1 -1
- package/dist/m/c-SMLAJASY.js +1 -0
- package/dist/m/{c-VSEUFJAO.js → c-SUAC5RSG.js} +1 -1
- package/dist/m/{c-6RGEQNVV.js → c-SXZYVXD6.js} +1 -1
- package/dist/m/{c-ZFO5RW6G.js → c-TCFVG3I4.js} +1 -1
- package/dist/m/{c-PZRWZZRO.js → c-TLQINES7.js} +2 -2
- package/dist/m/c-U4TWEIKZ.js +1 -0
- package/dist/m/{c-FZRZPIFS.js → c-U65PREMC.js} +1 -1
- package/dist/m/{c-7YETSAVY.js → c-U7F66I64.js} +1 -1
- package/dist/m/{c-WOELSNWS.js → c-UD4VDBGJ.js} +1 -1
- package/dist/m/{c-VI3BJHCM.js → c-V2FPBRO6.js} +1 -1
- package/dist/m/{c-2OIKXOY6.js → c-V4B3RUWB.js} +1 -1
- package/dist/m/{c-O5ARALMJ.js → c-V5L6HWVL.js} +1 -1
- package/dist/m/c-VMXCX52Q.js +1 -0
- package/dist/m/{c-PVJMJL3P.js → c-VO6RSFCO.js} +1 -1
- package/dist/m/c-WDFQ7PBU.js +1 -0
- package/dist/m/{c-OG6GFSUX.js → c-WGQVSKKC.js} +1 -1
- package/dist/m/c-WSTUCBHV.js +1 -0
- package/dist/m/{c-EY5KDUZF.js → c-WVCT3ITX.js} +1 -1
- package/dist/m/{c-SE3SIY7Y.js → c-X3V4RWMA.js} +1 -1
- package/dist/m/{c-HIX5H5L7.js → c-XHZIKRCV.js} +1 -1
- package/dist/m/{c-2BLVA6IN.js → c-XQTERKQA.js} +1 -1
- package/dist/m/{c-A5ZBFICF.js → c-YFO6ZJLQ.js} +1 -1
- package/dist/m/{c-PD5AKHY3.js → c-YX2MC7YL.js} +1 -1
- package/dist/m/{c-OYHI67B6.js → c-YYGUHSZR.js} +1 -1
- package/dist/m/{c-YYMZWQLR.js → c-Z2OZFX3N.js} +1 -1
- package/dist/m/{c-AT7GLQGV.js → c-ZEA3DCG3.js} +1 -1
- package/dist/m/{c-75N3KG35.js → c-ZJG6C2WL.js} +1 -1
- package/dist/m/{c-FVPNX2GO.js → c-ZORVIXUR.js} +1 -1
- package/dist/m/{c-FV46ZKGX.js → c-ZPI7M5U5.js} +1 -1
- package/dist/m/core/{admin-attrs-5UGMYCKW.js → admin-attrs-ZI6K6UGM.js} +1 -1
- package/dist/m/core/{autosave-4P66VXX7.js → autosave-BSM2ABJL.js} +1 -1
- package/dist/m/core/{conflict-notice-BAR23OWH.js → conflict-notice-7EQLDYPA.js} +1 -1
- package/dist/m/core/draft-mode-M5XNYBMY.js +1 -0
- package/dist/m/core/{host-permission-3HE4CFD6.js → host-permission-5BYP24GC.js} +1 -1
- package/dist/m/core/{is-edit-mode-GK7IFFMT.js → is-edit-mode-73OXKK5H.js} +1 -1
- package/dist/m/core/{persist-AIJGTKKI.js → persist-2SZO3PCE.js} +1 -1
- package/dist/m/core/{recovery-LPB6X72P.js → recovery-VZF7RJZF.js} +1 -1
- package/dist/m/core/{save-FV4ZCKBI.js → save-K6Q47J2X.js} +1 -1
- package/dist/m/core/{save-core-K7XFAWQ4.js → save-core-U3Z44XBO.js} +1 -1
- package/dist/m/core/{site-edits-TEQORBJU.js → site-edits-CFMF6GHD.js} +1 -1
- package/dist/m/core/{snapshot-FJP5IJL3.js → snapshot-PWCHYFL6.js} +1 -1
- package/dist/m/core/unsaved-warning-2WRZLJH5.js +1 -0
- package/dist/m/events/{index-B6TGDYMG.js → index-YKQIBZE5.js} +1 -1
- package/dist/m/internals/index-FSLC4M7J.js +1 -0
- package/dist/m/lib/{cache-bust-NH7VBWHO.js → cache-bust-BL4KUQAJ.js} +1 -1
- package/dist/m/loader-RC54UV7H.js +1 -0
- package/dist/m/manifest-3B84C829.js +1 -0
- package/dist/m/manifest-edit-1CB79AD4.js +1 -0
- package/dist/m/manifest.json +1215 -1129
- package/dist/m/options/{options-55LQHSJ4.js → options-LKIZ7YTG.js} +1 -1
- package/dist/m/page/{editor-KAZDJNND.js → editor-PKSPJ2XJ.js} +2 -2
- package/dist/m/plugins/{ai-edit-CUKQQWJX.js → ai-edit-3LG6MGS3.js} +1 -1
- package/dist/m/plugins/{cms-JANMGPHM.js → cms-T2HLI7JG.js} +16 -16
- package/dist/m/plugins/{demo-JIR7DXJA.js → demo-LMCMBILC.js} +1 -1
- package/dist/m/plugins/{derive-QVI3LI3A.js → derive-7DR5TUDH.js} +1 -1
- package/dist/m/plugins/{indicator-DAKZGAWI.js → indicator-CTKX4EY7.js} +1 -1
- package/dist/m/plugins/{live-UAJGWRNP.js → live-IFLL6YN5.js} +1 -1
- package/dist/m/plugins/{menu-BFJ644YX.js → menu-YJZ3JWBC.js} +1 -1
- package/dist/m/plugins/page-GTDPYGE4.js +2 -0
- package/dist/m/plugins/{page-view-OYN7Z5TS.js → page-view-NMCCU6MF.js} +1 -1
- package/dist/m/plugins/{people-VBDR5Y22.js → people-6WIWCZEG.js} +1 -1
- package/dist/m/plugins/{richclay-CBLZRFB4.js → richclay-L3BZMX4I.js} +3 -3
- package/dist/m/plugins/sortable-365SRRL3.js +1 -0
- package/dist/m/plugins/{source-ZXDD7F6X.js → source-7LSGOV46.js} +1 -1
- package/dist/m/plugins/{undo-5JI5G7CQ.js → undo-UES4KS7B.js} +1 -1
- package/dist/m/plugins/{upgrade-6B6QS3NR.js → upgrade-MMPZUCDM.js} +1 -1
- package/dist/m/plugins/{upload-DNJNW6H3.js → upload-NZET3DNQ.js} +1 -1
- package/dist/m/plugins/visitor-drafts-V5YB7XDH.js +20 -0
- package/dist/m/plugins/{visitor-drafts-owner-VIAZC3CF.js → visitor-drafts-owner-TLECTYWS.js} +1 -1
- package/dist/m/plugins/{visitor-menu-ZHKWIKKY.js → visitor-menu-QDV2MYJN.js} +1 -1
- package/dist/m/plugins/wire-3YGUO3GE.js +1 -0
- package/dist/m/{reader-core-F6RY3V64.js → reader-core-VR7EL4WV.js} +2 -2
- package/dist/m/sync/live-sync-XMXXZS3G.js +1 -0
- package/dist/m/vendor/sap-core.vendor-JANUQULT.js +7 -0
- package/dist/offline-manifest.json +724 -664
- package/docs/guide.md +1 -0
- package/entries/clay-data.js +1 -1
- package/entries/sap.js +6 -5
- package/package.json +2 -2
- package/src/core/draft-mode.js +11 -0
- package/src/core/save-core.js +16 -17
- package/src/core/save.js +1 -1
- package/src/core/unsaved-warning.js +2 -2
- package/src/lib/dirty-gate.js +19 -0
- package/src/lib/idb-error-code.js +20 -0
- package/src/loader.js +2 -0
- package/src/plugins/menu/visitor.js +33 -1
- package/src/plugins/page.js +19 -5
- package/src/plugins/sortable.js +7 -0
- package/src/plugins/visitor-drafts/commit.js +350 -0
- package/src/plugins/visitor-drafts/idb.js +227 -0
- package/src/plugins/visitor-drafts/prompt.js +23 -3
- package/src/plugins/visitor-drafts/recovery-menu.js +118 -0
- package/src/plugins/visitor-drafts/recovery.js +64 -0
- package/src/plugins/visitor-drafts/session.js +393 -0
- package/src/plugins/visitor-drafts/store.js +247 -27
- package/src/plugins/visitor-drafts.js +177 -28
- package/src/sync/live-sync.js +1 -1
- package/src/sync/wire-profile.js +9 -4
- package/src/vendor/hyper-html-api-data.vendor.js +1 -1
- package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
- package/src/vendor/hypercms.vendor.js +16 -16
- package/src/vendor/richclay.vendor.js +1 -1
- package/src/vendor/sap-core.vendor.js +6 -5
- package/src/vendor/sap-ownership.vendor.js +181 -11
- package/tool-generation/system.md +1 -1
- package/dist/m/attrs/save-freeze-JZHKRQLR.js +0 -1
- package/dist/m/c-234RSEIW.js +0 -1
- package/dist/m/c-2YIJ6ZA6.js +0 -1
- package/dist/m/c-H2H7TI6O.js +0 -1
- package/dist/m/c-HSNGLIFB.js +0 -3
- package/dist/m/c-JMDKPBMC.js +0 -4
- package/dist/m/c-LO4C4J77.js +0 -1
- package/dist/m/c-N7JPUPGC.js +0 -1
- package/dist/m/c-NEXFVJIT.js +0 -1
- package/dist/m/c-R23UHMXG.js +0 -1
- package/dist/m/c-T527QHOB.js +0 -1
- package/dist/m/c-V2U3F4QX.js +0 -1
- package/dist/m/c-VG5XIN4T.js +0 -1
- package/dist/m/c-YINHPF4V.js +0 -1
- package/dist/m/core/draft-mode-WSQAT67S.js +0 -1
- package/dist/m/core/unsaved-warning-FF63QGKI.js +0 -1
- package/dist/m/internals/index-AX4CG7B2.js +0 -1
- package/dist/m/loader-5IA5OJ3J.js +0 -1
- package/dist/m/manifest-98258645.js +0 -1
- package/dist/m/manifest-edit-ABF56000.js +0 -1
- package/dist/m/plugins/page-ZBIYHGK4.js +0 -2
- package/dist/m/plugins/sortable-HDOQ6GAC.js +0 -1
- package/dist/m/plugins/visitor-drafts-A5FDQ2G7.js +0 -20
- package/dist/m/plugins/wire-XOT7XKJL.js +0 -1
- package/dist/m/sync/live-sync-M2PP2IWY.js +0 -1
- package/dist/m/vendor/sap-core.vendor-OA5SKL7Q.js +0 -6
- /package/dist/m/plugins/{richclay-CBLZRFB4.js.LEGAL.txt → richclay-L3BZMX4I.js.LEGAL.txt} +0 -0
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { extractData, applyData, engine } from "../../vendor/hyper-html-api-data.vendor.js";
|
|
2
|
-
import { draftSlot, setDraftStoreFailed } from "../../core/draft-mode.js";
|
|
2
|
+
import { draftGate, draftSlot, setDraftStoreFailed } from "../../core/draft-mode.js";
|
|
3
3
|
|
|
4
4
|
export const KEY_PREFIX = "clay:visitor-drafts:";
|
|
5
5
|
export const RECORD_VERSION = 1;
|
|
6
6
|
const LINEAGE = 32;
|
|
7
|
+
// What one tab's journal slot in `localStorage` may hold, and what its unconfirmed
|
|
8
|
+
// operations may hold in memory, in characters of JSON.
|
|
9
|
+
export const SLOT_LIMIT = 512 * 1024;
|
|
10
|
+
export const PENDING_LIMIT = 16 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
export const newRecordId = () => Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 6);
|
|
13
|
+
|
|
14
|
+
function textMark(text) {
|
|
15
|
+
let hash = 0x811c9dc5;
|
|
16
|
+
for (let i = 0; i < text.length; i++) hash = Math.imul(hash ^ text.charCodeAt(i), 0x01000193);
|
|
17
|
+
return (hash >>> 0).toString(36) + "." + text.length.toString(36);
|
|
18
|
+
}
|
|
7
19
|
|
|
8
20
|
// The document a pathname names, spelled the way the hosts resolve it: the path up to
|
|
9
21
|
// and including the first segment that ends in .html or .htmlclay (anything after it is
|
|
@@ -55,12 +67,13 @@ export function fitsInto(wanted, got) {
|
|
|
55
67
|
return wanted === got;
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
function same(a, b) {
|
|
70
|
+
export function same(a, b) {
|
|
59
71
|
return fitsInto(a, b) && fitsInto(b, a);
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
const STORE_SEL = 'script[type="application/json"][merge][clay~="live"]';
|
|
63
75
|
const ARTICLE_SEL = "article[data-blockeditor]";
|
|
76
|
+
const HANDLER_SEL = "[onbeforesave], [onbeforesnapshot]";
|
|
64
77
|
const isPagePart = (name) => name === "blockeditor" || /^blockeditor\.\d+$/.test(name);
|
|
65
78
|
const canonical = (live) => JSON.stringify(Object.keys(live).sort().map((name) => [name, live[name]]));
|
|
66
79
|
|
|
@@ -78,6 +91,9 @@ function readUnit(source) {
|
|
|
78
91
|
|
|
79
92
|
const sameUnit = (a, b) => (!a || !b ? a === b : a.blockeditor.rev === b.blockeditor.rev && canonical(a.live) === canonical(b.live));
|
|
80
93
|
|
|
94
|
+
// One page content, one mark: two pages `sameUnit` calls the same carry the same one.
|
|
95
|
+
export const unitMark = (unit) => (isPlain(unit?.live) && isPlain(unit?.blockeditor) ? `${unit.blockeditor.rev}:${textMark(canonical(unit.live))}` : null);
|
|
96
|
+
|
|
81
97
|
function unitOf(record) {
|
|
82
98
|
if (!isPlain(record?.live) || !isPlain(record?.blockeditor)) return null;
|
|
83
99
|
const { live, blockeditor } = record;
|
|
@@ -189,7 +205,7 @@ export function merge3(base, mine, theirs) {
|
|
|
189
205
|
return merged;
|
|
190
206
|
}
|
|
191
207
|
|
|
192
|
-
function storageFailure(doc, error) {
|
|
208
|
+
export function storageFailure(doc, error) {
|
|
193
209
|
setDraftStoreFailed(true);
|
|
194
210
|
doc.dispatchEvent(new CustomEvent("clay:drafts-store-failed", { detail: { error } }));
|
|
195
211
|
const failure = new Error("Couldn't save in this browser. Storage is full or turned off.");
|
|
@@ -254,13 +270,20 @@ function withoutSiteKey(value) {
|
|
|
254
270
|
return rest;
|
|
255
271
|
}
|
|
256
272
|
|
|
257
|
-
export function createStore({ doc = document, storage = safeStorage(), now = () => new Date(), path = draftPath(), fallback = null } = {}) {
|
|
273
|
+
export function createStore({ doc = document, storage = safeStorage(), gate = draftGate, now = () => new Date(), path = draftPath(), fallback = null, slotLimit = SLOT_LIMIT, pendingLimit = PENDING_LIMIT } = {}) {
|
|
258
274
|
const slot = draftSlot(doc);
|
|
259
275
|
const key = draftKey(slot, path);
|
|
260
276
|
const rules = withoutSiteKey(engine.findRulesIn(doc, "api").rules);
|
|
261
277
|
|
|
262
278
|
let lastRaw = null;
|
|
263
279
|
let base = null;
|
|
280
|
+
// The served page's data, read before any restore, and the stored data this tab's next
|
|
281
|
+
// operation is built on: the record it restored or last took in with its own operations
|
|
282
|
+
// laid over, or the served page's while nothing is stored.
|
|
283
|
+
let seed = null;
|
|
284
|
+
let tip = null;
|
|
285
|
+
// Nothing of a draft is on this page yet: no restore, no operation, nothing taken in.
|
|
286
|
+
let fresh = true;
|
|
264
287
|
let unparked = null;
|
|
265
288
|
let stopped = false;
|
|
266
289
|
// This tab's own last write: its record id, its ancestry, and the data of the record it
|
|
@@ -317,7 +340,7 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
|
|
|
317
340
|
return true;
|
|
318
341
|
}
|
|
319
342
|
|
|
320
|
-
const newId =
|
|
343
|
+
const newId = newRecordId;
|
|
321
344
|
|
|
322
345
|
// A record's ancestry is the ids it descends from, nearest first, ending at the empty
|
|
323
346
|
// slot it grew from, or at a record from a runtime that kept no ids, named by its bytes.
|
|
@@ -328,11 +351,7 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
|
|
|
328
351
|
try { return "root:" + (storage?.getItem(resetKey) ?? ""); } catch { return "root:"; }
|
|
329
352
|
}
|
|
330
353
|
|
|
331
|
-
|
|
332
|
-
let hash = 0x811c9dc5;
|
|
333
|
-
for (let i = 0; i < raw.length; i++) hash = Math.imul(hash ^ raw.charCodeAt(i), 0x01000193);
|
|
334
|
-
return "legacy:" + (hash >>> 0).toString(36) + "." + raw.length.toString(36);
|
|
335
|
-
}
|
|
354
|
+
const legacyId = (raw) => "legacy:" + textMark(raw);
|
|
336
355
|
|
|
337
356
|
function lineageOf(record, raw) {
|
|
338
357
|
if (!record) return null;
|
|
@@ -456,31 +475,37 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
|
|
|
456
475
|
return null;
|
|
457
476
|
}
|
|
458
477
|
|
|
459
|
-
function parkedResult(raw) {
|
|
478
|
+
function parkedResult({ raw, record }) {
|
|
460
479
|
const parkedKey = park(raw);
|
|
461
480
|
if (!parkedKey) lastRaw = raw;
|
|
462
481
|
base = live();
|
|
482
|
+
tip = record?.data ?? base;
|
|
463
483
|
return parkedKey
|
|
464
484
|
? { restored: false, hadDraft: true, parked: true }
|
|
465
485
|
: { restored: false, hadDraft: true, parked: false, blocked: true };
|
|
466
486
|
}
|
|
467
487
|
|
|
468
|
-
|
|
469
|
-
|
|
488
|
+
const seedData = () => (seed ??= live());
|
|
489
|
+
|
|
490
|
+
// `found` is the record to put on the page: the one `localStorage` holds, or the
|
|
491
|
+
// database's when the page waited for it (null: it holds none).
|
|
492
|
+
function restore(found = read()) {
|
|
493
|
+
seedData();
|
|
470
494
|
if (!found) {
|
|
471
|
-
base =
|
|
495
|
+
base = tip = seed;
|
|
472
496
|
whenArticle(readServed);
|
|
473
497
|
return { restored: false, hadDraft: false };
|
|
474
498
|
}
|
|
475
|
-
|
|
499
|
+
fresh = false;
|
|
500
|
+
if (!found.record) return parkedResult(found);
|
|
476
501
|
const unit = unitOf(found.record);
|
|
477
502
|
const hasData = Object.keys(found.record.data).length > 0;
|
|
478
503
|
const fitted = hasData ? fit(found.record.data) : { data: {}, dropped: [] };
|
|
479
504
|
const dataFits = Boolean(fitted) && (!hasData || Object.keys(fitted.data).length > 0);
|
|
480
505
|
// The page does not depend on the rules: a page is placed even when no saved field fits,
|
|
481
506
|
// and the whole record is kept aside as well.
|
|
482
|
-
if (!unit && !dataFits) return parkedResult(found
|
|
483
|
-
if (!unit && !hasData) return parkedResult(found
|
|
507
|
+
if (!unit && !dataFits) return parkedResult(found);
|
|
508
|
+
if (!unit && !hasData) return parkedResult(found);
|
|
484
509
|
if (hasData && dataFits) applyData(doc, fitted.data, rules);
|
|
485
510
|
if (unit) {
|
|
486
511
|
whenArticle(() => {
|
|
@@ -493,6 +518,7 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
|
|
|
493
518
|
}
|
|
494
519
|
lastRaw = found.raw;
|
|
495
520
|
base = live();
|
|
521
|
+
tip = found.record.data;
|
|
496
522
|
if (!dataFits || fitted.dropped.length) {
|
|
497
523
|
const parkedKey = park(found.raw, { keepSlot: true });
|
|
498
524
|
return { restored: true, hadDraft: true, data: dataFits ? fitted.data : {}, partial: true, parked: Boolean(parkedKey), blocked: !parkedKey };
|
|
@@ -552,6 +578,165 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
|
|
|
552
578
|
return fitted.data;
|
|
553
579
|
}
|
|
554
580
|
|
|
581
|
+
// With the database as the record, a save is an operation: for each key the page changed,
|
|
582
|
+
// the stored value it was built on (`b`, absent when nothing was) and the value now (`m`).
|
|
583
|
+
// `tab.ops` are the ones the database has not confirmed, oldest first, under the Start
|
|
584
|
+
// over marker this page booted on; `sizes` runs beside them. A copy that fits goes into
|
|
585
|
+
// this tab's journal slot at once, so closing the tab loses nothing: `journaled` is the
|
|
586
|
+
// last sequence number the slot holds.
|
|
587
|
+
const tab = { id: newId(), reset: "", acked: 0, seq: 0, journaled: 0, ops: [], sizes: [] };
|
|
588
|
+
try { tab.reset = storage?.getItem(resetKey) ?? ""; } catch {}
|
|
589
|
+
try {
|
|
590
|
+
const known = JSON.parse(window.sessionStorage.getItem(key + ":generation"));
|
|
591
|
+
if (known?.marker === tab.reset && typeof known.reset === "string") tab.reset = known.reset;
|
|
592
|
+
} catch {}
|
|
593
|
+
const slotKey = `${key}:op:${tab.id}`;
|
|
594
|
+
const chain = () => ({ tab: tab.id, reset: tab.reset, acked: tab.acked, at: now().getTime(), ops: tab.ops });
|
|
595
|
+
|
|
596
|
+
function journal() {
|
|
597
|
+
try {
|
|
598
|
+
if (!tab.ops.length) {
|
|
599
|
+
tab.journaled = tab.seq;
|
|
600
|
+
storage.removeItem(slotKey);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const text = JSON.stringify(chain());
|
|
604
|
+
if (text.length > slotLimit) return;
|
|
605
|
+
storage.setItem(slotKey, text);
|
|
606
|
+
tab.journaled = tab.seq;
|
|
607
|
+
} catch {}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// What the page held before its unconfirmed operations, for a page load that falls back
|
|
611
|
+
// to `localStorage` after it made some.
|
|
612
|
+
let before = null;
|
|
613
|
+
|
|
614
|
+
// The dirty gate's token for the snapshot `base` was last compared with: taken before
|
|
615
|
+
// that snapshot, in its task. Null while that is not known.
|
|
616
|
+
let capturedAt = null;
|
|
617
|
+
|
|
618
|
+
const pageOf = (html) => (html === undefined ? doc : new DOMParser().parseFromString(html, "text/html"));
|
|
619
|
+
|
|
620
|
+
// `at` is the gate token of the snapshot `html` was serialized from.
|
|
621
|
+
function capture(html, { reset = tab.reset, at = null } = {}) {
|
|
622
|
+
if (stopped) return null;
|
|
623
|
+
const parsed = pageOf(html);
|
|
624
|
+
const op = captureData(extractData(parsed, rules), recordable(readUnit(parsed)), reset);
|
|
625
|
+
capturedAt = at;
|
|
626
|
+
return op;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// The page as it is now.
|
|
630
|
+
function captureNow(reset = tab.reset) {
|
|
631
|
+
if (stopped) return null;
|
|
632
|
+
const at = gate()?.token() ?? null;
|
|
633
|
+
return capture(window.clay.getHTML(), { reset, at });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// The last capture of a generation that has ended. A capture runs the page's save
|
|
637
|
+
// handlers, and a handler that writes a changing value makes a field differ with no
|
|
638
|
+
// edit behind it. So on a page with a save handler that has recorded no edit since
|
|
639
|
+
// `base` was captured, the fields are what `base` holds, and only the page editor's
|
|
640
|
+
// part, which no handler writes, is compared.
|
|
641
|
+
function captureLate(reset = tab.reset) {
|
|
642
|
+
if (stopped) return null;
|
|
643
|
+
const settled = capturedAt !== null && doc.querySelector(HANDLER_SEL) !== null && gate()?.unchangedSince(capturedAt) === true;
|
|
644
|
+
const parsed = pageOf(window.clay?.getHTML?.());
|
|
645
|
+
return captureData(settled ? base : extractData(parsed, rules), recordable(readUnit(parsed)), reset);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function captureData(mine, mineUnit, reset = tab.reset) {
|
|
649
|
+
if (unparked != null && park(unparked, { keepSlot: true }) === null) {
|
|
650
|
+
throw storageFailure(doc, new Error("an earlier saved copy could not be kept aside"));
|
|
651
|
+
}
|
|
652
|
+
// Too much is waiting: nothing more is taken until it drains, and the page keeps it.
|
|
653
|
+
if (tab.sizes.reduce((sum, size) => sum + size, 0) >= pendingLimit) throw storageFailure(doc, new Error("too many changes are waiting to be saved"));
|
|
654
|
+
const unitChanged = !sameUnit(mineUnit, editorUnit);
|
|
655
|
+
const keys = {};
|
|
656
|
+
for (const name of Object.keys(mine)) {
|
|
657
|
+
if (name in base && fitsInto(mine[name], base[name])) continue;
|
|
658
|
+
// Fields the stored value has and this page's rules lack are carried through.
|
|
659
|
+
keys[name] = name in tip ? { b: tip[name], m: complete(mine[name], tip[name]) } : { m: mine[name] };
|
|
660
|
+
}
|
|
661
|
+
if (!Object.keys(keys).length && !unitChanged) {
|
|
662
|
+
if (!tab.ops.length) {
|
|
663
|
+
setDraftStoreFailed(false);
|
|
664
|
+
doc.dispatchEvent(new CustomEvent("clay:drafts-saved", { detail: { data: mine, unchanged: true } }));
|
|
665
|
+
}
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
if (!tab.ops.length) before = { base, editorUnit, seenUnit };
|
|
669
|
+
const op = { seq: ++tab.seq, savedAt: now().toISOString(), keys, ...(unitChanged && { unit: { seen: unitMark(seenUnit), to: mineUnit } }) };
|
|
670
|
+
base = { ...base };
|
|
671
|
+
tip = { ...tip };
|
|
672
|
+
for (const name of Object.keys(keys)) {
|
|
673
|
+
base[name] = mine[name];
|
|
674
|
+
tip[name] = keys[name].m;
|
|
675
|
+
}
|
|
676
|
+
if (unitChanged) editorUnit = seenUnit = mineUnit;
|
|
677
|
+
fresh = false;
|
|
678
|
+
tab.reset = reset;
|
|
679
|
+
tab.ops.push(op);
|
|
680
|
+
tab.sizes.push(JSON.stringify(op).length);
|
|
681
|
+
journal();
|
|
682
|
+
return op;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// A hide's synchronous half of a save. False while an operation is neither in the
|
|
686
|
+
// journal slot nor confirmed: closing the tab now could lose it.
|
|
687
|
+
function keep() {
|
|
688
|
+
if (stopped) return true;
|
|
689
|
+
try {
|
|
690
|
+
if (changed()) captureNow();
|
|
691
|
+
} catch { return false; }
|
|
692
|
+
return tab.journaled === tab.seq;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function acknowledge(seq) {
|
|
696
|
+
if (seq <= tab.acked) return false;
|
|
697
|
+
tab.acked = seq;
|
|
698
|
+
const done = tab.ops.filter((op) => op.seq <= seq).length;
|
|
699
|
+
tab.ops.splice(0, done);
|
|
700
|
+
tab.sizes.splice(0, done);
|
|
701
|
+
journal();
|
|
702
|
+
return done > 0;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function drop() {
|
|
706
|
+
tab.ops.length = 0;
|
|
707
|
+
tab.sizes.length = 0;
|
|
708
|
+
journal();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// The database did not open: what the journal holds goes back to being this page's
|
|
712
|
+
// unsaved changes, for `write` to save.
|
|
713
|
+
function release() {
|
|
714
|
+
if (tab.ops.length) ({ base, editorUnit, seenUnit } = before);
|
|
715
|
+
capturedAt = null;
|
|
716
|
+
drop();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// A page with nothing unsaved takes in what the database holds: the keys whose stored
|
|
720
|
+
// value is no longer the one this tab was built on.
|
|
721
|
+
function take(data) {
|
|
722
|
+
const moved = Object.fromEntries(Object.entries(data).filter(([name, value]) => !same(value, tip[name])));
|
|
723
|
+
if (!Object.keys(moved).length) return false;
|
|
724
|
+
const shown = adopt(moved);
|
|
725
|
+
tip = { ...tip, ...moved };
|
|
726
|
+
if (shown) base = { ...base, ...shown };
|
|
727
|
+
fresh = false;
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// The generation this page shows has ended, by this tab's Start over or another tab's:
|
|
732
|
+
// nothing more is saved from it.
|
|
733
|
+
function stop({ keepPending = false } = {}) {
|
|
734
|
+
stopped = true;
|
|
735
|
+
if (!keepPending) drop();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const journalSlots = () => storedKeys().filter((name) => name.startsWith(key + ":op:"));
|
|
739
|
+
|
|
555
740
|
function write(html) {
|
|
556
741
|
if (stopped) return null;
|
|
557
742
|
if (unparked != null && park(unparked, { keepSlot: true }) === null) {
|
|
@@ -578,7 +763,7 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
|
|
|
578
763
|
const id = newId();
|
|
579
764
|
const raw = stored && sameUnit(unit, theirs)
|
|
580
765
|
? stored
|
|
581
|
-
: JSON.stringify({ v: RECORD_VERSION, savedAt: now().toISOString(), id, ancestry, data, ...(unit && { live: unit.live, blockeditor: unit.blockeditor }) });
|
|
766
|
+
: JSON.stringify({ v: RECORD_VERSION, savedAt: now().toISOString(), id, ancestry, reset: tab.reset, data, ...(unit && { live: unit.live, blockeditor: unit.blockeditor }) });
|
|
582
767
|
if (raw !== stored) {
|
|
583
768
|
try {
|
|
584
769
|
if (!storage) throw new Error("no storage");
|
|
@@ -634,11 +819,15 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
|
|
|
634
819
|
return true;
|
|
635
820
|
}
|
|
636
821
|
|
|
637
|
-
// The page another tab recorded since this tab last read or wrote one, or null.
|
|
638
|
-
|
|
822
|
+
// The page another tab recorded since this tab last read or wrote one, or null. `record`
|
|
823
|
+
// is the database's; without one the copy in `localStorage` is read.
|
|
824
|
+
function externalUnit(record) {
|
|
639
825
|
if (stopped) return null;
|
|
640
|
-
|
|
641
|
-
|
|
826
|
+
if (record === undefined) {
|
|
827
|
+
const raw = getRaw();
|
|
828
|
+
record = raw == null ? null : parse(raw);
|
|
829
|
+
}
|
|
830
|
+
const theirs = unitOf(record);
|
|
642
831
|
return theirs && !sameUnit(theirs, seenUnit) ? theirs : null;
|
|
643
832
|
}
|
|
644
833
|
|
|
@@ -648,12 +837,43 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
|
|
|
648
837
|
}
|
|
649
838
|
|
|
650
839
|
function clear() {
|
|
651
|
-
|
|
840
|
+
stop();
|
|
652
841
|
unparked = null;
|
|
653
842
|
setDraftStoreFailed(false);
|
|
654
843
|
try { storage.removeItem(key); } catch {}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
844
|
+
const match = /^(\d+)~/.exec(tab.reset);
|
|
845
|
+
const gen = match ? Number(match[1]) : tab.reset === "" ? 1 : null;
|
|
846
|
+
try { storage.setItem(resetKey, gen === null ? newId() : `${gen + 1}~${newId()}`); } catch {}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return {
|
|
850
|
+
slot, key, rules, read, restore, write, writeLive, flush, changed, adoptExternal, externalUnit, adoptedUnit, park, clear, snapshot,
|
|
851
|
+
tab: tab.id, slotKey, chain, capture, captureNow, captureLate, keep, acknowledge, release, take, stop, journalSlots,
|
|
852
|
+
seed: seedData,
|
|
853
|
+
servedUnit: () => { readServed(); return served; },
|
|
854
|
+
fitsInto,
|
|
855
|
+
same,
|
|
856
|
+
// For the session, which is fetched after the boot and imports nothing from here.
|
|
857
|
+
merge3,
|
|
858
|
+
unitMark,
|
|
859
|
+
newId,
|
|
860
|
+
failed: (error) => storageFailure(doc, error),
|
|
861
|
+
generation: () => tab.reset,
|
|
862
|
+
enter: (reset) => { tab.reset = reset; },
|
|
863
|
+
fresh: () => fresh,
|
|
864
|
+
shown: () => base !== null,
|
|
865
|
+
pending: () => tab.ops.length,
|
|
866
|
+
unsafe: () => tab.journaled < tab.seq,
|
|
867
|
+
stopped: () => stopped,
|
|
868
|
+
// What `localStorage` holds at the draft's key, whatever wrote it: a record, the head of
|
|
869
|
+
// a draft whose full record is only in the database (`idb`), or nothing readable.
|
|
870
|
+
held() {
|
|
871
|
+
try {
|
|
872
|
+
const held = JSON.parse(getRaw());
|
|
873
|
+
return isPlain(held) ? held : null;
|
|
874
|
+
} catch { return null; }
|
|
875
|
+
},
|
|
876
|
+
// A draft is kept when `localStorage` holds its copy or head, or any tab's journal.
|
|
877
|
+
kept: () => Boolean(read()) || tab.ops.length > 0 || journalSlots().length > 0,
|
|
878
|
+
};
|
|
659
879
|
}
|
|
@@ -1,17 +1,38 @@
|
|
|
1
1
|
import { isDraftMode, isEditMode, isOwner } from "../core/is-edit-mode.js";
|
|
2
2
|
import { hasSaveToken } from "../core/host-attrs.js";
|
|
3
|
-
import { draftSlot, setDraftTransport, setDraftFlush } from "../core/draft-mode.js";
|
|
4
|
-
import {
|
|
3
|
+
import { draftGate, draftSlot, draftStoreFailed, setDraftTransport, setDraftFlush, setDraftUnconfirmed } from "../core/draft-mode.js";
|
|
4
|
+
import { reloadAfterDiscard } from "../core/unsaved-warning.js";
|
|
5
|
+
import { createStore, adoptLegacy, draftPath, legacyKey, safeStorage, storageFailure } from "./visitor-drafts/store.js";
|
|
5
6
|
import { createUiMemory } from "./visitor-drafts/ui-memory.js";
|
|
6
7
|
import { rememberSource, downloadUrl } from "./visitor-drafts/source.js";
|
|
7
8
|
import { mountPrompt } from "./visitor-drafts/prompt.js";
|
|
8
9
|
import { DOWNLOAD_URL, backToDraft, editOriginal, inertVisitorDrafts } from "./visitor-drafts/inert.js";
|
|
9
10
|
|
|
10
11
|
let store = null;
|
|
12
|
+
// This tab's side of the draft database, once it has opened. Without one, `localStorage`
|
|
13
|
+
// holds the record for this page load, as it did before the database.
|
|
14
|
+
let session = null;
|
|
15
|
+
let direct = true;
|
|
16
|
+
let database = false;
|
|
17
|
+
let opening = Promise.resolve();
|
|
18
|
+
// The database did not open and the full draft is only there: nothing is saved over it
|
|
19
|
+
// for the rest of this page load.
|
|
20
|
+
let refused = false;
|
|
11
21
|
let restoredDetail = null;
|
|
12
22
|
let downloadLink = DOWNLOAD_URL;
|
|
13
23
|
let source = null;
|
|
14
24
|
let ready = Promise.resolve();
|
|
25
|
+
// Settles once the draft is on the page. Already settled for a first visit and for a
|
|
26
|
+
// draft `localStorage` holds in full; the loader awaits it before the next plugin loads.
|
|
27
|
+
let restoring = Promise.resolve();
|
|
28
|
+
// The synchronous half of a save: false while something this tab changed is neither
|
|
29
|
+
// stored nor in its journal slot.
|
|
30
|
+
let keep = () => true;
|
|
31
|
+
// A promise while the page's last changes are not yet where the download page reads them,
|
|
32
|
+
// null once they are.
|
|
33
|
+
let settle = () => null;
|
|
34
|
+
// Past this deadline the download uses this page's current HTML.
|
|
35
|
+
const DOWNLOAD_WAIT_MS = 3000;
|
|
15
36
|
|
|
16
37
|
// The chip is a convenience: a failed import leaves the draft working without it.
|
|
17
38
|
function mountChip() {
|
|
@@ -41,28 +62,102 @@ const rulesTag = document.querySelector('script[data-rules-name~="api"]');
|
|
|
41
62
|
if (isDraftMode && slot && rulesTag) {
|
|
42
63
|
const path = draftPath();
|
|
43
64
|
const moved = adoptLegacy(slot, { path });
|
|
65
|
+
const local = safeStorage();
|
|
44
66
|
store = createStore({ path, fallback: moved === "failed" ? legacyKey(slot) : null });
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
direct = !(typeof globalThis.indexedDB?.open === "function" && globalThis.IDBKeyRange);
|
|
68
|
+
database = !direct;
|
|
69
|
+
// The session loads beside the database open, off the path of a draft `localStorage` holds
|
|
70
|
+
// in full. Until it is there a save still makes its operation and journals it.
|
|
71
|
+
opening = direct ? Promise.resolve() : import("./visitor-drafts/session.js")
|
|
72
|
+
.then((m) => m.openSession({ store, storage: local, reload: () => reloadAfterDiscard({ isCurrent: () => true, reload: () => window.location.reload() }) }))
|
|
73
|
+
.then((opened) => { session = opened; }, () => { direct = true; });
|
|
74
|
+
|
|
75
|
+
setDraftTransport((html, at = null) => {
|
|
76
|
+
const recapture = !restoredDetail;
|
|
77
|
+
return restoring.then(() => {
|
|
78
|
+
if (refused) throw storageFailure(document, new Error("the draft database did not open"));
|
|
79
|
+
if (recapture) [at, html] = [draftGate()?.token() ?? null, window.clay.getHTML()];
|
|
80
|
+
if (direct) return void store.write(html);
|
|
81
|
+
store.capture(html, { at });
|
|
82
|
+
return opening.then(() => {
|
|
83
|
+
if (session) return session.sync().then(() => {});
|
|
84
|
+
store.write(html);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const memory = createUiMemory(store.key, local);
|
|
50
90
|
source = rememberSource(memory);
|
|
51
91
|
downloadLink = downloadUrl(source);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
92
|
+
|
|
93
|
+
function finish(result) {
|
|
94
|
+
restoredDetail = { restored: result.restored, hadDraft: result.hadDraft, parked: Boolean(result.parked), ...(result.partial && { partial: true }), ...(result.blocked && { blocked: true }) };
|
|
95
|
+
document.dispatchEvent(new CustomEvent("clay:drafts-restored", { detail: restoredDetail }));
|
|
96
|
+
const appName = document.querySelector('meta[name="application-name"]')?.content?.trim() || document.title.split(/:|\s\|\s/)[0].trim() || "This app";
|
|
97
|
+
const template = document.querySelector("template[visitor-intro]");
|
|
98
|
+
const start = () => {
|
|
99
|
+
ready = mountPrompt({ memory, appName, link: downloadLink, settle: () => settle(), canDownloadCurrent: () => !refused && !restoredDetail.blocked }).then((prompt) => mountIntroLate({ memory, template, appName, hadDraft: result.hadDraft || store.kept() }).then(() => prompt)).then((prompt) => {
|
|
100
|
+
document.addEventListener("clay:drafts-store-failed", () => prompt.showError());
|
|
101
|
+
document.addEventListener("clay:drafts-saved", () => prompt.clearError());
|
|
102
|
+
if (draftStoreFailed()) prompt.showError();
|
|
103
|
+
return (isOwner ? mountChip() : Promise.resolve(null)).then(() => prompt);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
document.readyState === "loading" ? document.addEventListener("DOMContentLoaded", start, { once: true }) : start();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
keep = () => {
|
|
110
|
+
if (refused) return false;
|
|
111
|
+
if (!restoredDetail) return true;
|
|
112
|
+
if (direct) return store.flush();
|
|
113
|
+
const safe = store.keep();
|
|
114
|
+
session?.hide();
|
|
115
|
+
return safe;
|
|
116
|
+
};
|
|
117
|
+
setDraftFlush(keep);
|
|
118
|
+
setDraftUnconfirmed(() => !direct && store.unsafe());
|
|
119
|
+
settle = () => {
|
|
120
|
+
if (refused) return Promise.resolve(false);
|
|
121
|
+
if (!restoredDetail) return restoring.then(() => settle());
|
|
122
|
+
if (direct) {
|
|
123
|
+
return store.flush() ? null : Promise.resolve(false);
|
|
124
|
+
}
|
|
125
|
+
const safe = store.keep();
|
|
126
|
+
if (!store.pending()) return safe ? null : Promise.resolve(false);
|
|
127
|
+
const committed = opening.then(() => session ? session.sync().then(() => true) : store.flush()).catch(() => false);
|
|
128
|
+
let timer;
|
|
129
|
+
return Promise.race([committed, new Promise((resolve) => { timer = setTimeout(() => resolve(false), DOWNLOAD_WAIT_MS); })])
|
|
130
|
+
.finally(() => clearTimeout(timer));
|
|
60
131
|
};
|
|
61
|
-
document.
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
//
|
|
132
|
+
document.addEventListener("visibilitychange", () => { if (document.visibilityState === "hidden") keep(); });
|
|
133
|
+
window.addEventListener("pagehide", keep);
|
|
134
|
+
// A link marked clay-wait-drafts (the host's download and host actions) waits for this
|
|
135
|
+
// page's last changes to reach the download page before it navigates, like the prompt's
|
|
136
|
+
// Download button. A new tab or a modified click leaves this page open, so it does not wait.
|
|
137
|
+
let leaving = false;
|
|
138
|
+
document.addEventListener("click", async (event) => {
|
|
139
|
+
// composedPath reaches into open shadow roots such as Hyperclay's trial bar.
|
|
140
|
+
const link = event.composedPath().find((node) => node instanceof Element && node.matches("a[clay-wait-drafts][href]"));
|
|
141
|
+
if (!link || event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
142
|
+
if (link.target && link.target !== "_self") return;
|
|
143
|
+
if (leaving) { event.preventDefault(); return; }
|
|
144
|
+
const waiting = settle();
|
|
145
|
+
if (!waiting) return;
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
leaving = true;
|
|
148
|
+
let timer;
|
|
149
|
+
await Promise.race([waiting, new Promise((resolve) => { timer = setTimeout(resolve, DOWNLOAD_WAIT_MS); })]);
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
leaving = false;
|
|
152
|
+
window.location.assign(link.href);
|
|
153
|
+
}, true);
|
|
154
|
+
|
|
155
|
+
// Another tab's save reaches a page on the database through its session. With
|
|
156
|
+
// `localStorage` as the record it arrives as that key's storage event, once the page
|
|
157
|
+
// editor listens (an event before that is missed), and when the tab comes back from the
|
|
158
|
+
// page cache.
|
|
65
159
|
const takeOtherTab = () => {
|
|
160
|
+
if (!direct || refused || !restoredDetail) return;
|
|
66
161
|
// Read before adopting: adopting reads the record as this tab's own.
|
|
67
162
|
const unit = store.externalUnit();
|
|
68
163
|
// A page another tab wrote goes to the page editor, which merges it into its own.
|
|
@@ -72,9 +167,36 @@ if (isDraftMode && slot && rulesTag) {
|
|
|
72
167
|
window.addEventListener("storage", (event) => { if (event.key === store.key) takeOtherTab(); });
|
|
73
168
|
document.addEventListener("clay:drafts-blockeditor-listening", takeOtherTab);
|
|
74
169
|
window.addEventListener("pageshow", (event) => { if (event.persisted) takeOtherTab(); });
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
170
|
+
|
|
171
|
+
const held = store.held();
|
|
172
|
+
const head = held?.v === 2 && held.idb === true;
|
|
173
|
+
const foreignReset = !direct && held?.v === 1 && held.w === undefined && store.generation() !== "";
|
|
174
|
+
if (head || foreignReset || (!direct && held?.w && held.w.reset !== store.generation())) {
|
|
175
|
+
// The full draft is only in the database, or this copy is from before the last Start
|
|
176
|
+
// over: the page waits for the database's record, and plugins that read the document
|
|
177
|
+
// wait with the page.
|
|
178
|
+
restoring = opening.then(() => session?.first).then((result) => {
|
|
179
|
+
if (result) return finish(store.restore(result.record && { raw: JSON.stringify(result.record), record: result.record }));
|
|
180
|
+
throw new Error("the draft database did not open");
|
|
181
|
+
}).catch((error) => {
|
|
182
|
+
if (!head) {
|
|
183
|
+
if (!foreignReset) { try { local?.removeItem(store.key); } catch {} }
|
|
184
|
+
return finish(store.restore());
|
|
185
|
+
}
|
|
186
|
+
refused = true;
|
|
187
|
+
storageFailure(document, error);
|
|
188
|
+
finish({ restored: false, hadDraft: true, blocked: true });
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
finish(store.restore());
|
|
192
|
+
// The database did not open: `localStorage` is the record for this page load, and what
|
|
193
|
+
// this tab journaled while it waited is saved there.
|
|
194
|
+
opening.then(() => {
|
|
195
|
+
if (session) return;
|
|
196
|
+
store.release();
|
|
197
|
+
store.flush();
|
|
198
|
+
});
|
|
199
|
+
}
|
|
78
200
|
} else if (!slot || !rulesTag) {
|
|
79
201
|
const missing = [];
|
|
80
202
|
if (!slot) missing.push('a visitor-drafts="<slot>" attribute on <html>');
|
|
@@ -84,18 +206,33 @@ if (isDraftMode && slot && rulesTag) {
|
|
|
84
206
|
ready = mountChip();
|
|
85
207
|
}
|
|
86
208
|
|
|
87
|
-
export { ready };
|
|
209
|
+
export { ready, restoring };
|
|
88
210
|
|
|
89
211
|
export const visitorDrafts = store ? {
|
|
90
212
|
active: true,
|
|
91
213
|
slot: store.slot,
|
|
92
214
|
key: store.key,
|
|
93
|
-
hasDraft: () =>
|
|
215
|
+
hasDraft: () => store.kept(),
|
|
94
216
|
restored: () => restoredDetail,
|
|
95
|
-
saveNow
|
|
217
|
+
saveNow() {
|
|
218
|
+
if (refused || !restoredDetail) return false;
|
|
219
|
+
if (direct) return store.writeLive();
|
|
220
|
+
try { store.captureNow(); } catch { return false; }
|
|
221
|
+
session?.sync().catch(() => {});
|
|
222
|
+
return !store.unsafe();
|
|
223
|
+
},
|
|
96
224
|
reset() {
|
|
97
|
-
|
|
98
|
-
|
|
225
|
+
if (!database) {
|
|
226
|
+
store.clear();
|
|
227
|
+
window.location.reload();
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
// The generation ends in the database before the page loads again.
|
|
231
|
+
opening.then(() => {
|
|
232
|
+
if (session) return session.startOver();
|
|
233
|
+
if (direct && !refused) return store.clear();
|
|
234
|
+
throw new Error("the draft database did not open");
|
|
235
|
+
}).then(() => window.location.reload(), (error) => storageFailure(document, error));
|
|
99
236
|
return true;
|
|
100
237
|
},
|
|
101
238
|
onRestore(fn) {
|
|
@@ -109,6 +246,18 @@ export const visitorDrafts = store ? {
|
|
|
109
246
|
editOriginal,
|
|
110
247
|
backToDraft,
|
|
111
248
|
downloadUrl: () => downloadLink,
|
|
112
|
-
download: () => ready.then((prompt) => {
|
|
249
|
+
download: () => ready.then(async (prompt) => {
|
|
250
|
+
const saved = await settle();
|
|
251
|
+
return prompt?.download(saved === false);
|
|
252
|
+
}),
|
|
113
253
|
source,
|
|
114
254
|
} : inertVisitorDrafts();
|
|
255
|
+
|
|
256
|
+
// Internal to the visitor menu; owner saving and the public visitorDrafts API stay separate.
|
|
257
|
+
export async function recoveryDrafts() {
|
|
258
|
+
await restoring;
|
|
259
|
+
await opening;
|
|
260
|
+
if (!store || !session || refused) return null;
|
|
261
|
+
const { createRecovery } = await import("./visitor-drafts/recovery.js");
|
|
262
|
+
return createRecovery({ store, session });
|
|
263
|
+
}
|
package/src/sync/live-sync.js
CHANGED
|
@@ -933,7 +933,7 @@ class LiveSync {
|
|
|
933
933
|
// Whichever wire discovery selected. start() does not call connect() until the
|
|
934
934
|
// profile is known, so this is never null on the normal path.
|
|
935
935
|
const profile = this._profile || WIRE_PROFILES.legacy;
|
|
936
|
-
const path = profile.streamPath(window.location.href, this.lane, this.resumeId, this.clientId);
|
|
936
|
+
const path = profile.streamPath(window.location.href, this.lane, this.resumeId, this.clientId, PAGE_FORMAT);
|
|
937
937
|
// A frame scheduled while the page showed waits for an animation frame, which a
|
|
938
938
|
// page hidden since then never runs. Move it to the hidden schedule.
|
|
939
939
|
if (!this._hiddenHandler) {
|