@dailies-studio/shot-editor 0.1.0 → 0.2.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 (432) hide show
  1. package/dist/.vite/manifest.json +382 -0
  2. package/dist/assets/AgentPanel-CQejLbiX.js +2 -0
  3. package/dist/assets/AgentPanel-DJEphah0.css +1 -0
  4. package/dist/assets/AgentQuestionDialog-BqkD0kXF.js +1 -0
  5. package/dist/assets/CodeStudio-Dv4ihHpz.js +23 -0
  6. package/dist/assets/CommentsPanel-B4yhhT1T.js +1 -0
  7. package/dist/assets/ComponentLibraryRail-Ch_dsAru.js +1 -0
  8. package/dist/assets/ComponentStage-BRhRotyH.js +1 -0
  9. package/dist/assets/CurvePane-BjFVabKX.css +1 -0
  10. package/dist/assets/CurvePane-R22WzllJ.js +1 -0
  11. package/dist/assets/ExportDialog-CWLQWTmG.js +2 -0
  12. package/dist/assets/FontPicker-DWKaYL7F.js +1 -0
  13. package/dist/assets/FontPicker-v0IOIArM.css +1 -0
  14. package/dist/assets/GenerateDrawer-Bh3SqDKt.js +2 -0
  15. package/dist/assets/InsertDrawer-Ce1fod1L.js +1 -0
  16. package/dist/assets/LibraryBrowser-D9sEGz5w.js +2 -0
  17. package/dist/assets/MakeItYoursSection-CMO4319_.js +2 -0
  18. package/dist/assets/MaterialGraphCanvas-BFzl4wWR.js +23 -0
  19. package/dist/assets/MaterialGraphCanvas-BIUD59ik.css +1 -0
  20. package/dist/assets/MaterialWorkspace-CqTWqaRA.js +14 -0
  21. package/dist/assets/PreferencesDialog-CBf9_OoD.js +1 -0
  22. package/dist/assets/PresetsSection-CUEaoCVU.js +1 -0
  23. package/dist/assets/SoundHistory-BeS8EW1h.js +1 -0
  24. package/dist/assets/SoundMode-BmcZru3i.js +3 -0
  25. package/dist/assets/StockSounds-DdAUGRYK.js +1 -0
  26. package/dist/assets/TemplateFieldSection-CaeXpLFo.js +1 -0
  27. package/dist/assets/VersionHistoryPanel-CxG6XhNP.js +1 -0
  28. package/dist/assets/board-B88EPuLO.css +1 -0
  29. package/dist/assets/board-CL-he7SL.js +3 -0
  30. package/dist/assets/components-0oOSPN0l.js +1 -0
  31. package/dist/assets/googleFontRanges-gp32wJMo.js +1 -0
  32. package/dist/assets/googleFonts-BNon6M7X.js +1 -0
  33. package/dist/assets/iconsAndAliases-DL7gXP5j.js +1863 -0
  34. package/dist/assets/index-4F5NQmI8.css +1 -0
  35. package/dist/assets/index-BGHQGV5z.js +117 -0
  36. package/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  37. package/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  38. package/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  39. package/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  40. package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  41. package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  42. package/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  43. package/dist/assets/libraryModel-CpIwfFha.js +1 -0
  44. package/dist/assets/lucide-6iK64tTB.js +9319 -0
  45. package/dist/assets/motionStamp-CH-Hew4E.js +1 -0
  46. package/dist/assets/takeMeasure-Tdg0TxEi.js +1 -0
  47. package/dist/assets/webmcp-BsB9gxWC.js +2 -0
  48. package/dist/index.html +34 -3
  49. package/package.json +12 -9
  50. package/src/addFamilies.ts +66 -0
  51. package/src/api.ts +1631 -55
  52. package/src/app/App.tsx +977 -198
  53. package/src/app/lazyPanel.tsx +137 -0
  54. package/src/app/lazySurfaces.tsx +203 -0
  55. package/src/audio/index.tsx +41 -10
  56. package/src/audio/meter.ts +485 -0
  57. package/src/audio/meterMath.ts +273 -0
  58. package/src/audio/meterStore.ts +155 -0
  59. package/src/audio/offlineMix.ts +289 -0
  60. package/src/audio/preview.ts +928 -118
  61. package/src/bridge.ts +15 -5
  62. package/src/bridgeTypes.ts +1611 -36
  63. package/src/channels.ts +17 -32
  64. package/src/fonts/googleFonts.ts +4 -0
  65. package/src/hostApi.ts +174 -0
  66. package/src/icons.tsx +770 -0
  67. package/src/layerWords.ts +61 -0
  68. package/src/main.tsx +56 -4
  69. package/src/panels/ResizeHandle.tsx +33 -43
  70. package/src/panels/agent/AgentDots.tsx +36 -0
  71. package/src/panels/agent/AgentPanel.tsx +119 -43
  72. package/src/panels/agent/AgentQuestionDialog.tsx +202 -0
  73. package/src/panels/agent/agent.css +113 -97
  74. package/src/panels/agent/agentGate.ts +35 -0
  75. package/src/panels/code/CodeDefaultsPanel.tsx +345 -0
  76. package/src/panels/code/CodeStudio.tsx +373 -0
  77. package/src/panels/code/FileTree.tsx +265 -0
  78. package/src/panels/code/codeBufferGuard.ts +52 -0
  79. package/src/panels/code/codeParts.tsx +321 -0
  80. package/src/panels/comments/CommentComposer.tsx +239 -0
  81. package/src/panels/comments/CommentThread.tsx +346 -0
  82. package/src/panels/comments/CommentsPanel.tsx +336 -0
  83. package/src/panels/comments/InboxButton.tsx +131 -0
  84. package/src/panels/comments/MentionMenu.tsx +98 -0
  85. package/src/panels/comments/parts.tsx +171 -0
  86. package/src/panels/export/ExportDialog.tsx +919 -96
  87. package/src/panels/export/ExportScrim.tsx +57 -0
  88. package/src/panels/files/AssetsTab.tsx +656 -0
  89. package/src/panels/files/FilePanel.tsx +1466 -216
  90. package/src/panels/files/LayerMarks.tsx +40 -0
  91. package/src/panels/files/LeftColumn.tsx +37 -0
  92. package/src/panels/files/McpSection.tsx +690 -47
  93. package/src/panels/files/MissingAssetsNotice.tsx +60 -0
  94. package/src/panels/files/assetDrag.ts +79 -0
  95. package/src/panels/files/resourceParts.tsx +255 -0
  96. package/src/panels/fonts/FontPicker.tsx +762 -0
  97. package/src/panels/fonts/faceCache.ts +207 -0
  98. package/src/panels/fonts/fontPicker.css +271 -0
  99. package/src/panels/fonts/pickerModel.ts +265 -0
  100. package/src/panels/fonts/pickerRequest.ts +46 -0
  101. package/src/panels/header/AccountAvatar.tsx +76 -0
  102. package/src/panels/header/AgentChip.tsx +77 -0
  103. package/src/panels/header/AppMenu.tsx +213 -0
  104. package/src/panels/header/Header.tsx +564 -84
  105. package/src/panels/header/HistoryControls.tsx +31 -5
  106. package/src/panels/header/PresenceStack.tsx +329 -0
  107. package/src/panels/header/ProjectTitle.tsx +232 -0
  108. package/src/panels/header/ShareButton.tsx +59 -0
  109. package/src/panels/header/StatusBadges.tsx +358 -27
  110. package/src/panels/header/SyncDeletedDialog.tsx +88 -0
  111. package/src/panels/header/ZoomControls.tsx +46 -15
  112. package/src/panels/header/agentHandoff.ts +52 -0
  113. package/src/panels/header/leave.ts +26 -0
  114. package/src/panels/help/HelpSection.tsx +189 -163
  115. package/src/panels/insert/InsertDrawer.tsx +723 -0
  116. package/src/panels/insert/iconSet.ts +109 -0
  117. package/src/panels/inspector/ExposeDot.tsx +117 -0
  118. package/src/panels/inspector/FramingRows.tsx +314 -44
  119. package/src/panels/inspector/InspectorFooter.tsx +91 -0
  120. package/src/panels/inspector/InspectorHead.tsx +425 -0
  121. package/src/panels/inspector/InspectorPanel.tsx +408 -124
  122. package/src/panels/inspector/KeyframeDot.tsx +373 -0
  123. package/src/panels/inspector/KeyframeSection.tsx +257 -0
  124. package/src/panels/inspector/MarkerSection.tsx +55 -11
  125. package/src/panels/inspector/ParamRow.tsx +84 -53
  126. package/src/panels/inspector/RightColumn.tsx +152 -0
  127. package/src/panels/inspector/TimeSection.tsx +866 -0
  128. package/src/panels/inspector/animate/EaseControl.tsx +244 -0
  129. package/src/panels/inspector/animate/PresetsSection.tsx +135 -0
  130. package/src/panels/inspector/animate/ShotSection.tsx +562 -48
  131. package/src/panels/inspector/design/AlignRow.tsx +95 -31
  132. package/src/panels/inspector/design/AppearanceSection.tsx +118 -9
  133. package/src/panels/inspector/design/ClonerSection.tsx +323 -62
  134. package/src/panels/inspector/design/ComponentSection.tsx +50 -43
  135. package/src/panels/inspector/design/CornerSection.tsx +45 -14
  136. package/src/panels/inspector/design/DefinitionProps.tsx +315 -0
  137. package/src/panels/inspector/design/DefinitionSection.tsx +136 -0
  138. package/src/panels/inspector/design/DeviceSection.tsx +210 -0
  139. package/src/panels/inspector/design/FillSection.tsx +83 -51
  140. package/src/panels/inspector/design/FontField.tsx +196 -0
  141. package/src/panels/inspector/design/HtmlSection.tsx +23 -12
  142. package/src/panels/inspector/design/InstanceSection.tsx +373 -154
  143. package/src/panels/inspector/design/MaskSection.tsx +84 -0
  144. package/src/panels/inspector/design/MaterialSection.tsx +177 -0
  145. package/src/panels/inspector/design/MediaSection.tsx +273 -120
  146. package/src/panels/inspector/design/MeshSection.tsx +910 -82
  147. package/src/panels/inspector/design/PaintRows.tsx +550 -0
  148. package/src/panels/inspector/design/PlateStyleSection.tsx +177 -0
  149. package/src/panels/inspector/design/ShadowSection.tsx +221 -48
  150. package/src/panels/inspector/design/SplineFollowSection.tsx +235 -0
  151. package/src/panels/inspector/design/StrokeSection.tsx +228 -43
  152. package/src/panels/inspector/design/TemplateFieldSection.tsx +57 -0
  153. package/src/panels/inspector/design/TransformSection.tsx +244 -50
  154. package/src/panels/inspector/design/TypographySection.tsx +679 -110
  155. package/src/panels/inspector/layout.ts +295 -0
  156. package/src/panels/inspector/propValue.ts +199 -0
  157. package/src/panels/inspector/scene/CameraSection.tsx +232 -22
  158. package/src/panels/inspector/scene/EffectsSection.tsx +722 -71
  159. package/src/panels/inspector/scene/MakeItYoursSection.tsx +180 -0
  160. package/src/panels/inspector/scene/SceneDocSection.tsx +191 -0
  161. package/src/panels/inspector/scene/SceneSection.tsx +219 -44
  162. package/src/panels/inspector/scene/TransitionSection.tsx +523 -72
  163. package/src/panels/inspector/storyboard/StoryboardReferenceSection.tsx +593 -0
  164. package/src/panels/inspector/storyboard/VideoStill.tsx +120 -0
  165. package/src/panels/inspector/timeModel.ts +190 -0
  166. package/src/panels/inspector/useDocWrite.ts +438 -146
  167. package/src/panels/library/LibraryBrowser.tsx +435 -0
  168. package/src/panels/library/LibraryGrid.tsx +190 -0
  169. package/src/panels/library/LibraryPreviewPanel.tsx +349 -0
  170. package/src/panels/library/LibraryRail.tsx +84 -0
  171. package/src/panels/library/LibrarySoundPanel.tsx +293 -0
  172. package/src/panels/library/SoundHistory.tsx +245 -0
  173. package/src/panels/library/StockSounds.tsx +390 -0
  174. package/src/panels/library/libraryModel.ts +147 -0
  175. package/src/panels/library/stockSearch.ts +77 -0
  176. package/src/panels/maskMarks.tsx +117 -0
  177. package/src/panels/material/MaterialCommentNode.tsx +54 -0
  178. package/src/panels/material/MaterialGraphCanvas.tsx +861 -0
  179. package/src/panels/material/MaterialGraphMenu.tsx +45 -0
  180. package/src/panels/material/MaterialGroupNode.tsx +56 -0
  181. package/src/panels/material/MaterialLibrary.tsx +253 -0
  182. package/src/panels/material/MaterialNode.tsx +453 -0
  183. package/src/panels/material/MaterialNodePalette.tsx +349 -0
  184. package/src/panels/material/MaterialParameterControls.tsx +525 -0
  185. package/src/panels/material/MaterialRerouteNode.tsx +42 -0
  186. package/src/panels/material/MaterialWorkspace.tsx +501 -0
  187. package/src/panels/material/NodePreview.tsx +70 -0
  188. package/src/panels/material/flowModel.ts +441 -0
  189. package/src/panels/material/parameterValues.ts +231 -0
  190. package/src/panels/material/previewScheduler.ts +235 -0
  191. package/src/panels/material/reactFlow.css +6 -0
  192. package/src/panels/material/socketGlyph.ts +85 -0
  193. package/src/panels/material/viewportMemory.ts +58 -0
  194. package/src/panels/preferences/PreferencesDialog.tsx +215 -0
  195. package/src/panels/share/ShareComponentDialog.tsx +247 -0
  196. package/src/panels/stage/StageMenu.tsx +35 -5
  197. package/src/panels/stage/StagePanel.tsx +338 -23
  198. package/src/panels/stage/cellSlots.tsx +70 -0
  199. package/src/panels/storyboard/BoardCanvas.tsx +739 -0
  200. package/src/panels/storyboard/BoardCursors.tsx +158 -0
  201. package/src/panels/storyboard/BoardDropDialog.tsx +161 -0
  202. package/src/panels/storyboard/BoardPlan.tsx +42 -0
  203. package/src/panels/storyboard/ComponentSheet.tsx +187 -0
  204. package/src/panels/storyboard/ComponentStage.tsx +235 -0
  205. package/src/panels/storyboard/MiniSketch.tsx +173 -0
  206. package/src/panels/storyboard/PartPicker.tsx +64 -0
  207. package/src/panels/storyboard/PlanPanel.tsx +617 -0
  208. package/src/panels/storyboard/ReferencePreview.tsx +217 -0
  209. package/src/panels/storyboard/ShotBrief.tsx +360 -0
  210. package/src/panels/storyboard/ShotCard.tsx +251 -0
  211. package/src/panels/storyboard/StagePreview.tsx +348 -0
  212. package/src/panels/storyboard/StillThumbs.tsx +162 -0
  213. package/src/panels/storyboard/StoryboardView.tsx +79 -0
  214. package/src/panels/storyboard/board.css +1065 -0
  215. package/src/panels/storyboard/board.ts +12 -0
  216. package/src/panels/storyboard/boardClipboard.ts +111 -0
  217. package/src/panels/storyboard/boardOrder.ts +68 -0
  218. package/src/panels/storyboard/boardRows.tsx +425 -0
  219. package/src/panels/storyboard/liveCap.ts +90 -0
  220. package/src/panels/storyboard/plan.css +295 -0
  221. package/src/panels/storyboard/posterCache.ts +133 -0
  222. package/src/panels/storyboard/shotboard/ComponentPicker.tsx +373 -0
  223. package/src/panels/storyboard/shotboard/ImagePicker.tsx +145 -0
  224. package/src/panels/storyboard/shotboard/ShotBoard.tsx +292 -0
  225. package/src/panels/storyboard/shotboard/SketchCanvas.tsx +443 -0
  226. package/src/panels/storyboard/shotboard/SketchInspector.tsx +471 -0
  227. package/src/panels/storyboard/shotboard/SketchItem.tsx +240 -0
  228. package/src/panels/storyboard/shotboard/SketchLayers.tsx +130 -0
  229. package/src/panels/storyboard/shotboard/SketchPlay.tsx +108 -0
  230. package/src/panels/storyboard/shotboard/SketchStrip.tsx +178 -0
  231. package/src/panels/storyboard/shotboard/SketchToolFooter.tsx +99 -0
  232. package/src/panels/storyboard/shotboard/sketch.css +1392 -0
  233. package/src/panels/storyboard/shotboard/sketchModel.ts +219 -0
  234. package/src/panels/storyboard/storyboardDrop.ts +164 -0
  235. package/src/panels/studio/ComponentLibraryRail.tsx +346 -0
  236. package/src/panels/studio/ComponentSidePanel.tsx +355 -0
  237. package/src/panels/studio/ComponentStudio.tsx +243 -0
  238. package/src/panels/studio/ComponentsHome.tsx +318 -0
  239. package/src/panels/studio/ComponentsPreview.tsx +179 -0
  240. package/src/panels/studio/EveryUse.tsx +244 -0
  241. package/src/panels/studio/ThisUsePanel.tsx +679 -0
  242. package/src/panels/studio/componentParts.tsx +341 -0
  243. package/src/panels/studio/components.ts +6 -0
  244. package/src/panels/studio/newCodeComponent.ts +66 -0
  245. package/src/panels/timeline/AudioFxButton.tsx +290 -0
  246. package/src/panels/timeline/AudioLane.tsx +674 -85
  247. package/src/panels/timeline/BlockRow.tsx +154 -85
  248. package/src/panels/timeline/ChannelRow.tsx +378 -129
  249. package/src/panels/timeline/CurvePane.tsx +508 -0
  250. package/src/panels/timeline/Filmstrip.tsx +361 -0
  251. package/src/panels/timeline/GenerateDrawer.tsx +407 -0
  252. package/src/panels/timeline/KeyStretch.tsx +115 -0
  253. package/src/panels/timeline/LifespanRow.tsx +730 -47
  254. package/src/panels/timeline/MarkerLane.tsx +46 -8
  255. package/src/panels/timeline/PlayButton.tsx +91 -0
  256. package/src/panels/timeline/PropertyPicker.tsx +254 -0
  257. package/src/panels/timeline/ShotRow.tsx +400 -0
  258. package/src/panels/timeline/SoundMode.tsx +1406 -0
  259. package/src/panels/timeline/TimelineDock.tsx +2338 -272
  260. package/src/panels/timeline/TransportVolume.tsx +163 -0
  261. package/src/panels/timeline/beatRuler.ts +74 -0
  262. package/src/panels/timeline/curveModel.ts +500 -0
  263. package/src/panels/timeline/duckUnderVoice.ts +68 -0
  264. package/src/panels/timeline/keyframeMenu.ts +138 -0
  265. package/src/panels/timeline/labelWidth.ts +49 -0
  266. package/src/panels/timeline/marquee.ts +155 -0
  267. package/src/panels/timeline/model.ts +1860 -199
  268. package/src/panels/timeline/snap.ts +125 -43
  269. package/src/panels/timeline/staggerPreview.tsx +92 -0
  270. package/src/panels/timeline/takeMeasure.ts +72 -0
  271. package/src/panels/timeline/useLive.ts +48 -0
  272. package/src/panels/timeline/viewport.ts +234 -0
  273. package/src/panels/toolbar/BackChip.tsx +116 -0
  274. package/src/panels/toolbar/CameraViewBar.tsx +118 -49
  275. package/src/panels/toolbar/CanvasBar.tsx +117 -0
  276. package/src/panels/toolbar/InsertButton.tsx +38 -0
  277. package/src/panels/toolbar/IsolationTrail.tsx +156 -0
  278. package/src/panels/toolbar/PanelToggles.tsx +86 -0
  279. package/src/panels/toolbar/ShadingBar.tsx +119 -26
  280. package/src/panels/toolbar/ToolDockBar.tsx +329 -80
  281. package/src/panels/toolbar/ToolFooter.tsx +102 -0
  282. package/src/panels/toolbar/ViewModeBar.tsx +247 -38
  283. package/src/panels/toolbar/WorkPlaneBar.tsx +94 -0
  284. package/src/panels/toolbar/useAddRows.tsx +326 -0
  285. package/src/panels/toolbar/useDismiss.ts +122 -0
  286. package/src/panels/versions/VersionHistoryPanel.tsx +274 -0
  287. package/src/panels/versions/versionModel.ts +103 -0
  288. package/src/panels/virtualWindow.ts +271 -0
  289. package/src/shotTokens.css +151 -60
  290. package/src/stage/FrustumGizmo.tsx +504 -84
  291. package/src/stage/GridPlane.tsx +225 -16
  292. package/src/stage/NodePreviews.tsx +628 -0
  293. package/src/stage/PerfHud.tsx +437 -47
  294. package/src/stage/SplitRenderer.tsx +816 -169
  295. package/src/stage/StageApp.tsx +4274 -486
  296. package/src/stage/StageRootBoundary.tsx +114 -0
  297. package/src/stage/StageTools.tsx +651 -151
  298. package/src/stage/TransformGizmo.tsx +696 -0
  299. package/src/stage/adaptivePresentation.ts +145 -0
  300. package/src/stage/bridgeClient.ts +2522 -41
  301. package/src/stage/exportWorker.ts +9 -0
  302. package/src/stage/feed.ts +305 -14
  303. package/src/stage/fontPreview.ts +176 -0
  304. package/src/stage/frustumGeometry.ts +319 -1
  305. package/src/stage/furnitureTokens.ts +33 -0
  306. package/src/stage/gizmoMath.ts +508 -16
  307. package/src/stage/gpuTimer.ts +276 -0
  308. package/src/stage/instanceModules.ts +194 -33
  309. package/src/stage/interactions/move.ts +24 -5
  310. package/src/stage/isolation.ts +291 -33
  311. package/src/stage/jsSelfProfiler.ts +325 -0
  312. package/src/stage/main.tsx +116 -16
  313. package/src/stage/modalState.ts +222 -23
  314. package/src/stage/moduleEpoch.ts +60 -0
  315. package/src/stage/motionPathStage.ts +279 -0
  316. package/src/stage/motionPreview.ts +74 -0
  317. package/src/stage/overlays/Overlays.tsx +1112 -159
  318. package/src/stage/overlays/Rulers.tsx +495 -0
  319. package/src/stage/panes.ts +159 -10
  320. package/src/stage/project.ts +68 -3
  321. package/src/stage/renderScaleController.ts +730 -0
  322. package/src/stage/rulers.ts +129 -0
  323. package/src/stage/sceneExtent.ts +75 -0
  324. package/src/stage/shading.ts +20 -0
  325. package/src/stage/splineStage.ts +632 -0
  326. package/src/stage/stagePerformanceTelemetry.ts +717 -0
  327. package/src/stage/stageTheme.ts +36 -0
  328. package/src/stage/stageWarmup.ts +11 -0
  329. package/src/stage/textAutoSize.ts +124 -0
  330. package/src/stage/tier1Export.ts +641 -0
  331. package/src/stage/tokenColor.ts +14 -9
  332. package/src/stage/view.ts +56 -9
  333. package/src/stage/workingView.ts +277 -33
  334. package/src/state/accountPrefs.ts +261 -0
  335. package/src/state/agentActions.ts +173 -15
  336. package/src/state/agentActivity.ts +198 -0
  337. package/src/state/agentQuestion.ts +239 -0
  338. package/src/state/bootOpen.ts +42 -0
  339. package/src/state/cameraVerbs.ts +139 -0
  340. package/src/state/clientId.ts +6 -1
  341. package/src/state/comments.ts +1099 -0
  342. package/src/state/context.tsx +19 -1
  343. package/src/state/editorPrefs.ts +220 -0
  344. package/src/state/errorReports.ts +121 -0
  345. package/src/state/fieldWrite.ts +161 -0
  346. package/src/state/fontRecents.ts +70 -0
  347. package/src/state/fonts.ts +656 -0
  348. package/src/state/graphClipboard.ts +414 -0
  349. package/src/state/imageThumbs.ts +216 -0
  350. package/src/state/insert.ts +444 -67
  351. package/src/state/isolationTrail.ts +92 -0
  352. package/src/state/keyMoves.ts +134 -0
  353. package/src/state/keyframeClipboard.ts +177 -0
  354. package/src/state/keymap.ts +1153 -117
  355. package/src/state/layoutPrefs.ts +272 -0
  356. package/src/state/layouts.ts +205 -0
  357. package/src/state/measureMedia.ts +167 -0
  358. package/src/state/menu.ts +2341 -28
  359. package/src/state/motionStamp.ts +7 -0
  360. package/src/state/panelSize.ts +375 -5
  361. package/src/state/pendingChanges.ts +119 -0
  362. package/src/state/playHold.ts +78 -0
  363. package/src/state/playbackPreparation.ts +562 -0
  364. package/src/state/posterCapture.ts +242 -0
  365. package/src/state/presence.ts +254 -0
  366. package/src/state/projectComponents.ts +160 -0
  367. package/src/state/renderEvents.ts +298 -18
  368. package/src/state/runtimeErrors.ts +46 -0
  369. package/src/state/selection.ts +12 -0
  370. package/src/state/shortcuts.ts +1606 -0
  371. package/src/state/sketchGuide.ts +51 -0
  372. package/src/state/splineRefs.ts +87 -0
  373. package/src/state/stageHealth.ts +259 -0
  374. package/src/state/stageWatchdog.ts +47 -0
  375. package/src/state/stillStage.ts +330 -0
  376. package/src/state/stillThumbs.ts +474 -0
  377. package/src/state/store.ts +23591 -2982
  378. package/src/state/storyboardModel.ts +528 -0
  379. package/src/state/sync.ts +1257 -0
  380. package/src/state/syncCache.ts +159 -0
  381. package/src/state/triangles.ts +13 -0
  382. package/src/state/types.ts +1698 -55
  383. package/src/state/unsavedGuard.ts +49 -0
  384. package/src/state/viewMode.ts +200 -0
  385. package/src/state/webmcp.ts +1128 -0
  386. package/src/state/workspace.ts +1147 -0
  387. package/src/styles/assets-tab.css +304 -0
  388. package/src/styles/canvas.css +545 -0
  389. package/src/styles/comments.css +1435 -0
  390. package/src/styles/controls.css +162 -0
  391. package/src/styles/dialogs.css +390 -0
  392. package/src/styles/insert-drawer.css +586 -0
  393. package/src/styles/inspector-board.css +207 -0
  394. package/src/styles/inspector-object.css +486 -0
  395. package/src/styles/inspector-shot.css +252 -0
  396. package/src/styles/inspector.css +360 -0
  397. package/src/styles/left-column.css +1628 -0
  398. package/src/styles/library-preview.css +575 -0
  399. package/src/styles/library.css +512 -0
  400. package/src/styles/modes/code.css +569 -0
  401. package/src/styles/modes/component.css +1561 -0
  402. package/src/styles/modes/material.css +1201 -0
  403. package/src/styles/modes/storyboard.css +518 -0
  404. package/src/styles/share-component.css +49 -0
  405. package/src/styles/shell.css +653 -0
  406. package/src/styles/shot-brief.css +360 -0
  407. package/src/styles/states.css +76 -0
  408. package/src/styles/sync.css +380 -0
  409. package/src/styles/timeline-graph.css +384 -0
  410. package/src/styles/timeline-meters.css +89 -0
  411. package/src/styles/timeline-rows.css +1959 -0
  412. package/src/styles/timeline-sound.css +1003 -0
  413. package/src/styles/timeline.css +754 -0
  414. package/src/styles/tool-footer.css +525 -0
  415. package/src/styles/topbar.css +886 -0
  416. package/src/styles/versions.css +262 -0
  417. package/src/styles.css +72 -2921
  418. package/src/tools.ts +108 -23
  419. package/dist/assets/index-B7notge7.css +0 -1
  420. package/dist/assets/index-BBE6Ta9L.js +0 -52
  421. package/src/panels/files/icons.tsx +0 -118
  422. package/src/panels/header/DocSwitcher.tsx +0 -85
  423. package/src/panels/header/TransportBar.tsx +0 -86
  424. package/src/panels/inspector/animate/BlockTiming.tsx +0 -341
  425. package/src/panels/inspector/animate/DuringBlocks.tsx +0 -85
  426. package/src/panels/inspector/animate/InOutPresets.tsx +0 -57
  427. package/src/panels/inspector/animate/ResolvedChannels.tsx +0 -150
  428. package/src/panels/inspector/animate/useBlocks.ts +0 -43
  429. package/src/panels/material/MaterialPicker.tsx +0 -172
  430. package/src/panels/timeline/SceneStrip.tsx +0 -145
  431. package/src/panels/toolbar/AddMenu.tsx +0 -230
  432. package/src/stage/MoveGizmo.tsx +0 -411
