@jant/core 0.6.14 → 0.6.15

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 (311) hide show
  1. package/bin/commands/import-site.js +81 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-BxCOR3Uc.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
  13. package/dist/client/.vite/manifest.json +3 -3
  14. package/dist/client/_assets/client-B_eGKN5p.css +2 -0
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
  16. package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
  17. package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
  18. package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
  19. package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
  20. package/dist/index.js +3 -3
  21. package/dist/node.js +5 -5
  22. package/package.json +11 -10
  23. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  24. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  25. package/src/__tests__/export-hugo-build.test.ts +63 -4
  26. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  27. package/src/__tests__/export-service.test.ts +142 -9
  28. package/src/__tests__/helpers/app.ts +3 -0
  29. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  30. package/src/__tests__/import-site-command.test.ts +65 -0
  31. package/src/__tests__/vite-config.test.ts +45 -0
  32. package/src/app.tsx +16 -3
  33. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  34. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  35. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  36. package/src/client/__tests__/compose-bridge.test.ts +64 -0
  37. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  38. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  39. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  40. package/src/client/__tests__/toast.test.ts +18 -1
  41. package/src/client/collection-created-notice.ts +66 -0
  42. package/src/client/collection-form-bridge.ts +17 -0
  43. package/src/client/collection-navigation.ts +44 -0
  44. package/src/client/collection-page-actions.ts +57 -1
  45. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  46. package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
  47. package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
  48. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
  49. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  50. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  51. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  52. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  53. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  54. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  55. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  56. package/src/client/components/collection-manager-types.ts +9 -3
  57. package/src/client/components/compose-types.ts +8 -0
  58. package/src/client/components/jant-collection-directory.ts +165 -30
  59. package/src/client/components/jant-command-palette.ts +2 -1
  60. package/src/client/components/jant-compose-dialog.ts +85 -21
  61. package/src/client/components/jant-compose-editor.ts +13 -7
  62. package/src/client/components/jant-compose-fullscreen.ts +24 -4
  63. package/src/client/components/jant-config-editor.ts +722 -0
  64. package/src/client/components/jant-media-lightbox.ts +100 -18
  65. package/src/client/components/jant-nav-manager.ts +1283 -54
  66. package/src/client/components/jant-post-form.ts +3 -3
  67. package/src/client/components/jant-post-menu.ts +86 -74
  68. package/src/client/components/nav-manager-types.ts +51 -2
  69. package/src/client/components/post-form-types.ts +1 -1
  70. package/src/client/compose-bridge.ts +4 -1
  71. package/src/client/feed-video-player.ts +62 -2
  72. package/src/client/footnote-rail.ts +478 -0
  73. package/src/client/hydrate-partial.ts +2 -0
  74. package/src/client/media-lightbox-events.ts +63 -0
  75. package/src/client/site-header-fragment.ts +38 -0
  76. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  77. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  78. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  79. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  80. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  81. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  82. package/src/client/tiptap/bubble-menu.ts +88 -10
  83. package/src/client/tiptap/continuous-lists.ts +102 -0
  84. package/src/client/tiptap/create-editor.ts +3 -0
  85. package/src/client/tiptap/extensions.ts +14 -5
  86. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  87. package/src/client/tiptap/slash-commands.ts +8 -1
  88. package/src/client/tiptap/structural-keymap.ts +211 -0
  89. package/src/client/tiptap/tab-indent.ts +8 -22
  90. package/src/client/tiptap/table-control-labels.ts +30 -0
  91. package/src/client/tiptap/table-controls.ts +537 -0
  92. package/src/client/tiptap/table-size-picker.ts +254 -0
  93. package/src/client/toast.ts +8 -6
  94. package/src/client-auth.ts +1 -0
  95. package/src/client.ts +1 -0
  96. package/src/db/__tests__/d1-query.test.ts +43 -1
  97. package/src/db/__tests__/migration-runner.test.ts +63 -0
  98. package/src/db/__tests__/migrations.test.ts +469 -0
  99. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  100. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  101. package/src/db/migrations/0028_superb_post.sql +2 -0
  102. package/src/db/migrations/0029_boring_magma.sql +1 -0
  103. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  104. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  105. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  106. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  107. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  108. package/src/db/migrations/meta/_journal.json +28 -0
  109. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  110. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  111. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  112. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  113. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  114. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  115. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  116. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  117. package/src/db/migrations/pg/meta/_journal.json +28 -0
  118. package/src/db/pg/schema.ts +28 -20
  119. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  120. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  121. package/src/db/schema.ts +28 -20
  122. package/src/i18n/__tests__/detect.test.ts +8 -2
  123. package/src/i18n/detect.ts +14 -5
  124. package/src/i18n/locales/public/en.po +133 -30
  125. package/src/i18n/locales/public/en.ts +1 -1
  126. package/src/i18n/locales/public/zh-Hans.po +128 -25
  127. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  128. package/src/i18n/locales/public/zh-Hant.po +128 -25
  129. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  130. package/src/i18n/locales/settings/en.po +513 -10
  131. package/src/i18n/locales/settings/en.ts +1 -1
  132. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  133. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  134. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  135. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  136. package/src/lib/__tests__/api-settings.test.ts +299 -0
  137. package/src/lib/__tests__/constants.test.ts +5 -0
  138. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  139. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  140. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  141. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  142. package/src/lib/__tests__/markdown.test.ts +8 -6
  143. package/src/lib/__tests__/navigation.test.ts +59 -2
  144. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  145. package/src/lib/__tests__/post-display.test.ts +64 -0
  146. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  147. package/src/lib/__tests__/summary.test.ts +82 -0
  148. package/src/lib/__tests__/timeline.test.ts +109 -70
  149. package/src/lib/__tests__/timezones.test.ts +31 -3
  150. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  151. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  152. package/src/lib/__tests__/view.test.ts +53 -0
  153. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  154. package/src/lib/api-settings.ts +239 -12
  155. package/src/lib/collection-sort.ts +7 -5
  156. package/src/lib/constants.ts +2 -0
  157. package/src/lib/feed-path.ts +25 -0
  158. package/src/lib/feed-policy.ts +64 -0
  159. package/src/lib/footnotes.ts +264 -0
  160. package/src/lib/github-sync-site-config.ts +2 -0
  161. package/src/lib/hugo-markdown.ts +10 -3
  162. package/src/lib/jant-branding.ts +1 -0
  163. package/src/lib/markdown-manager.ts +188 -5
  164. package/src/lib/markdown.ts +10 -3
  165. package/src/lib/navigation.ts +16 -3
  166. package/src/lib/post-body-html.ts +131 -0
  167. package/src/lib/post-display.ts +7 -1
  168. package/src/lib/render.tsx +9 -2
  169. package/src/lib/resolve-config.ts +100 -16
  170. package/src/lib/schemas.ts +133 -1
  171. package/src/lib/settings-paths.ts +5 -0
  172. package/src/lib/site-skill.ts +78 -0
  173. package/src/lib/summary.ts +16 -5
  174. package/src/lib/timeline.ts +98 -122
  175. package/src/lib/timezones.ts +54 -14
  176. package/src/lib/tiptap-render.ts +355 -81
  177. package/src/lib/view.ts +30 -20
  178. package/src/lib/worker-response-cache.ts +11 -18
  179. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  180. package/src/middleware/config.ts +2 -2
  181. package/src/middleware/public-content-access.ts +87 -0
  182. package/src/node/__tests__/cli-export.test.ts +14 -0
  183. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  184. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  185. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  186. package/src/node/index.ts +2 -1
  187. package/src/preset.css +16 -2
  188. package/src/routes/__tests__/compose.test.ts +51 -0
  189. package/src/routes/__tests__/site-skill.test.ts +112 -0
  190. package/src/routes/api/__tests__/collections.test.ts +91 -19
  191. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  192. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  193. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  194. package/src/routes/api/__tests__/settings.test.ts +217 -0
  195. package/src/routes/api/collections.ts +22 -26
  196. package/src/routes/api/export.ts +2 -0
  197. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  198. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  199. package/src/routes/api/internal/post-body-html.ts +43 -0
  200. package/src/routes/api/internal/sites.ts +24 -0
  201. package/src/routes/api/nav-items.ts +24 -1
  202. package/src/routes/api/posts.ts +2 -2
  203. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  204. package/src/routes/api/public/__tests__/posts.test.ts +208 -0
  205. package/src/routes/api/public/archive.ts +4 -0
  206. package/src/routes/api/public/posts.ts +41 -24
  207. package/src/routes/api/search.ts +3 -0
  208. package/src/routes/api/settings.ts +33 -0
  209. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  210. package/src/routes/compose.tsx +2 -1
  211. package/src/routes/dash/settings.tsx +45 -2
  212. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  213. package/src/routes/feed/feed.ts +23 -14
  214. package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
  215. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  216. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  217. package/src/routes/pages/__tests__/preview.test.ts +174 -0
  218. package/src/routes/pages/__tests__/search.test.ts +69 -0
  219. package/src/routes/pages/archive.tsx +27 -7
  220. package/src/routes/pages/collection.tsx +32 -19
  221. package/src/routes/pages/collections.tsx +6 -1
  222. package/src/routes/pages/page.tsx +83 -11
  223. package/src/routes/site-skill.ts +18 -0
  224. package/src/services/__tests__/auth.test.ts +7 -5
  225. package/src/services/__tests__/collection.test.ts +159 -44
  226. package/src/services/__tests__/navigation.test.ts +229 -21
  227. package/src/services/__tests__/post-timeline.test.ts +109 -30
  228. package/src/services/__tests__/post.test.ts +567 -5
  229. package/src/services/__tests__/search.test.ts +48 -1
  230. package/src/services/__tests__/settings.test.ts +8 -0
  231. package/src/services/__tests__/site-admin.test.ts +121 -1
  232. package/src/services/auth.ts +2 -2
  233. package/src/services/bootstrap.ts +3 -2
  234. package/src/services/collection.ts +226 -167
  235. package/src/services/export-theme/assets/client-site.css +1 -1
  236. package/src/services/export-theme/assets/client-site.js +46 -25
  237. package/src/services/export-theme/layouts/_default/list.html +18 -4
  238. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  239. package/src/services/export-theme/layouts/featured/list.html +5 -4
  240. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  241. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  242. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  243. package/src/services/export-theme/styles/main.css +30 -2
  244. package/src/services/export.ts +91 -80
  245. package/src/services/mcp.ts +37 -27
  246. package/src/services/media.ts +1 -1
  247. package/src/services/navigation.ts +185 -46
  248. package/src/services/post.ts +874 -271
  249. package/src/services/search.ts +8 -2
  250. package/src/services/settings.ts +2 -10
  251. package/src/services/site-admin.ts +63 -43
  252. package/src/style-cjk-jp.css +1 -1
  253. package/src/style-cjk-kr.css +1 -1
  254. package/src/style-cjk-tc.css +1 -1
  255. package/src/style-cjk.css +1 -1
  256. package/src/styles/components.css +540 -0
  257. package/src/styles/site-media.css +111 -19
  258. package/src/styles/tokens.css +14 -9
  259. package/src/styles/ui.css +829 -29
  260. package/src/types/bindings.ts +3 -0
  261. package/src/types/config.ts +243 -12
  262. package/src/types/constants.ts +22 -1
  263. package/src/types/entities.ts +9 -8
  264. package/src/types/operations.ts +18 -10
  265. package/src/types/props.ts +8 -2
  266. package/src/types/raw-assets.d.ts +5 -0
  267. package/src/types/views.ts +16 -4
  268. package/src/ui/__tests__/font-themes.test.ts +42 -30
  269. package/src/ui/compose/ComposeDialog.tsx +92 -2
  270. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  271. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  272. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  273. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  274. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  275. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  276. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  277. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  278. package/src/ui/feed/LinkCard.tsx +2 -12
  279. package/src/ui/feed/NoteCard.tsx +10 -20
  280. package/src/ui/feed/QuoteCard.tsx +2 -12
  281. package/src/ui/feed/ThreadPreview.tsx +26 -34
  282. package/src/ui/feed/TimelineFeed.tsx +2 -2
  283. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  284. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  285. package/src/ui/feed/thread-preview-state.ts +5 -7
  286. package/src/ui/font-themes.ts +74 -26
  287. package/src/ui/layouts/BaseLayout.tsx +22 -8
  288. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  289. package/src/ui/pages/CollectionPage.tsx +101 -22
  290. package/src/ui/pages/CollectionsPage.tsx +3 -1
  291. package/src/ui/pages/PostPage.tsx +39 -12
  292. package/src/ui/pages/SearchPage.tsx +6 -14
  293. package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
  294. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  295. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  296. package/src/ui/shared/CollectionsManager.tsx +17 -7
  297. package/src/ui/shared/DraftPreviewBar.tsx +33 -0
  298. package/src/ui/shared/HomePageBranding.tsx +2 -2
  299. package/src/ui/shared/MediaGallery.tsx +1 -0
  300. package/src/ui/shared/PostFooter.tsx +3 -1
  301. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  302. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  303. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  304. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  305. package/src/ui/shared/collection-management-labels.ts +33 -2
  306. package/src/ui/shared/navigation-labels.ts +2 -4
  307. package/src/ui/shared/post-article-attributes.ts +46 -0
  308. package/dist/app-DZDlmh7C.js +0 -6
  309. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  310. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  311. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -281,7 +281,7 @@ describe("CollectionService", () => {
281
281
  format: "note",
282
282
  bodyMarkdown: "Book note",
283
283
  });
