@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.
Files changed (209) 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 +16610 -14400
  11. package/dist/m/attrs/{refetch-on-save-DPGJJMGN.js → refetch-on-save-5CBFUJUF.js} +1 -1
  12. package/dist/m/attrs/save-freeze-MH3AOD56.js +1 -0
  13. package/dist/m/c-277QP5ET.js +1 -0
  14. package/dist/m/{c-SXGEWPFC.js → c-3E2IK5HU.js} +1 -1
  15. package/dist/m/{c-A4L7APIU.js → c-4AE2A2LK.js} +1 -1
  16. package/dist/m/{c-DZPO22O3.js → c-4Q6QIODW.js} +1 -1
  17. package/dist/m/{c-RO73MKKR.js → c-52OC3OH3.js} +1 -1
  18. package/dist/m/{c-VNAYHV2J.js → c-5PZYDWTW.js} +1 -1
  19. package/dist/m/c-5Q5XBN56.js +1 -0
  20. package/dist/m/c-5SZ6KFR5.js +1 -0
  21. package/dist/m/{c-ZRGEN6QQ.js → c-AI3H6ETU.js} +2 -2
  22. package/dist/m/{c-7V6MGMX2.js → c-BCWYEG6P.js} +1 -1
  23. package/dist/m/{c-A7N5TNSL.js → c-BJ2RRQBQ.js} +1 -1
  24. package/dist/m/{c-CTGRDT6E.js → c-C6YZNU6E.js} +1 -1
  25. package/dist/m/{c-DLBX4JCJ.js → c-C7SUMNMI.js} +1 -1
  26. package/dist/m/c-CITLTVQ4.js +4 -0
  27. package/dist/m/{c-TWOXQQKV.js → c-CL5LSATX.js} +1 -1
  28. package/dist/m/c-CU33V6RW.js +4 -0
  29. package/dist/m/{c-2R4OBWIP.js → c-D62HRSZK.js} +1 -1
  30. package/dist/m/{c-DD26VXAP.js → c-DT7E76Z2.js} +1 -1
  31. package/dist/m/{c-54ZZZYUN.js → c-EAV7A6BT.js} +1 -1
  32. package/dist/m/{c-RBPZNU2D.js → c-EQ5NBOGA.js} +1 -1
  33. package/dist/m/{c-KQKKIVLW.js → c-FBOO2UY3.js} +1 -1
  34. package/dist/m/{c-RTSRGGTP.js → c-FJDTDGDA.js} +1 -1
  35. package/dist/m/{c-MQKSKQHG.js → c-FKTALXNV.js} +1 -1
  36. package/dist/m/{c-GNIVHLEJ.js → c-GDNZEOOZ.js} +1 -1
  37. package/dist/m/{c-SUXHUL7U.js → c-GEAG5XAK.js} +1 -1
  38. package/dist/m/{c-UM7VZDTW.js → c-GY4BREDT.js} +1 -1
  39. package/dist/m/{c-4PO3YGDC.js → c-HGG7M2S3.js} +1 -1
  40. package/dist/m/c-HKSLOVU4.js +1 -0
  41. package/dist/m/{c-KJHVB2KG.js → c-HXGNH6GY.js} +1 -1
  42. package/dist/m/{c-FLEIU6CG.js → c-IESDKFO6.js} +1 -1
  43. package/dist/m/{c-SNVDWMM3.js → c-IJHF4IO6.js} +1 -1
  44. package/dist/m/{c-SGGGCIVD.js → c-IL5IVTOG.js} +1 -1
  45. package/dist/m/c-JB5YU57S.js +1 -0
  46. package/dist/m/{c-MP6S4AZU.js → c-JD2VFAYG.js} +2 -2
  47. package/dist/m/{c-CV4T65JE.js → c-JWW6BHWD.js} +3 -3
  48. package/dist/m/{c-GM7OA6EJ.js → c-K4M76VPM.js} +1 -1
  49. package/dist/m/{c-RXTPKCMC.js → c-KU3PX7LL.js} +1 -1
  50. package/dist/m/{c-2PVGJFI5.js → c-KYTVZT4D.js} +1 -1
  51. package/dist/m/{c-RJKKRWHW.js → c-M7ITYKMF.js} +1 -1
  52. package/dist/m/c-MFB74XVN.js +3 -0
  53. package/dist/m/c-MOP5KBWZ.js +1 -0
  54. package/dist/m/{c-YB3CDTDZ.js → c-MUM53J5J.js} +1 -1
  55. package/dist/m/{c-YK2L436K.js → c-MV7TEUTD.js} +1 -1
  56. package/dist/m/{c-LA2BW7UY.js → c-MZCOGNZL.js} +1 -1
  57. package/dist/m/{c-YGGO2G2L.js → c-N4GEJMXW.js} +1 -1
  58. package/dist/m/{c-NQAPWM7O.js → c-NADLYVAH.js} +1 -1
  59. package/dist/m/{c-RS77ODLY.js → c-NL4I4KNG.js} +1 -1
  60. package/dist/m/{c-6UPEKW6O.js → c-O2QZDVZ2.js} +1 -1
  61. package/dist/m/{c-LTSPHVAK.js → c-OBMZASYQ.js} +1 -1
  62. package/dist/m/{c-LUXAB2XI.js → c-ORLPQFNU.js} +1 -1
  63. package/dist/m/c-P73XPAOA.js +1 -0
  64. package/dist/m/{c-PWJSJEJU.js → c-PNZIJFG7.js} +1 -1
  65. package/dist/m/{c-ZKKAF4UT.js → c-PRZNJUYZ.js} +2 -2
  66. package/dist/m/{c-Q6VNQDJE.js → c-Q2CB57UG.js} +1 -1
  67. package/dist/m/{c-TWCXBWS3.js → c-QCDVDZIR.js} +1 -1
  68. package/dist/m/{c-P5NKFRL4.js → c-QDVG4NAZ.js} +1 -1
  69. package/dist/m/c-QWB53KRW.js +1 -0
  70. package/dist/m/{c-GNI7WHKF.js → c-RGAALCDU.js} +1 -1
  71. package/dist/m/c-RHPHMTF2.js +2 -0
  72. package/dist/m/{c-BULK5J4M.js → c-RWJ3OSCP.js} +1 -1
  73. package/dist/m/{c-5FXEKU4Y.js → c-SEVOS4UE.js} +1 -1
  74. package/dist/m/{c-PMRRMT72.js → c-SFYTKRU7.js} +1 -1
  75. package/dist/m/{c-47IBJGPQ.js → c-SFZRY6RT.js} +1 -1
  76. package/dist/m/{c-QENKN7QI.js → c-SGG6MUXW.js} +1 -1
  77. package/dist/m/c-SMLAJASY.js +1 -0
  78. package/dist/m/{c-VSEUFJAO.js → c-SUAC5RSG.js} +1 -1
  79. package/dist/m/{c-6RGEQNVV.js → c-SXZYVXD6.js} +1 -1
  80. package/dist/m/{c-ZFO5RW6G.js → c-TCFVG3I4.js} +1 -1
  81. package/dist/m/{c-PZRWZZRO.js → c-TLQINES7.js} +2 -2
  82. package/dist/m/c-U4TWEIKZ.js +1 -0
  83. package/dist/m/{c-FZRZPIFS.js → c-U65PREMC.js} +1 -1
  84. package/dist/m/{c-7YETSAVY.js → c-U7F66I64.js} +1 -1
  85. package/dist/m/{c-WOELSNWS.js → c-UD4VDBGJ.js} +1 -1
  86. package/dist/m/{c-VI3BJHCM.js → c-V2FPBRO6.js} +1 -1
  87. package/dist/m/{c-2OIKXOY6.js → c-V4B3RUWB.js} +1 -1
  88. package/dist/m/{c-O5ARALMJ.js → c-V5L6HWVL.js} +1 -1
  89. package/dist/m/c-VMXCX52Q.js +1 -0
  90. package/dist/m/{c-PVJMJL3P.js → c-VO6RSFCO.js} +1 -1
  91. package/dist/m/c-WDFQ7PBU.js +1 -0
  92. package/dist/m/{c-OG6GFSUX.js → c-WGQVSKKC.js} +1 -1
  93. package/dist/m/c-WSTUCBHV.js +1 -0
  94. package/dist/m/{c-EY5KDUZF.js → c-WVCT3ITX.js} +1 -1
  95. package/dist/m/{c-SE3SIY7Y.js → c-X3V4RWMA.js} +1 -1
  96. package/dist/m/{c-HIX5H5L7.js → c-XHZIKRCV.js} +1 -1
  97. package/dist/m/{c-2BLVA6IN.js → c-XQTERKQA.js} +1 -1
  98. package/dist/m/{c-A5ZBFICF.js → c-YFO6ZJLQ.js} +1 -1
  99. package/dist/m/{c-PD5AKHY3.js → c-YX2MC7YL.js} +1 -1
  100. package/dist/m/{c-OYHI67B6.js → c-YYGUHSZR.js} +1 -1
  101. package/dist/m/{c-YYMZWQLR.js → c-Z2OZFX3N.js} +1 -1
  102. package/dist/m/{c-AT7GLQGV.js → c-ZEA3DCG3.js} +1 -1
  103. package/dist/m/{c-75N3KG35.js → c-ZJG6C2WL.js} +1 -1
  104. package/dist/m/{c-FVPNX2GO.js → c-ZORVIXUR.js} +1 -1
  105. package/dist/m/{c-FV46ZKGX.js → c-ZPI7M5U5.js} +1 -1
  106. package/dist/m/core/{admin-attrs-5UGMYCKW.js → admin-attrs-ZI6K6UGM.js} +1 -1
  107. package/dist/m/core/{autosave-4P66VXX7.js → autosave-BSM2ABJL.js} +1 -1
  108. package/dist/m/core/{conflict-notice-BAR23OWH.js → conflict-notice-7EQLDYPA.js} +1 -1
  109. package/dist/m/core/draft-mode-M5XNYBMY.js +1 -0
  110. package/dist/m/core/{host-permission-3HE4CFD6.js → host-permission-5BYP24GC.js} +1 -1
  111. package/dist/m/core/{is-edit-mode-GK7IFFMT.js → is-edit-mode-73OXKK5H.js} +1 -1
  112. package/dist/m/core/{persist-AIJGTKKI.js → persist-2SZO3PCE.js} +1 -1
  113. package/dist/m/core/{recovery-LPB6X72P.js → recovery-VZF7RJZF.js} +1 -1
  114. package/dist/m/core/{save-FV4ZCKBI.js → save-K6Q47J2X.js} +1 -1
  115. package/dist/m/core/{save-core-K7XFAWQ4.js → save-core-U3Z44XBO.js} +1 -1
  116. package/dist/m/core/{site-edits-TEQORBJU.js → site-edits-CFMF6GHD.js} +1 -1
  117. package/dist/m/core/{snapshot-FJP5IJL3.js → snapshot-PWCHYFL6.js} +1 -1
  118. package/dist/m/core/unsaved-warning-2WRZLJH5.js +1 -0
  119. package/dist/m/events/{index-B6TGDYMG.js → index-YKQIBZE5.js} +1 -1
  120. package/dist/m/internals/index-FSLC4M7J.js +1 -0
  121. package/dist/m/lib/{cache-bust-NH7VBWHO.js → cache-bust-BL4KUQAJ.js} +1 -1
  122. package/dist/m/loader-RC54UV7H.js +1 -0
  123. package/dist/m/manifest-3B84C829.js +1 -0
  124. package/dist/m/manifest-edit-1CB79AD4.js +1 -0
  125. package/dist/m/manifest.json +1215 -1129
  126. package/dist/m/options/{options-55LQHSJ4.js → options-LKIZ7YTG.js} +1 -1
  127. package/dist/m/page/{editor-KAZDJNND.js → editor-PKSPJ2XJ.js} +2 -2
  128. package/dist/m/plugins/{ai-edit-CUKQQWJX.js → ai-edit-3LG6MGS3.js} +1 -1
  129. package/dist/m/plugins/{cms-JANMGPHM.js → cms-T2HLI7JG.js} +16 -16
  130. package/dist/m/plugins/{demo-JIR7DXJA.js → demo-LMCMBILC.js} +1 -1
  131. package/dist/m/plugins/{derive-QVI3LI3A.js → derive-7DR5TUDH.js} +1 -1
  132. package/dist/m/plugins/{indicator-DAKZGAWI.js → indicator-CTKX4EY7.js} +1 -1
  133. package/dist/m/plugins/{live-UAJGWRNP.js → live-IFLL6YN5.js} +1 -1
  134. package/dist/m/plugins/{menu-BFJ644YX.js → menu-YJZ3JWBC.js} +1 -1
  135. package/dist/m/plugins/page-GTDPYGE4.js +2 -0
  136. package/dist/m/plugins/{page-view-OYN7Z5TS.js → page-view-NMCCU6MF.js} +1 -1
  137. package/dist/m/plugins/{people-VBDR5Y22.js → people-6WIWCZEG.js} +1 -1
  138. package/dist/m/plugins/{richclay-CBLZRFB4.js → richclay-L3BZMX4I.js} +3 -3
  139. package/dist/m/plugins/sortable-365SRRL3.js +1 -0
  140. package/dist/m/plugins/{source-ZXDD7F6X.js → source-7LSGOV46.js} +1 -1
  141. package/dist/m/plugins/{undo-5JI5G7CQ.js → undo-UES4KS7B.js} +1 -1
  142. package/dist/m/plugins/{upgrade-6B6QS3NR.js → upgrade-MMPZUCDM.js} +1 -1
  143. package/dist/m/plugins/{upload-DNJNW6H3.js → upload-NZET3DNQ.js} +1 -1
  144. package/dist/m/plugins/visitor-drafts-V5YB7XDH.js +20 -0
  145. package/dist/m/plugins/{visitor-drafts-owner-VIAZC3CF.js → visitor-drafts-owner-TLECTYWS.js} +1 -1
  146. package/dist/m/plugins/{visitor-menu-ZHKWIKKY.js → visitor-menu-QDV2MYJN.js} +1 -1
  147. package/dist/m/plugins/wire-3YGUO3GE.js +1 -0
  148. package/dist/m/{reader-core-F6RY3V64.js → reader-core-VR7EL4WV.js} +2 -2
  149. package/dist/m/sync/live-sync-XMXXZS3G.js +1 -0
  150. package/dist/m/vendor/sap-core.vendor-JANUQULT.js +7 -0
  151. package/dist/offline-manifest.json +724 -664
  152. package/docs/guide.md +1 -0
  153. package/entries/clay-data.js +1 -1
  154. package/entries/sap.js +6 -5
  155. package/package.json +2 -2
  156. package/src/core/draft-mode.js +11 -0
  157. package/src/core/save-core.js +16 -17
  158. package/src/core/save.js +1 -1
  159. package/src/core/unsaved-warning.js +2 -2
  160. package/src/lib/dirty-gate.js +19 -0
  161. package/src/lib/idb-error-code.js +20 -0
  162. package/src/loader.js +2 -0
  163. package/src/plugins/menu/visitor.js +33 -1
  164. package/src/plugins/page.js +19 -5
  165. package/src/plugins/sortable.js +7 -0
  166. package/src/plugins/visitor-drafts/commit.js +350 -0
  167. package/src/plugins/visitor-drafts/idb.js +227 -0
  168. package/src/plugins/visitor-drafts/prompt.js +23 -3
  169. package/src/plugins/visitor-drafts/recovery-menu.js +118 -0
  170. package/src/plugins/visitor-drafts/recovery.js +64 -0
  171. package/src/plugins/visitor-drafts/session.js +393 -0
  172. package/src/plugins/visitor-drafts/store.js +247 -27
  173. package/src/plugins/visitor-drafts.js +177 -28
  174. package/src/sync/live-sync.js +1 -1
  175. package/src/sync/wire-profile.js +9 -4
  176. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  177. package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
  178. package/src/vendor/hypercms.vendor.js +16 -16
  179. package/src/vendor/richclay.vendor.js +1 -1
  180. package/src/vendor/sap-core.vendor.js +6 -5
  181. package/src/vendor/sap-ownership.vendor.js +181 -11
  182. package/tool-generation/system.md +1 -1
  183. package/dist/m/attrs/save-freeze-JZHKRQLR.js +0 -1
  184. package/dist/m/c-234RSEIW.js +0 -1
  185. package/dist/m/c-2YIJ6ZA6.js +0 -1
  186. package/dist/m/c-H2H7TI6O.js +0 -1
  187. package/dist/m/c-HSNGLIFB.js +0 -3
  188. package/dist/m/c-JMDKPBMC.js +0 -4
  189. package/dist/m/c-LO4C4J77.js +0 -1
  190. package/dist/m/c-N7JPUPGC.js +0 -1
  191. package/dist/m/c-NEXFVJIT.js +0 -1
  192. package/dist/m/c-R23UHMXG.js +0 -1
  193. package/dist/m/c-T527QHOB.js +0 -1
  194. package/dist/m/c-V2U3F4QX.js +0 -1
  195. package/dist/m/c-VG5XIN4T.js +0 -1
  196. package/dist/m/c-YINHPF4V.js +0 -1
  197. package/dist/m/core/draft-mode-WSQAT67S.js +0 -1
  198. package/dist/m/core/unsaved-warning-FF63QGKI.js +0 -1
  199. package/dist/m/internals/index-AX4CG7B2.js +0 -1
  200. package/dist/m/loader-5IA5OJ3J.js +0 -1
  201. package/dist/m/manifest-98258645.js +0 -1
  202. package/dist/m/manifest-edit-ABF56000.js +0 -1
  203. package/dist/m/plugins/page-ZBIYHGK4.js +0 -2
  204. package/dist/m/plugins/sortable-HDOQ6GAC.js +0 -1
  205. package/dist/m/plugins/visitor-drafts-A5FDQ2G7.js +0 -20
  206. package/dist/m/plugins/wire-XOT7XKJL.js +0 -1
  207. package/dist/m/sync/live-sync-M2PP2IWY.js +0 -1
  208. package/dist/m/vendor/sap-core.vendor-OA5SKL7Q.js +0 -6
  209. /package/dist/m/plugins/{richclay-CBLZRFB4.js.LEGAL.txt → richclay-L3BZMX4I.js.LEGAL.txt} +0 -0
