alinea 2.0.0-preview.11 → 2.0.0-preview.13

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 (268) hide show
  1. package/dist/.init/content/pages/welcome.json +1 -1
  2. package/dist/.mcp/later/dev.json +1 -1
  3. package/dist/.mcp/online/dev.json +1 -1
  4. package/dist/adapter/next/cms.js +16 -8
  5. package/dist/adapter/next/dashboardUrl.d.ts +6 -0
  6. package/dist/adapter/next/dashboardUrl.js +13 -0
  7. package/dist/adapter/next/syncCheck.d.ts +15 -2
  8. package/dist/adapter/next/syncCheck.js +26 -9
  9. package/dist/backend/Handler.js +5 -1
  10. package/dist/backend/api/BlobStore.d.ts +21 -0
  11. package/dist/backend/api/BlobStore.js +63 -0
  12. package/dist/backend/api/CreateBackend.js +10 -8
  13. package/dist/backend/api/DatabaseApi.d.ts +4 -1
  14. package/dist/backend/api/DatabaseApi.js +88 -4
  15. package/dist/backend/api/GithubApi.js +47 -24
  16. package/dist/backend/api/database.js +6 -6
  17. package/dist/bundled.d.ts +687 -248
  18. package/dist/chunks/{bun-sqlite-5SHXN3AT.js → bun-sqlite-RI6BYU5S.js} +3 -3
  19. package/dist/chunks/{chunk-YCQR5GRT.js → chunk-3QFSGTGF.js} +2 -3
  20. package/dist/chunks/{chunk-QJGGYF6E.js → chunk-4ZA6KZHS.js} +5 -5
  21. package/dist/chunks/{chunk-ASYKC6IE.js → chunk-5H4D5JQL.js} +1 -1
  22. package/dist/chunks/{chunk-ETWOY4AL.js → chunk-A2I6MCCS.js} +1 -1
  23. package/dist/chunks/{chunk-W5AXN7GH.js → chunk-CYVOGRSD.js} +1 -2
  24. package/dist/chunks/{chunk-TIJTT5ZI.js → chunk-G43W7AME.js} +1 -0
  25. package/dist/chunks/{chunk-RE523BQ7.js → chunk-HOETF6HN.js} +5829 -5830
  26. package/dist/chunks/{chunk-AIU6CMVT.js → chunk-PTYZMV5G.js} +1 -1
  27. package/dist/chunks/{chunk-JOF663EG.js → chunk-QHHMKJUO.js} +1 -1
  28. package/dist/chunks/{chunk-66YZ47TY.js → chunk-RAR44T5O.js} +1 -1
  29. package/dist/chunks/{chunk-QSN6RH6X.js → chunk-VA6NC3MI.js} +1 -1
  30. package/dist/chunks/{node-sqlite-XVNTNMCY.js → node-sqlite-C7VJ2NSN.js} +3 -3
  31. package/dist/cli/Mcp.js +1 -1
  32. package/dist/cli/Serve.js +1 -1
  33. package/dist/cli/bin.js +2 -3
  34. package/dist/cli/generate/GenerateDashboard.js +17 -5
  35. package/dist/cli/serve/mcp/DevMcp.js +1 -1
  36. package/dist/cli/serve/mcp/McpTools.js +29 -1
  37. package/dist/cli/static/dashboard/entry.js +2 -1
  38. package/dist/cloud/CloudRemote.js +1 -1
  39. package/dist/components/Alert.js +3 -3
  40. package/dist/components/AppShell.js +2 -2
  41. package/dist/components/Badge.d.ts +3 -1
  42. package/dist/components/Badge.js +5 -3
  43. package/dist/components/Breadcrumbs.js +6 -6
  44. package/dist/components/Button.d.ts +6 -1
  45. package/dist/components/Button.js +14 -5
  46. package/dist/components/Calendar.js +20 -20
  47. package/dist/components/Checkbox.js +9 -9
  48. package/dist/components/CheckboxGroup.js +4 -4
  49. package/dist/components/Collapsible.js +3 -3
  50. package/dist/components/ColorSwatch.js +2 -2
  51. package/dist/components/ColorSwatchPicker.js +2 -2
  52. package/dist/components/ComboBox.js +7 -7
  53. package/dist/components/Command.js +10 -9
  54. package/dist/components/ContentCard.d.ts +3 -1
  55. package/dist/components/ContentCard.js +39 -17
  56. package/dist/components/ContentGrid.js +15 -15
  57. package/dist/components/DataList.js +2 -2
  58. package/dist/components/DateField.js +2 -2
  59. package/dist/components/DatePicker.js +2 -2
  60. package/dist/components/DateRangePicker.js +2 -2
  61. package/dist/components/Dialog.js +6 -6
  62. package/dist/components/DropZone.js +4 -4
  63. package/dist/components/DropdownMenu.js +10 -10
  64. package/dist/components/Empty.js +4 -4
  65. package/dist/components/Field.js +8 -8
  66. package/dist/components/List.js +23 -23
  67. package/dist/components/MediaPreview.js +5 -5
  68. package/dist/components/MultipleSelect.js +20 -20
  69. package/dist/components/NavRail.js +4 -4
  70. package/dist/components/NumberField.js +5 -5
  71. package/dist/components/Page.js +5 -5
  72. package/dist/components/Popover.js +2 -2
  73. package/dist/components/PreviewFrame.js +5 -5
  74. package/dist/components/RadioGroup.js +6 -6
  75. package/dist/components/Resizable.js +5 -5
  76. package/dist/components/SearchField.d.ts +6 -1
  77. package/dist/components/SearchField.js +7 -5
  78. package/dist/components/Select.js +14 -14
  79. package/dist/components/Sheet.d.ts +50 -0
  80. package/dist/components/Sheet.js +202 -0
  81. package/dist/components/Sidebar.js +4 -4
  82. package/dist/components/SortableList.d.ts +34 -30
  83. package/dist/components/SortableList.js +162 -265
  84. package/dist/components/Spinner.js +1 -1
  85. package/dist/components/Surface.js +2 -2
  86. package/dist/components/Switch.js +7 -7
  87. package/dist/components/Table.d.ts +3 -1
  88. package/dist/components/Table.js +53 -42
  89. package/dist/components/Tabs.js +5 -5
  90. package/dist/components/TagGroup.js +9 -9
  91. package/dist/components/TextField.js +10 -10
  92. package/dist/components/TimeField.js +8 -8
  93. package/dist/components/Timestamp.js +1 -1
  94. package/dist/components/ToggleGroup.js +2 -2
  95. package/dist/components/Toolbar.js +4 -4
  96. package/dist/components/Tooltip.js +5 -5
  97. package/dist/components/Tree.js +29 -29
  98. package/dist/components/internal/DateControl.js +10 -10
  99. package/dist/components/internal/ListBoxOption.js +13 -13
  100. package/dist/components/internal/Locale.js +2 -2
  101. package/dist/components/internal/PopoverSurface.js +2 -2
  102. package/dist/components/internal/SelectionCheckbox.js +6 -6
  103. package/dist/components/internal/Slot.js +1 -1
  104. package/dist/components/internal/ToggleButton.js +3 -3
  105. package/dist/components/internal/Trigger.js +2 -2
  106. package/dist/components/internal/sortableDrag.d.ts +19 -0
  107. package/dist/components/internal/sortableDrag.js +175 -0
  108. package/dist/components/internal/useDragDrop.js +2 -2
  109. package/dist/components.d.ts +1 -0
  110. package/dist/components.js +1 -0
  111. package/dist/core/Config.d.ts +6 -1
  112. package/dist/core/Document.d.ts +9 -1
  113. package/dist/core/Document.js +31 -6
  114. package/dist/core/Overview.d.ts +2 -4
  115. package/dist/core/TextDoc.d.ts +4 -0
  116. package/dist/core/Validation.d.ts +0 -1
  117. package/dist/core/Validation.js +0 -6
  118. package/dist/core/field/RichTextField.js +23 -4
  119. package/dist/core/media/MediaTypes.js +1 -1
  120. package/dist/core/source/GithubSource.d.ts +4 -0
  121. package/dist/core/source/GithubSource.js +127 -25
  122. package/dist/core/source/GithubUsage.d.ts +21 -0
  123. package/dist/core/source/GithubUsage.js +47 -0
  124. package/dist/dashboard/App.d.ts +2 -2
  125. package/dist/dashboard/App.js +5 -3
  126. package/dist/dashboard/ViewKeys.d.ts +1 -0
  127. package/dist/dashboard/ViewKeys.js +1 -0
  128. package/dist/dashboard/app/ActivityStatus.js +11 -11
  129. package/dist/dashboard/app/AuthView.js +5 -5
  130. package/dist/dashboard/app/BlockSheet.d.ts +50 -0
  131. package/dist/dashboard/app/BlockSheet.js +207 -0
  132. package/dist/dashboard/app/BlockSheetStoryFrame.d.ts +6 -0
  133. package/dist/dashboard/app/BlockSheetStoryFrame.js +29 -0
  134. package/dist/dashboard/app/CompactField.js +15 -15
  135. package/dist/dashboard/app/CroppedImage.js +2 -2
  136. package/dist/dashboard/app/DashboardErrorBoundary.js +3 -3
  137. package/dist/dashboard/app/DashboardLayout.js +3 -1
  138. package/dist/dashboard/app/EntryHeader.js +32 -33
  139. package/dist/dashboard/app/EntryReferences.js +5 -5
  140. package/dist/dashboard/app/EntrySidebar.js +8 -8
  141. package/dist/dashboard/app/EntrySidebarPreview.js +4 -4
  142. package/dist/dashboard/app/EntryStatusIcon.d.ts +23 -0
  143. package/dist/dashboard/app/EntryStatusIcon.js +58 -0
  144. package/dist/dashboard/app/EntryValidationModal.d.ts +6 -5
  145. package/dist/dashboard/app/EntryValidationModal.js +18 -18
  146. package/dist/dashboard/app/Explorer.d.ts +3 -1
  147. package/dist/dashboard/app/Explorer.js +51 -47
  148. package/dist/dashboard/app/ExplorerBatchActions.d.ts +3 -1
  149. package/dist/dashboard/app/ExplorerBatchActions.js +68 -43
  150. package/dist/dashboard/app/ExplorerCards.js +7 -1
  151. package/dist/dashboard/app/ExplorerControls.js +6 -6
  152. package/dist/dashboard/app/ExplorerList.js +5 -5
  153. package/dist/dashboard/app/ExplorerModal.js +1 -1
  154. package/dist/dashboard/app/ExplorerTable.js +14 -11
  155. package/dist/dashboard/app/ImageEditor.js +25 -25
  156. package/dist/dashboard/app/ImagePicker.js +3 -13
  157. package/dist/dashboard/app/LinkPicker.js +29 -27
  158. package/dist/dashboard/app/LocaleMenu.js +7 -7
  159. package/dist/dashboard/app/MediaPreviewCell.js +2 -2
  160. package/dist/dashboard/app/MoveConfirmDialog.d.ts +5 -0
  161. package/dist/dashboard/app/MoveConfirmDialog.js +79 -0
  162. package/dist/dashboard/app/OverviewCell.js +8 -13
  163. package/dist/dashboard/app/ReadOnlyBadge.d.ts +1 -1
  164. package/dist/dashboard/app/RemoveDialog.d.ts +17 -0
  165. package/dist/dashboard/app/{DeleteDialog.js → RemoveDialog.js} +67 -27
  166. package/dist/dashboard/app/RemoveRedirect.d.ts +7 -0
  167. package/dist/dashboard/app/RemoveRedirect.js +123 -0
  168. package/dist/dashboard/app/SearchBar.d.ts +9 -0
  169. package/dist/dashboard/app/SearchBar.js +31 -0
  170. package/dist/dashboard/app/SidebarTree.js +29 -74
  171. package/dist/dashboard/app/WorkspaceMenu.js +8 -8
  172. package/dist/dashboard/app/WorkspaceRoots.js +11 -11
  173. package/dist/dashboard/app/editor/FileEditor.js +16 -16
  174. package/dist/dashboard/app/modals/CreateEntry.js +5 -5
  175. package/dist/dashboard/app/pages/EntryPage.d.ts +2 -1
  176. package/dist/dashboard/app/pages/EntryPage.js +35 -16
  177. package/dist/dashboard/app/pages/SplashPage.js +35 -35
  178. package/dist/dashboard/app/pages/UsersPage.js +14 -14
  179. package/dist/dashboard/app/ui/DashboardModal.js +17 -16
  180. package/dist/dashboard/atoms/dashboard.js +1 -1
  181. package/dist/dashboard/atoms/entry.js +1 -14
  182. package/dist/dashboard/atoms/entryTable.js +1 -0
  183. package/dist/dashboard/atoms/explorer.d.ts +4 -0
  184. package/dist/dashboard/atoms/explorer.js +19 -7
  185. package/dist/dashboard/atoms/graph.js +1 -1
  186. package/dist/dashboard/atoms/link.js +5 -2
  187. package/dist/dashboard/atoms/move.d.ts +5 -0
  188. package/dist/dashboard/atoms/move.js +29 -3
  189. package/dist/dashboard/atoms/moveConfirmation.d.ts +34 -0
  190. package/dist/dashboard/atoms/moveConfirmation.js +95 -0
  191. package/dist/dashboard/atoms/nav.d.ts +9 -5
  192. package/dist/dashboard/atoms/nav.js +120 -45
  193. package/dist/dashboard/atoms/overview.d.ts +6 -5
  194. package/dist/dashboard/atoms/overview.js +2 -16
  195. package/dist/dashboard/atoms/remove.d.ts +112 -0
  196. package/dist/dashboard/atoms/remove.js +268 -0
  197. package/dist/dashboard/atoms/root.d.ts +2 -2
  198. package/dist/dashboard/atoms/root.js +41 -26
  199. package/dist/dashboard/atoms/user.js +1 -1
  200. package/dist/dashboard/atoms/utils.js +1 -1
  201. package/dist/dashboard/boot/Boot.js +25 -17
  202. package/dist/dashboard/boot/BootProd.d.ts +6 -1
  203. package/dist/dashboard/boot/BootProd.js +2 -2
  204. package/dist/dashboard/boot/DashboardWorker.d.ts +1 -0
  205. package/dist/dashboard/boot/DashboardWorker.js +18 -18
  206. package/dist/dashboard/boot/LoadWorker.js +17 -10
  207. package/dist/dashboard/cms/EntryTable.js +6 -3
  208. package/dist/database/BrowserEntryStore.d.ts +17 -29
  209. package/dist/database/BrowserEntryStore.js +116 -144
  210. package/dist/database/DatabaseTables.js +2 -2
  211. package/dist/database/EntryDatabase.js +21 -1
  212. package/dist/database/EntryTransaction.js +0 -35
  213. package/dist/database/EntryUrlAliases.d.ts +4 -0
  214. package/dist/database/EntryUrlAliases.js +16 -10
  215. package/dist/database/driver/RuntimeDatabase.js +3 -3
  216. package/dist/database/driver/WasmDatabase.d.ts +5 -18
  217. package/dist/database/driver/WasmDatabase.js +15 -16
  218. package/dist/database/entry/EntryTable.js +2 -2
  219. package/dist/database/query/EntryQuery.js +1 -1
  220. package/dist/database/query/Relation.js +1 -1
  221. package/dist/database/query/Search.js +2 -2
  222. package/dist/field/check/CheckField.view.js +2 -2
  223. package/dist/field/code/CodeField.view.js +14 -14
  224. package/dist/field/link/EntryLink.d.ts +4 -4
  225. package/dist/field/link/FileLink.d.ts +4 -4
  226. package/dist/field/link/ImageLink.d.ts +4 -4
  227. package/dist/field/link/Link.d.ts +1 -1
  228. package/dist/field/link/LinkField.d.ts +5 -0
  229. package/dist/field/link/LinkField.view.js +246 -372
  230. package/dist/field/link/UrlLink.d.ts +4 -4
  231. package/dist/field/list/ListField.d.ts +2 -0
  232. package/dist/field/list/ListField.view.d.ts +15 -0
  233. package/dist/field/list/ListField.view.js +343 -301
  234. package/dist/field/metadata/MetadataField.d.ts +17 -7
  235. package/dist/field/metadata/MetadataField.js +14 -3
  236. package/dist/field/metadata/MetadataField.view.d.ts +1 -0
  237. package/dist/field/metadata/MetadataField.view.js +27 -23
  238. package/dist/field/richtext/RichTextBlock.d.ts +3 -1
  239. package/dist/field/richtext/RichTextBlock.js +178 -75
  240. package/dist/field/richtext/RichTextBlockHost.js +1 -1
  241. package/dist/field/richtext/RichTextExtensions.js +35 -35
  242. package/dist/field/richtext/RichTextField.view.js +14 -8
  243. package/dist/field/richtext/RichTextInsertMenu.js +43 -27
  244. package/dist/field/views.js +2 -0
  245. package/dist/index.css +490 -309
  246. package/dist/picker/url/UrlPicker.js +1 -0
  247. package/docs/components/badge.md +1 -0
  248. package/docs/components/button.md +1 -0
  249. package/docs/components/content-card.md +1 -0
  250. package/docs/components/search-field.md +1 -0
  251. package/docs/components/sortable-list.md +5 -20
  252. package/docs/components/table.md +1 -0
  253. package/docs/configuration.md +1 -1
  254. package/docs/deploy/self-host.md +1 -1
  255. package/docs/deploy.md +5 -4
  256. package/docs/fields.md +1 -1
  257. package/docs/index.md +1 -1
  258. package/docs/instant-publishing.md +7 -22
  259. package/docs/overviews.md +3 -4
  260. package/docs/upgrading.md +3 -2
  261. package/llms-full.txt +31 -55
  262. package/package.json +2 -3
  263. package/dist/core/db/EntryValidationError.d.ts +0 -13
  264. package/dist/core/db/EntryValidationError.js +0 -23
  265. package/dist/dashboard/app/DeleteDialog.d.ts +0 -11
  266. package/dist/dashboard/atoms/delete.d.ts +0 -68
  267. package/dist/dashboard/atoms/delete.js +0 -143
  268. package/dist/chunks/{chunk-MOWR65LM.js → chunk-PYDOMKJZ.js} +71 -71