284
- await collectionService.addPost(reading.id, post.id);
284
+ await collectionService.addThread(reading.id, post.id);
285
285
 
286
286
  const directory = await collectionService.listDirectoryData();
287
287
 
@@ -297,7 +297,7 @@ describe("CollectionService", () => {
297
297
  type: "collection",
298
298
  collection: expect.objectContaining({
299
299
  id: reading.id,
300
- postCount: 1,
300
+ threadCount: 1,
301
301
  recentActivityAt: post.lastActivityAt,
302
302
  }),
303
303
  }),
@@ -309,6 +309,30 @@ describe("CollectionService", () => {
309
309
  ]);
310
310
  });
311
311
 
312
+ it("uses a later Root edit as the Thread's recent activity", async () => {
313
+ vi.useFakeTimers();
314
+ try {
315
+ vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
316
+ const collection = await collectionService.create({
317
+ slug: "edited-thread",
318
+ title: "Edited Thread",
319
+ });
320
+ const root = await postService.create({
321
+ format: "note",
322
+ bodyMarkdown: "before",
323
+ });
324
+ await collectionService.addThread(collection.id, root.id);
325
+
326
+ vi.setSystemTime(new Date("2024-01-01T00:01:00Z"));
327
+ await postService.update(root.id, { bodyMarkdown: "after" });
328
+
329
+ const directory = await collectionService.listDirectoryData();
330
+ expect(directory.collections[0]?.recentActivityAt).toBe(1704067260);
331
+ } finally {
332
+ vi.useRealTimers();
333
+ }
334
+ });
335
+
312
336
  it("includes custom links in directory items", async () => {
313
337
  const link = await collectionService.createDirectoryItem({
314
338
  type: "link",
@@ -451,7 +475,7 @@ describe("CollectionService", () => {
451
475
  bodyMarkdown: "test post",
452
476
  });
453
477
 
454
- await collectionService.addPost(collection.id, post.id);
478
+ await collectionService.addThread(collection.id, post.id);
455
479
 
456
480
  // Verify association exists
457
481
  const before = await collectionService.getCollectionsByPostId(post.id);
@@ -736,15 +760,15 @@ describe("CollectionService", () => {
736
760
  });
737
761
  });
738
762
 
739
- describe("getPostCounts", () => {
763
+ describe("getThreadCounts", () => {
740
764
  it("returns empty map when no posts exist", async () => {
741
765
  await collectionService.create({ slug: "empty", title: "Empty" });
742
766
 
743
- const counts = await collectionService.getPostCounts();
767
+ const counts = await collectionService.getThreadCounts();
744
768
  expect(counts.size).toBe(0);
745
769
  });
746
770
 
747
- it("returns correct counts for collections with posts", async () => {
771
+ it("returns correct counts for collections with Threads", async () => {
748
772
  const col1 = await collectionService.create({
749
773
  slug: "col1",
750
774
  title: "Col 1",
@@ -767,16 +791,16 @@ describe("CollectionService", () => {
767
791
  bodyMarkdown: "post 3",
768
792
  });
769
793
 
770
- await collectionService.addPost(col1.id, p1.id);
771
- await collectionService.addPost(col1.id, p2.id);
772
- await collectionService.addPost(col2.id, p3.id);
794
+ await collectionService.addThread(col1.id, p1.id);
795
+ await collectionService.addThread(col1.id, p2.id);
796
+ await collectionService.addThread(col2.id, p3.id);
773
797
 
774
- const counts = await collectionService.getPostCounts();
798
+ const counts = await collectionService.getThreadCounts();
775
799
  expect(counts.get(col1.id)).toBe(2);
776
800
  expect(counts.get(col2.id)).toBe(1);
777
801
  });
778
802
 
779
- it("does not count posts without a collection", async () => {
803
+ it("does not count Threads without a collection", async () => {
780
804
  const col = await collectionService.create({
781
805
  slug: "col",
782
806
  title: "Col",
@@ -791,14 +815,14 @@ describe("CollectionService", () => {
791
815
  bodyMarkdown: "no collection",
792
816
  });
793
817
 
794
- await collectionService.addPost(col.id, p1.id);
818
+ await collectionService.addThread(col.id, p1.id);
795
819
 
796
- const counts = await collectionService.getPostCounts();
820
+ const counts = await collectionService.getThreadCounts();
797
821
  expect(counts.get(col.id)).toBe(1);
798
822
  expect(counts.size).toBe(1);
799
823
  });
800
824
 
801
- it("does not count soft-deleted posts", async () => {
825
+ it("does not count deleted Threads", async () => {
802
826
  const col = await collectionService.create({
803
827
  slug: "col",
804
828
  title: "Col",
@@ -813,19 +837,18 @@ describe("CollectionService", () => {
813
837
  bodyMarkdown: "still alive",
814
838
  });
815
839
 
816
- await collectionService.addPost(col.id, post.id);
817
- await collectionService.addPost(col.id, post2.id);
840
+ await collectionService.addThread(col.id, post.id);
841
+ await collectionService.addThread(col.id, post2.id);
818
842
 
819
- // Soft-delete one post
820
843
  await postService.delete(post.id);
821
844
 
822
- const counts = await collectionService.getPostCounts();
845
+ const counts = await collectionService.getThreadCounts();
823
846
  expect(counts.get(col.id)).toBe(1);
824
847
  });
825
848
  });
826
849
 
827
- describe("addPost / removePost", () => {
828
- it("adds a post to a collection", async () => {
850
+ describe("addThread / removeThread", () => {
851
+ it("adds a Thread to a collection", async () => {
829
852
  const col = await collectionService.create({
830
853
  slug: "test",
831
854
  title: "Test",
@@ -835,7 +858,7 @@ describe("CollectionService", () => {
835
858
  bodyMarkdown: "test",
836
859
  });
837
860
 
838
- await collectionService.addPost(col.id, post.id);
861
+ await collectionService.addThread(col.id, post.id);
839
862
 
840
863
  const collections = await collectionService.getCollectionsByPostId(
841
864
  post.id,
@@ -854,14 +877,14 @@ describe("CollectionService", () => {
854
877
  bodyMarkdown: "test",
855
878
  });
856
879
 
857
- await collectionService.addPost(col.id, post.id);
858
- await collectionService.addPost(col.id, post.id); // duplicate
880
+ await collectionService.addThread(col.id, post.id);
881
+ await collectionService.addThread(col.id, post.id); // duplicate
859
882
 
860
- const postIds = await collectionService.getPostIds(col.id);
861
- expect(postIds).toHaveLength(1);
883
+ const threadIds = await collectionService.getThreadIds(col.id);
884
+ expect(threadIds).toHaveLength(1);
862
885
  });
863
886
 
864
- it("removes a post from a collection", async () => {
887
+ it("removes a Thread from a collection", async () => {
865
888
  const col = await collectionService.create({
866
889
  slug: "test",
867
890
  title: "Test",
@@ -871,14 +894,43 @@ describe("CollectionService", () => {
871
894
  bodyMarkdown: "test",
872
895
  });
873
896
 
874
- await collectionService.addPost(col.id, post.id);
875
- await collectionService.removePost(col.id, post.id);
897
+ await collectionService.addThread(col.id, post.id);
898
+ await collectionService.removeThread(col.id, post.id);
876
899
 
877
900
  const collections = await collectionService.getCollectionsByPostId(
878
901
  post.id,
879
902
  );
880
903
  expect(collections).toHaveLength(0);
881
904
  });
905
+
906
+ it("normalizes Child mutations to the shared Thread root", async () => {
907
+ const collection = await collectionService.create({
908
+ slug: "child-mutation",
909
+ title: "Child Mutation",
910
+ });
911
+ const root = await postService.create({
912
+ format: "note",
913
+ bodyMarkdown: "root",
914
+ });
915
+ const child = await postService.create({
916
+ format: "note",
917
+ bodyMarkdown: "child",
918
+ replyToId: root.id,
919
+ });
920
+
921
+ await collectionService.addThread(collection.id, child.id);
922
+ expect(await collectionService.getThreadIds(collection.id)).toEqual([
923
+ root.id,
924
+ ]);
925
+ expect(
926
+ (await collectionService.getCollectionsByPostId(root.id)).map(
927
+ (item) => item.id,
928
+ ),
929
+ ).toEqual([collection.id]);
930
+
931
+ await collectionService.removeThread(collection.id, child.id);
932
+ expect(await collectionService.getThreadIds(collection.id)).toEqual([]);
933
+ });
882
934
  });
883
935
 
884
936
  describe("getCollectionsByPostId", () => {
@@ -899,8 +951,8 @@ describe("CollectionService", () => {
899
951
  bodyMarkdown: "test",
900
952
  });
901
953
 
902
- await collectionService.addPost(cols[0]?.id ?? "", post.id);
903
- await collectionService.addPost(cols[1]?.id ?? "", post.id);
954
+ await collectionService.addThread(cols[0]?.id ?? "", post.id);
955
+ await collectionService.addThread(cols[1]?.id ?? "", post.id);
904
956
 
905
957
  const collections = await collectionService.getCollectionsByPostId(
906
958
  post.id,
@@ -919,10 +971,70 @@ describe("CollectionService", () => {
919
971
  );
920
972
  expect(collections).toHaveLength(0);
921
973
  });
974
+
975
+ it("projects one shared membership set onto the root and every child", async () => {
976
+ const collection = await collectionService.create({
977
+ slug: "shared-thread",
978
+ title: "Shared Thread",
979
+ });
980
+ const root = await postService.create({
981
+ format: "note",
982
+ bodyMarkdown: "root",
983
+ });
984
+ const child = await postService.create({
985
+ format: "note",
986
+ bodyMarkdown: "child",
987
+ replyToId: root.id,
988
+ });
989
+
990
+ await collectionService.addThread(collection.id, root.id);
991
+
992
+ const [rootCollections, childCollections, batch] = await Promise.all([
993
+ collectionService.getCollectionsByPostId(root.id),
994
+ collectionService.getCollectionsByPostId(child.id),
995
+ collectionService.getCollectionsByPostIds([root.id, child.id]),
996
+ ]);
997
+
998
+ expect(rootCollections.map((item) => item.id)).toEqual([collection.id]);
999
+ expect(childCollections.map((item) => item.id)).toEqual([collection.id]);
1000
+ expect(batch.get(root.id)?.map((item) => item.id)).toEqual([
1001
+ collection.id,
1002
+ ]);
1003
+ expect(batch.get(child.id)?.map((item) => item.id)).toEqual([
1004
+ collection.id,
1005
+ ]);
1006
+ });
1007
+
1008
+ it("keeps shared membership when a Child is deleted and removes it with the Root", async () => {
1009
+ const collection = await collectionService.create({
1010
+ slug: "thread-lifecycle",
1011
+ title: "Thread Lifecycle",
1012
+ });
1013
+ const root = await postService.create({
1014
+ format: "note",
1015
+ bodyMarkdown: "root",
1016
+ });
1017
+ const child = await postService.create({
1018
+ format: "note",
1019
+ bodyMarkdown: "child",
1020
+ replyToId: root.id,
1021
+ });
1022
+ await collectionService.addThread(collection.id, root.id);
1023
+
1024
+ await postService.delete(child.id);
1025
+ expect(
1026
+ (await collectionService.getCollectionsByPostId(root.id)).map(
1027
+ (item) => item.id,
1028
+ ),
1029
+ ).toEqual([collection.id]);
1030
+
1031
+ await postService.delete(root.id);
1032
+ expect(await collectionService.getThreadIds(collection.id)).toEqual([]);
1033
+ });
922
1034
  });
923
1035
 
924
- describe("getPostIds", () => {
925
- it("returns all post IDs in a collection", async () => {
1036
+ describe("getThreadIds", () => {
1037
+ it("returns all Thread root IDs in a collection", async () => {
926
1038
  const col = await collectionService.create({
927
1039
  slug: "test",
928
1040
  title: "Test",
@@ -936,18 +1048,18 @@ describe("CollectionService", () => {
936
1048
  bodyMarkdown: "two",
937
1049
  });
938
1050
 
939
- await collectionService.addPost(col.id, p1.id);
940
- await collectionService.addPost(col.id, p2.id);
1051
+ await collectionService.addThread(col.id, p1.id);
1052
+ await collectionService.addThread(col.id, p2.id);
941
1053
 
942
- const ids = await collectionService.getPostIds(col.id);
1054
+ const ids = await collectionService.getThreadIds(col.id);
943
1055
  expect(ids).toHaveLength(2);
944
1056
  expect(ids).toContain(p1.id);
945
1057
  expect(ids).toContain(p2.id);
946
1058
  });
947
1059
  });
948
1060
 
949
- describe("syncPostCollections", () => {
950
- it("replaces all collection memberships for a post", async () => {
1061
+ describe("syncThreadCollections", () => {
1062
+ it("replaces all collection memberships for a Thread", async () => {
951
1063
  const col1 = await collectionService.create({
952
1064
  slug: "col1",
953
1065
  title: "Col 1",
@@ -967,11 +1079,14 @@ describe("CollectionService", () => {
967
1079
  });
968
1080
 
969
1081
  // Initially in col1 and col2
970
- await collectionService.addPost(col1.id, post.id);
971
- await collectionService.addPost(col2.id, post.id);
1082
+ await collectionService.addThread(col1.id, post.id);
1083
+ await collectionService.addThread(col2.id, post.id);
972
1084
 
973
1085
  // Sync to col2 and col3
974
- await collectionService.syncPostCollections(post.id, [col2.id, col3.id]);
1086
+ await collectionService.syncThreadCollections(post.id, [
1087
+ col2.id,
1088
+ col3.id,
1089
+ ]);
975
1090
 
976
1091
  const collections = await collectionService.getCollectionsByPostId(
977
1092
  post.id,
@@ -993,8 +1108,8 @@ describe("CollectionService", () => {
993
1108
  bodyMarkdown: "test",
994
1109
  });
995
1110
 
996
- await collectionService.addPost(col.id, post.id);
997
- await collectionService.syncPostCollections(post.id, []);
1111
+ await collectionService.addThread(col.id, post.id);
1112
+ await collectionService.syncThreadCollections(post.id, []);
998
1113
 
999
1114
  const collections = await collectionService.getCollectionsByPostId(
1000
1115
  post.id,
@@ -1018,13 +1133,13 @@ describe("CollectionService", () => {
1018
1133
  const originalTransaction = dbWithoutTransaction.transaction.bind(db);
1019
1134
  dbWithoutTransaction.transaction = async () => {
1020
1135
  throw new Error(
1021
- "sqlite syncPostCollections() should not call transaction()",
1136
+ "sqlite syncThreadCollections() should not call transaction()",
1022
1137
  );
1023
1138
  };
1024
1139
 
1025
1140
  try {
1026
1141
  await expect(
1027
- collectionService.syncPostCollections(post.id, [col.id]),
1142
+ collectionService.syncThreadCollections(post.id, [col.id]),
1028
1143
  ).resolves.toBeUndefined();
1029
1144
  } finally {
1030
1145
  dbWithoutTransaction.transaction = originalTransaction;