@@ -29,6 +29,7 @@ import Mutation from './mutation.js';
29
29
  import { isEditMode } from '../core/is-edit-mode.js';
30
30
  import { STRIP_FROM_DIRTY_CHECK, SNAPSHOT_REMOVE_SELECTOR } from './region-policy.js';
31
31
  import { PERSIST } from './attr-aliases.js';
32
+ import { setDraftGate } from '../core/draft-mode.js';
32
33
 
33
34
  let changes = 0;
34
35
  let clearedAt = 0;
@@ -121,6 +122,7 @@ export function startDirtyGate() {
121
122
  );
122
123
  document.addEventListener('input', onUserInput, true);
123
124
  document.addEventListener('change', onUserInput, true);
125
+ setDraftGate({ token: gateTokenNow, unchangedSince: gateUnchangedSince });
124
126
  }
125
127
 
126
128
  /** The morph-apply window: nothing that happens inside it is a user edit. */
@@ -210,6 +212,23 @@ export function gateCaptureToken() {
210
212
  return { gen: changes, probe };
211
213
  }
212
214
 
215
+ /** A token for the page as it is now: what the browser has queued is counted first. */
216
+ export function gateTokenNow() {
217
+ Mutation._deliverBeforePause();
218
+ return gateCaptureToken();
219
+ }
220
+
221
+ /**
222
+ * True when nothing was recorded since the token was taken and every [persist] control
223
+ * still holds what it held then. Records the browser has queued are delivered first, so
224
+ * the answer holds in the task that made a change too.
225
+ */
226
+ export function gateUnchangedSince(token) {
227
+ if (!token) return false;
228
+ Mutation._deliverBeforePause();
229
+ return changes === token.gen && token.probe.every(([el, sig]) => controlSignature(el) === sig);
230
+ }
231
+
213
232
  /** A change nothing in the DOM recorded: the page holds text a save has not written. */