@@ -1,5 +1,5 @@
1
1
  {
2
- "_id": "3KHB2PydgBPbUsKveWcaGSXkqjV",
2
+ "_id": "3Kd9mGx2n7q2f3qhMUmFsgaYVyu",
3
3
  "_type": "Page",
4
4
  "_index": "a0",
5
5
  "_seeded": "welcome.json",
@@ -1 +1 @@
1
- {"url":"http://localhost:1","pid":3065}
1
+ {"url":"http://localhost:1","pid":3062}
@@ -1 +1 @@
1
- {"url":"http://localhost:38409","pid":3065}
1
+ {"url":"http://localhost:36863","pid":3062}
@@ -8,10 +8,10 @@ import {
8
8
  applyPreview as applyPreviewUpdate,
9
9
  decodePreviewRequest
10
10
  } from "alinea/backend/resolver/ParsePreview";
11
+ import { dashboardUrl } from "./dashboardUrl.js";
11
12
  import { createThrottledSync } from "alinea/backend/util/Syncable";
12
13
  import { Client } from "alinea/core/Client";
13
14
  import { CMS } from "alinea/core/CMS";
14
- import { Config } from "alinea/core/Config";
15
15
  import {
16
16
  withDevelopmentKey
17
17
  } from "alinea/core/Connection";
@@ -24,7 +24,7 @@ import { cache } from "react";
24
24
  import { requestContext } from "./context.js";
25
25
  import { cachedResolve, developmentRevision } from "./devCache.js";
26
26
  import { RenderStats, summarizeQuery, timed } from "./renderStats.js";
27
- import { syncIfStale } from "./syncCheck.js";
27
+ import { contentShaWindow, syncIfStale, tagContentSha } from "./syncCheck.js";
28
28
  import { jsx } from "react/jsx-runtime";
29
29
  var preValidatedRemote = { validate: false };
30
30
  var NextCMS = class extends CMS {
@@ -66,6 +66,10 @@ var NextCMS = class extends CMS {
66
66
  * returns a fresh holder, so nothing is recorded there.
67
67
  */
68
68
  #render = cache(() => ({}));
69
+ /** Build renders join the content sha tag once, see `tagContentSha`. */
70
+ #tagContent = cache(
71
+ () => tagContentSha(contentShaWindow(this.config.syncInterval))
72
+ );
69
73
  /** One content revision per render, see `developmentRevision`. */
70
74
  #devRevision = cache(() => developmentRevision());
71
75
  #isDraft = cache(async () => {
@@ -201,12 +205,19 @@ var NextCMS = class extends CMS {
201
205
  const answer = () => stats && row ? stats.executed(row, () => db.resolve(request)) : db.resolve(request);
202
206
  const syncInterval = request.disableSync ? Number.POSITIVE_INFINITY : request.syncInterval ?? this.config.syncInterval;
203
207
  if (hasPreview) return answer();
204
- if (!isBuild) {
208
+ if (isBuild) {
209
+ await this.#tagContent();
210
+ } else {
205
211
  const tracked = {
206
212
  sha: db.sha,
207
213
  syncWith: () => this.#syncDb(db, client, stats)
208
214
  };
209
- const settled = !isDraft && await syncIfStale(tracked, client, syncInterval);
215
+ const settled = !isDraft && await syncIfStale(
216
+ tracked,
217
+ client,
218
+ syncInterval,
219
+ contentShaWindow(this.config.syncInterval)
220
+ );
210
221
  if (!settled)
211
222
  await this.throttle(() => this.#syncDb(db, client, stats), syncInterval);
212
223
  }
@@ -264,16 +275,13 @@ var NextCMS = class extends CMS {
264
275
  if (!stats) return null;
265
276
  const { default: dynamic } = await import("next/dynamic.js");
266
277
  const { isDev, handlerUrl } = await requestContext(this.config);
267
- let adminPath = Config.adminPath(this.config);
268
- if (!adminPath.startsWith("/")) adminPath = `/${adminPath}`;
269
- const dashboardUrl = isDev ? adminPath : new URL(`${adminPath}.html`, handlerUrl).href;
270
278
  const NextPreviews = dynamic(() => import("./previews.js"), {
271
279
  ssr: false
272
280
  });
273
281
  return /* @__PURE__ */ jsx(
274
282
  NextPreviews,
275
283
  {
276
- dashboardUrl,
284
+ dashboardUrl: dashboardUrl(this.config, isDev, handlerUrl),
277
285
  widget,
278
286
  stats: widget && showStats ? stats.settled() : void 0,
279
287
  workspace,
@@ -0,0 +1,6 @@
1
+ import { Config } from 'alinea/core/Config';
2
+ /**
3
+ * The dashboard as the site serves it: in development the site proxies the
4
+ * admin path to the dev server, in production it serves the generated file.
5
+ */
6
+ export declare function dashboardUrl(config: Config, isDev: boolean, handlerUrl: URL | string): string;
@@ -0,0 +1,13 @@
1
+ import "../../chunks/chunk-CQ52GEHY.js";
2
+
3
+ // src/adapter/next/dashboardUrl.ts
4
+ import { Config } from "alinea/core/Config";
5
+ function dashboardUrl(config, isDev, handlerUrl) {
6
+ if (!isDev)
7
+ return new URL(`/${Config.dashboardFile(config)}`, handlerUrl).href;
8
+ const adminPath = Config.adminPath(config);
9
+ return adminPath.startsWith("/") ? adminPath : `/${adminPath}`;
10
+ }
11
+ export {
12
+ dashboardUrl
13
+ };
@@ -6,13 +6,26 @@ interface SyncableDB {
6
6
  syncWith(source: RemoteSource, options?: SyncOptions): Promise<string>;
7
7
  }
8
8
  export declare const CONTENT_SHA_TAG = "alinea-content-sha";
9
+ /**
10
+ * How long renders share the latest content sha. Configured through the
11
+ * config's `syncInterval`, never a query's: all renders share one entry.
12
+ */
13
+ export declare function contentShaWindow(syncInterval?: number): number;
14
+ /**
15
+ * Tag a static render with the content sha without looking it up. Renders at
16
+ * build skip `syncIfStale`, so this is how their pages join the tag that
17
+ * commits revalidate, and get the same time-based safety net as pages
18
+ * rendered at runtime.
19
+ */
20
+ export declare function tagContentSha(revalidate?: number): Promise<void>;
9
21
  export declare function revalidateContentSha(): Promise<void>;
10
22
  /**
11
23
  * Sync the isolate-local db only when it is behind the shared content sha.
12
24
  * Returns true when the freshness question is settled here (synced, forced,
13
25
  * or explicitly disabled) and the caller can skip its throttled sync.
14
26
  * Returns false when the sha could not be determined, so the caller falls
15
- * back to its existing throttle behavior.
27
+ * back to its existing throttle behavior. `shaWindow` is how long renders
28
+ * share the latest sha, see `contentShaWindow`.
16
29
  */
17
- export declare function syncIfStale(db: SyncableDB, client: Client, syncInterval?: number, options?: SyncOptions): Promise<boolean>;
30
+ export declare function syncIfStale(db: SyncableDB, client: Client, syncInterval?: number, shaWindow?: number, options?: SyncOptions): Promise<boolean>;
18
31
  export {};
@@ -2,22 +2,27 @@ import "../../chunks/chunk-CQ52GEHY.js";
2
2
 
3
3
  // src/adapter/next/syncCheck.ts
4
4
  var CONTENT_SHA_TAG = "alinea-content-sha";
5
- var SHA_REVALIDATE_SECONDS = 60;
5
+ var SHA_REVALIDATE_SECONDS = 300;
6
+ function contentShaWindow(syncInterval) {
7
+ if (syncInterval !== void 0 && syncInterval > 0 && Number.isFinite(syncInterval))
8
+ return syncInterval;
9
+ return SHA_REVALIDATE_SECONDS;
10
+ }
6
11
  var inflight;
7
- function latestSha(client) {
12
+ function latestSha(client, revalidate) {
8
13
  if (inflight) return inflight;
9
- inflight = loadLatestSha(client).finally(() => {
14
+ inflight = loadLatestSha(client, revalidate).finally(() => {
10
15
  inflight = void 0;
11
16
  });
12
17
  return inflight;
13
18
  }
14
- async function loadLatestSha(client) {
19
+ async function loadLatestSha(client, revalidate) {
15
20
  try {
16
21
  const { unstable_cache } = await import("next/cache.js");
17
22
  const getSha = unstable_cache(
18
23
  () => client.getSha(),
19
24
  ["alinea-content-sha"],
20
- { revalidate: SHA_REVALIDATE_SECONDS, tags: [CONTENT_SHA_TAG] }
25
+ { revalidate, tags: [CONTENT_SHA_TAG] }
21
26
  );
22
27
  return await getSha();
23
28
  } catch {
@@ -28,26 +33,38 @@ async function loadLatestSha(client) {
28
33
  }
29
34
  }
30
35
  }
36
+ async function tagContentSha(revalidate = SHA_REVALIDATE_SECONDS) {
37
+ try {
38
+ const { unstable_cache } = await import("next/cache.js");
39
+ await unstable_cache(async () => true, ["alinea-content-sha-tag"], {
40
+ revalidate,
41
+ tags: [CONTENT_SHA_TAG]
42
+ })();
43
+ } catch {
44
+ }
45
+ }
31
46
  async function revalidateContentSha() {
32
47
  try {
33
48
  const { revalidateTag } = await import("next/cache.js");
34
- revalidateTag(CONTENT_SHA_TAG, { expire: 0 });
49
+ revalidateTag(CONTENT_SHA_TAG, "max");
35
50
  } catch {
36
51
  }
37
52
  }
38
- async function syncIfStale(db, client, syncInterval, options) {
53
+ async function syncIfStale(db, client, syncInterval, shaWindow = SHA_REVALIDATE_SECONDS, options) {
39
54
  if (syncInterval === Number.POSITIVE_INFINITY) return true;
40
55
  if (syncInterval === 0) {
41
56
  await db.syncWith(client, options);
42
57
  return true;
43
58
  }
44
- const expected = await latestSha(client);
59
+ const expected = await latestSha(client, shaWindow);
45
60
  if (!expected) return false;
46
61
  if (expected !== await db.sha) await db.syncWith(client, options);
47
62
  return true;
48
63
  }
49
64
  export {
50
65
  CONTENT_SHA_TAG,
66
+ contentShaWindow,
51
67
  revalidateContentSha,
52
- syncIfStale
68
+ syncIfStale,
69
+ tagContentSha
53
70
  };
@@ -328,7 +328,11 @@ function createHandler({
328
328
  }
329
329
  if (action === HandleAction.Blob && request.method === "POST") {
330
330
  const { shas } = object({ shas: array(string) })(await body);
331
- const tree = await local.source.getTree();
331
+ let tree = await local.source.getTree();
332
+ if (!dev && shas.some((sha) => !tree.hasSha(sha))) {
333
+ await syncForRead(cnx);
334
+ tree = await local.source.getTree();
335
+ }
332
336
  const fromLocal = [];
333
337
  const fromRemote = [];
334
338
  for (const sha of shas) {
@@ -0,0 +1,21 @@
1
+ import type { CommitApi, SyncApi } from 'alinea/core/Connection';
2
+ /**
3
+ * Content blobs kept where every handler instance finds them, such as the
4
+ * backend database: an instance that is behind reads what another one
5
+ * fetched or committed, instead of fetching it from the remote again.
6
+ */
7
+ export interface BlobStore {
8
+ /** The stored blobs among `shas`. */
9
+ readBlobs(shas: ReadonlyArray<string>): Promise<Map<string, Uint8Array>>;
10
+ /** Keep blobs, found again by their sha. */
11
+ storeBlobs(blobs: ReadonlyArray<[sha: string, blob: Uint8Array]>): Promise<void>;
12
+ }
13
+ export declare function isBlobStore<T extends object>(value: T): value is T & BlobStore;
14
+ /**
15
+ * Answer from the store what it has, and fetch the rest from the remote,
16
+ * storing it. The store only saves requests: when it fails, the remote
17
+ * answers instead.
18
+ */
19
+ export declare function storedBlobs(store: BlobStore, getBlobs: SyncApi['getBlobs']): SyncApi['getBlobs'];
20
+ /** Commit through the remote, and store the committed content. */
21
+ export declare function storedWrites(store: BlobStore, write: CommitApi['write']): CommitApi['write'];
@@ -0,0 +1,63 @@
1
+ import "../../chunks/chunk-CQ52GEHY.js";
2
+
3
+ // src/backend/api/BlobStore.ts
4
+ import { hashBlob } from "alinea/core/source/GitUtils";
5
+ var storeBatch = 100;
6
+ function isBlobStore(value) {
7
+ return "readBlobs" in value && typeof value.readBlobs === "function" && "storeBlobs" in value && typeof value.storeBlobs === "function";
8
+ }
9
+ function storedBlobs(store, getBlobs) {
10
+ return async function* (shas, options) {
11
+ const stored = await store.readBlobs(shas).catch((error) => {
12
+ console.warn("Alinea could not read stored blobs", error);
13
+ return /* @__PURE__ */ new Map();
14
+ });
15
+ for (const sha of shas) {
16
+ const blob = stored.get(sha);
17
+ if (blob) yield [sha, blob];
18
+ }
19
+ const missing = shas.filter((sha) => !stored.has(sha));
20
+ if (missing.length === 0) return;
21
+ let fetched = Array();
22
+ const keep = async () => {
23
+ const blobs = fetched;
24
+ fetched = [];
25
+ if (blobs.length > 0) await keepBlobs(store, blobs);
26
+ };
27
+ try {
28
+ for await (const entry of getBlobs(missing, options)) {
29
+ fetched.push(entry);
30
+ if (fetched.length >= storeBatch) await keep();
31
+ yield entry;
32
+ }
33
+ } finally {
34
+ await keep();
35
+ }
36
+ };
37
+ }
38
+ function storedWrites(store, write) {
39
+ return async (request) => {
40
+ const result = await write(request);
41
+ const encoder = new TextEncoder();
42
+ const blobs = Array();
43
+ for (const change of request.changes) {
44
+ if (change.op !== "addContent") continue;
45
+ const blob = encoder.encode(change.contents);
46
+ if (await hashBlob(blob) === change.sha) blobs.push([change.sha, blob]);
47
+ }
48
+ if (blobs.length > 0) await keepBlobs(store, blobs);
49
+ return result;
50
+ };
51
+ }
52
+ async function keepBlobs(store, blobs) {
53
+ try {
54
+ await store.storeBlobs(blobs);
55
+ } catch (error) {
56
+ console.warn("Alinea could not store blobs", error);
57
+ }
58
+ }
59
+ export {
60
+ isBlobStore,
61
+ storedBlobs,
62
+ storedWrites
63
+ };
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  driver_exports
3
- } from "../../chunks/chunk-QJGGYF6E.js";
4
- import "../../chunks/chunk-QSN6RH6X.js";
5
- import "../../chunks/chunk-ETWOY4AL.js";
6
- import "../../chunks/chunk-66YZ47TY.js";
7
- import "../../chunks/chunk-AIU6CMVT.js";
3
+ } from "../../chunks/chunk-4ZA6KZHS.js";
4
+ import "../../chunks/chunk-VA6NC3MI.js";
5
+ import "../../chunks/chunk-A2I6MCCS.js";
6
+ import "../../chunks/chunk-RAR44T5O.js";
7
+ import "../../chunks/chunk-PTYZMV5G.js";
8
8
  import "../../chunks/chunk-N47Y62CI.js";
9
9
  import "../../chunks/chunk-QBPYWU6C.js";
10
10
  import "../../chunks/chunk-2BJ2IFQM.js";
11
11
  import "../../chunks/chunk-XCZXR3C2.js";
12
- import "../../chunks/chunk-MOWR65LM.js";
12
+ import "../../chunks/chunk-PYDOMKJZ.js";
13
13
  import "../../chunks/chunk-76VMZTLN.js";
14
14
  import "../../chunks/chunk-2PIFHEYR.js";
15
15
  import "../../chunks/chunk-K62JP6HR.js";
@@ -20,6 +20,7 @@ import "../../chunks/chunk-CQ52GEHY.js";
20
20
  // src/backend/api/CreateBackend.ts
21
21
  import { assert } from "alinea/core/util/Assert";
22
22
  import { BasicAuth } from "./BasicAuth.js";
23
+ import { isBlobStore, storedBlobs, storedWrites } from "./BlobStore.js";
23
24
  import { DatabaseApi } from "./DatabaseApi.js";
24
25
  import { GithubApi } from "./GithubApi.js";
25
26
  import { OAuth2 } from "./OAuth2.js";
@@ -65,13 +66,14 @@ function composeBackend(...impl) {
65
66
  throw new Error(`Backend does not implement ${name}`);
66
67
  };
67
68
  };
69
+ const blobStore = impl.find(isBlobStore);
68
70
  return {
69
71
  authenticate: call("authenticate"),
70
72
  verify: call("verify"),
71
73
  capabilities: call("capabilities"),
72
74
  getTreeIfDifferent: call("getTreeIfDifferent"),
73
- getBlobs: call("getBlobs"),
74
- write: call("write"),
75
+ getBlobs: blobStore ? storedBlobs(blobStore, call("getBlobs")) : call("getBlobs"),
76
+ write: blobStore ? storedWrites(blobStore, call("write")) : call("write"),
75
77
  revisions: call("revisions"),
76
78
  revisionData: call("revisionData"),
77
79
  getDraft: call("getDraft"),
@@ -2,10 +2,11 @@ import type { DraftsApi, RequestContext, UploadResponse, UploadsApi, UserApi } f
2
2
  import { type Draft, type DraftKey } from 'alinea/core/Draft';
3
3
  import type { User, UserInput } from 'alinea/core/User';
4
4
  import { type Database } from 'rado';
5
+ import type { BlobStore } from './BlobStore.js';
5
6
  export interface DatabaseOptions {
6
7
  db: Database;
7
8
  }
8
- export declare class DatabaseApi implements DraftsApi, UploadsApi, UserApi {
9
+ export declare class DatabaseApi implements DraftsApi, UploadsApi, UserApi, BlobStore {
9
10
  #private;
10
11
  constructor(context: RequestContext, { db }: DatabaseOptions);
11
12
  getDraft(draftKey: DraftKey): Promise<Draft | undefined>;
@@ -13,6 +14,8 @@ export declare class DatabaseApi implements DraftsApi, UploadsApi, UserApi {
13
14
  prepareUpload(file: string): Promise<UploadResponse>;
14
15
  handleUpload(entryId: string, file: Blob): Promise<void>;
15
16
  previewUpload(entryId: string): Promise<Response>;
17
+ readBlobs(shas: ReadonlyArray<string>): Promise<Map<string, Uint8Array>>;
18
+ storeBlobs(blobs: ReadonlyArray<[sha: string, blob: Uint8Array]>): Promise<void>;
16
19
  enrichUser(user: User): Promise<User>;
17
20
  listUsers(): Promise<Array<User>>;
18
21
  createUser(user: UserInput): Promise<User>;
@@ -7,16 +7,21 @@ import {
7
7
  integer,
8
8
  text,
9
9
  varchar
10
- } from "../../chunks/chunk-ASYKC6IE.js";
10
+ } from "../../chunks/chunk-5H4D5JQL.js";
11
11
  import {
12
12
  Builder
13
13
  } from "../../chunks/chunk-2BJ2IFQM.js";
14
14
  import {
15
- eq
15
+ and,
16
+ eq,
17
+ inArray,
18
+ lt
16
19
  } from "../../chunks/chunk-XCZXR3C2.js";
17
20
  import {
21
+ Column,
22
+ ColumnType,
18
23
  table
19
- } from "../../chunks/chunk-MOWR65LM.js";
24
+ } from "../../chunks/chunk-PYDOMKJZ.js";
20
25
  import "../../chunks/chunk-76VMZTLN.js";
21
26
  import "../../chunks/chunk-2PIFHEYR.js";
22
27
  import "../../chunks/chunk-K62JP6HR.js";
@@ -33,6 +38,7 @@ import "../../chunks/chunk-CQ52GEHY.js";
33
38
  import { createId } from "alinea/core/Id";
34
39
  import { assert } from "alinea/core/util/Assert";
35
40
  import { basename, extname } from "alinea/core/util/Paths";
41
+ import { chunks } from "alinea/core/util/Arrays";
36
42
  import { slugify } from "alinea/core/util/Slugs";
37
43
  import { HandleAction } from "../HandleAction.js";
38
44
  var UploadTable = table("alinea_upload", {
@@ -48,7 +54,17 @@ var UserRoleTable = table("alinea_user_role", {
48
54
  userId: integer().notNull().references(UserTable.id),
49
55
  role: text().notNull()
50
56
  });
51
- var tables = [UploadTable, UserTable, UserRoleTable];
57
+ var BlobTable = table("alinea_blob", {
58
+ sha: varchar(void 0, { length: 40 }).primaryKey(),
59
+ content: largeBlob().notNull(),
60
+ /** When the blob was last stored or read, in seconds */
61
+ usedAt: integer().notNull()
62
+ });
63
+ var tables = [UploadTable, UserTable, UserRoleTable, BlobTable];
64
+ var blobsPerStatement = 50;
65
+ var day = 24 * 60 * 60;
66
+ var blobRetention = 30 * day;
67
+ var blobCleanups = /* @__PURE__ */ new WeakMap();
52
68
  var preparedDatabases = /* @__PURE__ */ new WeakMap();
53
69
  var selectUser = {
54
70
  ...UserTable,
@@ -108,6 +124,35 @@ var DatabaseApi = class {
108
124
  }
109
125
  });
110
126
  }
127
+ async readBlobs(shas) {
128
+ const db = await this.#db;
129
+ const found = /* @__PURE__ */ new Map();
130
+ for (const batch of chunks(shas, blobsPerStatement)) {
131
+ const rows = await db.select({ sha: BlobTable.sha, content: BlobTable.content }).from(BlobTable).where(inArray(BlobTable.sha, batch));
132
+ for (const row of rows) found.set(row.sha, row.content);
133
+ }
134
+ const now = seconds();
135
+ for (const batch of chunks([...found.keys()], blobsPerStatement))
136
+ await renewBlobs(db, batch, now);
137
+ await removeUnusedBlobs(db, now);
138
+ return found;
139
+ }
140
+ async storeBlobs(blobs) {
141
+ const db = await this.#db;
142
+ const now = seconds();
143
+ const unique = [...new Map(blobs)];
144
+ for (const batch of chunks(unique, blobsPerStatement)) {
145
+ const shas = batch.map(([sha]) => sha);
146
+ const stored = new Set(
147
+ await db.select(BlobTable.sha).from(BlobTable).where(inArray(BlobTable.sha, shas))
148
+ );
149
+ await renewBlobs(db, [...stored], now);
150
+ const rows = batch.filter(([sha]) => !stored.has(sha)).map(([sha, content]) => ({ sha, content, usedAt: now }));
151
+ if (rows.length === 0) continue;
152
+ await db.insert(BlobTable).values(rows).catch(() => insertMissingBlobs(db, rows));
153
+ }
154
+ await removeUnusedBlobs(db, now);
155
+ }
111
156
  async enrichUser(user) {
112
157
  const normalized = normalizeUser(user);
113
158
  const found = await this.#getUser(normalized.email);
@@ -191,6 +236,45 @@ function prepareDatabase(db) {
191
236
  preparedDatabases.set(db, prepared);
192
237
  return prepared;
193
238
  }
239
+ function largeBlob() {
240
+ return new Column({
241
+ type: new ColumnType(
242
+ "blob",
243
+ [],
244
+ sql.universal({
245
+ postgres: sql`bytea`,
246
+ mysql: sql`longblob`,
247
+ default: sql`blob`
248
+ })
249
+ ),
250
+ mapFromDriverValue(value) {
251
+ return new Uint8Array(value);
252
+ }
253
+ });
254
+ }
255
+ async function renewBlobs(db, shas, now) {
256
+ if (shas.length === 0) return;
257
+ await db.update(BlobTable).set({ usedAt: now }).where(and(inArray(BlobTable.sha, shas), lt(BlobTable.usedAt, now - day)));
258
+ }
259
+ async function insertMissingBlobs(db, rows) {
260
+ for (const row of rows) {
261
+ try {
262
+ await db.insert(BlobTable).values(row);
263
+ } catch (error) {
264
+ const stored = await db.select(BlobTable.sha).from(BlobTable).where(eq(BlobTable.sha, row.sha)).get();
265
+ if (!stored) throw error;
266
+ }
267
+ }
268
+ }
269
+ async function removeUnusedBlobs(db, now) {
270
+ const last = blobCleanups.get(db) ?? 0;
271
+ if (now - last < 60 * 60) return;
272
+ blobCleanups.set(db, now);
273
+ await db.delete(BlobTable).where(lt(BlobTable.usedAt, now - blobRetention));
274
+ }
275
+ function seconds() {
276
+ return Math.floor(Date.now() / 1e3);
277
+ }
194
278
  async function enablePostgresRowLevelSecurity(db) {
195
279
  if (db.dialect.runtime !== "postgres") return;
196
280
  for (const table2 of tables) {