@@ -0,0 +1,1257 @@
1
+ // THE EDITOR'S LIVE SYNC CLIENT (collaboration lanes R4 and R5).
2
+ //
3
+ // When the server turns live sync on for a Project (`LoadedShot.sync`), the
4
+ // editor stops PUTting whole documents and sends what changed instead, as
5
+ // batches of sync ops over one socket to the document's session (store's
6
+ // shotSession.ts). The session orders every batch and tells everybody.
7
+ //
8
+ // C the confirmed document: the server's, at `seq`
9
+ // S batches of this document's OTHER open tabs on this device, shared over
10
+ // a BroadcastChannel: shown, never sent
11
+ // P batches this tab sends: its own queue's, and those of any closed tab
12
+ // it adopted, oldest first
13
+ //
14
+ // What the author sees is C with S and P replayed on top. Three paths change it:
15
+ //
16
+ // a local edit diffed against what was last minted, into one batch.
17
+ // A drag mints at most every 50 ms (20 Hz). Each batch is
18
+ // written to IndexedDB first, then sent
19
+ // our own ack folds into C and pops P. When the server applied it
20
+ // differently (a rename, a repair, an op a concurrent delete
21
+ // beat) the view is rebuilt from C
22
+ // another entry folds into C. When it touches nothing pending it lands on
23
+ // the view directly; otherwise the pending batches are
24
+ // replayed on the new C, and one of ours that no longer
25
+ // validates is dropped and said
26
+ //
27
+ // ── ONE BATCH IN FLIGHT ─────────────────────────────────────────────────────
28
+ //
29
+ // Stop and wait: the next send waits for the ack (or nack) of the last. What
30
+ // piled up meanwhile goes as ONE wire batch under the last counter, so a drag
31
+ // over a slow link is one message per round trip, not one per pointermove.
32
+ // The server's dedupe (`n <= acked`) then covers every batch it folded. Only
33
+ // batches of one queue are combined.
34
+ //
35
+ // ── BEFORE ANY SERVER MESSAGE, MINT ─────────────────────────────────────────
36
+ //
37
+ // The overlap test and the rebuild both reason about P, so the local edits
38
+ // not yet in P (a drag between two 50 ms mints) are minted first. After that
39
+ // the editor's document IS the view, and a remote change can be landed on it
40
+ // as a diff (`host.adopt`), which an open drag survives (engine's
41
+ // `history.adopt` rebases the gesture).
42
+ //
43
+ // ── QUEUES THAT OUTLIVE THEIR TAB (R5) ───────────────────────────────────────
44
+ //
45
+ // Each tab holds the Web Lock `dailies-sync-q:<q>` for its life. A queue in
46
+ // IndexedDB whose lock is free belongs to a tab that is gone: this tab claims
47
+ // the lock, adopts the batches and sends them as its own, then forgets the
48
+ // queue. A queue whose lock is held is a sibling tab's: its batches (from
49
+ // IndexedDB, then each new one over the channel) are shown here, and a
50
+ // waiting lock request adopts them the moment that tab closes. The join names
51
+ // every such queue, and the reply says how far each was acked: a batch the
52
+ // server already applied is never replayed nor sent twice (a resent `new`
53
+ // would make a second copy).
54
+ //
55
+ // ── HOLDS ───────────────────────────────────────────────────────────────────
56
+ //
57
+ // Three things stop editing until they clear, each said by the editor:
58
+ //
59
+ // upgrade the server runs a newer schema: the editor reloads
60
+ // older-server this editor is newer than the server: it retries
61
+ // quota IndexedDB is full: "Try again" stores what waits
62
+ //
63
+ // A queue written at an older schema (the tab that reloaded for an upgrade)
64
+ // is migrated on the join reply: its base and its result both go through
65
+ // `migrateShotDoc` and are diffed again, as one batch.
66
+ import {
67
+ CURRENT_SHOT_SCHEMA_VERSION,
68
+ applyOps,
69
+ canonicalize,
70
+ diffDoc,
71
+ invertOps,
72
+ migrateShotDoc,
73
+ remapIds,
74
+ renameCollisions,
75
+ syncValidateScope,
76
+ validateShotDoc,
77
+ } from "@dailies-studio/shot/doc";
78
+ import type {
79
+ Doc3D,
80
+ SyncEntry,
81
+ SyncOp,
82
+ SyncPresence,
83
+ SyncPresenceMessage,
84
+ SyncQueueAcks,
85
+ SyncServerMessage,
86
+ SyncSnapshot,
87
+ } from "@dailies-studio/shot/doc";
88
+ import type { HistoryDelta } from "@dailies-studio/engine";
89
+ import { isQuotaError } from "./syncCache.js";
90
+ import type { SyncCache, SyncStoredBatch } from "./syncCache.js";
91
+
92
+ /** The server closes a frame over 512 KB (1009). Margin for the envelope. */
93
+ const SEND_BUDGET = 512 * 1024 - 8 * 1024;
94
+ /** A drag mints at most this often. */
95
+ const SCRUB_MS = 50;
96
+ /** How long an edit makes its layer "ours" for measured writes (text fit). */
97
+ const TOUCH_MS = 10_000;
98
+ const RETRY_MS = 1_000;
99
+ /** The most other queues one join names (the protocol's cap). */
100
+ const MAX_JOIN_QUEUES = 64;
101
+
102
+ export interface SyncSocket {
103
+ send(text: string): void;
104
+ close(): void;
105
+ }
106
+
107
+ /** Open the document's socket. `close` gets the close code. */
108
+ export type SyncConnect = (
109
+ docId: string,
110
+ on: { open(): void; message(text: string): void; close(code: number): void },
111
+ ) => SyncSocket;
112
+
113
+ /** What stops editing until it clears (see HOLDS above). */
114
+ export type SyncHold = "upgrade" | "older-server" | "quota";
115
+
116
+ export interface SyncStatus {
117
+ /** This tab's batches the server has not applied (its own and adopted). */
118
+ pending: number;
119
+ /** Every change on this device the server has not applied: `pending`
120
+ * plus what this document's other tabs hold. */
121
+ waiting: number;
122
+ /** An edit exists that is only in memory: not minted yet, or a batch not
123
+ * (yet) in IndexedDB. What the close guard warns about. */
124
+ unstored: boolean;
125
+ /** Joined to the session. */
126
+ connected: boolean;
127
+ /** When the next reconnect is due (ms since the epoch), while one waits. */
128
+ retryAt: number | null;
129
+ /** Open tabs of this document on this device, this one included. */
130
+ tabs: number;
131
+ hold: SyncHold | null;
132
+ }
133
+
134
+ export interface SyncHost {
135
+ /** Land a server change on the editor's document with no undo entry, and
136
+ * answer the document now current. */
137
+ adopt(fn: (doc: Doc3D) => Doc3D): Doc3D;
138
+ status(s: SyncStatus): void;
139
+ /** A checkpoint stored the session's document as `rev`. `clean`: the
140
+ * editor's document is that one (nothing of ours is pending). */
141
+ saved(rev: string, clean: boolean): void;
142
+ /** The session refused this tab's writes by role. */
143
+ forbidden(): void;
144
+ /** The document was deleted (close 4004). `info`: who trashed it, and the
145
+ * Trash handle, when the session said (R10). `pending`: what this tab
146
+ * holds that never reached the server, as ops on the confirmed document;
147
+ * `revive()` sends it once the document is back. */
148
+ gone(info?: SyncGoneInfo | null, pending?: { base: Doc3D; ops: SyncOp[] } | null): void;
149
+ /** The Project was renamed to `to` (R10). The socket follows the id, so
150
+ * nothing else changes. `by`: who did it, when the session knows. */
151
+ renamed?(to: string, by: string | null): void;
152
+ /** The server runs schema `sv`, newer than this editor: reload. */
153
+ upgrade(sv: number): void;
154
+ toast(message: string): void;
155
+ /** Who else is here (R7): a session's `peers`, `peer` or `peer-left`, or
156
+ * null when the socket went and nobody is known to be here any more. */
157
+ presence?(msg: SyncPresenceMessage | null): void;
158
+ }
159
+
160
+ /** What the session said of a delete before it closed the socket. */
161
+ export interface SyncGoneInfo {
162
+ by: string | null;
163
+ trashId?: string;
164
+ }
165
+
166
+ export interface SyncClient {
167
+ /** The editor's document changed by the author. `scrubbing`: a gesture is
168
+ * open, so minting is throttled. */
169
+ localChange(doc: Doc3D, scrubbing: boolean): void;
170
+ /** Resolves once every edit so far is acked AND checkpointed. */
171
+ flush(): Promise<void>;
172
+ /** Resolves once a checkpoint as `rev` was announced (a server write this
173
+ * tab asked for over HTTP, such as a library import, has arrived). */
174
+ reach(rev: string): Promise<void>;
175
+ /** Did this tab edit the layer lately? Measured writes (an auto text fit)
176
+ * are made only by the tab that caused them, or every open tab would. */
177
+ touched(layerId: string): boolean;
178
+ /** Where this tab is now (R7). Sent while joined; the latest is sent again
179
+ * after every join. Never stored. */
180
+ presence(p: SyncPresence): void;
181
+ /** Reconnect now instead of when the backoff says ("Try now", the
182
+ * browser coming back online). */
183
+ retryNow(): void;
184
+ /** Store what is only in memory again, after a quota refusal; `prune`
185
+ * first forgets other documents' confirmed copies. True once nothing
186
+ * waits only in memory and the hold is gone. */
187
+ retryStorage(prune: boolean): Promise<boolean>;
188
+ /** Join again after `gone` (the Project was restored from the Trash): the
189
+ * restored session is a new epoch, so its snapshot replays what waits. */
190
+ revive(): void;
191
+ /** Close, and forget every edit this tab holds for the document, stored
192
+ * ones too: they were saved somewhere else ("Save as copy"), and a later
193
+ * restore must not replay them. */
194
+ discard(): void;
195
+ close(): void;
196
+ }
197
+
198
+ /** The part of the Web Locks API this uses (`navigator.locks`). */
199
+ export interface SyncLocks {
200
+ request(name: string, options: LockOptions, callback: (lock: Lock | null) => Promise<void> | void): Promise<unknown>;
201
+ }
202
+
203
+ /** The part of a BroadcastChannel this uses. */
204
+ export interface SyncChannel {
205
+ postMessage(message: unknown): void;
206
+ addEventListener(type: "message", listener: (event: MessageEvent) => void): void;
207
+ close(): void;
208
+ }
209
+
210
+ export interface SyncClientOptions {
211
+ docId: string;
212
+ /** The Project's name, kept with the confirmed copy for an offline boot. */
213
+ name?: string;
214
+ /** The editor's document as loaded, and the rev it was read at. */
215
+ doc: Doc3D;
216
+ rev: string;
217
+ connect: SyncConnect;
218
+ cache: SyncCache | null;
219
+ host: SyncHost;
220
+ /** `GET /api/shots/:name?sync=1`, for a snapshot too large to send inline. */
221
+ fetchSnapshot(): Promise<SyncSnapshot>;
222
+ /** This tab's queue. Fresh per client. */
223
+ q?: string;
224
+ /** Web Locks, or null where there are none: then no queue of another tab
225
+ * is adopted or shown. */
226
+ locks?: SyncLocks | null;
227
+ /** The channel this document's tabs share (`dailies-sync:<ws>:<docId>`). */
228
+ channel?: SyncChannel | null;
229
+ /** The editor booted from this device's copy with no network: the cached
230
+ * confirmed document and every stored batch are the view at once. */
231
+ offline?: boolean;
232
+ now?(): number;
233
+ }
234
+
235
+ interface Pending {
236
+ q: string;
237
+ n: number;
238
+ base: number;
239
+ ops: SyncOp[];
240
+ bytes: number;
241
+ /** IndexedDB answered (or there is none): sendable. */
242
+ ready: boolean;
243
+ /** In IndexedDB. */
244
+ stored: boolean;
245
+ }
246
+
247
+ /** What tabs of one document tell each other. */
248
+ type ChannelMessage =
249
+ | { t: "hello"; q: string }
250
+ | { t: "here"; q: string }
251
+ | { t: "batch"; q: string; n: number; base: number; ops: SyncOp[] }
252
+ | { t: "drop"; q: string; n: number };
253
+
254
+ const encoder = new TextEncoder();
255
+ const bytesOf = (x: unknown): number => encoder.encode(JSON.stringify(x)).length;
256
+ const noop = (): void => undefined;
257
+ export const syncLockName = (q: string): string => `dailies-sync-q:${q}`;
258
+
259
+ /** Every path an op changes, with an ordered-list op standing for its list. */
260
+ function touchedPaths(ops: readonly SyncOp[]): string[][] {
261
+ return ops.map((op) => (op.op === "ins" || op.op === "mv" || op.op === "rm" ? op.p.slice(0, -1) : [...op.p]));
262
+ }
263
+
264
+ const nested = (a: readonly string[], b: readonly string[]): boolean => {
265
+ const n = Math.min(a.length, b.length);
266
+ for (let i = 0; i < n; i++) if (a[i] !== b[i]) return false;
267
+ return true;
268
+ };
269
+
270
+ /** Does any of `a` write a path at, above or below one `b` writes? */
271
+ export function opsOverlap(a: readonly SyncOp[], b: readonly SyncOp[]): boolean {
272
+ const pa = touchedPaths(a);
273
+ return touchedPaths(b).some((q) => pa.some((p) => nested(p, q)));
274
+ }
275
+
276
+ /**
277
+ * Undo under live sync (R6, Paper page 21 "Undo is yours only"). An undo or
278
+ * redo is the inverse ops of this author's own change, and an op is skipped
279
+ * when `theirs` (what others changed since) wrote at, above or below its path,
280
+ * or when its target is gone. The result goes out through sync like any edit.
281
+ *
282
+ * ponytail: an undo that would leave the document less valid than it found it
283
+ * (half of a layer a peer still edits, a layer whose material a peer deleted)
284
+ * is dropped whole rather than repaired op by op.
285
+ */
286
+ export const undoDelta: HistoryDelta<Doc3D, SyncOp> = {
287
+ diff: diffDoc,
288
+ invert: invertOps,
289
+ apply(doc, ops, theirs) {
290
+ const taken = theirs.map((t) => t.p);
291
+ const kept = ops.filter((op) => !taken.some((p) => nested(p, op.p)));
292
+ const { doc: next, skipped } = applyOps(doc, kept);
293
+ const applied = kept.filter((op) => !skipped.some((s) => s.op === op));
294
+ if (applied.length === 0) return { doc, applied };
295
+ const scope = syncValidateScope(applied) ?? undefined;
296
+ if (validateShotDoc(next, scope).length > validateShotDoc(doc, scope).length) return { doc, applied: [] };
297
+ return { doc: next, applied };
298
+ },
299
+ };
300
+
301
+ /** Replay one batch on `view` the way the server would apply it. */
302
+ function replay(view: Doc3D, ops: readonly SyncOp[]): { next: Doc3D; ops: SyncOp[]; renamed: boolean; valid: boolean } {
303
+ const renamed = renameCollisions(view, ops);
304
+ const next = canonicalize(applyOps(view, renamed.ops).doc, renamed.ops);
305
+ const valid = validateShotDoc(next, syncValidateScope(renamed.ops) ?? undefined).length === 0;
306
+ return { next, ops: renamed.ops, renamed: renamed.renames.length > 0, valid };
307
+ }
308
+
309
+ export function createSyncClient(o: SyncClientOptions): SyncClient {
310
+ const { docId, connect, cache, host } = o;
311
+ const q = o.q ?? crypto.randomUUID();
312
+ const now = o.now ?? (() => Date.now());
313
+ const locks = o.locks ?? null;
314
+ // Sharing between tabs needs the locks: they are what says a tab is gone.
315
+ const channel = locks === null ? null : (o.channel ?? null);
316
+ const sv = CURRENT_SHOT_SCHEMA_VERSION;
317
+
318
+ let C: { doc: Doc3D; seq: number; epoch: string; rev: string } | null = null;
319
+ /** The cached confirmed copy when it is at an older schema: the base an
320
+ * old queue's batches are migrated from. */
321
+ let oldBase: Doc3D | null = null;
322
+ const P: Pending[] = [];
323
+ const S = new Map<string, Pending[]>();
324
+ /** The highest counter of each sibling queue the server applied: a channel
325
+ * batch at or under it is already in C. */
326
+ const siblingAcked = new Map<string, number>();
327
+ /** The queues this tab sends: its own and the adopted ones. */
328
+ const mine = new Set([q]);
329
+ /** Adopted queues the server has not yet said how far it acked: nothing of
330
+ * them is sent before it does. */
331
+ const unconfirmed = new Set<string>();
332
+ /** Adopted batches written at an older schema, migrated on the join reply. */
333
+ const stale = new Map<string, SyncStoredBatch[]>();
334
+ /** Other tabs of this document that are open (their lock is held). */
335
+ const siblings = new Set<string>();
336
+ /** A waiting lock request per other queue: cancelled on close. */
337
+ const watching = new Map<string, AbortController>();
338
+ /** Releases the lock of each adopted queue, once it is drained. */
339
+ const holding = new Map<string, () => void>();
340
+ let releaseOwn: () => void = noop;
341
+ /** The queues the last join named. */
342
+ let named: string[] = [];
343
+ let counter = 0;
344
+ /** The wire batch awaiting its ack: its queue, the last counter it carried
345
+ * and how many ops it sent. */
346
+ let inflight: { q: string; n: number; ops: number } | null = null;
347
+ /** Send one stored batch at a time up to this counter (a combined batch
348
+ * was refused: find which one). */
349
+ let solo: { q: string; n: number } | null = null;
350
+ let local = o.doc;
351
+ let minted = o.doc;
352
+ let sock: SyncSocket | null = null;
353
+ let joined = false;
354
+ /** C is the server's (a join was answered), or this device's copy on an
355
+ * offline boot: the view may be rebuilt from it. Before that a cached C
356
+ * can be older than the editor's document and would flash. */
357
+ let trusted = false;
358
+ let hold: SyncHold | null = null;
359
+ let stopped = false;
360
+ /** `close()` ran: nothing revives this client. */
361
+ let closed = false;
362
+ let attempts = 0;
363
+ let retryAt: number | null = null;
364
+ let savedSeq = -1;
365
+ const revs: string[] = [o.rev];
366
+ /** Messages that arrived while a too-large snapshot is fetched. */
367
+ let held: SyncServerMessage[] | null = null;
368
+ let scrubTimer: ReturnType<typeof setTimeout> | null = null;
369
+ let retryTimer: ReturnType<typeof setTimeout> | null = null;
370
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
371
+ const touchedAt = new Map<string, number>();
372
+ /** Where this tab last said it is, and whether the session has told us who
373
+ * is here since the last join (then presence goes out). */
374
+ let where: SyncPresence | null = null;
375
+ let present = false;
376
+ /** The Project's name now: a rename under the session moves it. */
377
+ let name = o.name;
378
+ /** The session's `deleted` notice, for the 4004 that follows it. */
379
+ let goneInfo: SyncGoneInfo | null = null;
380
+ const waiters: Array<{ test: () => boolean; resolve: () => void; reject: (e: Error) => void }> = [];
381
+
382
+ // ── status and waiting ─────────────────────────────────────────────────────
383
+
384
+ function emit(): void {
385
+ let shown = 0;
386
+ for (const list of S.values()) shown += list.length;
387
+ for (const list of stale.values()) shown += list.length;
388
+ host.status({
389
+ pending: P.length,
390
+ waiting: P.length + shown,
391
+ unstored: local !== minted || P.some((b) => !b.stored),
392
+ connected: joined,
393
+ retryAt,
394
+ tabs: 1 + siblings.size,
395
+ hold,
396
+ });
397
+ }
398
+
399
+ function wait(test: () => boolean): Promise<void> {
400
+ if (test()) return Promise.resolve();
401
+ return new Promise((resolve, reject) => waiters.push({ test, resolve, reject }));
402
+ }
403
+
404
+ function settle(): void {
405
+ for (const w of [...waiters]) {
406
+ if (!w.test()) continue;
407
+ waiters.splice(waiters.indexOf(w), 1);
408
+ w.resolve();
409
+ }
410
+ }
411
+
412
+ function fail(why: string): void {
413
+ for (const w of waiters.splice(0)) w.reject(new Error(why));
414
+ }
415
+
416
+ function post(m: ChannelMessage): void {
417
+ try {
418
+ channel?.postMessage(m);
419
+ } catch {
420
+ // A closed channel: the siblings learn the rest from the server.
421
+ }
422
+ }
423
+
424
+ // ── storing ────────────────────────────────────────────────────────────────
425
+
426
+ const storedOf = (b: Pending): SyncStoredBatch => ({ q: b.q, n: b.n, docId, base: b.base, sv, ops: b.ops });
427
+
428
+ /** Write one batch to IndexedDB, then tell the other tabs. */
429
+ function store(b: Pending): Promise<void> {
430
+ return cache!.putBatch(storedOf(b)).then(
431
+ () => {
432
+ b.stored = true;
433
+ post({ t: "batch", q: b.q, n: b.n, base: b.base, ops: b.ops });
434
+ },
435
+ (e: unknown) => {
436
+ // Out of space: editing pauses so nothing more lives only in memory.
437
+ // Any other refusal: this batch stays in memory and the close guard
438
+ // keeps warning until it is acked.
439
+ if (isQuotaError(e) && hold === null) hold = "quota";
440
+ },
441
+ );
442
+ }
443
+
444
+ // ── local edits ────────────────────────────────────────────────────────────
445
+
446
+ function mint(): void {
447
+ if (scrubTimer !== null) clearTimeout(scrubTimer);
448
+ scrubTimer = null;
449
+ if (local === minted || stopped) return;
450
+ const ops = diffDoc(minted, local);
451
+ minted = local;
452
+ if (ops.length === 0) return;
453
+ const t = now();
454
+ for (const op of ops) {
455
+ const i = op.p.indexOf("layersById");
456
+ const id = i < 0 ? undefined : op.p[i + 1];
457
+ if (id !== undefined) touchedAt.set(id, t);
458
+ }
459
+ const b: Pending = { q, n: ++counter, base: C?.seq ?? 0, ops, bytes: bytesOf(ops), ready: cache === null, stored: false };
460
+ P.push(b);
461
+ if (cache !== null) {
462
+ void store(b).finally(() => {
463
+ b.ready = true;
464
+ pump();
465
+ emit();
466
+ });
467
+ }
468
+ pump();
469
+ }
470
+
471
+ /** Forget an adopted queue once nothing of it waits, and let its lock go. */
472
+ function release(qq: string): void {
473
+ if (qq === q || P.some((b) => b.q === qq) || stale.has(qq)) return;
474
+ const free = holding.get(qq);
475
+ if (free === undefined) return;
476
+ holding.delete(qq);
477
+ mine.delete(qq);
478
+ unconfirmed.delete(qq);
479
+ void cache?.dropQueue(qq).catch(noop).finally(free);
480
+ }
481
+
482
+ /** Remove batches from P and IndexedDB, and tell the other tabs. */
483
+ function drop(batches: readonly Pending[]): void {
484
+ for (const b of batches) {
485
+ const i = P.indexOf(b);
486
+ if (i >= 0) P.splice(i, 1);
487
+ void cache?.dropBatches(b.q, b.n, b.n).catch(noop);
488
+ post({ t: "drop", q: b.q, n: b.n });
489
+ }
490
+ for (const qq of new Set(batches.map((b) => b.q))) release(qq);
491
+ }
492
+
493
+ /** Forget every batch of queue `qq` the server has applied, up to `n`. */
494
+ function popQueue(qq: string, n: number): void {
495
+ if (n <= 0) return;
496
+ if (mine.has(qq)) {
497
+ for (let i = P.length - 1; i >= 0; i--) if (P[i]!.q === qq && P[i]!.n <= n) P.splice(i, 1);
498
+ if (inflight !== null && inflight.q === qq && inflight.n <= n) inflight = null;
499
+ void cache?.dropBatches(qq, 0, n).catch(noop);
500
+ release(qq);
501
+ return;
502
+ }
503
+ // A sibling's: it forgets its own copy in IndexedDB.
504
+ siblingAcked.set(qq, Math.max(n, siblingAcked.get(qq) ?? 0));
505
+ const list = S.get(qq);
506
+ if (list === undefined) return;
507
+ const rest = list.filter((b) => b.n > n);
508
+ if (rest.length > 0) S.set(qq, rest);
509
+ else S.delete(qq);
510
+ }
511
+
512
+ /** Does a batch some other tab of this device holds have `n`? */
513
+ const showsSibling = (qq: string, n: number): boolean => S.get(qq)?.some((b) => b.n <= n) === true;
514
+
515
+ // ── sending ────────────────────────────────────────────────────────────────
516
+
517
+ function pump(): void {
518
+ // A quota hold still sends: an ack is what frees memory.
519
+ if (!joined || inflight !== null || stopped || sock === null || retryTimer !== null) return;
520
+ const batch: Pending[] = [];
521
+ let bytes = 0;
522
+ for (const b of P) {
523
+ if (!b.ready || unconfirmed.has(b.q)) break;
524
+ const single = solo !== null && solo.q === b.q && b.n <= solo.n;
525
+ if (batch.length > 0 && (b.q !== batch[0]!.q || bytes + b.bytes > SEND_BUDGET || single)) break;
526
+ batch.push(b);
527
+ bytes += b.bytes;
528
+ if (single) break;
529
+ }
530
+ const head = batch[0];
531
+ if (head === undefined) return;
532
+ if (head.bytes > SEND_BUDGET) {
533
+ // ponytail: one edit over a socket frame (a huge paste) is dropped and
534
+ // said; a whole-document merge PUT is the upgrade path.
535
+ drop([head]);
536
+ host.toast("That change was too large to send, so it was not kept. Try it in smaller steps.");
537
+ rebuild();
538
+ pump();
539
+ return;
540
+ }
541
+ const ops = batch.flatMap((b) => b.ops);
542
+ const last = batch[batch.length - 1]!;
543
+ inflight = { q: head.q, n: last.n, ops: ops.length };
544
+ sock.send(JSON.stringify({ t: "batch", doc: docId, q: head.q, n: last.n, base: head.base, sv, ops }));
545
+ }
546
+
547
+ // ── landing the server's changes ───────────────────────────────────────────
548
+
549
+ const entryOps = (e: SyncEntry): SyncOp[] => (e.extra === undefined ? e.ops : [...e.ops, ...e.extra]);
550
+
551
+ /** Put the view on the editor. `local === minted` holds (minted first).
552
+ * An open drag replays its last capture on the result, which may differ
553
+ * from the view where the rebuild moved what it drags: the drag is the
554
+ * newer edit, so that difference is minted and sent. */
555
+ function land(view: Doc3D): void {
556
+ const ops = diffDoc(minted, view);
557
+ minted = view;
558
+ if (ops.length > 0) local = host.adopt((d) => applyOps(d, ops).doc);
559
+ mint();
560
+ }
561
+
562
+ /** Land ops on the view as they are: nothing pending replays after them. */
563
+ function landDirect(ops: readonly SyncOp[]): void {
564
+ minted = applyOps(minted, ops).doc;
565
+ local = host.adopt((d) => applyOps(d, ops).doc);
566
+ mint();
567
+ }
568
+
569
+ const remember = (rev: string): void => {
570
+ revs.push(rev);
571
+ if (revs.length > 32) revs.shift();
572
+ };
573
+
574
+ /** The view again: C with every pending batch replayed the way the server
575
+ * would apply it, the other tabs' first. One of ours that no longer
576
+ * validates is dropped and said, unless it is already in flight (its nack
577
+ * will say so); one of another tab's is only left out (that tab drops it). */
578
+ function rebuild(): void {
579
+ if (C === null || !trusted) return;
580
+ let view = C.doc;
581
+ for (const list of S.values()) {
582
+ for (const b of list) {
583
+ const r = replay(view, b.ops);
584
+ if (r.valid) view = r.next;
585
+ }
586
+ }
587
+ for (const b of [...P]) {
588
+ const flying = inflight !== null && b.q === inflight.q && b.n <= inflight.n;
589
+ const r = replay(view, b.ops);
590
+ if (!flying && !r.valid) {
591
+ drop([b]);
592
+ host.toast("A change of yours conflicted with someone else's and was undone.");
593
+ continue;
594
+ }
595
+ if (!flying && r.renamed) {
596
+ b.ops = r.ops;
597
+ b.base = C.seq;
598
+ }
599
+ view = r.next;
600
+ }
601
+ land(view);
602
+ }
603
+
604
+ function foldEntry(e: SyncEntry): void {
605
+ C = { ...C!, doc: applyOps(C!.doc, entryOps(e)).doc, seq: e.seq };
606
+ }
607
+
608
+ /** Does anything pending on this device write where `ops` do? */
609
+ const pendingOverlap = (ops: readonly SyncOp[]): boolean =>
610
+ P.some((b) => opsOverlap(b.ops, ops)) || [...S.values()].some((list) => list.some((b) => opsOverlap(b.ops, ops)));
611
+
612
+ /** Rejoin from where C stands: a gap in the log, a session that forgot us,
613
+ * or a queue adopted since the last join. */
614
+ function resync(): void {
615
+ const s = sock;
616
+ sock = null;
617
+ joined = false;
618
+ absent();
619
+ inflight = null;
620
+ s?.close();
621
+ open();
622
+ }
623
+
624
+ /**
625
+ * Batches an older editor wrote (it reloaded for an upgrade), as ONE batch
626
+ * at this schema: the base they were written on and the result, both
627
+ * migrated, diffed again. What the server already applied (`acked`) is part
628
+ * of the base. The batch keeps the last counter, so a resend still dedupes.
629
+ *
630
+ * ponytail: the base is this device's last confirmed copy at that schema,
631
+ * which can be older than the one the old tab wrote on; an op that only
632
+ * made sense on a newer base lands as a plain write or fails validation
633
+ * (then the rebuild drops it and says so).
634
+ */
635
+ function migrateStale(qq: string, acked: number): void {
636
+ const list = stale.get(qq);
637
+ if (list === undefined) return;
638
+ stale.delete(qq);
639
+ const sorted = [...list].sort((a, b) => a.n - b.n);
640
+ const rest = sorted.filter((b) => b.n > acked);
641
+ const last = sorted[sorted.length - 1]!.n;
642
+ if (rest.length === 0) {
643
+ void cache?.dropBatches(qq, 0, last).catch(noop);
644
+ return;
645
+ }
646
+ let ops: SyncOp[] | null = null;
647
+ if (oldBase !== null) {
648
+ try {
649
+ let base = oldBase;
650
+ for (const b of sorted) if (b.n <= acked) base = applyOps(base, b.ops).doc;
651
+ let top = base;
652
+ for (const b of rest) top = applyOps(top, b.ops).doc;
653
+ ops = diffDoc(migrateShotDoc(base), migrateShotDoc(top));
654
+ } catch {
655
+ ops = null;
656
+ }
657
+ }
658
+ if (ops === null) {
659
+ void cache?.dropBatches(qq, 0, last).catch(noop);
660
+ host.toast("Changes made before the editor was updated could not be brought over, so they were not kept.");
661
+ return;
662
+ }
663
+ if (ops.length === 0) {
664
+ void cache?.dropBatches(qq, 0, last).catch(noop);
665
+ return;
666
+ }
667
+ const b: Pending = { q: qq, n: last, base: C?.seq ?? 0, ops, bytes: bytesOf(ops), ready: cache === null, stored: false };
668
+ adoptInto([b]);
669
+ if (cache !== null) {
670
+ // The new batch overwrites the old one under the same key, then the
671
+ // older ones go: the edits are in IndexedDB at every moment.
672
+ void store(b)
673
+ .then(() => (b.stored ? cache.dropBatches(qq, 0, last - 1) : undefined))
674
+ .catch(noop)
675
+ .finally(() => {
676
+ b.ready = true;
677
+ pump();
678
+ emit();
679
+ });
680
+ }
681
+ }
682
+
683
+ /** Put adopted batches into P after whatever is in flight, before this
684
+ * tab's own: they are older. */
685
+ function adoptInto(batches: readonly Pending[]): void {
686
+ let at = 0;
687
+ while (at < P.length && P[at]!.q !== q && !batches.some((b) => b.q === P[at]!.q)) at++;
688
+ while (at < P.length && inflight !== null && P[at]!.q === inflight.q && P[at]!.n <= inflight.n) at++;
689
+ P.splice(at, 0, ...batches);
690
+ }
691
+
692
+ /** What a join reply says of the queues: drop what was applied, migrate
693
+ * what an older editor wrote, and let the adopted ones send. */
694
+ function joinedQueues(acked: number, queues: SyncQueueAcks | undefined): void {
695
+ popQueue(q, acked);
696
+ for (const qq of named) {
697
+ const n = queues?.[qq] ?? 0;
698
+ migrateStale(qq, n);
699
+ popQueue(qq, n);
700
+ unconfirmed.delete(qq);
701
+ }
702
+ joined = true;
703
+ trusted = true;
704
+ attempts = 0;
705
+ retryAt = null;
706
+ if (hold === "older-server") hold = null;
707
+ }
708
+
709
+ function onSnapshot(s: SyncSnapshot, acked: number, queues: SyncQueueAcks | undefined): void {
710
+ if (C === null || s.epoch !== C.epoch) savedSeq = -1;
711
+ C = { doc: s.doc, seq: s.seq, epoch: s.epoch, rev: s.rev };
712
+ remember(s.rev);
713
+ saveConfirmed();
714
+ // A snapshot replaces the log position: anything in flight is resent.
715
+ inflight = null;
716
+ joinedQueues(acked, queues);
717
+ rebuild();
718
+ }
719
+
720
+ function sendPresence(): void {
721
+ if (present && where !== null) sock?.send(JSON.stringify({ t: "presence", doc: docId, ...where }));
722
+ }
723
+
724
+ function absent(): void {
725
+ if (!present) return;
726
+ present = false;
727
+ host.presence?.(null);
728
+ }
729
+
730
+ function saveConfirmed(): void {
731
+ if (C === null) return;
732
+ void cache?.saveConfirmed({ docId, sv, ...C, ...(name === undefined ? {} : { name }) }).catch(noop);
733
+ }
734
+
735
+ /** The server holds another schema version than this editor. */
736
+ function schemaMismatch(server: number): void {
737
+ inflight = null;
738
+ if (server > sv) {
739
+ // Newer: nothing more is sent or folded. The editor reloads, and the
740
+ // reloaded tab adopts this queue and migrates it.
741
+ hold = "upgrade";
742
+ stopped = true;
743
+ const s = sock;
744
+ sock = null;
745
+ joined = false;
746
+ absent();
747
+ s?.close();
748
+ fail("this Project was upgraded by a newer editor");
749
+ emit();
750
+ host.upgrade(server);
751
+ return;
752
+ }
753
+ // Older (a deploy still rolling out): read only, and ask again later.
754
+ hold = "older-server";
755
+ const s = sock;
756
+ sock = null;
757
+ joined = false;
758
+ absent();
759
+ s?.close();
760
+ fail("this editor is newer than the server");
761
+ later(1006);
762
+ emit();
763
+ }
764
+
765
+ function receive(msg: SyncServerMessage): void {
766
+ // Presence is not the document: no mint, never held behind a snapshot.
767
+ if (msg.t === "peers" || msg.t === "peer" || msg.t === "peer-left") {
768
+ if (msg.t === "peers") present = true;
769
+ host.presence?.(msg);
770
+ if (msg.t === "peers") sendPresence();
771
+ return;
772
+ }
773
+ // Neither is the document (R10). The socket is by id, so a rename keeps it.
774
+ if (msg.t === "doc") {
775
+ name = msg.renamedTo;
776
+ saveConfirmed();
777
+ host.renamed?.(msg.renamedTo, msg.by);
778
+ return;
779
+ }
780
+ if (msg.t === "deleted") {
781
+ goneInfo = { by: msg.by, ...(msg.trashId === undefined ? {} : { trashId: msg.trashId }) };
782
+ return;
783
+ }
784
+ if (held !== null) {
785
+ held.push(msg);
786
+ return;
787
+ }
788
+ mint();
789
+ switch (msg.t) {
790
+ case "welcome": {
791
+ if (msg.sv !== sv) return schemaMismatch(msg.sv);
792
+ if (C === null) return resync();
793
+ for (const e of msg.entries) {
794
+ if (e.seq <= C.seq) continue;
795
+ foldEntry(e);
796
+ popQueue(e.q, e.n);
797
+ }
798
+ joinedQueues(msg.acked, msg.queues);
799
+ rebuild();
800
+ break;
801
+ }
802
+ case "snapshot": {
803
+ if (msg.sv !== sv) return schemaMismatch(msg.sv);
804
+ const { t: _t, acked, queues, ...at } = msg;
805
+ if (at.doc !== undefined) {
806
+ onSnapshot({ ...at, doc: at.doc }, acked, queues);
807
+ break;
808
+ }
809
+ // Too large to send inline: read it over HTTP, and hold what arrives
810
+ // meanwhile (a later entry applies on top of it).
811
+ held = [];
812
+ o.fetchSnapshot().then(
813
+ (s) => {
814
+ const later = held ?? [];
815
+ held = null;
816
+ mint();
817
+ onSnapshot(s, acked, queues);
818
+ for (const m of later) receive(m);
819
+ pump();
820
+ settle();
821
+ emit();
822
+ },
823
+ () => {
824
+ held = null;
825
+ resync();
826
+ },
827
+ );
828
+ return;
829
+ }
830
+ case "ack":
831
+ case "entry": {
832
+ const { t, ...e } = msg;
833
+ if (C === null) return;
834
+ const own = mine.has(e.q);
835
+ if (e.seq <= C.seq) {
836
+ popQueue(e.q, e.n);
837
+ break;
838
+ }
839
+ if (e.seq !== C.seq + 1) return resync();
840
+ const sent = inflight;
841
+ foldEntry(e);
842
+ if (own) {
843
+ popQueue(e.q, e.n);
844
+ // The server applied it as sent: the view already has it.
845
+ const asSent =
846
+ t === "ack" &&
847
+ sent !== null &&
848
+ sent.q === e.q &&
849
+ sent.n === e.n &&
850
+ e.extra === undefined &&
851
+ e.remap === undefined &&
852
+ e.ops.length === sent.ops;
853
+ if (e.remap !== undefined) for (const b of P) if (b.q === e.q) b.ops = remapIds(b.ops, e.remap);
854
+ if (!asSent) rebuild();
855
+ break;
856
+ }
857
+ // A sibling tab's batch landed: it is in C now, not in S.
858
+ if (showsSibling(e.q, e.n)) {
859
+ popQueue(e.q, e.n);
860
+ rebuild();
861
+ break;
862
+ }
863
+ popQueue(e.q, e.n);
864
+ const ops = entryOps(e);
865
+ // Nothing pending touches it, so it lands the same on the view as on
866
+ // C, under an open drag too (that drag's edit is in P).
867
+ if (pendingOverlap(ops)) rebuild();
868
+ else landDirect(ops);
869
+ break;
870
+ }
871
+ case "nack": {
872
+ const sent = inflight;
873
+ if (sent === null || msg.q !== sent.q || msg.n !== sent.n) break;
874
+ inflight = null;
875
+ const covered = P.filter((b) => b.q === sent.q && b.n <= sent.n);
876
+ if (msg.code === "unavailable") {
877
+ retryTimer = setTimeout(() => {
878
+ retryTimer = null;
879
+ pump();
880
+ }, RETRY_MS);
881
+ break;
882
+ }
883
+ if (msg.code === "schema") return schemaMismatch(msg.sv);
884
+ if (msg.code === "forbidden") {
885
+ if (sent.q !== q && msg.problems.some((p) => p.at === "q")) {
886
+ // An adopted queue another user wrote (a shared device): it is
887
+ // not this user's to send.
888
+ drop(P.filter((b) => b.q === sent.q));
889
+ rebuild();
890
+ break;
891
+ }
892
+ drop([...P]);
893
+ host.forbidden();
894
+ rebuild();
895
+ break;
896
+ }
897
+ if (covered.length > 1) {
898
+ // Several batches went as one: send them singly to find the one.
899
+ solo = { q: sent.q, n: sent.n };
900
+ break;
901
+ }
902
+ drop(covered);
903
+ const why = msg.problems.map((p) => p.message).join("; ");
904
+ host.toast(`A change was refused${why === "" ? "" : `: ${why}`}`);
905
+ rebuild();
906
+ break;
907
+ }
908
+ case "saved": {
909
+ savedSeq = msg.seq;
910
+ remember(msg.rev);
911
+ if (C === null || C.seq !== msg.seq) break;
912
+ C = { ...C, rev: msg.rev };
913
+ saveConfirmed();
914
+ host.saved(msg.rev, P.length === 0 && local === minted);
915
+ break;
916
+ }
917
+ case "error": {
918
+ if (msg.code === "unavailable") fail(`the Project could not be saved: ${msg.message}`);
919
+ else if (msg.code === "not-joined" || msg.code === "closed") return resync();
920
+ else host.toast(`Live sync: ${msg.message}`);
921
+ break;
922
+ }
923
+ }
924
+ pump();
925
+ settle();
926
+ emit();
927
+ }
928
+
929
+ // ── the socket ─────────────────────────────────────────────────────────────
930
+
931
+ /** The other queues a join names: adopted, and the open siblings'. */
932
+ function joinQueues(): string[] {
933
+ return [...new Set([...mine, ...stale.keys(), ...siblings, ...S.keys()])].filter((qq) => qq !== q).slice(0, MAX_JOIN_QUEUES);
934
+ }
935
+
936
+ function open(): void {
937
+ if (stopped) return;
938
+ retryAt = null;
939
+ const s = connect(docId, {
940
+ open() {
941
+ if (sock !== s) return;
942
+ named = joinQueues();
943
+ s.send(
944
+ JSON.stringify({
945
+ t: "join",
946
+ doc: docId,
947
+ q,
948
+ sv,
949
+ ...(C === null ? {} : { epoch: C.epoch, seq: C.seq }),
950
+ ...(named.length > 0 ? { queues: named } : {}),
951
+ }),
952
+ );
953
+ },
954
+ message(text) {
955
+ if (sock !== s) return;
956
+ let msg: SyncServerMessage;
957
+ try {
958
+ msg = JSON.parse(text) as SyncServerMessage;
959
+ } catch {
960
+ return;
961
+ }
962
+ receive(msg);
963
+ },
964
+ close(code) {
965
+ if (sock !== s) return;
966
+ sock = null;
967
+ joined = false;
968
+ absent();
969
+ inflight = null;
970
+ fail("the live session disconnected");
971
+ if (code === 4004) {
972
+ // What is only in memory joins P first, so it is offered (and
973
+ // stored) with the rest.
974
+ mint();
975
+ stopped = true;
976
+ emit();
977
+ const info = goneInfo;
978
+ goneInfo = null;
979
+ const ops = P.flatMap((b) => b.ops);
980
+ host.gone(info, C === null || ops.length === 0 ? null : { base: C.doc, ops });
981
+ return;
982
+ }
983
+ later(code);
984
+ emit();
985
+ },
986
+ });
987
+ sock = s;
988
+ }
989
+
990
+ /** Reconnect after a backoff with jitter. 4001: access changed or the
991
+ * session is stale, so at once, and the server says what this tab may do. */
992
+ function later(code: number): void {
993
+ if (stopped) return;
994
+ const delay = code === 4001 ? 0 : Math.min(30_000, 500 * 2 ** attempts) * (0.5 + Math.random() / 2);
995
+ attempts++;
996
+ retryAt = now() + delay;
997
+ if (reconnectTimer !== null) clearTimeout(reconnectTimer);
998
+ reconnectTimer = setTimeout(() => {
999
+ reconnectTimer = null;
1000
+ open();
1001
+ }, delay);
1002
+ }
1003
+
1004
+ // ── the other tabs of this document ────────────────────────────────────────
1005
+
1006
+ /** Show a sibling's batches (deduped by counter, skipping applied ones). */
1007
+ function showSibling(qq: string, batches: readonly SyncStoredBatch[]): Pending[] {
1008
+ const list = S.get(qq) ?? [];
1009
+ const acked = siblingAcked.get(qq) ?? 0;
1010
+ const added: Pending[] = [];
1011
+ for (const b of batches) {
1012
+ if (b.sv !== sv || b.n <= acked || list.some((x) => x.n === b.n)) continue;
1013
+ const p: Pending = { q: qq, n: b.n, base: b.base, ops: b.ops, bytes: 0, ready: true, stored: true };
1014
+ list.push(p);
1015
+ added.push(p);
1016
+ }
1017
+ list.sort((a, b) => a.n - b.n);
1018
+ if (list.length > 0) S.set(qq, list);
1019
+ return added;
1020
+ }
1021
+
1022
+ /**
1023
+ * A queue's lock came free: the tab that held it is gone. Read what it left
1024
+ * and send it as this tab's (adopted), holding the lock until it drains so
1025
+ * no other tab sends it too. Resolves when the lock may go.
1026
+ */
1027
+ async function claim(qq: string): Promise<void> {
1028
+ siblings.delete(qq);
1029
+ S.delete(qq);
1030
+ if (stopped || cache === null) return;
1031
+ const all = await cache.batchesOf(docId).catch((): SyncStoredBatch[] => []);
1032
+ if (stopped) return;
1033
+ const left = all.filter((b) => b.q === qq);
1034
+ const current = left.filter((b) => b.sv === sv).sort((a, b) => a.n - b.n);
1035
+ const old = left.filter((b) => b.sv < sv);
1036
+ if (current.length === 0 && old.length === 0) {
1037
+ // ponytail: a queue a NEWER editor wrote is left where it is, for that
1038
+ // editor; this one cannot read it.
1039
+ if (left.length === 0) await cache.dropQueue(qq).catch(noop);
1040
+ if (trusted) rebuild();
1041
+ emit();
1042
+ return;
1043
+ }
1044
+ mine.add(qq);
1045
+ unconfirmed.add(qq);
1046
+ if (old.length > 0) stale.set(qq, old);
1047
+ if (current.length > 0) {
1048
+ adoptInto(
1049
+ current.map((b) => ({ q: qq, n: b.n, base: b.base, ops: b.ops, bytes: bytesOf(b.ops), ready: true, stored: true })),
1050
+ );
1051
+ }
1052
+ const freed = new Promise<void>((resolve) => holding.set(qq, resolve));
1053
+ // The join names it, so its reply says how far it was acked.
1054
+ if (sock !== null) resync();
1055
+ else if (trusted) rebuild();
1056
+ emit();
1057
+ return freed;
1058
+ }
1059
+
1060
+ /** Follow another queue of this document: adopt it once its tab is gone. */
1061
+ function watch(qq: string, live: boolean): Promise<void> {
1062
+ if (qq === q || mine.has(qq) || watching.has(qq) || locks === null || stopped) return Promise.resolve();
1063
+ const ac = new AbortController();
1064
+ watching.set(qq, ac);
1065
+ const done = (): void => void watching.delete(qq);
1066
+ if (live) {
1067
+ // A tab that just said hello: alive, whatever the lock says this instant.
1068
+ siblings.add(qq);
1069
+ void locks
1070
+ .request(syncLockName(qq), { signal: ac.signal }, () => claim(qq))
1071
+ .catch(noop)
1072
+ .finally(done);
1073
+ return Promise.resolve();
1074
+ }
1075
+ return new Promise<void>((decided) => {
1076
+ locks
1077
+ .request(syncLockName(qq), { ifAvailable: true }, (lock) => {
1078
+ if (lock !== null) {
1079
+ decided();
1080
+ return claim(qq).finally(done);
1081
+ }
1082
+ siblings.add(qq);
1083
+ decided();
1084
+ void locks
1085
+ .request(syncLockName(qq), { signal: ac.signal }, () => claim(qq))
1086
+ .catch(noop)
1087
+ .finally(done);
1088
+ })
1089
+ .catch(() => {
1090
+ done();
1091
+ decided();
1092
+ });
1093
+ });
1094
+ }
1095
+
1096
+ function onChannel(event: MessageEvent): void {
1097
+ const m = event.data as ChannelMessage | null;
1098
+ if (stopped || typeof m !== "object" || m === null || typeof m.q !== "string" || m.q === q || mine.has(m.q)) return;
1099
+ if (m.t === "hello") post({ t: "here", q });
1100
+ if (!watching.has(m.q)) void watch(m.q, true);
1101
+ if (m.t === "batch") {
1102
+ const added = showSibling(m.q, [{ q: m.q, n: m.n, docId, base: m.base, sv, ops: m.ops }]);
1103
+ if (added.length > 0 && trusted && C !== null) {
1104
+ mint();
1105
+ const r = renameCollisions(minted, m.ops);
1106
+ // Nothing of ours replays after it: it lands as is.
1107
+ if (r.renames.length === 0 && !P.some((b) => opsOverlap(b.ops, m.ops))) landDirect(m.ops);
1108
+ else rebuild();
1109
+ }
1110
+ } else if (m.t === "drop") {
1111
+ const list = S.get(m.q);
1112
+ if (list !== undefined && list.some((b) => b.n === m.n)) {
1113
+ const rest = list.filter((b) => b.n !== m.n);
1114
+ if (rest.length > 0) S.set(m.q, rest);
1115
+ else S.delete(m.q);
1116
+ rebuild();
1117
+ }
1118
+ }
1119
+ emit();
1120
+ }
1121
+
1122
+ // ── boot ───────────────────────────────────────────────────────────────────
1123
+
1124
+ async function boot(): Promise<void> {
1125
+ if (locks !== null) {
1126
+ void locks.request(syncLockName(q), {}, () => new Promise<void>((resolve) => (releaseOwn = resolve))).catch(noop);
1127
+ }
1128
+ channel?.addEventListener("message", onChannel);
1129
+ post({ t: "hello", q });
1130
+ const none: [null, SyncStoredBatch[], string[]] = [null, [], []];
1131
+ const [c, stored, queues] =
1132
+ cache === null
1133
+ ? none
1134
+ : await Promise.all([cache.confirmed(docId), locks === null ? [] : cache.batchesOf(docId), locks === null ? [] : cache.queuesOf(docId)]).catch(
1135
+ () => none,
1136
+ );
1137
+ if (stopped) return;
1138
+ // The confirmed copy from an earlier visit lets the join ask only for what
1139
+ // it missed; the editor's document is replaced by the view on the reply.
1140
+ if (c !== null && c.sv === sv && C === null) C = { doc: c.doc, seq: c.seq, epoch: c.epoch, rev: c.rev };
1141
+ else if (c !== null && c.sv < sv) oldBase = c.doc;
1142
+ const byQueue = new Map<string, SyncStoredBatch[]>(queues.map((qq) => [qq, []]));
1143
+ for (const b of stored) byQueue.set(b.q, [...(byQueue.get(b.q) ?? []), b]);
1144
+ byQueue.delete(q);
1145
+ await Promise.all([...byQueue.keys()].map((qq) => watch(qq, false)));
1146
+ if (stopped) return;
1147
+ // What the siblings hold right now. Each new batch arrives on the channel.
1148
+ for (const qq of siblings) showSibling(qq, byQueue.get(qq) ?? []);
1149
+ if (o.offline === true && C !== null) {
1150
+ trusted = true;
1151
+ rebuild();
1152
+ }
1153
+ emit();
1154
+ open();
1155
+ }
1156
+ void boot();
1157
+
1158
+ const client: SyncClient = {
1159
+ localChange(doc, scrubbing) {
1160
+ local = doc;
1161
+ if (!scrubbing) mint();
1162
+ else scrubTimer ??= setTimeout(() => {
1163
+ mint();
1164
+ emit();
1165
+ }, SCRUB_MS);
1166
+ emit();
1167
+ },
1168
+ async flush() {
1169
+ mint();
1170
+ emit();
1171
+ if (hold === "upgrade") throw new Error("this Project was upgraded by a newer editor: reload to keep editing");
1172
+ if (hold === "older-server") throw new Error("this editor is newer than the server: editing waits until the server catches up");
1173
+ if (retryAt !== null || stopped) throw new Error("the live session is not connected");
1174
+ await wait(() => joined && P.length === 0);
1175
+ const want = C!.seq;
1176
+ if (savedSeq < want) sock?.send(JSON.stringify({ t: "flush" }));
1177
+ await wait(() => savedSeq >= want);
1178
+ },
1179
+ reach(rev) {
1180
+ return wait(() => revs.includes(rev));
1181
+ },
1182
+ touched(layerId) {
1183
+ const t = touchedAt.get(layerId);
1184
+ return t !== undefined && now() - t < TOUCH_MS;
1185
+ },
1186
+ presence(p) {
1187
+ where = p;
1188
+ sendPresence();
1189
+ },
1190
+ retryNow() {
1191
+ if (stopped || reconnectTimer === null) return;
1192
+ clearTimeout(reconnectTimer);
1193
+ reconnectTimer = null;
1194
+ open();
1195
+ emit();
1196
+ },
1197
+ async retryStorage(prune) {
1198
+ if (cache === null) return true;
1199
+ if (prune) await cache.prune(docId).catch(noop);
1200
+ const loose = P.filter((b) => !b.stored);
1201
+ const full = (): boolean => hold === "quota";
1202
+ if (full()) hold = null;
1203
+ for (const b of loose) {
1204
+ await store(b);
1205
+ if (full()) break;
1206
+ }
1207
+ pump();
1208
+ emit();
1209
+ return !full();
1210
+ },
1211
+ revive() {
1212
+ if (!stopped || closed || hold === "upgrade") return;
1213
+ stopped = false;
1214
+ attempts = 0;
1215
+ open();
1216
+ emit();
1217
+ },
1218
+ close() {
1219
+ stopped = true;
1220
+ closed = true;
1221
+ for (const t of [scrubTimer, retryTimer, reconnectTimer]) if (t !== null) clearTimeout(t);
1222
+ fail("the Project was closed");
1223
+ for (const ac of watching.values()) ac.abort();
1224
+ watching.clear();
1225
+ // Undrained adopted queues go back to IndexedDB's care: the next tab
1226
+ // of this document adopts them.
1227
+ for (const free of holding.values()) free();
1228
+ holding.clear();
1229
+ releaseOwn();
1230
+ try {
1231
+ channel?.close();
1232
+ } catch {
1233
+ // already closed
1234
+ }
1235
+ const s = sock;
1236
+ sock = null;
1237
+ s?.close();
1238
+ },
1239
+ discard() {
1240
+ const queues = new Set([q, ...P.map((b) => b.q)]);
1241
+ P.length = 0;
1242
+ client.close();
1243
+ for (const qq of queues) void cache?.dropQueue(qq).catch(noop);
1244
+ },
1245
+ };
1246
+ return client;
1247
+ }
1248
+
1249
+ /** The browser's socket: same origin, `/api/sync`. */
1250
+ export const browserSyncConnect: SyncConnect = (docId, on) => {
1251
+ const scheme = location.protocol === "https:" ? "wss" : "ws";
1252
+ const ws = new WebSocket(`${scheme}://${location.host}/api/sync?doc=${encodeURIComponent(docId)}`);
1253
+ ws.onopen = () => on.open();
1254
+ ws.onmessage = (e) => on.message(typeof e.data === "string" ? e.data : "");
1255
+ ws.onclose = (e) => on.close(e.code);
1256
+ return { send: (text) => ws.send(text), close: () => ws.close() };
1257
+ };