@elytracms/studio 0.0.91 → 0.0.93

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 (353) hide show
  1. package/convex/asset-alt-migration.test.ts +127 -0
  2. package/convex/asset-replace.test.ts +168 -0
  3. package/convex/asset-revalidate.test.ts +6 -6
  4. package/convex/assets.ts +41 -5
  5. package/convex/cms.ts +537 -177
  6. package/convex/contract-parity.test.ts +173 -19
  7. package/convex/delivery.ts +26 -13
  8. package/convex/document-revalidate.test.ts +17 -16
  9. package/convex/orphan-hydration.test.ts +116 -0
  10. package/convex/presence-cost.test.ts +89 -0
  11. package/convex/rail-write-parity.test.ts +17 -14
  12. package/convex/referenceMaintenance.ts +15 -15
  13. package/convex/schema.ts +70 -5
  14. package/convex/triggers.ts +21 -8
  15. package/convex/write-limits.ts +1 -1
  16. package/dist/components/ai/ai-chat-panel.js +33 -22
  17. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  18. package/dist/components/ai/proposal-card.js +2 -1
  19. package/dist/components/ai/proposal-card.js.map +1 -1
  20. package/dist/components/ai/translate-document-action.d.ts +8 -2
  21. package/dist/components/ai/translate-document-action.js.map +1 -1
  22. package/dist/components/ai/translate-document-dialog.d.ts +1 -1
  23. package/dist/components/ai/translate-document-dialog.js +14 -8
  24. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  25. package/dist/components/ai/translate-skipped.d.ts +19 -0
  26. package/dist/components/ai/translate-skipped.js +46 -0
  27. package/dist/components/ai/translate-skipped.js.map +1 -0
  28. package/dist/components/app-views/default-modules.d.ts +23 -6
  29. package/dist/components/app-views/default-modules.js +33 -14
  30. package/dist/components/app-views/default-modules.js.map +1 -1
  31. package/dist/components/assets/asset-detail.js +18 -8
  32. package/dist/components/assets/asset-detail.js.map +1 -1
  33. package/dist/components/assets/asset-metadata.d.ts +15 -4
  34. package/dist/components/assets/asset-metadata.js +15 -2
  35. package/dist/components/assets/asset-metadata.js.map +1 -1
  36. package/dist/components/assets/asset-search.js +4 -1
  37. package/dist/components/assets/asset-search.js.map +1 -1
  38. package/dist/components/assets/asset-selection.d.ts +0 -8
  39. package/dist/components/assets/asset-selection.js +4 -23
  40. package/dist/components/assets/asset-selection.js.map +1 -1
  41. package/dist/components/assets/asset-thumb.js +3 -2
  42. package/dist/components/assets/asset-thumb.js.map +1 -1
  43. package/dist/components/assets/assets-collection-view-source.d.ts +3 -3
  44. package/dist/components/assets/assets-collection-view-source.js +9 -5
  45. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  46. package/dist/components/assets/assets-store.d.ts +14 -0
  47. package/dist/components/assets/assets-store.js +23 -1
  48. package/dist/components/assets/assets-store.js.map +1 -1
  49. package/dist/components/assets/replace-file.d.ts +22 -0
  50. package/dist/components/assets/replace-file.js +59 -0
  51. package/dist/components/assets/replace-file.js.map +1 -0
  52. package/dist/components/builder/blocks-model.d.ts +14 -26
  53. package/dist/components/builder/blocks-model.js +35 -41
  54. package/dist/components/builder/blocks-model.js.map +1 -1
  55. package/dist/components/builder/blocks-panel.d.ts +7 -3
  56. package/dist/components/builder/blocks-panel.js +6 -5
  57. package/dist/components/builder/blocks-panel.js.map +1 -1
  58. package/dist/components/builder/builder-store.d.ts +126 -30
  59. package/dist/components/builder/builder-store.js +169 -67
  60. package/dist/components/builder/builder-store.js.map +1 -1
  61. package/dist/components/builder/canvas-body.d.ts +30 -0
  62. package/dist/components/builder/canvas-body.js +38 -0
  63. package/dist/components/builder/canvas-body.js.map +1 -0
  64. package/dist/components/builder/canvas-chrome-css.js +7 -0
  65. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  66. package/dist/components/builder/canvas-dnd.d.ts +7 -7
  67. package/dist/components/builder/canvas-dnd.js +5 -5
  68. package/dist/components/builder/canvas-dnd.js.map +1 -1
  69. package/dist/components/builder/canvas-render-context.d.ts +30 -22
  70. package/dist/components/builder/canvas-render-context.js +44 -44
  71. package/dist/components/builder/canvas-render-context.js.map +1 -1
  72. package/dist/components/builder/canvas-runs.d.ts +45 -0
  73. package/dist/components/builder/canvas-runs.js +51 -0
  74. package/dist/components/builder/canvas-runs.js.map +1 -0
  75. package/dist/components/builder/canvas.d.ts +5 -4
  76. package/dist/components/builder/canvas.js +34 -6
  77. package/dist/components/builder/canvas.js.map +1 -1
  78. package/dist/components/builder/clipboard-actions.d.ts +15 -0
  79. package/dist/components/builder/clipboard-actions.js +50 -13
  80. package/dist/components/builder/clipboard-actions.js.map +1 -1
  81. package/dist/components/builder/composition-field-editor.d.ts +10 -16
  82. package/dist/components/builder/composition-field-editor.js +30 -40
  83. package/dist/components/builder/composition-field-editor.js.map +1 -1
  84. package/dist/components/builder/composition-patches.d.ts +7 -0
  85. package/dist/components/builder/composition-patches.js +256 -0
  86. package/dist/components/builder/composition-patches.js.map +1 -0
  87. package/dist/components/builder/delete-key.d.ts +5 -0
  88. package/dist/components/builder/delete-key.js +4 -0
  89. package/dist/components/builder/delete-key.js.map +1 -1
  90. package/dist/components/builder/insert-context.d.ts +17 -25
  91. package/dist/components/builder/insert-context.js +45 -92
  92. package/dist/components/builder/insert-context.js.map +1 -1
  93. package/dist/components/builder/page-tree.d.ts +3 -3
  94. package/dist/components/builder/page-tree.js +3 -3
  95. package/dist/components/builder/page-tree.js.map +1 -1
  96. package/dist/components/builder/placement-refusal.d.ts +18 -0
  97. package/dist/components/builder/placement-refusal.js +46 -0
  98. package/dist/components/builder/placement-refusal.js.map +1 -0
  99. package/dist/components/builder/property-inspector.js +4 -0
  100. package/dist/components/builder/property-inspector.js.map +1 -1
  101. package/dist/components/builder/reuse-context.js +65 -26
  102. package/dist/components/builder/reuse-context.js.map +1 -1
  103. package/dist/components/builder/reuse.d.ts +79 -8
  104. package/dist/components/builder/reuse.js +71 -23
  105. package/dist/components/builder/reuse.js.map +1 -1
  106. package/dist/components/builder/run-inspector.d.ts +11 -0
  107. package/dist/components/builder/run-inspector.js +33 -0
  108. package/dist/components/builder/run-inspector.js.map +1 -0
  109. package/dist/components/cms/asset-field.d.ts +38 -1
  110. package/dist/components/cms/asset-field.js +182 -58
  111. package/dist/components/cms/asset-field.js.map +1 -1
  112. package/dist/components/cms/asset-picker-core.d.ts +16 -0
  113. package/dist/components/cms/asset-picker-core.js +20 -0
  114. package/dist/components/cms/asset-picker-core.js.map +1 -1
  115. package/dist/components/cms/asset-picker.js +12 -18
  116. package/dist/components/cms/asset-picker.js.map +1 -1
  117. package/dist/components/cms/bulk-actions.d.ts +4 -20
  118. package/dist/components/cms/bulk-actions.js +3 -24
  119. package/dist/components/cms/bulk-actions.js.map +1 -1
  120. package/dist/components/cms/cell-edit.d.ts +5 -5
  121. package/dist/components/cms/cell-edit.js.map +1 -1
  122. package/dist/components/cms/cell.js.map +1 -1
  123. package/dist/components/cms/cms-store.d.ts +87 -45
  124. package/dist/components/cms/cms-store.js +347 -203
  125. package/dist/components/cms/cms-store.js.map +1 -1
  126. package/dist/components/cms/collection-table.js +7 -2
  127. package/dist/components/cms/collection-table.js.map +1 -1
  128. package/dist/components/cms/commit-policy.d.ts +24 -0
  129. package/dist/components/cms/commit-policy.js +37 -0
  130. package/dist/components/cms/commit-policy.js.map +1 -0
  131. package/dist/components/cms/detail-editor.js +83 -137
  132. package/dist/components/cms/detail-editor.js.map +1 -1
  133. package/dist/components/cms/document-diff-view.d.ts +3 -1
  134. package/dist/components/cms/document-diff-view.js +12 -7
  135. package/dist/components/cms/document-diff-view.js.map +1 -1
  136. package/dist/components/cms/draft-commit.d.ts +70 -0
  137. package/dist/components/cms/draft-commit.js +82 -0
  138. package/dist/components/cms/draft-commit.js.map +1 -0
  139. package/dist/components/cms/duplicate-document.d.ts +11 -13
  140. package/dist/components/cms/duplicate-document.js +26 -34
  141. package/dist/components/cms/duplicate-document.js.map +1 -1
  142. package/dist/components/cms/field-anchors.d.ts +7 -4
  143. package/dist/components/cms/field-anchors.js +9 -7
  144. package/dist/components/cms/field-anchors.js.map +1 -1
  145. package/dist/components/cms/field-collaboration.d.ts +47 -0
  146. package/dist/components/cms/field-collaboration.js +87 -0
  147. package/dist/components/cms/field-collaboration.js.map +1 -0
  148. package/dist/components/cms/field-control-lib.d.ts +22 -1
  149. package/dist/components/cms/field-control-lib.js +33 -4
  150. package/dist/components/cms/field-control-lib.js.map +1 -1
  151. package/dist/components/cms/field-control.js +17 -14
  152. package/dist/components/cms/field-control.js.map +1 -1
  153. package/dist/components/cms/gate-validation.d.ts +21 -30
  154. package/dist/components/cms/gate-validation.js +11 -22
  155. package/dist/components/cms/gate-validation.js.map +1 -1
  156. package/dist/components/cms/object-array-field.js +4 -4
  157. package/dist/components/cms/object-array-field.js.map +1 -1
  158. package/dist/components/cms/prop-fields.d.ts +7 -2
  159. package/dist/components/cms/prop-fields.js +13 -6
  160. package/dist/components/cms/prop-fields.js.map +1 -1
  161. package/dist/components/cms/reference-confirm.js +10 -4
  162. package/dist/components/cms/reference-confirm.js.map +1 -1
  163. package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
  164. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  165. package/dist/components/cms/rich-text/insert.d.ts +20 -9
  166. package/dist/components/cms/rich-text/insert.js +25 -17
  167. package/dist/components/cms/rich-text/insert.js.map +1 -1
  168. package/dist/components/cms/use-document-draft.d.ts +96 -0
  169. package/dist/components/cms/use-document-draft.js +283 -0
  170. package/dist/components/cms/use-document-draft.js.map +1 -0
  171. package/dist/components/cms/use-document-versions.js +2 -2
  172. package/dist/components/cms/use-document-versions.js.map +1 -1
  173. package/dist/components/cms/use-live-url.d.ts +6 -12
  174. package/dist/components/cms/use-live-url.js +16 -15
  175. package/dist/components/cms/use-live-url.js.map +1 -1
  176. package/dist/components/cms/use-publish-controller.d.ts +13 -18
  177. package/dist/components/cms/use-publish-controller.js +24 -41
  178. package/dist/components/cms/use-publish-controller.js.map +1 -1
  179. package/dist/components/cms/use-published-perspective.d.ts +2 -3
  180. package/dist/components/cms/use-published-perspective.js +4 -3
  181. package/dist/components/cms/use-published-perspective.js.map +1 -1
  182. package/dist/components/cms/versions-pane.js +8 -1
  183. package/dist/components/cms/versions-pane.js.map +1 -1
  184. package/dist/components/collection-view/cms-collection-view-source.js +24 -7
  185. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  186. package/dist/components/components/transfer-dialog.js +28 -23
  187. package/dist/components/components/transfer-dialog.js.map +1 -1
  188. package/dist/components/components/transfer.d.ts +31 -21
  189. package/dist/components/components/transfer.js +49 -35
  190. package/dist/components/components/transfer.js.map +1 -1
  191. package/dist/components/project-list.js.map +1 -1
  192. package/dist/components/project-session.d.ts +6 -3
  193. package/dist/components/project-session.js +14 -8
  194. package/dist/components/project-session.js.map +1 -1
  195. package/dist/components/project-switcher.js.map +1 -1
  196. package/dist/components/rail-view-source.js +2 -1
  197. package/dist/components/rail-view-source.js.map +1 -1
  198. package/dist/components/section-nav.js +1 -1
  199. package/dist/components/section-nav.js.map +1 -1
  200. package/dist/lib/ai/apply-content.js +56 -116
  201. package/dist/lib/ai/apply-content.js.map +1 -1
  202. package/dist/lib/ai/chat-context.d.ts +8 -0
  203. package/dist/lib/ai/chat-context.js +37 -6
  204. package/dist/lib/ai/chat-context.js.map +1 -1
  205. package/dist/lib/ai/chat-state.d.ts +14 -1
  206. package/dist/lib/ai/chat-state.js.map +1 -1
  207. package/dist/lib/ai/constraints.d.ts +20 -5
  208. package/dist/lib/ai/constraints.js +125 -33
  209. package/dist/lib/ai/constraints.js.map +1 -1
  210. package/dist/lib/ai/fake-provider.d.ts +16 -3
  211. package/dist/lib/ai/fake-provider.js +22 -6
  212. package/dist/lib/ai/fake-provider.js.map +1 -1
  213. package/dist/lib/ai/index.d.ts +2 -0
  214. package/dist/lib/ai/index.js +2 -0
  215. package/dist/lib/ai/index.js.map +1 -1
  216. package/dist/lib/ai/leash.d.ts +34 -0
  217. package/dist/lib/ai/leash.js +28 -0
  218. package/dist/lib/ai/leash.js.map +1 -0
  219. package/dist/lib/ai/provenance.d.ts +1 -1
  220. package/dist/lib/ai/provenance.js.map +1 -1
  221. package/dist/lib/ai/rich-text-input.d.ts +8 -8
  222. package/dist/lib/ai/rich-text-input.js +2 -1
  223. package/dist/lib/ai/rich-text-input.js.map +1 -1
  224. package/dist/lib/ai/rich-text-translate.d.ts +27 -4
  225. package/dist/lib/ai/rich-text-translate.js +28 -10
  226. package/dist/lib/ai/rich-text-translate.js.map +1 -1
  227. package/dist/lib/ai/shape.d.ts +13 -13
  228. package/dist/lib/ai/shape.js +69 -93
  229. package/dist/lib/ai/shape.js.map +1 -1
  230. package/dist/lib/ai/tools.d.ts +36 -41
  231. package/dist/lib/ai/tools.js +85 -87
  232. package/dist/lib/ai/tools.js.map +1 -1
  233. package/dist/lib/ai/translate-document.d.ts +39 -5
  234. package/dist/lib/ai/translate-document.js +160 -51
  235. package/dist/lib/ai/translate-document.js.map +1 -1
  236. package/dist/lib/ai/types.d.ts +34 -23
  237. package/dist/lib/ai/types.js.map +1 -1
  238. package/dist/lib/ai/update-fields.d.ts +21 -12
  239. package/dist/lib/ai/update-fields.js +118 -177
  240. package/dist/lib/ai/update-fields.js.map +1 -1
  241. package/dist/lib/ai/writer-form.d.ts +116 -0
  242. package/dist/lib/ai/writer-form.js +429 -0
  243. package/dist/lib/ai/writer-form.js.map +1 -0
  244. package/dist/lib/ai-current-document.d.ts +11 -11
  245. package/dist/lib/ai-current-document.js +4 -4
  246. package/dist/lib/ai-current-document.js.map +1 -1
  247. package/dist/lib/assets/upload.d.ts +31 -2
  248. package/dist/lib/assets/upload.js +61 -6
  249. package/dist/lib/assets/upload.js.map +1 -1
  250. package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
  251. package/dist/lib/clipboard/os-clipboard.js +8 -4
  252. package/dist/lib/clipboard/os-clipboard.js.map +1 -1
  253. package/dist/lib/clipboard/paste-target.d.ts +18 -9
  254. package/dist/lib/clipboard/paste-target.js +8 -7
  255. package/dist/lib/clipboard/paste-target.js.map +1 -1
  256. package/dist/lib/cms/field-patch.d.ts +15 -0
  257. package/dist/lib/cms/field-patch.js +25 -0
  258. package/dist/lib/cms/field-patch.js.map +1 -0
  259. package/dist/lib/cms/published-documents.d.ts +1 -1
  260. package/dist/lib/cms/published-documents.js +1 -1
  261. package/dist/lib/cms/published-documents.js.map +1 -1
  262. package/dist/lib/cms-views/view-model.d.ts +3 -3
  263. package/dist/lib/collection-view/__fixtures__/document-writes.d.ts +6 -0
  264. package/dist/lib/collection-view/__fixtures__/document-writes.js +23 -0
  265. package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -0
  266. package/dist/lib/collection-view/collection-view-source.d.ts +69 -6
  267. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  268. package/dist/lib/data/collection-content.d.ts +4 -13
  269. package/dist/lib/data/collection-content.js +11 -5
  270. package/dist/lib/data/collection-content.js.map +1 -1
  271. package/dist/lib/dnd/drop-rules.d.ts +25 -31
  272. package/dist/lib/dnd/drop-rules.js +53 -59
  273. package/dist/lib/dnd/drop-rules.js.map +1 -1
  274. package/dist/lib/engine-version.d.ts +1 -1
  275. package/dist/lib/engine-version.js +1 -1
  276. package/dist/lib/engine-version.js.map +1 -1
  277. package/dist/lib/i18n/messages.de.js +72 -15
  278. package/dist/lib/i18n/messages.de.js.map +1 -1
  279. package/dist/lib/i18n/messages.en.d.ts +61 -12
  280. package/dist/lib/i18n/messages.en.js +76 -16
  281. package/dist/lib/i18n/messages.en.js.map +1 -1
  282. package/dist/lib/operations/__fixtures__/write-document.d.ts +30 -0
  283. package/dist/lib/operations/__fixtures__/write-document.js +16 -0
  284. package/dist/lib/operations/__fixtures__/write-document.js.map +1 -0
  285. package/dist/lib/operations/cms-persistence.d.ts +124 -31
  286. package/dist/lib/operations/cms-persistence.js +447 -68
  287. package/dist/lib/operations/cms-persistence.js.map +1 -1
  288. package/dist/lib/operations/config-manifest-lookup.d.ts +35 -0
  289. package/dist/lib/operations/config-manifest-lookup.js +56 -0
  290. package/dist/lib/operations/config-manifest-lookup.js.map +1 -0
  291. package/dist/lib/operations/project-runtime-config.d.ts +39 -0
  292. package/dist/lib/operations/project-runtime-config.js +54 -0
  293. package/dist/lib/operations/project-runtime-config.js.map +1 -0
  294. package/dist/lib/operations/publish-with-revision.d.ts +64 -0
  295. package/dist/lib/operations/publish-with-revision.js +66 -0
  296. package/dist/lib/operations/publish-with-revision.js.map +1 -0
  297. package/dist/lib/operations/runtime-validator.d.ts +32 -0
  298. package/dist/lib/operations/runtime-validator.js +18 -0
  299. package/dist/lib/operations/runtime-validator.js.map +1 -0
  300. package/dist/lib/operations/session-hydration.d.ts +31 -14
  301. package/dist/lib/operations/session-hydration.js +12 -16
  302. package/dist/lib/operations/session-hydration.js.map +1 -1
  303. package/dist/lib/operations/studio-operations.d.ts +59 -2
  304. package/dist/lib/operations/studio-operations.js +76 -14
  305. package/dist/lib/operations/studio-operations.js.map +1 -1
  306. package/dist/lib/persistence/convex-adapter.d.ts +13 -5
  307. package/dist/lib/persistence/convex-adapter.js +189 -74
  308. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  309. package/dist/lib/persistence/convex-client.d.ts +65 -5
  310. package/dist/lib/persistence/convex-client.js +106 -3
  311. package/dist/lib/persistence/convex-client.js.map +1 -1
  312. package/dist/lib/persistence/index.js +17 -18
  313. package/dist/lib/persistence/index.js.map +1 -1
  314. package/dist/lib/persona/labels.js +4 -1
  315. package/dist/lib/persona/labels.js.map +1 -1
  316. package/dist/lib/query/operations-query.d.ts +1 -1
  317. package/dist/lib/query/operations-query.js +1 -1
  318. package/dist/lib/query/operations-query.js.map +1 -1
  319. package/dist/lib/references/impact.d.ts +21 -15
  320. package/dist/lib/references/impact.js +13 -26
  321. package/dist/lib/references/impact.js.map +1 -1
  322. package/dist/lib/references/references-store.d.ts +9 -0
  323. package/dist/lib/references/references-store.js +24 -4
  324. package/dist/lib/references/references-store.js.map +1 -1
  325. package/dist/lib/routes/route-issues.d.ts +6 -6
  326. package/dist/lib/routes/route-issues.js +26 -80
  327. package/dist/lib/routes/route-issues.js.map +1 -1
  328. package/dist/lib/workspace/config-issues.js +2 -1
  329. package/dist/lib/workspace/config-issues.js.map +1 -1
  330. package/dist/lib/workspace/index.d.ts +31 -1
  331. package/dist/lib/workspace/index.js +64 -17
  332. package/dist/lib/workspace/index.js.map +1 -1
  333. package/dist/lib/workspace/switcher-source.d.ts +3 -3
  334. package/dist/lib/workspace/switcher-source.js.map +1 -1
  335. package/dist/lib/workspace/workspace-source.d.ts +2 -2
  336. package/dist/lib/workspace/workspace-source.js +2 -1
  337. package/dist/lib/workspace/workspace-source.js.map +1 -1
  338. package/package.json +3 -3
  339. package/dist/components/cms/cms-composition-validators.d.ts +0 -16
  340. package/dist/components/cms/cms-composition-validators.js +0 -133
  341. package/dist/components/cms/cms-composition-validators.js.map +0 -1
  342. package/dist/components/cms/publish-clean.d.ts +0 -28
  343. package/dist/components/cms/publish-clean.js +0 -38
  344. package/dist/components/cms/publish-clean.js.map +0 -1
  345. package/dist/components/cms/use-save-mode.d.ts +0 -35
  346. package/dist/components/cms/use-save-mode.js +0 -54
  347. package/dist/components/cms/use-save-mode.js.map +0 -1
  348. package/dist/lib/assets/asset-placements.d.ts +0 -28
  349. package/dist/lib/assets/asset-placements.js +0 -83
  350. package/dist/lib/assets/asset-placements.js.map +0 -1
  351. package/dist/lib/cms/document-gate.d.ts +0 -47
  352. package/dist/lib/cms/document-gate.js +0 -61
  353. package/dist/lib/cms/document-gate.js.map +0 -1
