@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.
Files changed (136) hide show
  1. package/dist/all.js +1 -1
  2. package/dist/clay-dom.js +1 -1
  3. package/dist/clay-events.js +1 -1
  4. package/dist/clay-internals.js +1 -1
  5. package/dist/clay-options.js +1 -1
  6. package/dist/clay-router.js +1 -1
  7. package/dist/clay-ui.js +1 -1
  8. package/dist/clay-utils.js +1 -1
  9. package/dist/clay.js +1 -1
  10. package/dist/clay.standalone.js +15016 -13603
  11. package/dist/m/c-277QP5ET.js +1 -0
  12. package/dist/m/{c-BULK5J4M.js → c-3UW5EJ77.js} +1 -1
  13. package/dist/m/{c-GM7OA6EJ.js → c-42ZNV7LI.js} +1 -1
  14. package/dist/m/{c-RXTPKCMC.js → c-6IJEVE6B.js} +1 -1
  15. package/dist/m/{c-2R4OBWIP.js → c-6LEYVQJK.js} +1 -1
  16. package/dist/m/{c-LO4C4J77.js → c-6Q6RGFBF.js} +1 -1
  17. package/dist/m/{c-PD5AKHY3.js → c-7CM4GMTS.js} +1 -1
  18. package/dist/m/c-APGHGDZM.js +1 -0
  19. package/dist/m/{c-Q6VNQDJE.js → c-B7GOFGQR.js} +1 -1
  20. package/dist/m/{c-RJKKRWHW.js → c-BGN7RCTJ.js} +1 -1
  21. package/dist/m/{c-A7N5TNSL.js → c-C5AKJVRH.js} +1 -1
  22. package/dist/m/{c-SNVDWMM3.js → c-CJ7QYEWI.js} +1 -1
  23. package/dist/m/{c-PVJMJL3P.js → c-CZHTXGJS.js} +1 -1
  24. package/dist/m/{c-KJHVB2KG.js → c-DDS23EY6.js} +1 -1
  25. package/dist/m/{c-VSEUFJAO.js → c-DFKBGSN2.js} +1 -1
  26. package/dist/m/{c-DLBX4JCJ.js → c-EDE45NM7.js} +1 -1
  27. package/dist/m/c-EILLN2H2.js +1 -0
  28. package/dist/m/{c-EY5KDUZF.js → c-FEC6LGFF.js} +1 -1
  29. package/dist/m/{c-PMRRMT72.js → c-HDVH3MVC.js} +1 -1
  30. package/dist/m/c-HKSLOVU4.js +1 -0
  31. package/dist/m/{c-GNIVHLEJ.js → c-HL6BLJNA.js} +1 -1
  32. package/dist/m/c-HTLYXCUH.js +1 -0
  33. package/dist/m/{c-RTSRGGTP.js → c-JQF2EUCG.js} +1 -1
  34. package/dist/m/{c-O5ARALMJ.js → c-JWDCQL6J.js} +1 -1
  35. package/dist/m/{c-WOELSNWS.js → c-LATNQ6Q5.js} +1 -1
  36. package/dist/m/{c-UM7VZDTW.js → c-LSVN3OLG.js} +1 -1
  37. package/dist/m/{c-HSNGLIFB.js → c-MFOSMAVM.js} +2 -2
  38. package/dist/m/{c-FZRZPIFS.js → c-MQYEDF3Q.js} +1 -1
  39. package/dist/m/{c-2OIKXOY6.js → c-ND3Q2G2I.js} +1 -1
  40. package/dist/m/{c-PZRWZZRO.js → c-OCYG4NBQ.js} +1 -1
  41. package/dist/m/{c-54ZZZYUN.js → c-P2HKMJAW.js} +1 -1
  42. package/dist/m/{c-VI3BJHCM.js → c-PCNJETUA.js} +1 -1
  43. package/dist/m/{c-7YETSAVY.js → c-PDBNCJCT.js} +1 -1
  44. package/dist/m/{c-A5ZBFICF.js → c-PYG2NGWU.js} +1 -1
  45. package/dist/m/{c-RBPZNU2D.js → c-QQFHEODI.js} +1 -1
  46. package/dist/m/{c-YINHPF4V.js → c-SGH3UTF3.js} +1 -1
  47. package/dist/m/{c-FV46ZKGX.js → c-SKMIR5HQ.js} +1 -1
  48. package/dist/m/{c-TWOXQQKV.js → c-STRI73GQ.js} +1 -1
  49. package/dist/m/{c-75N3KG35.js → c-T3P3AXEW.js} +1 -1
  50. package/dist/m/c-U4NTUVDZ.js +1 -0
  51. package/dist/m/{c-P5NKFRL4.js → c-UOOIF2AL.js} +1 -1
  52. package/dist/m/c-VHSMPITY.js +1 -0
  53. package/dist/m/{c-6UPEKW6O.js → c-X75G43SZ.js} +1 -1
  54. package/dist/m/{c-YGGO2G2L.js → c-XW5TIYYB.js} +1 -1
  55. package/dist/m/{c-DZPO22O3.js → c-YDW4VCHP.js} +1 -1
  56. package/dist/m/{c-YB3CDTDZ.js → c-YLHTD3KX.js} +1 -1
  57. package/dist/m/c-Z4G2UK6T.js +4 -0
  58. package/dist/m/{c-VNAYHV2J.js → c-ZGCO4J3H.js} +1 -1
  59. package/dist/m/core/{admin-attrs-5UGMYCKW.js → admin-attrs-ONRWVXPK.js} +1 -1
  60. package/dist/m/core/{autosave-4P66VXX7.js → autosave-TBKAAAFB.js} +1 -1
  61. package/dist/m/core/{conflict-notice-BAR23OWH.js → conflict-notice-FOEE6I7E.js} +1 -1
  62. package/dist/m/core/draft-mode-4MCL3LYI.js +1 -0
  63. package/dist/m/core/{host-permission-3HE4CFD6.js → host-permission-X4BENZEU.js} +1 -1
  64. package/dist/m/core/{is-edit-mode-GK7IFFMT.js → is-edit-mode-FFHCQ5DR.js} +1 -1
  65. package/dist/m/core/{persist-AIJGTKKI.js → persist-6ILUR34S.js} +1 -1
  66. package/dist/m/core/{recovery-LPB6X72P.js → recovery-APWADU4K.js} +1 -1
  67. package/dist/m/core/{save-FV4ZCKBI.js → save-6HT2LABE.js} +1 -1
  68. package/dist/m/core/{save-core-K7XFAWQ4.js → save-core-EVMNZQ4V.js} +1 -1
  69. package/dist/m/core/{site-edits-TEQORBJU.js → site-edits-G4BOZMV4.js} +1 -1
  70. package/dist/m/core/{unsaved-warning-FF63QGKI.js → unsaved-warning-ITZDIZ2I.js} +1 -1
  71. package/dist/m/internals/{index-AX4CG7B2.js → index-HS3CQDFW.js} +1 -1
  72. package/dist/m/loader-GLLVDUZA.js +1 -0
  73. package/dist/m/manifest-50FF1C45.js +1 -0
  74. package/dist/m/manifest-edit-BE1A621D.js +1 -0
  75. package/dist/m/manifest.json +710 -674
  76. package/dist/m/plugins/{ai-edit-CUKQQWJX.js → ai-edit-LABECCSN.js} +1 -1
  77. package/dist/m/plugins/{cms-JANMGPHM.js → cms-T2HLI7JG.js} +16 -16
  78. package/dist/m/plugins/{demo-JIR7DXJA.js → demo-T7B4RIKL.js} +1 -1
  79. package/dist/m/plugins/{derive-QVI3LI3A.js → derive-6LPJOCAV.js} +1 -1
  80. package/dist/m/plugins/{indicator-DAKZGAWI.js → indicator-J4ABFARX.js} +1 -1
  81. package/dist/m/plugins/{live-UAJGWRNP.js → live-PGBIEMGB.js} +1 -1
  82. package/dist/m/plugins/{menu-BFJ644YX.js → menu-YZL3X2AJ.js} +1 -1
  83. package/dist/m/plugins/page-WOCNX2IN.js +2 -0
  84. package/dist/m/plugins/{page-view-OYN7Z5TS.js → page-view-SJODOOGE.js} +1 -1
  85. package/dist/m/plugins/{people-VBDR5Y22.js → people-C4TCWFOQ.js} +1 -1
  86. package/dist/m/plugins/{richclay-CBLZRFB4.js → richclay-Y2NV44NA.js} +3 -3
  87. package/dist/m/plugins/sortable-XCHQG65Z.js +1 -0
  88. package/dist/m/plugins/{source-ZXDD7F6X.js → source-YK7MO23W.js} +1 -1
  89. package/dist/m/plugins/{undo-5JI5G7CQ.js → undo-VLRBQNPN.js} +1 -1
  90. package/dist/m/plugins/{upgrade-6B6QS3NR.js → upgrade-NNTJBSK2.js} +1 -1
  91. package/dist/m/plugins/{upload-DNJNW6H3.js → upload-ZKKJV76X.js} +1 -1
  92. package/dist/m/plugins/visitor-drafts-RTZNONHT.js +20 -0
  93. package/dist/m/plugins/{visitor-drafts-owner-VIAZC3CF.js → visitor-drafts-owner-LHUHFU3L.js} +1 -1
  94. package/dist/m/plugins/{visitor-menu-ZHKWIKKY.js → visitor-menu-FY4KM2SW.js} +1 -1
  95. package/dist/m/plugins/wire-M2NR354P.js +1 -0
  96. package/dist/m/{reader-core-F6RY3V64.js → reader-core-T2S3JC4E.js} +2 -2
  97. package/dist/m/sync/{live-sync-M2PP2IWY.js → live-sync-Z64YREWQ.js} +1 -1
  98. package/dist/m/vendor/{sap-core.vendor-OA5SKL7Q.js → sap-core.vendor-QAEPMETX.js} +1 -1
  99. package/dist/offline-manifest.json +455 -425
  100. package/entries/clay-data.js +1 -1
  101. package/entries/sap.js +1 -1
  102. package/package.json +2 -2
  103. package/src/core/draft-mode.js +6 -0
  104. package/src/core/save-core.js +7 -8
  105. package/src/core/unsaved-warning.js +2 -2
  106. package/src/lib/idb-error-code.js +20 -0
  107. package/src/loader.js +2 -0
  108. package/src/plugins/page.js +19 -5
  109. package/src/plugins/sortable.js +7 -0
  110. package/src/plugins/visitor-drafts/commit.js +303 -0
  111. package/src/plugins/visitor-drafts/idb.js +221 -0
  112. package/src/plugins/visitor-drafts/prompt.js +23 -3
  113. package/src/plugins/visitor-drafts/session.js +345 -0
  114. package/src/plugins/visitor-drafts/store.js +210 -26
  115. package/src/plugins/visitor-drafts.js +148 -28
  116. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  117. package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
  118. package/src/vendor/hypercms.vendor.js +16 -16
  119. package/src/vendor/richclay.vendor.js +1 -1
  120. package/src/vendor/sap-core.vendor.js +1 -1
  121. package/dist/m/c-234RSEIW.js +0 -1
  122. package/dist/m/c-2YIJ6ZA6.js +0 -1
  123. package/dist/m/c-H2H7TI6O.js +0 -1
  124. package/dist/m/c-JMDKPBMC.js +0 -4
  125. package/dist/m/c-N7JPUPGC.js +0 -1
  126. package/dist/m/c-NEXFVJIT.js +0 -1
  127. package/dist/m/c-T527QHOB.js +0 -1
  128. package/dist/m/core/draft-mode-WSQAT67S.js +0 -1
  129. package/dist/m/loader-5IA5OJ3J.js +0 -1
  130. package/dist/m/manifest-98258645.js +0 -1
  131. package/dist/m/manifest-edit-ABF56000.js +0 -1
  132. package/dist/m/plugins/page-ZBIYHGK4.js +0 -2
  133. package/dist/m/plugins/sortable-HDOQ6GAC.js +0 -1
  134. package/dist/m/plugins/visitor-drafts-A5FDQ2G7.js +0 -20
  135. package/dist/m/plugins/wire-XOT7XKJL.js +0 -1
  136. /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
- export async function mountPrompt({ memory, appName, link }) {
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 (root.getAttribute("data-hcp-reason") === "error") {
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: () => linkEl.click() };
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
+ }