214
233
  export function gateMarkDirty() {
215
234
  changes++;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The failure code a thrown IndexedDB error means to a caller.
3
+ *
4
+ * Only one of these is about this device being full (`quota`); the rest are about
5
+ * the database being unreachable, refused, or mid-upgrade somewhere else. None of
6
+ * them is a save result, and none of them is durable.
7
+ *
8
+ * @param {*} error
9
+ * @returns {string}
10
+ */
11
+ export function codeFor(error) {
12
+ const name = error?.name || "";
13
+ if (name === "QuotaExceededError" || name === "NS_ERROR_DOM_QUOTA_REACHED") return "quota";
14
+ if (name === "VersionError") return "unknown-schema";
15
+ if (name === "DataCloneError") return "uncloneable";
16
+ if (name === "AbortError") return "aborted";
17
+ if (name === "TransactionInactiveError" || name === "InvalidStateError") return "aborted";
18
+ if (name === "SecurityError" || name === "UnknownError" || name === "NotSupportedError") return "unavailable";
19
+ return "error";
20
+ }
package/src/loader.js CHANGED
@@ -174,6 +174,8 @@ export async function boot(base, params, readyResolve, manifest = null, suppleme
174
174
  continue;
175
175
  }
176
176
  loaded[path] = mod;
177
+ // A draft held only in the database goes onto the page before a plugin that reads the page loads.
178
+ if (path === "plugins/visitor-drafts.js") await mod.restoring;
177
179
  if (path === "plugins/page-view.js") mod.start(readerOptions);
178
180
  attachPluginMember(path, mod, loaded); // immediately, not after the loop: hypercms's ?cms=true
179
181
  // auto-open runs as a microtask queued during ITS evaluation
@@ -72,17 +72,49 @@ function copyUrl(loc = window.location) {
72
72
  return `/_/copy${path === "/" ? "" : path}`;
73
73
  }
74
74
 
75
+ // Hyperclay's trial bar, once the visitor closes it, leaves its Host action and a way back to the
76
+ // page (hyperclay server-lib/drop/trial-bar.js): `window.hyperclayBar` holds its state, set before
77
+ // this menu loads, and `hyperclay:bar` reports each change. Download copy is already in the menu.
78
+ function attachBar({ addItem, removeItem }) {
79
+ const draw = (bar) => {
80
+ if (bar?.kind !== "trial" || !bar.dismissed) {
81
+ removeItem("clay.bar.host");
82
+ removeItem("clay.bar.show");
83
+ return;
84
+ }
85
+ const host = Array.isArray(bar.actions) ? bar.actions.find((action) => action?.id === "host") : null;
86
+ if (typeof host?.href === "string") addItem({ id: "clay.bar.host", label: "Host and share online", href: host.href });
87
+ if (typeof bar.show === "function") addItem({ id: "clay.bar.show", label: "Show draft bar", run: () => bar.show() });
88
+ };
89
+ document.addEventListener("hyperclay:bar", (event) => draw(event.detail));
90
+ draw(window.hyperclayBar);
91
+ }
92
+
75
93
  // A visitor's menu holds exactly Undo, Redo, Download copy and Make your own copy (David, Nov 11):
76
- // no theme row, no page items, no Start over.
94
+ // no theme row, no page items, no Start over. While Hyperclay's trial bar is closed it also holds
95
+ // Host and share online and Show draft bar (David, Nov 11, sheet hosts review), and Earlier
96
+ // drafts when this page has recovery copies (David, Nov 13).
77
97
  export function start() {
78
98
  if (document.querySelector('clay-editor-ui[data-clay-ui="menu"]')) return;
79
99
  const shell = createShell({ sections: [["clay.undo"], null], icon: GEAR, label: "Settings" });
80
100
  const { embedded, skin, addItem, removeItem, open, close, note, goo, beforeOpen } = shell;
81
101
  shell.mount(embedded ? [MENU_SAFETY, SHELL_BASE, skin ?? MENU_SKIN, MENU_TOUCH] : [MENU_SAFETY, SHELL_BASE, SHELL_FLOATING, MENU_SKIN, MENU_TOUCH]);
102
+ let recoveryMenu;
103
+ beforeOpen.push(() => {
104
+ if (typeof globalThis.indexedDB?.open !== "function" || !globalThis.IDBKeyRange) return;
105
+ recoveryMenu ??= import("../visitor-drafts/recovery-menu.js").then(async ({ attachRecoveryMenu }) => {
106
+ const { recoveryDrafts } = await import("../visitor-drafts.js");
107
+ attachRecoveryMenu(shell, recoveryDrafts);
108
+ }).catch((error) => {
109
+ recoveryMenu = null;
110
+ console.warn("visitor-menu: earlier drafts did not load", error);
111
+ });
112
+ });
82
113
  publishLooks();
83
114
  attachUndo({ addItem, beforeOpen });
84
115
  addItem({ id: "clay.download", label: "Download copy", run: () => window.clay.visitorDrafts.download() });
85
116
  addItem({ id: "clay.copy", label: "Make your own copy", href: copyUrl() });
117
+ attachBar({ addItem, removeItem });
86
118
  window.clay.menu = { addItem, removeItem, open, close, note, embedded, button: goo };
87
119
  document.dispatchEvent(new CustomEvent("clay:menu-ready"));
88
120
  }
@@ -1,6 +1,7 @@
1
1
  import { FEATURES, featureForNode, featureForType } from "../page/features/registry.js";
2
2
  import { mergeEngine } from "../sync/merge-engine.js";
3
3
  import { registerUnsavedState } from "../lib/unsaved-state.js";
4
+ import Mutation from "../lib/mutation.js";
4
5
  import { hostMeta } from "../core/host-meta.js";
5
6
  import { savePageForce, holdAllSaves, releaseAllSaves, suspendAutosave, resumeAutosave, getLastSavedBytes, savesAdmitted, claimFailure } from "../core/save.js";
6
7
  import { setSaveHeader, onSaveAccepted } from "../core/save-core.js";
@@ -1250,9 +1251,16 @@ function mountAndAttach(inst, source, loaded) {
1250
1251
  if (shell) shell.inert = true;
1251
1252
  if (loaded.featureIds.length && shell) shell.style.visibility = "hidden";
1252
1253
  // One synchronous move: the finished shell replaces the saved surface as a
1253
- // single change, and the article element itself stays where it is.
1254
- if (!loaded.featureIds.length) for (const node of savedContent) node.remove();
1255
- if (shell) article.appendChild(shell);
1254
+ // single change, and the article element itself stays where it is. The editor
1255
+ // chunk can land after the boot settle, so the move is paused: it is the editor
1256
+ // presenting the saved surface, never an edit to save.
1257
+ Mutation.pause();
1258
+ try {
1259
+ if (!loaded.featureIds.length) for (const node of savedContent) node.remove();
1260
+ if (shell) article.appendChild(shell);
1261
+ } finally {
1262
+ Mutation.resume();
1263
+ }
1256
1264
  loaded.refreshView?.(view);
1257
1265
  binding.attach(view);
1258
1266
  // The page's history tells whoever presents it once it exists (a menu opened before the
@@ -1462,9 +1470,15 @@ function mountAndAttach(inst, source, loaded) {
1462
1470
  // the commit that enters edit cannot fail here.
1463
1471
  function openInput(inst, mounted) {
1464
1472
  const shell = mounted.view.dom.closest(".blockeditor-shell");
1473
+ // Like the boot move, revealing the editor over the saved surface is presentation, not an edit.
1465
1474
  const showEditor = () => {
1466
- for (const node of savedContent) node.remove();
1467
- if (shell) shell.style.visibility = "";
1475
+ Mutation.pause();
1476
+ try {
1477
+ for (const node of savedContent) node.remove();
1478
+ if (shell) shell.style.visibility = "";
1479
+ } finally {
1480
+ Mutation.resume();
1481
+ }
1468
1482
  };
1469
1483
  // A replacement that landed during the feature wait keeps the editor read-only,
1470
1484
  // exactly as one that landed before it does.
@@ -188,6 +188,13 @@ function makeSortable(sortableElem, Sortable, record = loose) {
188
188
  let options = {};
189
189
  options.draggable = draggableSelector(sortableElem);
190
190
 
191
+ // A reorder is not a text drag. SortableJS writes the item's text onto the drag by
192
+ // default, and a rich text field under the drop pastes it in, duplicating the text.
193
+ options.setData = (dataTransfer) => {
194
+ dataTransfer.clearData();
195
+ dataTransfer.setData('application/x-clay-sortable', '');
196
+ };
197
+
191
198
  // Check if Sortable instance already exists
192
199
  if (Sortable.get(sortableElem)) return;
193
200
 
@@ -0,0 +1,350 @@
1
+ /**
2
+ * The one change a visitor draft's stored state ever takes.
3
+ *
4
+ * IndexedDB holds one record per draft key K, and only `transact()` changes it: one
5
+ * readwrite transaction reads what is stored, runs `commit()` over it, and writes the
6
+ * result. IndexedDB runs readwrite transactions on a store one after another across every
7
+ * tab of the origin, so this is the single writer, with no lock and no leader tab.
8
+ *
9
+ * What it applies:
10
+ *
11
+ * - operations: for each top-level key a tab changed, the value before (`b`) and after
12
+ * (`m`), a missing one meaning the key was absent. Each carries its own merge base, so
13
+ * the store's `merge3` lays it over whatever is stored. A tab's operations are a
14
+ * chain `{tab, reset, acked, ops: [{seq, savedAt, keys, unit?}]}`, where `reset` is
15
+ * the Start over marker of the generation the tab shows (a tab knows that before its
16
+ * first transaction; it does not know the number). An operation applies only when
17
+ * its chain's `reset` is the stored one and its `seq` is the next for its tab, so
18
+ * each applies exactly once and in order, whichever tab delivers it;
19
+ * - Start over: "end generation g", given explicitly or as a marker `<g+1>~<random>`
20
+ * in `localStorage`; an unverified marker starts empty and keeps recovery copies;
21
+ * - the move: the full version 1 record in `localStorage`, taken in as the first record
22
+ * when nothing is stored yet;
23
+ * - another writer's record: a version 1 record in `localStorage` without the cache's
24
+ * `w` field, written by a tab on a released ClayJS or by a page load of this one that
25
+ * had no database. It is taken in once: merged when the copy it was built on is held
26
+ * here, kept whole as a recovery copy when it is not. Nothing is merged on a guess.
27
+ *
28
+ * Stored rows (JSON text): K (the record, today's format), `K:meta`
29
+ * `{gen, rev, reset, ended?, tabs: {tab: {seq, at}}, foreign, unit, recovery?}`, `K:v1base` (the
30
+ * last record of another writer that was merged), `K:op:<tab>` (a chain a tab left at a
31
+ * hide), `K:conflict:<id>` with `K:conflict` naming the latest, and `K:recovery:<mark>`.
32
+ * `meta.unit` is the mark of the Block editor page the record holds (`unitMark`). A record
33
+ * id or ancestry identifies another writer's held base or the reset its copy follows.
34
+ * Operations of this ClayJS never read one. Recovery rows can also hold exact ended chains.
35
+ *
36
+ * This module imports nothing: the page fetches it with the database, after the boot, and
37
+ * the store that is already on the page hands it `merge3` and `unitMark`.
38
+ */
39
+
40
+ export const LINEAGE = 32;
41
+ // Recovery copies kept per draft, newest first.
42
+ export const RECOVERY_KEEP = 8;
43
+ // A tab not heard from in this long is forgotten; a chain it still sends carries `acked`.
44
+ export const TAB_TTL_MS = 30 * 24 * 60 * 60 * 1000;
45
+
46
+ const clone = (value) => (value === undefined ? undefined : JSON.parse(JSON.stringify(value)));
47
+ const isPlain = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
48
+
49
+ export const metaKey = (key) => key + ":meta";
50
+ export const rowKey = (key, tab) => `${key}:op:${tab}`;
51
+ export const conflictKey = (key, id) => (id === undefined ? key + ":conflict" : `${key}:conflict:${id}`);
52
+ export const recoveryKey = (key, mark) => `${key}:recovery:${mark}`;
53
+
54
+ // A version 1 record without the cache's `w` field is another writer's.
55
+ export const isForeign = (value) => isPlain(value) && value.v === 1 && value.w === undefined && isPlain(value.data);
56
+ // What names one: its id, or its time for a record from before ids (ClayJS 1.22.0 and earlier).
57
+ export const foreignMark = (record) => (typeof record.id === "string" ? record.id : "at:" + record.savedAt);
58
+
59
+ export const markerFor = (gen, nonce) => `${gen}~${nonce}`;
60
+ export function markerGen(marker) {
61
+ const match = /^(\d+)~/.exec(marker ?? "");
62
+ return match ? Number(match[1]) : null;
63
+ }
64
+
65
+ // One operation's keys laid over `data` with the store's three-way merge.
66
+ export function applyKeys(data, keys, merge3) {
67
+ const base = { ...data };
68
+ const mine = { ...data };
69
+ for (const [name, pair] of Object.entries(keys)) {
70
+ if ("b" in pair) base[name] = pair.b; else delete base[name];
71
+ if ("m" in pair) mine[name] = pair.m; else delete mine[name];
72
+ }
73
+ return merge3(base, mine, data);
74
+ }
75
+
76
+ export const isChain = (value) => isPlain(value) && typeof value.tab === "string" && typeof value.reset === "string" && Array.isArray(value.ops);
77
+ export const recoveryMark = (copy) => isChain(copy) ? `op:${copy.tab}:${copy.ops.at(-1)?.seq ?? copy.acked ?? 0}` : foreignMark(copy);
78
+
79
+ // Ended journals carry only the fields they changed; other fields use the served page.
80
+ export function recoveryRecord(copy, seed) {
81
+ if (isPlain(copy) && copy.v === 1 && isPlain(copy.data)) return clone(copy);
82
+ if (!isChain(copy) || !copy.ops.length) return null;
83
+ const data = clone(seed);
84
+ let unit = null;
85
+ for (const op of copy.ops) {
86
+ for (const [name, pair] of Object.entries(op.keys ?? {})) {
87
+ if ("m" in pair) Object.defineProperty(data, name, { value: clone(pair.m), enumerable: true, writable: true, configurable: true });
88
+ else delete data[name];
89
+ }
90
+ if (op.unit !== undefined) unit = op.unit.to;
91
+ }
92
+ return { v: 1, savedAt: copy.ops.at(-1).savedAt, data, ...(unit && { live: clone(unit.live), blockeditor: clone(unit.blockeditor) }) };
93
+ }
94
+
95
+ // Cleanup needs proof for this exact chain, even when its generation has ended.
96
+ export function settled(chain, meta) {
97
+ if (isPlain(chain) && meta.recovery?.includes(recoveryMark(chain))) return true;
98
+ if (!isChain(chain)) return false;
99
+ const at = Math.max(chain.acked ?? 0, chain.reset === meta.reset ? meta.tabs[chain.tab]?.seq ?? 0 : 0);
100
+ return chain.ops.every((op) => op.seq <= at);
101
+ }
102
+
103
+ export function emptyMeta(marker = null) {
104
+ return { gen: markerGen(marker) ?? 1, rev: 0, reset: marker ?? "", tabs: {}, foreign: null, unit: null };
105
+ }
106
+
107
+ const unitOf = (record) => (record && record.blockeditor ? { live: record.live, blockeditor: record.blockeditor } : null);
108
+
109
+ // The ids a record built on `record` descends from, nearest first.
110
+ function lineage(first, record, meta) {
111
+ const line = [...first, record?.id, ...(record?.ancestry ?? [])].filter((id) => typeof id === "string");
112
+ return line.length ? line.slice(0, LINEAGE) : ["root:" + meta.reset];
113
+ }
114
+
115
+ // The copy another writer's record was built on, when this side holds it and what it held
116
+ // is in the stored record: the stored record itself, a cache copy of this generation, or
117
+ // the last record of another writer that was merged. A record that descends from one of
118
+ // them (its ancestry names it, or a storage event says it replaced that cache copy)
119
+ // carries every change made since as its difference; the nearest one is the base.
120
+ function foreignBase(line, { record, seen, direct, v1base, meta }) {
121
+ const cache = isPlain(seen) && seen.v === 1 && isPlain(seen.data) && seen.w?.reset === meta.reset ? seen : null;
122
+ const at = (held) => (held && typeof held.id === "string" ? line.indexOf(held.id) : -1);
123
+ const held = [[record, at(record)], [cache, cache && direct ? 0 : at(cache)], [v1base, at(v1base)]].filter(([, index]) => index >= 0);
124
+ return held.length ? held.reduce((nearest, next) => (next[1] < nearest[1] ? next : nearest))[0] : null;
125
+ }
126
+
127
+ /**
128
+ * @param {{meta: ?Object, record: ?Object, v1base: ?Object}} stored
129
+ * @param {Object} input
130
+ * @param {Object} input.seed - the data a generation starts from (the served page's)
131
+ * @param {() => string} input.newId
132
+ * @param {number} input.now - milliseconds
133
+ * @param {Function} input.merge3 - the store's three-way merge
134
+ * @param {Function} input.unitMark - the store's mark of a Block editor page
135
+ * @param {?string} [input.marker] - the Start over marker `localStorage` holds now
136
+ * @param {Array<{ends: number, marker: string}>} [input.resets]
137
+ * @param {Array<Object>} [input.chains] - oldest carriers first, the caller's own last
138
+ * @param {?Object} [input.local] - what `localStorage` holds at the draft's key now, parsed
139
+ * @param {?Object} [input.seen] - the latest cache copy the caller saw at the draft's key
140
+ * @param {boolean} [input.direct] - a storage event says `local` replaced exactly that copy
141
+ */
142
+ export function commit(stored, { seed, newId, now, merge3, same, unitMark, marker = null, resets = [], chains = [], local = null, seen = null, direct = false, recovery = null, copies = [] }) {
143
+ const meta = stored.meta ? clone(stored.meta) : emptyMeta(marker);
144
+ // A database the browser cleared counts on from this ClayJS's cache, so the cache never
145
+ // reads as newer than the record.
146
+ if (!stored.meta && Number.isInteger(local?.w?.rev)) meta.rev = local.w.rev;
147
+ let record = stored.record ? clone(stored.record) : null;
148
+ let v1base = stored.v1base ? clone(stored.v1base) : null;
149
+ const out = { conflicts: [], recovered: [], released: [], dropped: [], stale: [], applied: {}, changed: !stored.meta };
150
+
151
+ function recover(copy, supersedes = []) {
152
+ const mark = recoveryMark(copy);
153
+ if (meta.recovery?.includes(mark)) return;
154
+ out.recovered.push(clone(copy));
155
+ const kept = [mark, ...(meta.recovery ?? []).filter((held) => held !== mark && !supersedes.includes(held))].slice(0, RECOVERY_KEEP);
156
+ out.dropped = [...new Set([...out.dropped, ...(meta.recovery ?? [])])].filter((held) => !kept.includes(held));
157
+ meta.recovery = kept;
158
+ out.changed = true;
159
+ return true;
160
+ }
161
+
162
+ const ends = [...resets];
163
+ if (marker && marker !== meta.reset) ends.push({ ends: markerGen(marker) === null ? null : markerGen(marker) - 1, marker });
164
+ for (const reset of ends) {
165
+ if (reset.marker === meta.reset || meta.ended?.includes(reset.marker) || (reset.ends !== null && reset.ends < meta.gen)) continue;
166
+ if (reset.ends !== meta.gen) {
167
+ if (record) recover(record);
168
+ }
169
+ const follows = typeof local?.reset === "string" ? local.reset === reset.marker
170
+ : reset.ends === null || (Array.isArray(local?.ancestry) && local.ancestry.includes("root:" + reset.marker));
171
+ if (isForeign(local) && foreignMark(local) !== meta.foreign && !follows) {
172
+ recover(local);
173
+ meta.foreign = foreignMark(local);
174
+ }
175
+ meta.ended = [...(meta.ended ?? []), meta.reset].slice(-LINEAGE);
176
+ Object.assign(meta, { gen: meta.gen + 1, reset: reset.marker, tabs: {}, unit: null });
177
+ meta.rev++;
178
+ record = null;
179
+ v1base = null;
180
+ out.changed = true;
181
+ }
182
+
183
+ // Nothing stored yet: the full copy in `localStorage` is the first record, whether a
184
+ // 1.23.0 tab wrote it (the move) or it is this ClayJS's cache, of this generation, of a
185
+ // database the browser has since cleared.
186
+ if (!stored.meta && !record && isPlain(local) && local.v === 1 && isPlain(local.data) && (local.w === undefined || local.w.reset === meta.reset)) {
187
+ const { w, ...copy } = local;
188
+ record = clone(copy);
189
+ if (w === undefined) {
190
+ v1base = clone(copy);
191
+ meta.foreign = foreignMark(local);
192
+ meta.rev++;
193
+ }
194
+ }
195
+ if (!stored.meta) meta.unit = unitMark(unitOf(record));
196
+
197
+ // Another writer's record met after the move, taken in once and before any chain.
198
+ if (stored.meta && isForeign(local) && foreignMark(local) !== meta.foreign) {
199
+ const mark = foreignMark(local);
200
+ const line = Array.isArray(local.ancestry) ? local.ancestry : [];
201
+ const base = record ? foreignBase(line, { record, seen, direct, v1base, meta }) : null;
202
+ if (typeof local.reset === "string" && meta.ended?.includes(local.reset)) {
203
+ if (recover(local, line)) out.released.push(clone(local));
204
+ } else if (!record) {
205
+ // Nothing is stored in this generation: its page is the draft, as it is between two
206
+ // tabs of that ClayJS.
207
+ record = clone(local);
208
+ v1base = clone(local);
209
+ meta.unit = unitMark(unitOf(record));
210
+ meta.rev++;
211
+ } else if (base) {
212
+ const merged = merge3(base.data, local.data, record.data);
213
+ // A page it carries counts as changed only when it differs from its base's.
214
+ const page = unitOf(local);
215
+ const paged = Boolean(page) && unitMark(page) !== unitMark(unitOf(base));
216
+ const pageMoved = paged && unitOf(record) && unitMark(unitOf(base)) !== meta.unit;
217
+ if (merged.conflict || pageMoved) out.conflicts.push(clone(record));
218
+ const unit = paged ? page : unitOf(record);
219
+ record = {
220
+ v: 1,
221
+ savedAt: local.savedAt ?? new Date(now).toISOString(),
222
+ id: newId(),
223
+ ancestry: lineage(typeof local.id === "string" ? [local.id] : [], record, meta),
224
+ data: merged.value,
225
+ ...(unit && { live: unit.live, blockeditor: unit.blockeditor }),
226
+ };
227
+ if (paged) meta.unit = unitMark(unit);
228
+ v1base = clone(local);
229
+ meta.rev++;
230
+ } else {
231
+ // The copy it was built on is not held here: it is kept whole, beside the draft. A
232
+ // later copy of the same line replaces the one it descends from.
233
+ if (recover(local, line)) out.released.push(clone(local));
234
+ }
235
+ meta.foreign = mark;
236
+ out.changed = true;
237
+ }
238
+
239
+ for (const chain of chains) {
240
+ if (!isChain(chain)) continue;
241
+ if (chain.reset !== meta.reset) {
242
+ const acked = chain.reset === stored.meta?.reset ? stored.meta.tabs[chain.tab]?.seq ?? 0 : 0;
243
+ if (chain.ops.some((op) => op.seq > Math.max(acked, chain.acked ?? 0))) {
244
+ recover(chain);
245
+ }
246
+ if (!out.stale.includes(chain.tab)) out.stale.push(chain.tab);
247
+ continue;
248
+ }
249
+ const before = meta.tabs[chain.tab]?.seq ?? 0;
250
+ let at = Math.max(before, Number.isInteger(chain.acked) ? chain.acked : 0);
251
+ for (const op of chain.ops) {
252
+ if (op.seq <= at) continue;
253
+ if (op.seq !== at + 1) break;
254
+ const merged = applyKeys(record ? record.data : seed, op.keys ?? {}, merge3);
255
+ const unitMoved = op.unit !== undefined && record && unitOf(record) && op.unit.seen !== meta.unit;
256
+ if (record && (merged.conflict || unitMoved)) out.conflicts.push(clone(record));
257
+ const unit = op.unit !== undefined ? op.unit.to : unitOf(record);
258
+ const id = newId();
259
+ record = {
260
+ v: 1,
261
+ savedAt: op.savedAt ?? new Date(now).toISOString(),
262
+ id,
263
+ ancestry: lineage([], record, meta),
264
+ data: merged.value,
265
+ ...(unit && { live: unit.live, blockeditor: unit.blockeditor }),
266
+ };
267
+ if (op.unit !== undefined) meta.unit = unitMark(unit);
268
+ meta.rev++;
269
+ at = op.seq;
270
+ out.changed = true;
271
+ }
272
+ if (at !== before || meta.tabs[chain.tab]) {
273
+ meta.tabs[chain.tab] = { seq: at, at: now };
274
+ out.changed = true;
275
+ }
276
+ out.applied[chain.tab] = at;
277
+ }
278
+
279
+ if (recovery) {
280
+ const found = copies.find(([mark]) => mark === recovery.mark);
281
+ if (!found) throw new Error("That draft is no longer available. Open the list again.");
282
+ if (recovery.type === "remove") {
283
+ meta.recovery = (meta.recovery ?? []).filter((mark) => mark !== recovery.mark);
284
+ out.dropped.push(recovery.mark);
285
+ out.removedMark = recovery.mark;
286
+ out.changed = true;
287
+ } else if (recovery.type === "restore" && recovery.reset === meta.reset) {
288
+ const chosen = recoveryRecord(found[1], seed);
289
+ if (!chosen) throw new Error("This draft cannot be restored by this version.");
290
+ if (meta.recovery?.includes(recovery.mark)) meta.recovery = [recovery.mark, ...meta.recovery.filter((mark) => mark !== recovery.mark)];
291
+ const alreadyKept = record && copies.some(([mark, copy]) => {
292
+ if (!meta.recovery?.includes(mark)) return false;
293
+ const kept = recoveryRecord(copy, seed);
294
+ return kept && same(record.data, kept.data) && same(unitOf(record), unitOf(kept));
295
+ });
296
+ if (record && !alreadyKept) recover(record);
297
+ record = {
298
+ v: 1, savedAt: new Date(now).toISOString(), id: newId(),
299
+ ancestry: lineage([], record, meta), data: chosen.data,
300
+ ...(chosen.blockeditor && { live: chosen.live, blockeditor: chosen.blockeditor }),
301
+ };
302
+ meta.unit = unitMark(unitOf(record));
303
+ meta.rev++;
304
+ out.changed = true;
305
+ out.restoredCopy = true;
306
+ }
307
+ }
308
+
309
+ for (const [tab, entry] of Object.entries(meta.tabs)) {
310
+ if (now - (entry.at ?? 0) > TAB_TTL_MS) {
311
+ delete meta.tabs[tab];
312
+ out.changed = true;
313
+ }
314
+ }
315
+ return { meta, record, v1base, ...out };
316
+ }
317
+
318
+ const parse = (text) => {
319
+ if (text == null) return null;
320
+ try { return JSON.parse(text); } catch { return null; }
321
+ };
322
+
323
+ /**
324
+ * Run `commit()` in one readwrite transaction and write what it returns. `input` is a
325
+ * function, called inside the transaction once its reads are in, so what it reads from
326
+ * `localStorage` (journal slots, the marker, the cache) is read at that moment.
327
+ * Resolves on the transaction's `complete` event with `commit()`'s result.
328
+ */
329
+ export function transact(drafts, key, input, options) {
330
+ return drafts.change(key, (raw) => {
331
+ const stored = { meta: parse(raw.meta), record: parse(raw.record), v1base: parse(raw.v1base) };
332
+ const rows = raw.rows.map(([name, text]) => [name, parse(text)]);
333
+ const given = input(stored);
334
+ const copies = (raw.recovery ?? []).map(([name, text]) => [name.slice((key + ":recovery:").length), parse(text)]);
335
+ const result = commit(stored, { ...given, copies, chains: [...rows.map(([, chain]) => chain), ...(given.chains ?? [])] });
336
+ const puts = [];
337
+ const deletes = [];
338
+ if (result.changed) {
339
+ puts.push([metaKey(key), JSON.stringify(result.meta)]);
340
+ if (result.record) puts.push([key, JSON.stringify(result.record)]); else if (stored.record) deletes.push(key);
341
+ if (result.v1base) puts.push([key + ":v1base", JSON.stringify(result.v1base)]); else if (stored.v1base) deletes.push(key + ":v1base");
342
+ }
343
+ for (const copy of result.conflicts) puts.push([conflictKey(key, foreignMark(copy)), JSON.stringify(copy)]);
344
+ if (result.conflicts.length) puts.push([conflictKey(key), JSON.stringify(result.conflicts[result.conflicts.length - 1])]);
345
+ for (const copy of result.recovered) puts.push([recoveryKey(key, recoveryMark(copy)), JSON.stringify(copy)]);
346
+ for (const mark of result.dropped) deletes.push(recoveryKey(key, mark));
347
+ for (const [name, chain] of rows) if (settled(chain, result.meta) || (result.removedMark && isChain(chain) && recoveryMark(chain) === result.removedMark)) deletes.push(name);
348
+ return { puts, deletes, result };
349
+ }, options);
350
+ }