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
@@ -9,13 +9,16 @@ import { getScope } from "alinea/core/Scope";
9
9
  import { trigger } from "alinea/core/Trigger";
10
10
  import { IndexEvent } from "alinea/core/db/IndexEvent";
11
11
  import { EntryUrlConflictError } from "alinea/core/db/EntryUrlConflictError";
12
- import { EntryValidationError } from "alinea/core/db/EntryValidationError";
13
- import { BrowserEntryStore } from "alinea/database/BrowserEntryStore";
12
+ import {
13
+ BrowserEntryStore,
14
+ isCorruptDatabaseError
15
+ } from "alinea/database/BrowserEntryStore";
14
16
  import { EntryStore } from "alinea/database/EntryStore";
15
17
  import {
16
18
  ActivityEvent
17
19
  } from "./ActivityEvent.js";
18
20
  var remote = pLimit(1);
21
+ var cacheName = "alinea-entry-database";
19
22
  var syncInterval = 12e4;
20
23
  var activityHistoryLimit = 100;
21
24
  var DashboardWorker = class extends EventTarget {
@@ -24,14 +27,13 @@ var DashboardWorker = class extends EventTarget {
24
27
  #localClient;
25
28
  #nextLoad = trigger();
26
29
  #defer;
27
- /** The store kept in IndexedDB, which a replacement takes over from. */
28
- #stored;
29
30
  #currentRevision;
30
31
  #mutations = [];
31
32
  #activities = [];
32
33
  #local = pLimit(1);
33
34
  #blocked = false;
34
35
  #syncInterval;
36
+ /** Keep the content in memory with `source`, instead of in the browser. */
35
37
  constructor(source) {
36
38
  super();
37
39
  this.#source = source;
@@ -114,7 +116,7 @@ Remote updates: ${remote2}`
114
116
  void this.#flush(item);
115
117
  return item.sha;
116
118
  } catch (error) {
117
- if (error instanceof EntryUrlConflictError || error instanceof EntryValidationError) {
119
+ if (error instanceof EntryUrlConflictError) {
118
120
  this.#cancelMutation(item, error);
119
121
  throw error;
120
122
  }
@@ -278,17 +280,17 @@ Remote updates: ${remote2}`
278
280
  this.#localDB = void 0;
279
281
  this.#localClient = void 0;
280
282
  try {
281
- const db = globalThis.indexedDB ? await BrowserEntryStore.open(config, {
282
- name: "alinea-entry-database",
283
- revision,
284
- replaces: this.#stored
285
- }) : await EntryStore.memory(config, this.#fallbackSource());
286
- if (db instanceof BrowserEntryStore) this.#stored = db;
283
+ let db = await this.#open(config, revision, false);
287
284
  if (this.#defer)
288
285
  void this.#defer().catch(() => {
289
286
  });
290
287
  this.#defer = void 0;
291
- const cacheFailure = await this.#syncLocalIndex(db);
288
+ let cacheFailure = await this.#syncLocalIndex(db);
289
+ if (cacheFailure && isCorruptDatabaseError(cacheFailure.error)) {
290
+ await db.close();
291
+ db = await this.#open(config, revision, true);
292
+ cacheFailure = await this.#syncLocalIndex(db);
293
+ }
292
294
  if (hadPrevious && (await db.source.getTree()).isEmpty)
293
295
  await this.#syncWithClient(db, client).catch(() => {
294
296
  });
@@ -306,8 +308,7 @@ Remote updates: ${remote2}`
306
308
  });
307
309
  this.#defer = async () => {
308
310
  unsubscribe();
309
- if (db instanceof BrowserEntryStore) await db.abandon();
310
- else await db.close();
311
+ await db.close();
311
312
  };
312
313
  } catch (cause) {
313
314
  this.#currentRevision = void 0;
@@ -316,10 +317,9 @@ Remote updates: ${remote2}`
316
317
  throw cause;
317
318
  }
318
319
  }