@@ -1,100 +1,479 @@
1
- import { selfTypeDocument } from '@elytracms/core/cms-core';
1
+ import { applyDocumentPatches, documentKey, formatValuePath, selfTypeDocument, touchedPaths, valuePathsOverlap, } from '@elytracms/core/cms-core';
2
2
  import { createPersistQueue } from './persist-queue.js';
3
+ /** A patch's identity within one call: op + target path (no two value writes of one path share a run). */
4
+ function targetKey(op, path) {
5
+ return `${op} ${formatValuePath(path)}`;
6
+ }
7
+ function patchTarget(patch) {
8
+ return patch.op === 'insert' ? [...patch.at.parent, { kind: 'node', id: patch.node.id }] : patch.path;
9
+ }
10
+ function conflictId(ref, path) {
11
+ return `${documentKey(ref)}|${formatValuePath(path)}`;
12
+ }
13
+ /** Paths two patches have in common (either touches a prefix of the other's). */
14
+ function patchesOverlap(a, b) {
15
+ return touchedPaths(a).some((p) => touchedPaths(b).some((q) => valuePathsOverlap(p, q)));
16
+ }
17
+ /** A `set`/`unset` on exactly one path — the shape a later edit of the same field supersedes. */
18
+ function isValueWrite(patch) {
19
+ return patch.op === 'set' || patch.op === 'unset';
20
+ }
21
+ function samePath(a, b) {
22
+ return a.length === b.length && valuePathsOverlap(a, b);
23
+ }
3
24
  export function createCmsPersister(options) {
4
- const queue = createPersistQueue();
5
- let baselineDone = false;
6
- /**
7
- * EC-277 → EC-586: canonicalise a document (+ the manifest for composition static
8
- * props) before it is dispatched — THE write canonicaliser (`selfTypeDocument` →
9
- * `FIELD_TYPES`), the same function `documents.upsert` runs on arrival and `writeField`
10
- * runs on the first in-session write. It is idempotent and identity-preserving, so the
11
- * operation's own pass over an already-canonical document is a pointer walk; this call
12
- * is here because the studio's operations client carries no manifest lookup and the
13
- * session does. Drift-tolerant: an undeclared collection passes through unchanged.
14
- */
15
- function selfType(doc) {
25
+ const queue = createPersistQueue((error) => {
26
+ options.onError({
27
+ code: 'unknown',
28
+ message: error instanceof Error ? error.message : String(error),
29
+ });
30
+ });
31
+ const entries = new Map();
32
+ function entryOf(ref) {
33
+ const key = documentKey(ref);
34
+ let entry = entries.get(key);
35
+ if (!entry) {
36
+ entry = {
37
+ ref: { collection: ref.collection, id: ref.id },
38
+ base: undefined,
39
+ provisional: undefined,
40
+ queue: [],
41
+ inFlight: [],
42
+ busy: false,
43
+ conflicts: [],
44
+ held: undefined,
45
+ scheduled: false,
46
+ deleted: false,
47
+ };
48
+ entries.set(key, entry);
49
+ }
50
+ return entry;
51
+ }
52
+ /** EC-586: THE write canonicaliser over a whole document a writer hands us (create/import). */
53
+ function canonical(doc) {
16
54
  const collection = options.getCollections().find((c) => c.id === doc.collection);
17
55
  if (!collection)
18
56
  return doc;
19
57
  return selfTypeDocument(doc, collection, options.lookupManifest ? { lookupManifest: options.lookupManifest } : {});
20
58
  }
21
- async function ensureBaselineNow() {
22
- if (baselineDone)
23
- return true;
24
- const operations = await options.getOperations();
25
- const schema = await operations.schema.get({ projectId: options.projectId });
26
- if (schema.ok) {
27
- // A persisted schema exists (seeded sample project or an earlier
28
- // session) — the session started from the same fixtures, so commands
29
- // can apply directly.
30
- baselineDone = true;
31
- return true;
59
+ /** The session's view: base ⊕ unresolved mine ⊕ in-flight ⊕ queued (the pure merge, no log). */
60
+ function display(entry) {
61
+ const baseDocument = entry.base?.document ?? entry.provisional;
62
+ if (entry.deleted || !baseDocument)
63
+ return undefined;
64
+ const patches = [
65
+ ...entry.conflicts.map((conflict) => conflict.patch),
66
+ ...entry.inFlight.map((queued) => queued.patch),
67
+ ...entry.queue.map((queued) => queued.patch),
68
+ ];
69
+ if (patches.length === 0)
70
+ return baseDocument;
71
+ return applyDocumentPatches({ document: baseDocument, patches, since: [] }).document;
72
+ }
73
+ function emitDocument(entry) {
74
+ if (entry.deleted)
75
+ return;
76
+ const shown = display(entry);
77
+ if (shown)
78
+ options.onDocument?.(entry.ref, shown);
79
+ }
80
+ function emitConflicts(entry) {
81
+ options.onConflicts?.(entry.ref, [...entry.conflicts]);
82
+ }
83
+ /** Adopt a server row as the base (never a lower revision), re-derive the view. */
84
+ function adopt(entry, record) {
85
+ if (entry.base && record.revision < entry.base.revision)
86
+ return;
87
+ entry.base = record;
88
+ options.onRecord?.(record);
89
+ emitDocument(entry);
90
+ }
91
+ /** After a write: apply a subscription row that arrived meanwhile, if newer. */
92
+ function adoptHeld(entry) {
93
+ const held = entry.held;
94
+ entry.held = undefined;
95
+ if (held === undefined)
96
+ return;
97
+ if (held === null) {
98
+ markGone(entry);
99
+ return;
32
100
  }
33
- const saved = await operations.schema.save({
34
- projectId: options.projectId,
35
- collections: [...options.getCollections()],
101
+ if (!entry.base || held.revision > entry.base.revision)
102
+ adopt(entry, held);
103
+ }
104
+ function markGone(entry) {
105
+ if (entry.deleted)
106
+ return;
107
+ entry.deleted = true;
108
+ entry.queue = [];
109
+ options.onDocument?.(entry.ref, undefined);
110
+ }
111
+ function schedule(entry) {
112
+ if (entry.scheduled)
113
+ return;
114
+ entry.scheduled = true;
115
+ queue.push(async () => {
116
+ entry.scheduled = false;
117
+ await drain(entry);
118
+ adoptHeld(entry);
36
119
  });
37
- if (!saved.ok) {
38
- options.onError(saved.error);
39
- return false;
120
+ }
121
+ /** Who changed a refused path — the newest patch-log row in (base, current] touching it. Best-effort. */
122
+ async function resolveActors(entry, sentBase, refused) {
123
+ try {
124
+ const operations = await options.getOperations();
125
+ const log = await operations.documents.listPatches({
126
+ projectId: options.projectId,
127
+ ref: entry.ref,
128
+ afterRevision: sentBase,
129
+ });
130
+ if (!log.ok)
131
+ return;
132
+ let changed = false;
133
+ entry.conflicts = entry.conflicts.map((conflict) => {
134
+ if (conflict.actor !== undefined || !refused.some((r) => r.id === conflict.id))
135
+ return conflict;
136
+ const row = [...log.value]
137
+ .reverse()
138
+ .find((candidate) => candidate.kind === 'replace' ||
139
+ candidate.patches.some((patch) => touchedPaths(patch).some((path) => valuePathsOverlap(path, conflict.path))));
140
+ if (!row?.actor)
141
+ return conflict;
142
+ changed = true;
143
+ return { ...conflict, actor: row.actor };
144
+ });
145
+ if (changed)
146
+ emitConflicts(entry);
40
147
  }
41
- for (const document of options.getDocuments()) {
42
- const result = await operations.documents.upsert({
148
+ catch {
149
+ // The actor is decoration on a conflict the user can already resolve.
150
+ }
151
+ }
152
+ /** Send the queue, one run of equal base at a time, until it is empty or a write fails. */
153
+ async function drain(entry) {
154
+ while (entry.queue.length > 0 && !entry.deleted) {
155
+ const operations = await options.getOperations();
156
+ if (!entry.base) {
157
+ // Untracked (a document the session never hydrated a revision for): read it.
158
+ // The user's view of it is the session's, so this is the narrowest window left.
159
+ const read = await operations.documents.get({ projectId: options.projectId, ref: entry.ref });
160
+ if (!read.ok) {
161
+ options.onError(read.error);
162
+ return;
163
+ }
164
+ entry.base = read.value;
165
+ }
166
+ const resolveBase = (queued) => queued.base ?? entry.base.revision;
167
+ const first = resolveBase(entry.queue[0]);
168
+ let end = 1;
169
+ while (end < entry.queue.length && resolveBase(entry.queue[end]) === first)
170
+ end += 1;
171
+ const run = entry.queue.splice(0, end);
172
+ entry.inFlight = run;
173
+ const result = await operations.documents.patch({
43
174
  projectId: options.projectId,
44
- document: selfType(document),
175
+ ref: entry.ref,
176
+ baseRevision: first,
177
+ patches: run.map((queued) => queued.patch),
45
178
  });
46
- if (!result.ok) {
47
- options.onError(result.error);
48
- return false;
179
+ entry.inFlight = [];
180
+ if (result.ok) {
181
+ rebaseCovered(entry, run, result.value.revision);
182
+ adopt(entry, result.value);
183
+ adoptHeld(entry);
184
+ settleAll(run, { status: 'applied', record: result.value });
185
+ continue;
49
186
  }
187
+ const error = result.error;
188
+ if (error.code === 'conflict' && error.refused && error.current) {
189
+ const current = error.current;
190
+ // Refusals are matched to the run by op + target path (the operation re-parses
191
+ // and canonicalises the patches, so object identity does not survive the call).
192
+ const refusedByKey = new Map(error.refused.map((refused) => [targetKey(refused.patch.op, refused.path), refused]));
193
+ const applied = [];
194
+ const conflicts = [];
195
+ for (const queued of run) {
196
+ const refused = refusedByKey.get(targetKey(queued.patch.op, patchTarget(queued.patch)));
197
+ if (!refused) {
198
+ applied.push(queued);
199
+ continue;
200
+ }
201
+ // MINE is what the user wrote (the queued patch), not the canonicalised echo.
202
+ const conflict = upsertConflict(entry, { ...refused, patch: queued.patch }, current.revision);
203
+ conflicts.push(conflict);
204
+ for (const settle of queued.settle)
205
+ settle({ status: 'refused', conflicts: [conflict] });
206
+ }
207
+ rebaseCovered(entry, applied, current.revision);
208
+ adopt(entry, current);
209
+ adoptHeld(entry);
210
+ emitConflicts(entry);
211
+ settleAll(applied, { status: 'applied', record: current });
212
+ void resolveActors(entry, first, conflicts);
213
+ continue;
214
+ }
215
+ if (error.code === 'conflict' && 'current' in error && error.current === null) {
216
+ // The document was deleted since — nothing to write into.
217
+ markGone(entry);
218
+ options.onError(error);
219
+ settleAll(run, { status: 'failed', error });
220
+ return;
221
+ }
222
+ // A transport / authorization failure: the patches stay local (shown, unsent) and
223
+ // the error is visible; the next commit or forced flush retries them. The commits
224
+ // waiting on them learn of the failure now.
225
+ settleAll(run, { status: 'failed', error });
226
+ entry.queue.unshift(...run.map((queued) => ({ ...queued, settle: [] })));
227
+ adoptHeld(entry);
228
+ options.onError(error);
229
+ return;
230
+ }
231
+ }
232
+ function settleAll(queued, outcome) {
233
+ for (const item of queued)
234
+ for (const settle of item.settle)
235
+ settle(outcome);
236
+ }
237
+ /** Queued patches on a path our own write just covered move their base to that write's revision. */
238
+ function rebaseCovered(entry, written, revision) {
239
+ if (written.length === 0)
240
+ return;
241
+ for (const queued of entry.queue) {
242
+ if (queued.base !== undefined && queued.base >= revision)
243
+ continue;
244
+ if (written.some((done) => patchesOverlap(done.patch, queued.patch)))
245
+ queued.base = revision;
50
246
  }
51
- baselineDone = true;
52
- return true;
53
247
  }
54
- function command(run) {
248
+ function upsertConflict(entry, refused, revision) {
249
+ const conflict = {
250
+ id: conflictId(entry.ref, refused.path),
251
+ ref: entry.ref,
252
+ patch: refused.patch,
253
+ path: refused.path,
254
+ theirs: refused.current,
255
+ reason: refused.reason,
256
+ revision,
257
+ };
258
+ entry.conflicts = [...entry.conflicts.filter((c) => c.id !== conflict.id), conflict];
259
+ return conflict;
260
+ }
261
+ function command(entry, run) {
55
262
  queue.push(async () => {
56
- // A failed baseline already surfaced through onError — running the
57
- // command against missing records would only add a confusing follow-up.
58
- if (!(await ensureBaselineNow()))
59
- return;
60
263
  const operations = await options.getOperations();
61
- const result = await run(operations);
62
- if (!result.ok)
63
- options.onError(result.error);
264
+ entry.busy = true;
265
+ try {
266
+ const result = await run(operations);
267
+ if (!result.ok)
268
+ options.onError(result.error);
269
+ }
270
+ finally {
271
+ entry.busy = false;
272
+ adoptHeld(entry);
273
+ }
64
274
  });
65
275
  }
66
276
  return {
67
- upsertDocument(doc) {
68
- const document = selfType(doc);
69
- command(async (operations) => {
70
- const result = await operations.documents.upsert({ projectId: options.projectId, document });
71
- // EC-449: hand the authoritative saved record back to the session so it
72
- // adopts the server-computed `hasUnpublishedChanges` verdict (the queue
73
- // swallows successful results otherwise).
74
- if (result.ok)
75
- options.onDocumentSaved?.(result.value);
76
- return result;
77
- });
277
+ track(row) {
278
+ const entry = entryOf(row.document);
279
+ if (!entry.base || row.revision > entry.base.revision)
280
+ entry.base = row;
78
281
  },
79
- deleteDocument(ref) {
80
- command((operations) => operations.documents.delete({ projectId: options.projectId, ref }));
282
+ revisionOf: (ref) => entries.get(documentKey(ref))?.base?.revision,
283
+ documentOf: (ref) => {
284
+ const entry = entries.get(documentKey(ref));
285
+ return entry ? display(entry) : undefined;
81
286
  },
82
- publishDocument(ref) {
83
- command((operations) => operations.documents.publish({ projectId: options.projectId, ref }));
287
+ adoptRecord(record) {
288
+ const entry = entryOf(record.document);
289
+ if (entry.deleted)
290
+ return;
291
+ if (!entry.base || record.revision > entry.base.revision)
292
+ adopt(entry, record);
84
293
  },
85
- unpublishDocument(ref) {
86
- command((operations) => operations.documents.unpublish({ projectId: options.projectId, ref }));
294
+ commit(ref, patches, view, bases) {
295
+ const entry = entryOf(ref);
296
+ if (patches.length === 0 || entry.deleted) {
297
+ return Promise.resolve({
298
+ status: 'failed',
299
+ error: { code: 'not-found', message: 'The document is gone.', entity: 'cms-document', key: documentKey(ref) },
300
+ });
301
+ }
302
+ if (!entry.base && !entry.provisional && view)
303
+ entry.provisional = view;
304
+ let conflictsChanged = false;
305
+ const waits = [];
306
+ for (const [index, patch] of patches.entries()) {
307
+ const madeAt = bases?.[index] ?? entry.base?.revision;
308
+ let settle;
309
+ waits.push(new Promise((resolve) => (settle = resolve)));
310
+ // A field with an unresolved conflict: the new value replaces MINE, unsent —
311
+ // the user decides (keep mine sends it), never an implicit overwrite of theirs.
312
+ const open = entry.conflicts.find((conflict) => patchesOverlap(conflict.patch, patch));
313
+ if (open && isValueWrite(patch) && isValueWrite(open.patch) && samePath(open.patch.path, patch.path)) {
314
+ const replaced = { ...open, patch };
315
+ entry.conflicts = entry.conflicts.map((c) => (c === open ? replaced : c));
316
+ conflictsChanged = true;
317
+ settle({ status: 'refused', conflicts: [replaced] });
318
+ continue;
319
+ }
320
+ // A later value write of the same field supersedes an unsent one (keeping the
321
+ // EARLIER base — the user never saw a foreign value of a field they were typing in).
322
+ if (isValueWrite(patch)) {
323
+ const pending = entry.queue.findIndex((queued) => isValueWrite(queued.patch) && samePath(queued.patch.path, patch.path));
324
+ if (pending >= 0) {
325
+ const superseded = entry.queue[pending];
326
+ entry.queue[pending] = { patch, base: superseded.base, settle: [...superseded.settle, settle] };
327
+ continue;
328
+ }
329
+ }
330
+ entry.queue.push({ patch, base: madeAt, settle: [settle] });
331
+ }
332
+ emitDocument(entry);
333
+ if (conflictsChanged)
334
+ emitConflicts(entry);
335
+ schedule(entry);
336
+ // The commit's outcome: failed if any of its patches failed, refused if any was.
337
+ return Promise.all(waits).then((outcomes) => {
338
+ const failed = outcomes.find((o) => o.status === 'failed');
339
+ if (failed)
340
+ return failed;
341
+ const refused = outcomes.flatMap((o) => (o.status === 'refused' ? o.conflicts : []));
342
+ if (refused.length > 0)
343
+ return { status: 'refused', conflicts: refused };
344
+ return outcomes[outcomes.length - 1];
345
+ });
87
346
  },
88
- upsertCollection(collection) {
89
- command((operations) => operations.schema.upsertCollection({ projectId: options.projectId, collection }));
347
+ create(document) {
348
+ const doc = canonical(document);
349
+ const entry = entryOf(doc);
350
+ entry.deleted = false;
351
+ // Optimistic: the session shows the document at once; the base is the created row.
352
+ entry.provisional = doc;
353
+ options.onDocument?.(entry.ref, doc);
354
+ queue.push(async () => {
355
+ const operations = await options.getOperations();
356
+ entry.busy = true;
357
+ try {
358
+ const result = await operations.documents.replace({
359
+ projectId: options.projectId,
360
+ document: doc,
361
+ precondition: { ifAbsent: true },
362
+ });
363
+ if (result.ok)
364
+ adopt(entry, result.value);
365
+ else
366
+ options.onError(result.error);
367
+ }
368
+ finally {
369
+ entry.busy = false;
370
+ adoptHeld(entry);
371
+ }
372
+ if (entry.queue.length > 0)
373
+ await drain(entry);
374
+ });
90
375
  },
91
- async ensureBaseline() {
376
+ replace(document) {
377
+ const doc = canonical(document);
378
+ const entry = entryOf(doc);
379
+ if (!entry.base)
380
+ entry.provisional = doc;
381
+ options.onDocument?.(entry.ref, doc);
92
382
  queue.push(async () => {
93
- await ensureBaselineNow();
383
+ await drain(entry);
384
+ const operations = await options.getOperations();
385
+ entry.busy = true;
386
+ try {
387
+ const result = await operations.documents.replace({
388
+ projectId: options.projectId,
389
+ document: doc,
390
+ precondition: entry.base ? { revision: entry.base.revision } : { ifAbsent: true },
391
+ });
392
+ if (result.ok)
393
+ adopt(entry, result.value);
394
+ else {
395
+ if (result.error.code === 'conflict' && 'current' in result.error && result.error.current) {
396
+ adopt(entry, result.error.current);
397
+ }
398
+ options.onError(result.error);
399
+ }
400
+ }
401
+ finally {
402
+ entry.busy = false;
403
+ adoptHeld(entry);
404
+ }
94
405
  });
406
+ },
407
+ deleteDocument(ref) {
408
+ const entry = entryOf(ref);
409
+ entry.queue = [];
410
+ entry.conflicts = [];
411
+ entry.deleted = true;
412
+ emitConflicts(entry);
413
+ command(entry, (operations) => operations.documents.delete({ projectId: options.projectId, ref }));
414
+ },
415
+ unpublishDocument(ref) {
416
+ const entry = entryOf(ref);
417
+ queue.push(async () => {
418
+ await drain(entry);
419
+ const operations = await options.getOperations();
420
+ entry.busy = true;
421
+ try {
422
+ const result = await operations.documents.unpublish({ projectId: options.projectId, ref });
423
+ if (result.ok)
424
+ adopt(entry, result.value);
425
+ else
426
+ options.onError(result.error);
427
+ }
428
+ finally {
429
+ entry.busy = false;
430
+ adoptHeld(entry);
431
+ }
432
+ });
433
+ },
434
+ adoptSnapshot(ref, record) {
435
+ const entry = entryOf(ref);
436
+ if (entry.inFlight.length > 0 || entry.busy || entry.scheduled) {
437
+ // Hold it: the write's own result decides first; the newer of the two wins.
438
+ if (record === undefined)
439
+ entry.held = null;
440
+ else if (!entry.held || record.revision > entry.held.revision)
441
+ entry.held = record;
442
+ return;
443
+ }
444
+ if (record === undefined) {
445
+ if (entry.base)
446
+ markGone(entry);
447
+ return;
448
+ }
449
+ if (!entry.base || record.revision > entry.base.revision)
450
+ adopt(entry, record);
451
+ },
452
+ conflicts: (ref) => entries.get(documentKey(ref))?.conflicts ?? [],
453
+ resolveConflict(ref, id, choice) {
454
+ const entry = entries.get(documentKey(ref));
455
+ if (!entry)
456
+ return;
457
+ const conflict = entry.conflicts.find((c) => c.id === id);
458
+ if (!conflict)
459
+ return;
460
+ entry.conflicts = entry.conflicts.filter((c) => c !== conflict);
461
+ if (choice === 'mine') {
462
+ // Keep mine: re-send at the CURRENT base — a visible, explicit second write.
463
+ entry.queue.push({ patch: conflict.patch, base: entry.base?.revision, settle: [] });
464
+ schedule(entry);
465
+ }
466
+ emitDocument(entry);
467
+ emitConflicts(entry);
468
+ },
469
+ async flush(ref) {
470
+ if (ref) {
471
+ const entry = entries.get(documentKey(ref));
472
+ if (entry && entry.queue.length > 0)
473
+ schedule(entry);
474
+ }
95
475
  await queue.flush();
96
476
  },
97
- flush: () => queue.flush(),
98
477
  };
99
478
  }
100
479
  //# sourceMappingURL=cms-persistence.js.map