@panphora/clayjs 1.24.0 → 1.24.1
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 +15016 -13603
- package/dist/m/c-277QP5ET.js +1 -0
- package/dist/m/{c-BULK5J4M.js → c-3UW5EJ77.js} +1 -1
- package/dist/m/{c-GM7OA6EJ.js → c-42ZNV7LI.js} +1 -1
- package/dist/m/{c-RXTPKCMC.js → c-6IJEVE6B.js} +1 -1
- package/dist/m/{c-2R4OBWIP.js → c-6LEYVQJK.js} +1 -1
- package/dist/m/{c-LO4C4J77.js → c-6Q6RGFBF.js} +1 -1
- package/dist/m/{c-PD5AKHY3.js → c-7CM4GMTS.js} +1 -1
- package/dist/m/c-APGHGDZM.js +1 -0
- package/dist/m/{c-Q6VNQDJE.js → c-B7GOFGQR.js} +1 -1
- package/dist/m/{c-RJKKRWHW.js → c-BGN7RCTJ.js} +1 -1
- package/dist/m/{c-A7N5TNSL.js → c-C5AKJVRH.js} +1 -1
- package/dist/m/{c-SNVDWMM3.js → c-CJ7QYEWI.js} +1 -1
- package/dist/m/{c-PVJMJL3P.js → c-CZHTXGJS.js} +1 -1
- package/dist/m/{c-KJHVB2KG.js → c-DDS23EY6.js} +1 -1
- package/dist/m/{c-VSEUFJAO.js → c-DFKBGSN2.js} +1 -1
- package/dist/m/{c-DLBX4JCJ.js → c-EDE45NM7.js} +1 -1
- package/dist/m/c-EILLN2H2.js +1 -0
- package/dist/m/{c-EY5KDUZF.js → c-FEC6LGFF.js} +1 -1
- package/dist/m/{c-PMRRMT72.js → c-HDVH3MVC.js} +1 -1
- package/dist/m/c-HKSLOVU4.js +1 -0
- package/dist/m/{c-GNIVHLEJ.js → c-HL6BLJNA.js} +1 -1
- package/dist/m/c-HTLYXCUH.js +1 -0
- package/dist/m/{c-RTSRGGTP.js → c-JQF2EUCG.js} +1 -1
- package/dist/m/{c-O5ARALMJ.js → c-JWDCQL6J.js} +1 -1
- package/dist/m/{c-WOELSNWS.js → c-LATNQ6Q5.js} +1 -1
- package/dist/m/{c-UM7VZDTW.js → c-LSVN3OLG.js} +1 -1
- package/dist/m/{c-HSNGLIFB.js → c-MFOSMAVM.js} +2 -2
- package/dist/m/{c-FZRZPIFS.js → c-MQYEDF3Q.js} +1 -1
- package/dist/m/{c-2OIKXOY6.js → c-ND3Q2G2I.js} +1 -1
- package/dist/m/{c-PZRWZZRO.js → c-OCYG4NBQ.js} +1 -1
- package/dist/m/{c-54ZZZYUN.js → c-P2HKMJAW.js} +1 -1
- package/dist/m/{c-VI3BJHCM.js → c-PCNJETUA.js} +1 -1
- package/dist/m/{c-7YETSAVY.js → c-PDBNCJCT.js} +1 -1
- package/dist/m/{c-A5ZBFICF.js → c-PYG2NGWU.js} +1 -1
- package/dist/m/{c-RBPZNU2D.js → c-QQFHEODI.js} +1 -1
- package/dist/m/{c-YINHPF4V.js → c-SGH3UTF3.js} +1 -1
- package/dist/m/{c-FV46ZKGX.js → c-SKMIR5HQ.js} +1 -1
- package/dist/m/{c-TWOXQQKV.js → c-STRI73GQ.js} +1 -1
- package/dist/m/{c-75N3KG35.js → c-T3P3AXEW.js} +1 -1
- package/dist/m/c-U4NTUVDZ.js +1 -0
- package/dist/m/{c-P5NKFRL4.js → c-UOOIF2AL.js} +1 -1
- package/dist/m/c-VHSMPITY.js +1 -0
- package/dist/m/{c-6UPEKW6O.js → c-X75G43SZ.js} +1 -1
- package/dist/m/{c-YGGO2G2L.js → c-XW5TIYYB.js} +1 -1
- package/dist/m/{c-DZPO22O3.js → c-YDW4VCHP.js} +1 -1
- package/dist/m/{c-YB3CDTDZ.js → c-YLHTD3KX.js} +1 -1
- package/dist/m/c-Z4G2UK6T.js +4 -0
- package/dist/m/{c-VNAYHV2J.js → c-ZGCO4J3H.js} +1 -1
- package/dist/m/core/{admin-attrs-5UGMYCKW.js → admin-attrs-ONRWVXPK.js} +1 -1
- package/dist/m/core/{autosave-4P66VXX7.js → autosave-TBKAAAFB.js} +1 -1
- package/dist/m/core/{conflict-notice-BAR23OWH.js → conflict-notice-FOEE6I7E.js} +1 -1
- package/dist/m/core/draft-mode-4MCL3LYI.js +1 -0
- package/dist/m/core/{host-permission-3HE4CFD6.js → host-permission-X4BENZEU.js} +1 -1
- package/dist/m/core/{is-edit-mode-GK7IFFMT.js → is-edit-mode-FFHCQ5DR.js} +1 -1
- package/dist/m/core/{persist-AIJGTKKI.js → persist-6ILUR34S.js} +1 -1
- package/dist/m/core/{recovery-LPB6X72P.js → recovery-APWADU4K.js} +1 -1
- package/dist/m/core/{save-FV4ZCKBI.js → save-6HT2LABE.js} +1 -1
- package/dist/m/core/{save-core-K7XFAWQ4.js → save-core-EVMNZQ4V.js} +1 -1
- package/dist/m/core/{site-edits-TEQORBJU.js → site-edits-G4BOZMV4.js} +1 -1
- package/dist/m/core/{unsaved-warning-FF63QGKI.js → unsaved-warning-ITZDIZ2I.js} +1 -1
- package/dist/m/internals/{index-AX4CG7B2.js → index-HS3CQDFW.js} +1 -1
- package/dist/m/loader-GLLVDUZA.js +1 -0
- package/dist/m/manifest-50FF1C45.js +1 -0
- package/dist/m/manifest-edit-BE1A621D.js +1 -0
- package/dist/m/manifest.json +710 -674
- package/dist/m/plugins/{ai-edit-CUKQQWJX.js → ai-edit-LABECCSN.js} +1 -1
- package/dist/m/plugins/{cms-JANMGPHM.js → cms-T2HLI7JG.js} +16 -16
- package/dist/m/plugins/{demo-JIR7DXJA.js → demo-T7B4RIKL.js} +1 -1
- package/dist/m/plugins/{derive-QVI3LI3A.js → derive-6LPJOCAV.js} +1 -1
- package/dist/m/plugins/{indicator-DAKZGAWI.js → indicator-J4ABFARX.js} +1 -1
- package/dist/m/plugins/{live-UAJGWRNP.js → live-PGBIEMGB.js} +1 -1
- package/dist/m/plugins/{menu-BFJ644YX.js → menu-YZL3X2AJ.js} +1 -1
- package/dist/m/plugins/page-WOCNX2IN.js +2 -0
- package/dist/m/plugins/{page-view-OYN7Z5TS.js → page-view-SJODOOGE.js} +1 -1
- package/dist/m/plugins/{people-VBDR5Y22.js → people-C4TCWFOQ.js} +1 -1
- package/dist/m/plugins/{richclay-CBLZRFB4.js → richclay-Y2NV44NA.js} +3 -3
- package/dist/m/plugins/sortable-XCHQG65Z.js +1 -0
- package/dist/m/plugins/{source-ZXDD7F6X.js → source-YK7MO23W.js} +1 -1
- package/dist/m/plugins/{undo-5JI5G7CQ.js → undo-VLRBQNPN.js} +1 -1
- package/dist/m/plugins/{upgrade-6B6QS3NR.js → upgrade-NNTJBSK2.js} +1 -1
- package/dist/m/plugins/{upload-DNJNW6H3.js → upload-ZKKJV76X.js} +1 -1
- package/dist/m/plugins/visitor-drafts-RTZNONHT.js +20 -0
- package/dist/m/plugins/{visitor-drafts-owner-VIAZC3CF.js → visitor-drafts-owner-LHUHFU3L.js} +1 -1
- package/dist/m/plugins/{visitor-menu-ZHKWIKKY.js → visitor-menu-FY4KM2SW.js} +1 -1
- package/dist/m/plugins/wire-M2NR354P.js +1 -0
- package/dist/m/{reader-core-F6RY3V64.js → reader-core-T2S3JC4E.js} +2 -2
- package/dist/m/sync/{live-sync-M2PP2IWY.js → live-sync-Z64YREWQ.js} +1 -1
- package/dist/m/vendor/{sap-core.vendor-OA5SKL7Q.js → sap-core.vendor-QAEPMETX.js} +1 -1
- package/dist/offline-manifest.json +455 -425
- package/entries/clay-data.js +1 -1
- package/entries/sap.js +1 -1
- package/package.json +2 -2
- package/src/core/draft-mode.js +6 -0
- package/src/core/save-core.js +7 -8
- package/src/core/unsaved-warning.js +2 -2
- package/src/lib/idb-error-code.js +20 -0
- package/src/loader.js +2 -0
- package/src/plugins/page.js +19 -5
- package/src/plugins/sortable.js +7 -0
- package/src/plugins/visitor-drafts/commit.js +303 -0
- package/src/plugins/visitor-drafts/idb.js +221 -0
- package/src/plugins/visitor-drafts/prompt.js +23 -3
- package/src/plugins/visitor-drafts/session.js +345 -0
- package/src/plugins/visitor-drafts/store.js +210 -26
- package/src/plugins/visitor-drafts.js +148 -28
- 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 +1 -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-JMDKPBMC.js +0 -4
- package/dist/m/c-N7JPUPGC.js +0 -1
- package/dist/m/c-NEXFVJIT.js +0 -1
- package/dist/m/c-T527QHOB.js +0 -1
- package/dist/m/core/draft-mode-WSQAT67S.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/plugins/{richclay-CBLZRFB4.js.LEGAL.txt → richclay-Y2NV44NA.js.LEGAL.txt} +0 -0
|
@@ -21,7 +21,9 @@ const ASIDE = `<aside class="hcp" id="hcp" hidden aria-label="Download this app"
|
|
|
21
21
|
</div>
|
|
22
22
|
</aside>`;
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// `settle` returns a promise while the page's last changes are not yet where the download
|
|
25
|
+
// page reads them, and null once they are. False requests a download from this page.
|
|
26
|
+
export async function mountPrompt({ memory, appName, link, settle, canDownloadCurrent = () => true }) {
|
|
25
27
|
const { PROMPT_CSS } = await import("./prompt-style.js");
|
|
26
28
|
const existing = document.querySelector('clay-editor-ui[data-clay-ui="download-prompt"]');
|
|
27
29
|
let root = existing?.shadowRoot.getElementById("hcp");
|
|
@@ -118,6 +120,7 @@ export async function mountPrompt({ memory, appName, link }) {
|
|
|
118
120
|
}
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
let rescueRequested = false;
|
|
121
124
|
root.addEventListener("click", async (e) => {
|
|
122
125
|
const t = e.target.closest("[data-hcp]");
|
|
123
126
|
if (t) {
|
|
@@ -131,9 +134,23 @@ export async function mountPrompt({ memory, appName, link }) {
|
|
|
131
134
|
return;
|
|
132
135
|
}
|
|
133
136
|
if (e.target.closest("[data-hcp-link]")) {
|
|
134
|
-
if (
|
|
137
|
+
if (!canDownloadCurrent()) return;
|
|
138
|
+
let rescue = rescueRequested || root.getAttribute("data-hcp-reason") === "error";
|
|
139
|
+
if (!rescue) {
|
|
140
|
+
const waiting = settle?.();
|
|
141
|
+
if (waiting) {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
if (await waiting !== false) {
|
|
144
|
+
linkEl.click();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
rescue = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (rescue) {
|
|
135
151
|
e.preventDefault();
|
|
136
152
|
if (!(await downloadCurrent())) {
|
|
153
|
+
showError();
|
|
137
154
|
root.querySelector(".hcp-body [data-hcp-for='error']").textContent = "The download could not be made. Keep this page open and try Download free again.";
|
|
138
155
|
return;
|
|
139
156
|
}
|
|
@@ -152,7 +169,10 @@ export async function mountPrompt({ memory, appName, link }) {
|
|
|
152
169
|
function clearError() {
|
|
153
170
|
if (root.getAttribute("data-hcp-reason") === "error") root.setAttribute("data-hcp-reason", "use");
|
|
154
171
|
}
|
|
155
|
-
const api = { set, root, showError, clearError, download: () =>
|
|
172
|
+
const api = { set, root, showError, clearError, download: (rescue = false) => {
|
|
173
|
+
rescueRequested = rescue;
|
|
174
|
+
try { linkEl.click(); } finally { rescueRequested = false; }
|
|
175
|
+
} };
|
|
156
176
|
|
|
157
177
|
document.addEventListener("input", onUse, true);
|
|
158
178
|
document.addEventListener("change", onUse, true);
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One tab's side of the draft database.
|
|
3
|
+
*
|
|
4
|
+
* The store (`store.js`) turns each save into an operation and keeps it in this tab's
|
|
5
|
+
* journal slot. This module delivers them: `sync()` runs one `transact()` (`commit.js`)
|
|
6
|
+
* carrying this tab's unconfirmed operations and every journal slot `localStorage` holds,
|
|
7
|
+
* a closed tab's included, and `took()` acts on what that transaction stored. An operation
|
|
8
|
+
* is saved once its transaction completed; a failed one stays where it is and goes again
|
|
9
|
+
* with the next `sync()`.
|
|
10
|
+
*
|
|
11
|
+
* After each transaction `localStorage` gets a cache of the record: the record itself with
|
|
12
|
+
* a `w` field `{rev, reset}` while it is at most FULL_COPY_LIMIT characters, beyond that a
|
|
13
|
+
* head (`{v: 2, idb: true, data: the small keys}`). A page boots from the cache, the
|
|
14
|
+
* first-paint look and the download page read it, and a tab on a released ClayJS (1.24.0
|
|
15
|
+
* and earlier) takes it for the record. It is never read back as the record here.
|
|
16
|
+
*
|
|
17
|
+
* Such a tab writes its own record over the cache. That record has no `w`, so the next
|
|
18
|
+
* transaction takes it in (`commit.js`), and the cache is never written over one that has
|
|
19
|
+
* not been taken in.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { openDrafts } from "./idb.js";
|
|
23
|
+
import { emptyMeta, foreignMark, isForeign, markerFor, markerGen, recoveryKey, recoveryMark, rowKey, settled, transact } from "./commit.js";
|
|
24
|
+
import { setDraftStoreFailed } from "../../core/draft-mode.js";
|
|
25
|
+
|
|
26
|
+
export const FULL_COPY_LIMIT = 256 * 1024;
|
|
27
|
+
const HEAD_FIELD_LIMIT = 2 * 1024;
|
|
28
|
+
const HEAD_KEYS = new Set(["v", "savedAt", "id", "ancestry", "reset", "data"]);
|
|
29
|
+
// A settled journal slot younger than this may belong to a tab that is still writing it.
|
|
30
|
+
const SLOT_AGE_MS = 10 * 1000;
|
|
31
|
+
// Start over reloads the page once its transaction completed, or after this long: the
|
|
32
|
+
// marker in `localStorage` then carries it to the next page load's first transaction.
|
|
33
|
+
const START_OVER_WAIT_MS = 1500;
|
|
34
|
+
const FIRST_TRANSACTION_TIMEOUT_MS = 2000;
|
|
35
|
+
const STARTED_OVER = "This draft was started over in another tab.";
|
|
36
|
+
const RESET_KEPT_ASIDE = "This draft was started over in another tab. The earlier draft was kept aside in this browser.";
|
|
37
|
+
const KEPT_ASIDE = "A tab that was open before an update saved another copy of this draft. That copy was kept aside, and this draft was not changed. Reload that tab to go on from this draft.";
|
|
38
|
+
|
|
39
|
+
const parse = (text) => {
|
|
40
|
+
if (text == null) return null;
|
|
41
|
+
try { return JSON.parse(text); } catch { return null; }
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Only data fits in a head: a page (`live`, `blockeditor`) or any field a later version adds
|
|
45
|
+
// is held only in the database, so a head of such a record never says it is `full`.
|
|
46
|
+
function headOf(record, w) {
|
|
47
|
+
const data = {};
|
|
48
|
+
let full = Object.keys(record).every((name) => HEAD_KEYS.has(name));
|
|
49
|
+
for (const [name, value] of Object.entries(record.data)) {
|
|
50
|
+
if (JSON.stringify(value).length < HEAD_FIELD_LIMIT) data[name] = value;
|
|
51
|
+
else full = false;
|
|
52
|
+
}
|
|
53
|
+
return JSON.stringify({ v: 2, savedAt: record.savedAt, id: record.id, ancestry: record.ancestry, idb: true, ...(full && { full: true }), data, w });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Open the database for this tab and run its first transaction, which takes in what closed
|
|
58
|
+
* tabs left in their journal slots. Rejects when the database does not open.
|
|
59
|
+
*
|
|
60
|
+
* @param {Object} options
|
|
61
|
+
* @param {Object} options.store - the tab's store (`createStore`)
|
|
62
|
+
* @param {?Storage} options.storage
|
|
63
|
+
*/
|
|
64
|
+
export async function openSession({ store, storage, doc = document, now = () => Date.now(), reload = () => window.location.reload() }) {
|
|
65
|
+
const drafts = await openDrafts();
|
|
66
|
+
const { key } = store;
|
|
67
|
+
const resetKey = key + ":reset";
|
|
68
|
+
const read = (name) => { try { return storage?.getItem(name) ?? null; } catch { return null; } };
|
|
69
|
+
const remove = (name) => { try { storage?.removeItem(name); } catch {} };
|
|
70
|
+
|
|
71
|
+
// A page that loaded again because another tab started over says so once, and keeps
|
|
72
|
+
// saying it until it is closed: the reload happened while the visitor was in the other
|
|
73
|
+
// tab. The flag is per tab and outlives only that reload.
|
|
74
|
+
const noticeKey = key + ":started-over";
|
|
75
|
+
const recoveryNoticeKey = key + ":recovery-notice";
|
|
76
|
+
try {
|
|
77
|
+
if (window.sessionStorage.getItem(noticeKey)) {
|
|
78
|
+
window.sessionStorage.removeItem(noticeKey);
|
|
79
|
+
if (!window.sessionStorage.getItem(recoveryNoticeKey)) import("../../ui/toast.js").then((m) => m.toastPersistent(STARTED_OVER, "info")).catch(() => {});
|
|
80
|
+
}
|
|
81
|
+
} catch {}
|
|
82
|
+
|
|
83
|
+
// The stored revision this page last matched, and the marker the transaction in flight read.
|
|
84
|
+
let rev = null;
|
|
85
|
+
let marker = "";
|
|
86
|
+
// The number of the generation this page shows, once a transaction has said it.
|
|
87
|
+
let gen = null;
|
|
88
|
+
// This tab's own Start over: the generation it ends, until a transaction has carried it
|
|
89
|
+
// (`sent` is what the transaction in flight carries).
|
|
90
|
+
let ending = null;
|
|
91
|
+
let sent = null;
|
|
92
|
+
let carried = 0;
|
|
93
|
+
let retiring = false;
|
|
94
|
+
let shownId = store.held()?.id;
|
|
95
|
+
// The latest full cache copy this tab saw at the key, and the text of another writer's
|
|
96
|
+
// record a storage event says replaced it: such a record merges from that copy.
|
|
97
|
+
let lastCache = null;
|
|
98
|
+
let replacedBy = null;
|
|
99
|
+
const told = new Set();
|
|
100
|
+
try {
|
|
101
|
+
const notice = window.sessionStorage.getItem(recoveryNoticeKey);
|
|
102
|
+
if (notice) {
|
|
103
|
+
window.sessionStorage.removeItem(recoveryNoticeKey);
|
|
104
|
+
const saved = parse(notice);
|
|
105
|
+
keptAside({ id: saved?.mark ?? notice }, Boolean(saved?.reset));
|
|
106
|
+
}
|
|
107
|
+
} catch {}
|
|
108
|
+
|
|
109
|
+
function rememberRecovery(mark) {
|
|
110
|
+
try { window.sessionStorage.setItem(recoveryNoticeKey, JSON.stringify({ mark, reset: true })); } catch {}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function rememberGeneration(reset) {
|
|
114
|
+
try { window.sessionStorage.setItem(key + ":generation", JSON.stringify({ marker, reset })); } catch {}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function input() {
|
|
118
|
+
const held = read(resetKey);
|
|
119
|
+
marker = held ?? "";
|
|
120
|
+
const others = store.journalSlots().filter((name) => name !== store.slotKey).map((name) => parse(read(name)));
|
|
121
|
+
sent = store.stopped() ? null : ending;
|
|
122
|
+
carried = store.chain().ops.at(-1)?.seq ?? 0;
|
|
123
|
+
const local = store.held();
|
|
124
|
+
if (local?.v === 1 && local.w !== undefined) lastCache = local;
|
|
125
|
+
const resets = sent ? [sent] : [];
|
|
126
|
+
return {
|
|
127
|
+
seed: store.seed(),
|
|
128
|
+
newId: store.newId,
|
|
129
|
+
merge3: store.merge3,
|
|
130
|
+
unitMark: store.unitMark,
|
|
131
|
+
now: now(),
|
|
132
|
+
marker: held,
|
|
133
|
+
resets,
|
|
134
|
+
local,
|
|
135
|
+
seen: lastCache,
|
|
136
|
+
direct: replacedBy !== null && read(key) === replacedBy,
|
|
137
|
+
chains: store.pending() ? [...others, store.chain()] : others,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Another tab's slot is removed once nothing more can come of it and its tab has gone quiet.
|
|
142
|
+
function sweep(meta) {
|
|
143
|
+
const at = now();
|
|
144
|
+
for (const name of store.journalSlots()) {
|
|
145
|
+
if (name === store.slotKey) continue;
|
|
146
|
+
const chain = parse(read(name));
|
|
147
|
+
if (settled(chain, meta) && at - (chain?.at ?? 0) > SLOT_AGE_MS) remove(name);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function cache({ meta, record }) {
|
|
152
|
+
const held = store.held();
|
|
153
|
+
// Another writer's record stays until the database has taken it in: one written since
|
|
154
|
+
// this transaction read `localStorage` goes with the next.
|
|
155
|
+
if (isForeign(held) && foreignMark(held) !== meta.foreign) return void setTimeout(quietly, 0);
|
|
156
|
+
if ((read(resetKey) ?? "") !== meta.reset) return;
|
|
157
|
+
if (!record) return void (held && remove(key));
|
|
158
|
+
if (held?.w && held.w.reset === meta.reset && held.w.rev >= meta.rev) return;
|
|
159
|
+
const w = { rev: meta.rev, reset: meta.reset };
|
|
160
|
+
const full = JSON.stringify({ ...record, w });
|
|
161
|
+
try {
|
|
162
|
+
if (full.length <= FULL_COPY_LIMIT) {
|
|
163
|
+
storage.setItem(key, full);
|
|
164
|
+
lastCache = { ...record, w };
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
} catch {}
|
|
168
|
+
// No room for the full copy: a head still says a draft exists.
|
|
169
|
+
try { storage.setItem(key, headOf(record, w)); } catch {}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Operations captured after the transaction read stay journaled for the next load.
|
|
173
|
+
async function ended(result) {
|
|
174
|
+
retiring = true;
|
|
175
|
+
const chain = store.chain();
|
|
176
|
+
const mark = recoveryMark(chain);
|
|
177
|
+
const kept = chain.ops.some((op) => op.seq > carried) || result.meta.recovery?.includes(mark)
|
|
178
|
+
? mark : result.meta.recovery?.includes(shownId) ? shownId : null;
|
|
179
|
+
store.acknowledge(carried);
|
|
180
|
+
store.stop({ keepPending: true });
|
|
181
|
+
if (store.unsafe()) {
|
|
182
|
+
try { await drafts.putRow(rowKey(key, store.tab), JSON.stringify(store.chain())); }
|
|
183
|
+
catch (error) { throw store.failed(error); }
|
|
184
|
+
store.acknowledge(store.chain().ops.at(-1)?.seq ?? 0);
|
|
185
|
+
}
|
|
186
|
+
cache(result);
|
|
187
|
+
if (kept) rememberRecovery(kept);
|
|
188
|
+
const held = store.held();
|
|
189
|
+
if (held?.w && held.w.reset !== result.meta.reset) remove(key);
|
|
190
|
+
try { window.sessionStorage.setItem(noticeKey, "1"); } catch {}
|
|
191
|
+
rememberGeneration(result.meta.reset);
|
|
192
|
+
retiring = false;
|
|
193
|
+
reload();
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Start over ends the generation this page shows, and only that one: when another Start
|
|
198
|
+
// over has ended it since this page loaded, what was saved after that one stays. A page
|
|
199
|
+
// that does not know its generation's number yet waits for the first transaction.
|
|
200
|
+
async function startOver() {
|
|
201
|
+
const shown = store.generation();
|
|
202
|
+
const stop = () => { store.stop(); setDraftStoreFailed(false); };
|
|
203
|
+
const stale = () => (read(resetKey) ?? "") !== shown && (gen === null || (read(resetKey) ?? "") !== marker);
|
|
204
|
+
if (stale()) return stop();
|
|
205
|
+
let known = gen ?? markerGen(shown);
|
|
206
|
+
if (known === null) {
|
|
207
|
+
const result = await first.catch(() => drafts.change(key, ({ meta }) => ({
|
|
208
|
+
puts: [],
|
|
209
|
+
deletes: [],
|
|
210
|
+
result: { meta: parse(meta) ?? emptyMeta(shown) },
|
|
211
|
+
}), { timeout: FIRST_TRANSACTION_TIMEOUT_MS }));
|
|
212
|
+
if (stale()) return stop();
|
|
213
|
+
known = result.meta.gen;
|
|
214
|
+
if (result.meta.reset !== shown) {
|
|
215
|
+
ending = { ends: null, marker: shown };
|
|
216
|
+
const applied = await sync();
|
|
217
|
+
if (store.stopped()) return stop();
|
|
218
|
+
known = applied.meta.gen;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const next = markerFor(known + 1, store.newId());
|
|
222
|
+
ending = { ends: known, marker: next };
|
|
223
|
+
const before = read(key);
|
|
224
|
+
if (store.held()?.w) remove(key);
|
|
225
|
+
try { storage.setItem(resetKey, next); } catch {}
|
|
226
|
+
let timer;
|
|
227
|
+
try {
|
|
228
|
+
const committed = sync();
|
|
229
|
+
if (read(resetKey) !== next) await committed;
|
|
230
|
+
else await Promise.race([committed.catch(() => {}), new Promise((resolve) => { timer = setTimeout(resolve, START_OVER_WAIT_MS); })]);
|
|
231
|
+
stop();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
ending = null;
|
|
234
|
+
if (before !== null && read(key) === null && (read(resetKey) ?? "") === shown) {
|
|
235
|
+
try { storage.setItem(key, before); } catch {}
|
|
236
|
+
}
|
|
237
|
+
throw error;
|
|
238
|
+
} finally { clearTimeout(timer); }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Each kind of recovery notice is shown once per page load.
|
|
242
|
+
function keptAside(copy, reset = false) {
|
|
243
|
+
console.warn(reset
|
|
244
|
+
? "visitor-drafts: the draft was started over; the earlier draft was kept in this browser's draft database under"
|
|
245
|
+
: "visitor-drafts: a tab on an earlier ClayJS saved a copy that could not be merged exactly; it was kept in this browser's draft database under", recoveryKey(key, recoveryMark(copy)));
|
|
246
|
+
if (told.has(reset)) return;
|
|
247
|
+
told.add(reset);
|
|
248
|
+
import("../../ui/toast.js").then((m) => m.toastPersistent(reset ? RESET_KEPT_ASIDE : KEPT_ASIDE, "warning")).catch(() => {});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function took(result) {
|
|
252
|
+
const { meta, record } = result;
|
|
253
|
+
// A marker the transaction did not take named a generation that had already ended.
|
|
254
|
+
if (marker !== meta.reset && (read(resetKey) ?? "") === marker) {
|
|
255
|
+
try { storage.setItem(resetKey, meta.reset); } catch {}
|
|
256
|
+
}
|
|
257
|
+
if (sent && sent === ending && meta.reset !== store.generation()) {
|
|
258
|
+
if (sent.marker === meta.reset) rememberGeneration(meta.reset);
|
|
259
|
+
store.stop();
|
|
260
|
+
setDraftStoreFailed(false);
|
|
261
|
+
}
|
|
262
|
+
if (sent === ending) ending = null;
|
|
263
|
+
// A stopped page is on its way out, after its own Start over or another tab's: it only
|
|
264
|
+
// keeps the cache in step, for the page load that follows.
|
|
265
|
+
if (store.stopped()) {
|
|
266
|
+
if (retiring) return ended(result);
|
|
267
|
+
cache(result);
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
if (result.conflicts.length) console.warn("visitor-drafts: another tab changed the same thing; its copy was kept in this browser's draft database under", key + ":conflict");
|
|
271
|
+
if (result.stale.includes(store.tab)) return ended(result);
|
|
272
|
+
if (meta.reset !== store.generation()) {
|
|
273
|
+
if (!store.fresh()) return ended(result);
|
|
274
|
+
store.enter(meta.reset);
|
|
275
|
+
}
|
|
276
|
+
if (result.released.length) keptAside(result.released[0]);
|
|
277
|
+
gen = meta.gen;
|
|
278
|
+
rememberGeneration(meta.reset);
|
|
279
|
+
const saved = store.acknowledge(meta.tabs[store.tab]?.seq ?? 0);
|
|
280
|
+
sweep(meta);
|
|
281
|
+
cache(result);
|
|
282
|
+
if (store.pending()) return result;
|
|
283
|
+
if (saved) {
|
|
284
|
+
setDraftStoreFailed(false);
|
|
285
|
+
doc.dispatchEvent(new CustomEvent("clay:drafts-saved", { detail: { data: record.data } }));
|
|
286
|
+
}
|
|
287
|
+
if (!store.shown()) {
|
|
288
|
+
rev = meta.rev;
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
// A page with unsaved changes is never rewritten: its next operation merges instead.
|
|
292
|
+
if (!store.changed()) {
|
|
293
|
+
if (record && meta.rev !== rev) store.take(record.data);
|
|
294
|
+
shownId = record?.id;
|
|
295
|
+
rev = meta.rev;
|
|
296
|
+
}
|
|
297
|
+
const unit = store.externalUnit(record);
|
|
298
|
+
if (unit) doc.dispatchEvent(new CustomEvent("clay:drafts-blockeditor-external", { detail: { live: unit.live, adopted: () => store.adoptedUnit(unit) } }));
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// One transaction at a time, and at most one waiting: it reads the chain when it starts,
|
|
303
|
+
// so it carries every operation made before then.
|
|
304
|
+
let active = null;
|
|
305
|
+
let waiting = null;
|
|
306
|
+
function run(options) {
|
|
307
|
+
active = transact(drafts, key, input, options).then(took, (error) => {
|
|
308
|
+
throw store.pending() ? store.failed(error) : error;
|
|
309
|
+
}).finally(() => { active = null; });
|
|
310
|
+
return active;
|
|
311
|
+
}
|
|
312
|
+
function sync() {
|
|
313
|
+
if (waiting) return waiting;
|
|
314
|
+
if (!active) return run();
|
|
315
|
+
waiting = active.then(() => {}, () => {}).then(() => {
|
|
316
|
+
waiting = null;
|
|
317
|
+
return run();
|
|
318
|
+
});
|
|
319
|
+
return waiting;
|
|
320
|
+
}
|
|
321
|
+
const quietly = () => { sync().catch(() => {}); };
|
|
322
|
+
|
|
323
|
+
// At a hide, an operation too large for the journal slot goes into the database blind, with
|
|
324
|
+
// no read and no callback, so it can land while the page is going away.
|
|
325
|
+
function hide() {
|
|
326
|
+
if (store.unsafe()) drafts.putRow(rowKey(key, store.tab), JSON.stringify(store.chain())).catch(() => {});
|
|
327
|
+
if (store.pending()) quietly();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
window.addEventListener("storage", (event) => {
|
|
331
|
+
if (event.key === key) {
|
|
332
|
+
const [was, is] = [parse(event.oldValue), parse(event.newValue)];
|
|
333
|
+
if (is?.v === 1 && is.w !== undefined) lastCache = is;
|
|
334
|
+
else if (isForeign(is) && was?.v === 1 && was.w !== undefined) [lastCache, replacedBy] = [was, event.newValue];
|
|
335
|
+
}
|
|
336
|
+
if (event.key === null || event.key === key || event.key === resetKey) quietly();
|
|
337
|
+
});
|
|
338
|
+
window.addEventListener("pageshow", (event) => { if (event.persisted) quietly(); });
|
|
339
|
+
// The page editor missed a page another tab stored before it listened.
|
|
340
|
+
doc.addEventListener("clay:drafts-blockeditor-listening", quietly);
|
|
341
|
+
|
|
342
|
+
const first = run({ timeout: FIRST_TRANSACTION_TIMEOUT_MS });
|
|
343
|
+
first.catch(() => {});
|
|
344
|
+
return { first, sync, hide, startOver };
|
|
345
|
+
}
|