319
- #fallbackSource() {
320
- if (!this.#source)
321
- throw new Error("A source is required when IndexedDB is unavailable");
322
- return this.#source;
320
+ #open(config, fingerprint, reset) {
321
+ if (this.#source) return EntryStore.memory(config, this.#source);
322
+ return BrowserEntryStore.open(config, { name: cacheName, fingerprint, reset });
323
323
  }
324
324
  async #syncLocalIndex(db) {
325
325
  const sourceTree = await db.source.getTree();
@@ -5,19 +5,14 @@ import "../../chunks/chunk-CQ52GEHY.js";
5
5
 
6
6
  // src/dashboard/boot/LoadWorker.ts
7
7
  import { IndexEvent } from "alinea/core/db/IndexEvent";
8
- import { MemorySource } from "alinea/core/source/MemorySource";
9
8
  import { ActivityEvent } from "./ActivityEvent.js";
10
9
  import { DashboardWorker } from "./DashboardWorker.js";
11
10
  async function loadWorker(gen) {
12
- const worker = new DashboardWorker(new MemorySource());
13
- addEventListener("connect", (event) => {
14
- if (!(event instanceof MessageEvent)) return;
15
- console.info("Worker connected");
16
- const port = event.ports[0];
17
- expose(worker, port);
18
- const listen = (event2) => {
11
+ const worker = new DashboardWorker();
12
+ function forward(target) {
13
+ const listen = (event) => {
19
14
  try {
20
- port.postMessage({ ...event2, type: event2.type });
15
+ target.postMessage({ ...event, type: event.type });
21
16
  } catch {
22
17
  worker.removeEventListener(IndexEvent.type, listen);
23
18
  worker.removeEventListener(ActivityEvent.type, listen);
@@ -25,7 +20,19 @@ async function loadWorker(gen) {
25
20
  };
26
21
  worker.addEventListener(IndexEvent.type, listen);
27
22
  worker.addEventListener(ActivityEvent.type, listen);
28
- });
23
+ }
24
+ if ("onconnect" in globalThis) {
25
+ addEventListener("connect", (event) => {
26
+ if (!(event instanceof MessageEvent)) return;
27
+ console.info("Worker connected");
28
+ const port = event.ports[0];
29
+ expose(worker, port);
30
+ forward(port);
31
+ });
32
+ } else {
33
+ expose(worker, globalThis);
34
+ forward(globalThis);
35
+ }
29
36
  for await (const batch of gen) {
30
37
  await worker.load(batch.configFingerprint, batch.config, batch.client);
31
38
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  unwrap
3
- } from "../../chunks/chunk-TIJTT5ZI.js";
3
+ } from "../../chunks/chunk-G43W7AME.js";
4
4
  import {
5
5
  useAtomValueRaw,
6
6
  useAtomValueRawSync,
@@ -37,6 +37,7 @@ import {
37
37
  import { Schema } from "alinea/core/Schema";
38
38
  import { getScope } from "alinea/core/Scope";
39
39
  import { Type } from "alinea/core/Type";
40
+ import { EntryStatusIcon, entryStatus } from "../app/EntryStatusIcon.js";
40
41
  import {
41
42
  OverviewCell,
42
43
  overviewCellText,
@@ -234,17 +235,19 @@ function EntryTableRow({ config, overview, row }) {
234
235
  const texts = overview.columns.map(
235
236
  (column) => overviewCellText(config, column, row, links)
236
237
  );
238
+ const status = entryStatus(row);
237
239
  return /* @__PURE__ */ jsxs(
238
240
  TableRow,
239
241
  {
240
242
  id: row.id,
241
- textValue: [row.title, ...texts].filter(Boolean).join(" "),
243
+ textValue: [row.title, status?.label, ...texts].filter(Boolean).join(" "),
242
244
  children: [
243
245
  /* @__PURE__ */ jsx(
244
246
  TableTitle,
245
247
  {
246
248
  icon: type && getType(type).icon || LucideFile,
247
- title: row.title
249
+ title: row.title,
250
+ suffix: status && /* @__PURE__ */ jsx(EntryStatusIcon, { status })
248
251
  }
249
252
  ),
250
253
  overview.columns.map((column, index) => /* @__PURE__ */ jsx(
@@ -1,47 +1,35 @@
1
1
  import type { Config } from 'alinea/core/Config';
2
- import type { Mutation } from 'alinea/core/db/Mutation';
3
- import type { CommitRequest } from 'alinea/core/db/CommitRequest';
4
2
  import type { SyncOptions } from 'alinea/core/db/LocalStore';
5
3
  import type { RemoteSource } from 'alinea/core/source/Source';
4
+ import { type SnapshotStorage } from '@alinea/sqlite-wasm/snapshots';
6
5
  import { EntryStore } from './EntryStore.js';
7
6
  export interface BrowserEntryStoreOptions {
8
7
  name: string;
9
- revision: string;
8
+ /** Stores of one config fingerprint open the bases it wrote. */
9
+ fingerprint: string;
10
+ /** Where bases are kept, Blobs in IndexedDB by default. */
11
+ storage?: SnapshotStorage;
10
12
  /**
11
- * The store this one replaces, such as one of a previous dashboard build:
12
- * its content carries over from memory instead of loading it again, and it
13
- * stops storing without waiting for its work in flight.
13
+ * Start over from an empty database instead of a stored base, such as
14
+ * when it turned out to be corrupt.
14
15
  */
15
- replaces?: BrowserEntryStore;
16
- /** The IndexedDB implementation, the global one by default. */
17
- indexedDB?: IDBFactory;
18
- IDBKeyRange?: typeof IDBKeyRange;
16
+ reset?: boolean;
19
17
  }
20
18
  /**
21
- * WASM entry store kept in IndexedDB: every commit stores the pages it
22
- * changed.
19
+ * WASM entry store on a copy-on-write overlay over the newest stored base:
20
+ * it reads pages from the base as queries need them, and keeps its changes
21
+ * in memory. Every sync stores them as a new base in the background, which
22
+ * the next store opens; the stored bases never change, so stores of any
23
+ * number of workers and dashboard builds can use them at once.
23
24
  */
24
25
  export declare class BrowserEntryStore extends EntryStore {
25
26
  #private;
26
- static open(config: Config, options: BrowserEntryStoreOptions, retried?: boolean): Promise<BrowserEntryStore>;
27
+ static open(config: Config, options: BrowserEntryStoreOptions): Promise<BrowserEntryStore>;
27
28
  private constructor();
28
- /**
29
- * Opening, and syncing, resolve once the content is in memory: storing it
30
- * is a cache that finishes in the background, which takes seconds for a
31
- * first sync or a rebuild of a large project.
32
- */
33
29
  sync(): Promise<string>;
34
30
  syncWith(remote: RemoteSource, options?: SyncOptions): Promise<string>;
35
- mutate(mutations: Array<Mutation>): Promise<{
36
- sha: string;
37
- }>;
38
- write(request: CommitRequest): Promise<{
39
- sha: string;
40
- }>;
31
+ /** Close once the last checkpoint is stored. */
41
32
  close(): Promise<void>;
42
- /**
43
- * Close a store that a replacement took over from: its work in flight
44
- * changes only its memory copy, which is not stored.
45
- */
46
- abandon(): Promise<void>;
47
33
  }
34
+ /** Whether an error, or one it holds, reports a damaged SQLite database. */
35
+ export declare function isCorruptDatabaseError(error: unknown): boolean;
@@ -5,177 +5,148 @@ import "../chunks/chunk-CQ52GEHY.js";
5
5
 
6
6
  // src/database/BrowserEntryStore.ts
7
7
  import { ReadonlyTree } from "alinea/core/source/Tree";
8
- import { TaskQueue } from "alinea/core/util/Async";
9
- import { indexedDBStorage } from "@alinea/sqlite-wasm/indexeddb";
10
- import { versionedCacheName } from "./Version.js";
8
+ import { isRecord } from "alinea/core/util/Objects";
9
+ import {
10
+ indexedDBSnapshots
11
+ } from "@alinea/sqlite-wasm/snapshots";
12
+ import { databaseVersion, versionedCacheName } from "./Version.js";
11
13
  import { EntryDatabase } from "./EntryDatabase.js";
12
14
  import { EntryStore } from "./EntryStore.js";
13
15
  import { DatabaseSource } from "./DatabaseSource.js";
14
- import {
15
- syncWasmDatabase
16
- } from "./driver/WasmDatabase.js";
17
- var buildTable = sql.identifier("alinea_dashboard_build");
16
+ import { wasmHandle, wasmSqlite } from "./driver/WasmDatabase.js";
17
+ var cacheSize = -65536;
18
+ var keepBranches = 2;
18
19
  var BrowserEntryStore = class _BrowserEntryStore extends EntryStore {
19
- #handle;
20
- #indexedDB;
21
- #baseName;
22
- #storageName;
23
- #persistQueue = new TaskQueue();
24
- #closed = false;
25
- #detached = false;
26
- static async open(config, options, retried = false) {
27
- const factory = options.indexedDB ?? indexedDB;
28
- const storageName = storageNameOf(options.name);
29
- const storage = indexedDBStorage(storageName, {
30
- indexedDB: factory,
31
- IDBKeyRange: options.IDBKeyRange ?? IDBKeyRange
32
- });
33
- let handle;
34
- try {
35
- handle = options.replaces ? await options.replaces.#handOver(storage) : await syncWasmDatabase(storage);
36
- const db = handle.database;
37
- await db.run(sql`pragma page_size = 65536`);
38
- const stored = await storedBuild(db);
39
- await EntryDatabase.createSchema(db, config, ReadonlyTree.EMPTY.sha);
40
- const database = new EntryDatabase(config, db, { fork: handle.fork });
41
- if (stored !== void 0 && stored !== options.revision)
42
- await database.reindex(config);
43
- if (stored !== options.revision)
44
- await db.run(
45
- sql`insert or replace into ${buildTable} (id, revision)
46
- values (1, ${options.revision})`
47
- );
48
- return new _BrowserEntryStore(
20
+ #session;
21
+ #storage;
22
+ #stored;
23
+ #written = Promise.resolve();
24
+ #cleanup = Promise.resolve();
25
+ static async open(config, options) {
26
+ const { name, fingerprint } = options;
27
+ const storage = options.storage ?? indexedDBSnapshots(`${versionedCacheName(name)}-snapshots`);
28
+ const cleanup = !options.storage && globalThis.indexedDB ? cleanupOldCaches(indexedDB, name).catch(() => {
29
+ }) : Promise.resolve();
30
+ const Database = await wasmSqlite();
31
+ const session = options.reset ? storage.session(new Database(), { branch: fingerprint }) : await openBase(storage, Database, fingerprint);
32
+ const store = await _BrowserEntryStore.#on(
33
+ config,
34
+ session,
35
+ storage,
36
+ fingerprint
37
+ ).catch((error) => {
38
+ if (!session.snapshot) throw error;
39
+ return _BrowserEntryStore.#on(
49
40
  config,
50
- database,
51
- handle,
52
- factory,
53
- options.name,
54
- storageName
41
+ storage.session(new Database(), { branch: fingerprint }),
42
+ storage,
43
+ fingerprint
55
44
  );
56
- } catch (error) {
57
- if (handle) await handle.database.close();
58
- if (retried) throw error;
59
- await storage.delete();
60
- return _BrowserEntryStore.open(
61
- config,
62
- { ...options, replaces: void 0 },
63
- true
64
- );
65
- }
45
+ });
46
+ store.#cleanup = cleanup;
47
+ return store;
66
48
  }
67
- /**
68
- * Copy this store's database for a replacement stored in `storage`, and
69
- * stop storing this one. The replacement is stored once this store's
70
- * commits so far are written.
71
- */
72
- async #handOver(storage) {
73
- const copy = await this.database.whileIdle(() => this.#handle.fork());
74
- this.#detach();
75
- try {
76
- await copy.attach(storage);
49
+ static async #on(config, session, storage, fingerprint) {
50
+ const sqlite = session.db;
51
+ const branch = session.snapshot?.branch;
52
+ sqlite.run(`pragma cache_size = ${cacheSize}`);
53
+ const { database: db, fork } = wasmHandle(sqlite);
54
+ async function preview() {
55
+ const copy = await fork();
56
+ await copy.database.run(sql`pragma cache_size = -8192`);
77
57
  return copy;
58
+ }
59
+ try {
60
+ await db.run(sql`pragma page_size = 16384`);
61
+ await EntryDatabase.createSchema(db, config, ReadonlyTree.EMPTY.sha);
62
+ const database = new EntryDatabase(config, db, { fork: preview });
63
+ if (branch !== void 0 && branch !== fingerprint)
64
+ await withoutJournal(sqlite, () => database.reindex(config));
65
+ return new _BrowserEntryStore(config, database, session, storage);
78
66
  } catch (error) {
79
- await copy.database.close();
67
+ await db.close();
80
68
  throw error;
81
69
  }
82
70
  }
83
- #detach() {
84
- if (this.#detached) return;
85
- this.#detached = true;
86
- this.#handle.detach();
87
- }
88
- constructor(config, database, handle, factory, baseName, storageName) {
71
+ constructor(config, database, session, storage) {
89
72
  super(config, database, new DatabaseSource(database), {
90
73
  ownsDatabase: true,
91
74
  sourceFollowsDatabase: true
92
75
  });
93
- this.#handle = handle;
94
- this.#indexedDB = factory;
95
- this.#baseName = baseName;
96
- this.#storageName = storageName;
97
- }
98
- /**
99
- * Opening, and syncing, resolve once the content is in memory: storing it
100
- * is a cache that finishes in the background, which takes seconds for a
101
- * first sync or a rebuild of a large project.
102
- */
103
- sync() {
104
- return this.#run(() => super.sync());
105
- }
106
- syncWith(remote, options) {
107
- return this.#run(() => super.syncWith(remote, options));
108
- }
109
- mutate(mutations) {
110
- return this.#persistAfter(() => super.mutate(mutations));
111
- }
112
- write(request) {
113
- return this.#persistAfter(() => super.write(request));
76
+ this.#session = session;
77
+ this.#storage = storage;
114
78
  }
115
- /** Resolve once the commits of a task that changed this store are stored. */
116
- #persistAfter(task) {
117
- return this.#run(async () => {
118
- const result = await task();
119
- await this.#handle.flush();
120
- return result;
121
- });
79
+ async sync() {
80
+ return this.#checkpoint(await super.sync());
122
81
  }
123
- #run(task) {
124
- if (this.#closed)
125
- return Promise.reject(new Error("BrowserEntryStore is closed"));
126
- return this.#persistQueue.run(task);
82
+ async syncWith(remote, options) {
83
+ return this.#checkpoint(await super.syncWith(remote, options));
127
84
  }
128
- close() {
129
- if (this.#closed) return this.#persistQueue.drain();
130
- this.#closed = true;
131
- return this.#persistQueue.run(async () => {
132
- try {
133
- await super.close();
134
- } finally {
135
- await cleanupOldCaches(
136
- this.#indexedDB,
137
- this.#baseName,
138
- this.#storageName
139
- );
140
- }
85
+ /** Store the content at `revision` as a base, in the background. */
86
+ #checkpoint(revision) {
87
+ if (!this.#storage.supported() || this.#stored === revision) return revision;
88
+ this.#stored = revision;
89
+ const session = this.#session;
90
+ const storage = this.#storage;
91
+ const key = `${revision}-${session.branch}`;
92
+ const save = (key2) => this.database.whileIdle(async () => ({
93
+ saved: session.held ? session.save({ key: key2, meta: { tree: revision } }) : void 0
94
+ })).then(({ saved }) => saved);
95
+ const written = save(key).then(
96
+ (result) => result?.status === "mismatch" ? save(`${key}-${crypto.randomUUID()}`) : result
97
+ ).then(() => storage.retain({ branches: keepBranches })).catch(() => {
98
+ if (this.#stored === revision) this.#stored = void 0;
141
99
  });
100
+ this.#written = Promise.all([this.#written, written]);
101
+ return revision;
142
102
  }
143
- /**
144
- * Close a store that a replacement took over from: its work in flight
145
- * changes only its memory copy, which is not stored.
146
- */
147
- abandon() {
148
- this.#detach();
149
- if (this.#closed) return this.#persistQueue.drain();
150
- this.#closed = true;
151
- return this.#persistQueue.run(() => super.close());
103
+ /** Close once the last checkpoint is stored. */
104
+ async close() {
105
+ await Promise.all([this.#cleanup, this.#written]);
106
+ await super.close();
152
107
  }
153
108
  };
154
- function storageNameOf(name) {
155
- return `${versionedCacheName(name)}-pages`;
109
+ var corruptCodes = /* @__PURE__ */ new Set(["SQLITE_CORRUPT", "SQLITE_NOTADB"]);
110
+ var corruptMessage = /database disk image is malformed|file is not a database/;
111
+ function isCorruptDatabaseError(error) {
112
+ if (!isRecord(error)) return false;
113
+ if (typeof error.code === "string" && corruptCodes.has(error.code))
114
+ return true;
115
+ if (typeof error.message === "string" && corruptMessage.test(error.message))
116
+ return true;
117
+ const errors = Array.isArray(error.errors) ? error.errors : [];
118
+ return isCorruptDatabaseError(error.cause) || errors.some(isCorruptDatabaseError);
156
119
  }
157
- async function storedBuild(db) {
158
- await db.run(sql`create table if not exists ${buildTable} (
159
- id integer primary key,
160
- revision text not null
161
- )`);
162
- const row = await db.get(
163
- sql`select revision from ${buildTable} where id = 1`
164
- );
165
- if (row) return row.revision;
166
- const entries = await db.get(
167
- sql`select count(*) as count from sqlite_master
168
- where type = 'table' and name = 'alinea_entry_index'`
169
- );
170
- return entries?.count ? "" : void 0;
120
+ async function withoutJournal(sqlite, task) {
121
+ const [[mode]] = sqlite.exec("pragma journal_mode")[0].values;
122
+ sqlite.run("pragma journal_mode = off");
123
+ try {
124
+ return await task();
125
+ } finally {
126
+ sqlite.run(`pragma journal_mode = ${mode}`);
127
+ }
128
+ }
129
+ async function openBase(storage, Database, fingerprint) {
130
+ const empty = () => storage.session(new Database(), { branch: fingerprint });
131
+ if (!storage.supported()) return empty();
132
+ return storage.open(Database, {
133
+ branch: fingerprint,
134
+ choose: (bases) => bases.find((base) => base.branch === fingerprint) ?? bases[0]
135
+ }).catch(empty);
171
136
  }
172
- async function cleanupOldCaches(factory, baseName, currentName) {
137
+ async function cleanupOldCaches(factory, name) {
173
138
  if (!factory.databases) return;
174
139
  const databases = await factory.databases().catch(() => []);
175
- const names = databases.flatMap(
176
- (database) => database.name && database.name !== currentName && (database.name === baseName || database.name.startsWith(`${baseName}-`)) ? [database.name] : []
140
+ const stale = databases.flatMap(
141
+ (database) => database.name && isStale(database.name, name) ? [database.name] : []
177
142
  );
178
- await Promise.all(names.map((name) => deleteCache(factory, name)));
143
+ await Promise.all(stale.map((cache) => deleteCache(factory, cache)));
144
+ }
145
+ function isStale(cache, name) {
146
+ if (cache === name || cache === `${versionedCacheName(name)}-pages`)
147
+ return true;
148
+ if (!cache.startsWith(`${name}-v`)) return false;
149
+ return Number.parseInt(cache.slice(name.length + 2)) < databaseVersion;
179
150
  }
180
151
  function deleteCache(factory, name) {
181
152
  return new Promise((resolve) => {
@@ -186,5 +157,6 @@ function deleteCache(factory, name) {
186
157
  });
187
158
  }
188
159
  export {
189
- BrowserEntryStore
160
+ BrowserEntryStore,
161
+ isCorruptDatabaseError
190
162
  };
@@ -4,10 +4,10 @@ import {
4
4
  number,
5
5
  text,
6
6
  varchar
7
- } from "../chunks/chunk-ASYKC6IE.js";
7
+ } from "../chunks/chunk-5H4D5JQL.js";
8
8
  import {
9
9
  table
10
- } from "../chunks/chunk-MOWR65LM.js";
10
+ } from "../chunks/chunk-PYDOMKJZ.js";
11
11
  import "../chunks/chunk-76VMZTLN.js";
12
12
  import "../chunks/chunk-2PIFHEYR.js";
13
13
  import "../chunks/chunk-K62JP6HR.js";
@@ -11,6 +11,7 @@ import "../chunks/chunk-2PIFHEYR.js";
11
11
  import "../chunks/chunk-K62JP6HR.js";
12
12
  import "../chunks/chunk-ATF42T2B.js";
13
13
  import {
14
+ getTable,
14
15
  sql
15
16
  } from "../chunks/chunk-ETBQTP2S.js";
16
17
  import "../chunks/chunk-CQ52GEHY.js";
@@ -171,6 +172,7 @@ var EntryDatabase = class _EntryDatabase extends Graph {
171
172
  tree: revision === ReadonlyTree.EMPTY.sha ? ReadonlyTree.EMPTY : null
172
173
  });
173
174
  await syncFieldIndexes(db, config);
175
+ await analyze(db);
174
176
  }
175
177
  /**
176
178
  * Synchronize a source through this database's single prepared syncer,
@@ -194,6 +196,7 @@ var EntryDatabase = class _EntryDatabase extends Graph {
194
196
  recordsTree: this.#recordsTree
195
197
  });
196
198
  this.#tree = tree;
199
+ if (changed.length > 0) await this.#optimize();
197
200
  return changed;
198
201
  });
199
202
  const change = { revision: tree.sha, changedEntryIds };
@@ -222,7 +225,7 @@ var EntryDatabase = class _EntryDatabase extends Graph {
222
225
  const { tree } = await this.#readTreeState();
223
226
  if (!tree)
224
227
  throw new Error("Cannot reindex a database without a source tree");
225
- return this.#db.transaction(
228
+ const reindexed = await this.#db.transaction(
226
229
  async (tx) => {
227
230
  await tx.run(sql`drop table if exists temp.${blobs}`);
228
231
  await tx.run(sql`create temp table ${blobs} as
@@ -273,6 +276,8 @@ var EntryDatabase = class _EntryDatabase extends Graph {
273
276
  },
274
277
  { async: true }
275
278
  );
279
+ await this.#optimize();
280
+ return reindexed;
276
281
  });
277
282
  this.#tree = result.tree;
278
283
  this.#vocabulary = new SearchVocabulary();
@@ -386,6 +391,9 @@ var EntryDatabase = class _EntryDatabase extends Graph {
386
391
  [Symbol.asyncDispose]() {
387
392
  return this.close();
388
393
  }
394
+ async #optimize() {
395
+ if (!this.#transactional) await analyze(this.#db);
396
+ }
389
397
  getRevision() {
390
398
  return this.#queue.run(() => this.#getRevision(this.#db));
391
399
  }
@@ -527,6 +535,18 @@ var EntryDatabase = class _EntryDatabase extends Graph {
527
535
  });
528
536
  }
529
537
  };
538
+ async function analyze(db) {
539
+ try {
540
+ await db.run(sql`pragma analysis_limit = 400`);
541
+ const entries = getTable(EntryIndexTable).name;
542
+ const stats = await db.get(
543
+ sql`select 1 from sqlite_master where name = 'sqlite_stat1'`
544
+ );
545
+ const analyzed = stats && await db.get(sql`select 1 from sqlite_stat1 where tbl = ${entries}`);
546
+ await db.run(analyzed ? sql`pragma optimize = 0x10002` : sql`analyze`);
547
+ } catch {
548
+ }
549
+ }
530
550
  export {
531
551
  EntryDatabase
532
552
  };