@starlove/orbit 1.4.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/Dockerfile +10 -8
  2. package/README.md +146 -164
  3. package/dist/full/csp.txt +1 -0
  4. package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-subset.woff2 +0 -0
  5. package/dist/full/fonts/JetBrains_Mono/README.txt +5 -0
  6. package/dist/full/fonts/Mona_Sans/MonaSans-subset.woff2 +0 -0
  7. package/{public/fonts/Open_Sans → dist/full/fonts/Mona_Sans}/OFL.txt +93 -93
  8. package/dist/full/fonts/Mona_Sans/README.txt +10 -0
  9. package/{public/fonts/JetBrains_Mono → dist/full/fonts/Newsreader}/OFL.txt +93 -93
  10. package/dist/full/fonts/Newsreader/README.txt +8 -0
  11. package/dist/full/fonts/Newsreader/newsreader.woff2 +0 -0
  12. package/dist/full/index.html +36 -653
  13. package/dist/full/js/app.BmuponPJ.js +32 -0
  14. package/dist/full/js/app.BmuponPJ.js.map +1 -0
  15. package/dist/full/js/chunk.1fJGQGbW.js +2 -0
  16. package/dist/full/js/chunk.1fJGQGbW.js.map +1 -0
  17. package/dist/full/js/chunk.5D7JO2zp.js +2 -0
  18. package/dist/full/js/chunk.5D7JO2zp.js.map +1 -0
  19. package/dist/full/js/chunk.BrNn_D79.js +2 -0
  20. package/dist/full/js/chunk.BrNn_D79.js.map +1 -0
  21. package/dist/full/js/chunk.CokJXIqM.js +2 -0
  22. package/dist/full/js/chunk.CokJXIqM.js.map +1 -0
  23. package/dist/full/js/chunk.D_Haqy6R.js +2 -0
  24. package/dist/full/js/chunk.D_Haqy6R.js.map +1 -0
  25. package/dist/full/js/chunk.RO0jsH6I.js +15 -0
  26. package/dist/full/js/chunk.RO0jsH6I.js.map +1 -0
  27. package/dist/full/js/chunk.dqWOEtBM.js +2 -0
  28. package/dist/full/js/chunk.dqWOEtBM.js.map +1 -0
  29. package/dist/full/js/warm.f260bbbc.js +15 -0
  30. package/dist/full/orbits-favicon.png +0 -0
  31. package/dist/full/orbits-logo-bg-wide.png +0 -0
  32. package/dist/full/orbits-logo-bg.png +0 -0
  33. package/dist/full/orbits-logo.png +0 -0
  34. package/dist/full/styles.2b154ffe.css +1 -0
  35. package/dist/full/vendor/starlove-ui/theme-init.js +66 -0
  36. package/dist/full/version.json +2 -2
  37. package/docs/AGENT_GUIDE.md +114 -0
  38. package/docs/UPDATING.md +249 -64
  39. package/lib/adapters/indexeddb/blobs.d.ts +3 -0
  40. package/lib/adapters/indexeddb/blobs.js +54 -0
  41. package/lib/adapters/indexeddb/database.d.ts +60 -0
  42. package/lib/adapters/indexeddb/database.js +126 -0
  43. package/lib/adapters/indexeddb/index.d.ts +5 -0
  44. package/lib/adapters/indexeddb/index.js +3 -0
  45. package/lib/adapters/indexeddb/locks.d.ts +14 -0
  46. package/lib/adapters/indexeddb/locks.js +40 -0
  47. package/lib/adapters/indexeddb/store.d.ts +40 -0
  48. package/lib/adapters/indexeddb/store.js +424 -0
  49. package/lib/adapters/indexeddb/tables.d.ts +43 -0
  50. package/lib/adapters/indexeddb/tables.js +127 -0
  51. package/lib/adapters/memory/blobs.d.ts +3 -0
  52. package/lib/adapters/memory/blobs.js +44 -0
  53. package/lib/adapters/memory/board.d.ts +41 -0
  54. package/lib/adapters/memory/board.js +521 -0
  55. package/lib/adapters/memory/index.d.ts +2 -0
  56. package/lib/adapters/memory/index.js +2 -0
  57. package/lib/adapters/memory/search.d.ts +6 -0
  58. package/lib/adapters/memory/search.js +30 -0
  59. package/lib/adapters/memory/store.d.ts +34 -0
  60. package/lib/adapters/memory/store.js +266 -0
  61. package/lib/adapters/sqlite/backups.d.ts +53 -0
  62. package/lib/adapters/sqlite/backups.js +232 -0
  63. package/lib/adapters/sqlite/blobs.d.ts +14 -0
  64. package/lib/adapters/sqlite/blobs.js +109 -0
  65. package/lib/adapters/sqlite/board.d.ts +77 -0
  66. package/lib/adapters/sqlite/board.js +328 -0
  67. package/lib/adapters/sqlite/database.d.ts +67 -0
  68. package/lib/adapters/sqlite/database.js +285 -0
  69. package/lib/adapters/sqlite/index.d.ts +3 -0
  70. package/lib/adapters/sqlite/index.js +2 -0
  71. package/lib/adapters/sqlite/lock.d.ts +11 -0
  72. package/lib/adapters/sqlite/lock.js +34 -0
  73. package/lib/adapters/sqlite/records.d.ts +54 -0
  74. package/lib/adapters/sqlite/records.js +546 -0
  75. package/lib/adapters/sqlite/schema.d.ts +54 -0
  76. package/lib/adapters/sqlite/schema.js +276 -0
  77. package/lib/adapters/sqlite/store.d.ts +39 -0
  78. package/lib/adapters/sqlite/store.js +385 -0
  79. package/lib/browser/in-page.d.ts +31 -0
  80. package/lib/browser/in-page.js +46 -0
  81. package/lib/browser/old-preview.d.ts +13 -0
  82. package/lib/browser/old-preview.js +80 -0
  83. package/lib/browser/preview-board.d.ts +8 -0
  84. package/lib/browser/preview-board.js +69 -0
  85. package/lib/cli/main.d.ts +7 -0
  86. package/lib/cli/main.js +343 -0
  87. package/lib/cli/orbit.d.ts +8 -0
  88. package/lib/cli/orbit.js +25 -0
  89. package/lib/client/http.d.ts +38 -0
  90. package/lib/client/http.js +334 -0
  91. package/lib/client/local.d.ts +26 -0
  92. package/lib/client/local.js +190 -0
  93. package/lib/client/object-url.d.ts +8 -0
  94. package/lib/client/object-url.js +21 -0
  95. package/lib/client/types.d.ts +268 -0
  96. package/lib/client/types.js +8 -0
  97. package/lib/commands/agent-tools.d.ts +31 -0
  98. package/lib/commands/agent-tools.js +100 -0
  99. package/lib/commands/index.d.ts +56 -0
  100. package/lib/commands/index.js +298 -0
  101. package/lib/commands/parse.d.ts +73 -0
  102. package/lib/commands/parse.js +231 -0
  103. package/lib/commands/present.d.ts +134 -0
  104. package/lib/commands/present.js +198 -0
  105. package/lib/commands/session.d.ts +85 -0
  106. package/lib/commands/session.js +138 -0
  107. package/lib/commands/verbs.d.ts +35 -0
  108. package/lib/commands/verbs.js +842 -0
  109. package/lib/core/actors.d.ts +18 -0
  110. package/lib/core/actors.js +45 -0
  111. package/lib/core/agent.d.ts +128 -0
  112. package/lib/core/agent.js +323 -0
  113. package/lib/core/boards.d.ts +113 -0
  114. package/lib/core/boards.js +288 -0
  115. package/lib/core/bootstrap.d.ts +47 -0
  116. package/lib/core/bootstrap.js +100 -0
  117. package/lib/core/bytes.d.ts +10 -0
  118. package/lib/core/bytes.js +52 -0
  119. package/lib/core/card-paths.d.ts +25 -0
  120. package/lib/core/card-paths.js +44 -0
  121. package/lib/core/context.d.ts +60 -0
  122. package/lib/core/context.js +42 -0
  123. package/lib/core/cross-board.d.ts +81 -0
  124. package/lib/core/cross-board.js +228 -0
  125. package/lib/core/describe.d.ts +21 -0
  126. package/lib/core/describe.js +81 -0
  127. package/lib/core/errors.d.ts +159 -0
  128. package/lib/core/errors.js +160 -0
  129. package/lib/core/files.d.ts +79 -0
  130. package/lib/core/files.js +262 -0
  131. package/lib/core/history.d.ts +20 -0
  132. package/lib/core/history.js +53 -0
  133. package/lib/core/image-types.d.ts +16 -0
  134. package/lib/core/image-types.js +36 -0
  135. package/lib/core/model.d.ts +263 -0
  136. package/lib/core/model.js +34 -0
  137. package/lib/core/nebula-link.d.ts +14 -0
  138. package/lib/core/nebula-link.js +39 -0
  139. package/lib/core/relations.d.ts +58 -0
  140. package/lib/core/relations.js +112 -0
  141. package/lib/core/search.d.ts +38 -0
  142. package/lib/core/search.js +92 -0
  143. package/lib/core/sessions.d.ts +44 -0
  144. package/lib/core/sessions.js +158 -0
  145. package/lib/core/snapshot-schema.d.ts +295 -0
  146. package/lib/core/snapshot-schema.js +82 -0
  147. package/lib/core/snapshots.d.ts +75 -0
  148. package/lib/core/snapshots.js +409 -0
  149. package/lib/core/states.d.ts +25 -0
  150. package/lib/core/states.js +126 -0
  151. package/lib/core/text.d.ts +40 -0
  152. package/lib/core/text.js +113 -0
  153. package/lib/core/tickets.d.ts +69 -0
  154. package/lib/core/tickets.js +311 -0
  155. package/lib/core/trello.d.ts +17 -0
  156. package/lib/core/trello.js +269 -0
  157. package/lib/core/verdicts.d.ts +28 -0
  158. package/lib/core/verdicts.js +92 -0
  159. package/lib/core/views.d.ts +95 -0
  160. package/lib/core/views.js +186 -0
  161. package/lib/http/app.d.ts +46 -0
  162. package/lib/http/app.js +491 -0
  163. package/lib/mcp-server.d.ts +10 -0
  164. package/lib/mcp-server.js +120 -0
  165. package/lib/node/assets.d.ts +7 -0
  166. package/lib/node/assets.js +68 -0
  167. package/lib/node/data-folder.d.ts +21 -0
  168. package/lib/node/data-folder.js +34 -0
  169. package/lib/node/env.d.ts +38 -0
  170. package/lib/node/env.js +49 -0
  171. package/lib/node/host-check.d.ts +24 -0
  172. package/lib/node/host-check.js +53 -0
  173. package/lib/node/host-services.d.ts +9 -0
  174. package/lib/node/host-services.js +150 -0
  175. package/lib/node/local.d.ts +22 -0
  176. package/lib/node/local.js +61 -0
  177. package/lib/node/migrate.d.ts +53 -0
  178. package/lib/node/migrate.js +635 -0
  179. package/lib/node/package.d.ts +7 -0
  180. package/lib/node/package.js +14 -0
  181. package/lib/node/repo-board.d.ts +49 -0
  182. package/lib/node/repo-board.js +245 -0
  183. package/lib/node/serve.d.ts +8 -0
  184. package/lib/node/serve.js +106 -0
  185. package/lib/orbit.d.ts +47 -0
  186. package/lib/orbit.js +44 -0
  187. package/lib/quiet-sqlite-warning.d.ts +1 -0
  188. package/lib/quiet-sqlite-warning.js +18 -0
  189. package/lib/store/blob-path.d.ts +6 -0
  190. package/lib/store/blob-path.js +12 -0
  191. package/lib/store/conformance.d.ts +52 -0
  192. package/lib/store/conformance.js +1449 -0
  193. package/lib/store/search-words.d.ts +10 -0
  194. package/lib/store/search-words.js +19 -0
  195. package/lib/store/types.d.ts +452 -0
  196. package/lib/store/types.js +34 -0
  197. package/llms.txt +63 -0
  198. package/package.json +109 -25
  199. package/src/cli/orbit.js +11 -658
  200. package/src/mcp-server.js +16 -876
  201. package/SKILL-ORBIT.md +0 -151
  202. package/build.mjs +0 -210
  203. package/dist/full/js/app.4e6fbe20.js +0 -952
  204. package/dist/full/js/app.4e6fbe20.js.map +0 -7
  205. package/dist/full/styles.4e60d824.css +0 -1
  206. package/docs/AGENT_PROTOCOL.md +0 -424
  207. package/docs/MCP_SETUP.md +0 -54
  208. package/docs/ORBIT_DISPATCH.md +0 -280
  209. package/docs/SS-Orbit-Dark.png +0 -0
  210. package/docs/SS-Orbit-Light.png +0 -0
  211. package/docs/mcp-client.example.json +0 -13
  212. package/docs/setup/orbit-init-wizard.md +0 -174
  213. package/public/apple-touch-icon.png +0 -0
  214. package/public/cursors/live_arrow.cur +0 -0
  215. package/public/cursors/live_link.cur +0 -0
  216. package/public/cursors/live_move.cur +0 -0
  217. package/public/cursors/live_select.cur +0 -0
  218. package/public/cursors/live_unavail.cur +0 -0
  219. package/public/favicon-32x32.png +0 -0
  220. package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
  221. package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
  222. package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
  223. package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
  224. package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
  225. package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
  226. package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
  227. package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
  228. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
  229. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
  230. package/public/fonts/Chakra_Petch/OFL.txt +0 -93
  231. package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
  232. package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
  233. package/public/fonts/Inter/OFL.txt +0 -93
  234. package/public/fonts/Inter/README.txt +0 -118
  235. package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
  236. package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
  237. package/public/fonts/JetBrains_Mono/README.txt +0 -79
  238. package/public/fonts/OpenRing-Regular.ttf +0 -0
  239. package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  240. package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
  241. package/public/fonts/Open_Sans/README.txt +0 -100
  242. package/public/fonts/Orbitron/OFL.txt +0 -93
  243. package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
  244. package/public/fonts/Orbitron/README.txt +0 -68
  245. package/public/fonts/Rajdhani/OFL.txt +0 -93
  246. package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
  247. package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
  248. package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
  249. package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
  250. package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
  251. package/public/fonts/runescape-uf.ttf +0 -0
  252. package/public/index.html +0 -769
  253. package/public/js/actor-labels.js +0 -65
  254. package/public/js/api.js +0 -39
  255. package/public/js/app.js +0 -45
  256. package/public/js/artifact-drawer.js +0 -79
  257. package/public/js/board-export.js +0 -161
  258. package/public/js/board-menu.js +0 -374
  259. package/public/js/card-actions.js +0 -545
  260. package/public/js/config.js +0 -20
  261. package/public/js/create-card.js +0 -190
  262. package/public/js/dom.js +0 -19
  263. package/public/js/drawer.js +0 -273
  264. package/public/js/font-preference.js +0 -230
  265. package/public/js/format.js +0 -433
  266. package/public/js/help-menu.js +0 -31
  267. package/public/js/icons.js +0 -16
  268. package/public/js/kanban.js +0 -694
  269. package/public/js/local-backend.js +0 -1015
  270. package/public/js/main.js +0 -161
  271. package/public/js/modal.js +0 -33
  272. package/public/js/motion-preference.js +0 -64
  273. package/public/js/panel.js +0 -58
  274. package/public/js/router.js +0 -134
  275. package/public/js/search.js +0 -56
  276. package/public/js/settings-tabs.js +0 -7
  277. package/public/js/settings.js +0 -1634
  278. package/public/js/sse.js +0 -159
  279. package/public/js/starscape.js +0 -332
  280. package/public/js/state.js +0 -129
  281. package/public/js/text-editing.js +0 -128
  282. package/public/js/theme-preference.js +0 -95
  283. package/public/js/ticket-detail.js +0 -1516
  284. package/public/js/ticket-typeahead.js +0 -49
  285. package/public/js/toast.js +0 -89
  286. package/public/js/topbar-notes.js +0 -155
  287. package/public/js/transport.js +0 -22
  288. package/public/js/typeahead.js +0 -206
  289. package/public/js/unread.js +0 -121
  290. package/public/js/update-check.js +0 -58
  291. package/public/js/url-routes.js +0 -94
  292. package/public/orbits-favicon.png +0 -0
  293. package/public/orbits-logo-bg-wide.png +0 -0
  294. package/public/orbits-logo-bg.png +0 -0
  295. package/public/orbits-logo.png +0 -0
  296. package/public/styles.css +0 -7709
  297. package/src/core/agent.js +0 -387
  298. package/src/core/agents-md.js +0 -109
  299. package/src/core/artifacts.js +0 -149
  300. package/src/core/attachments.js +0 -238
  301. package/src/core/auth.js +0 -85
  302. package/src/core/backups.js +0 -142
  303. package/src/core/board.js +0 -148
  304. package/src/core/boards.js +0 -409
  305. package/src/core/bootstrap.js +0 -170
  306. package/src/core/css-bundle.js +0 -30
  307. package/src/core/db.js +0 -315
  308. package/src/core/delete-board.js +0 -163
  309. package/src/core/dispatch.js +0 -1007
  310. package/src/core/events.js +0 -314
  311. package/src/core/http.js +0 -115
  312. package/src/core/migrate.js +0 -198
  313. package/src/core/paths.js +0 -55
  314. package/src/core/provision-repo-board.js +0 -119
  315. package/src/core/queries.js +0 -246
  316. package/src/core/registry.js +0 -211
  317. package/src/core/relations.js +0 -163
  318. package/src/core/review-verdicts.js +0 -138
  319. package/src/core/router.js +0 -556
  320. package/src/core/search.js +0 -183
  321. package/src/core/seed.js +0 -234
  322. package/src/core/snapshots.js +0 -692
  323. package/src/core/states.js +0 -167
  324. package/src/core/system-picker.js +0 -109
  325. package/src/core/tickets.js +0 -400
  326. package/src/core/util.js +0 -106
  327. package/src/core/version-check.js +0 -154
  328. package/src/mcp/orbit-client.js +0 -287
  329. package/src/server.js +0 -115
@@ -0,0 +1,424 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * The IndexedDB adapter: Orbit's storage port in a browser, shared by every
4
+ * tab of the origin. `openIndexedDbStore()` gives the store and, as its
5
+ * `blobs`, the blob store of the same database.
6
+ *
7
+ * - **Writes.** An IndexedDB transaction commits by itself once its code
8
+ * waits for anything else, so a write can't be one. A write takes the
9
+ * board's exclusive lock (a Web Lock, held across tabs), runs its body
10
+ * against a copy of the board in memory (the memory adapter's writer, so
11
+ * its reads see its own changes and the port's checks are the same), then
12
+ * commits what changed in one readwrite transaction. A body that throws
13
+ * commits nothing. Card numbers are given under the lock.
14
+ * - **Reads.** A read loads the board in one readonly transaction, which is
15
+ * one committed state, and runs its body against it. It takes no lock, so
16
+ * a read never waits for a write. A tab keeps the board it last loaded and
17
+ * reuses it while the board's revision is unchanged.
18
+ * - **watch().** This tab's commits reach its watchers at once; other tabs'
19
+ * through a BroadcastChannel, after which the board is read again and the
20
+ * events past the last one heard are delivered, in order, once.
21
+ * - **Search** filters a board's documents in memory, as the memory adapter
22
+ * does: a board's documents are few.
23
+ * - **Backups:** none (`backups` is null); people export a board instead.
24
+ */
25
+ import { OrbitError } from "../../core/errors.js";
26
+ import { changeOf } from "../../core/model.js";
27
+ import { byCodePoint, emptyTables, reader, writer } from "../memory/board.js";
28
+ import { createIndexedDbBlobStore } from "./blobs.js";
29
+ import { BOARDS, HEADS, ROWS, SETTINGS, openDatabase, rowRange, transact } from "./database.js";
30
+ import { createLocalLocks, webLocks } from "./locks.js";
31
+ import { changesOf, cloneTables, eventSeqs, snapshotOf } from "./tables.js";
32
+ const yieldTurn = () => Promise.resolve();
33
+ /** Listings in the order boards were made. */
34
+ const made = (listed) => [...listed].sort((a, b) => a.n - b.n).map((entry) => entry.board);
35
+ /** The directory's order: by name, then by when each was made. */
36
+ const directoryOrder = (listed) => made(listed).sort((a, b) => byCodePoint(a.name, b.name));
37
+ export async function openIndexedDbStore(options = {}) {
38
+ const name = options.name ?? "orbit";
39
+ const factory = options.indexedDB ?? globalThis.indexedDB;
40
+ const locks = options.locks ?? webLocks() ?? createLocalLocks();
41
+ const db = await openDatabase(factory, name);
42
+ const makeChannel = options.channel === undefined ? (typeof BroadcastChannel === "undefined" ? null : (channelName) => new BroadcastChannel(channelName)) : options.channel;
43
+ const channel = makeChannel ? makeChannel(`orbit-indexeddb:${name}`) : null;
44
+ const lockName = (boardId) => `orbit-indexeddb:${name}:board:${boardId}`;
45
+ /** The board as this tab last saw it. */
46
+ const cache = new Map();
47
+ const feeds = new Map();
48
+ const remember = (boardId, snapshot) => {
49
+ const known = cache.get(boardId);
50
+ if (!known || known.revision < snapshot.revision)
51
+ cache.set(boardId, snapshot);
52
+ };
53
+ /**
54
+ * The board's committed state, in one transaction: this tab's copy when
55
+ * the revision matches, else its rows. With `directory`, every listing too.
56
+ */
57
+ function load(boardId, withDirectory = false) {
58
+ return transact(db, [BOARDS, HEADS, ROWS], "readonly", (tx, refuse) => {
59
+ let snapshot = null;
60
+ let directory = [];
61
+ tx.objectStore(BOARDS).get(boardId).onsuccess = (event) => {
62
+ if (!event.target.result)
63
+ refuse(new OrbitError("board_not_found"));
64
+ };
65
+ if (withDirectory) {
66
+ tx.objectStore(BOARDS).getAll().onsuccess = (event) => {
67
+ directory = made(event.target.result);
68
+ };
69
+ }
70
+ tx.objectStore(HEADS).get(boardId).onsuccess = (event) => {
71
+ const head = event.target.result;
72
+ if (!head) {
73
+ refuse(new OrbitError("board_not_found"));
74
+ return;
75
+ }
76
+ const known = cache.get(boardId);
77
+ if (known && known.revision === head.revision) {
78
+ snapshot = known;
79
+ return;
80
+ }
81
+ tx.objectStore(ROWS).getAll(rowRange(boardId)).onsuccess = (rowsEvent) => {
82
+ snapshot = snapshotOf(head.revision, head.seq, rowsEvent.target.result);
83
+ };
84
+ };
85
+ return () => {
86
+ if (!snapshot)
87
+ throw new OrbitError("board_not_found");
88
+ remember(boardId, snapshot);
89
+ return { snapshot, directory };
90
+ };
91
+ });
92
+ }
93
+ /** Writes a write's changes in one transaction, if the board is still where the write found it. */
94
+ function commit(boardId, before, changes, board) {
95
+ return transact(db, [BOARDS, HEADS, ROWS], "readwrite", (tx, refuse) => {
96
+ const boards = tx.objectStore(BOARDS);
97
+ const rows = tx.objectStore(ROWS);
98
+ tx.objectStore(HEADS).get(boardId).onsuccess = (event) => {
99
+ const head = event.target.result;
100
+ if (!head) {
101
+ refuse(new OrbitError("board_not_found"));
102
+ return;
103
+ }
104
+ // The lock makes this impossible; a page without shared locks would otherwise lose a write.
105
+ if (head.revision !== before.revision) {
106
+ refuse(new OrbitError("storage_busy", { message: "Another tab changed the board during this write." }));
107
+ return;
108
+ }
109
+ const next = { board_id: boardId, revision: changes.after.revision, seq: changes.after.seq };
110
+ tx.objectStore(HEADS).put(next);
111
+ for (const key of changes.deletes)
112
+ rows.delete(key);
113
+ for (const { key, row } of changes.puts)
114
+ rows.put(row, key);
115
+ };
116
+ boards.getAll().onsuccess = (event) => {
117
+ const entries = event.target.result;
118
+ const listed = entries.find((candidate) => candidate.board.id === boardId);
119
+ if (!listed) {
120
+ refuse(new OrbitError("board_not_found"));
121
+ return;
122
+ }
123
+ if (!changes.boardChanged || !board)
124
+ return;
125
+ if (entries.some((other) => other.board.id !== boardId && other.board.slug === board.slug)) {
126
+ refuse(new OrbitError("board_slug_taken"));
127
+ return;
128
+ }
129
+ const entry = listed.board;
130
+ boards.put({ n: listed.n, board: { ...entry, slug: board.slug, name: board.name, repo_url: board.repo_url, default_branch: board.default_branch, updated_at: board.updated_at } }, boardId);
131
+ };
132
+ return () => undefined;
133
+ });
134
+ }
135
+ /** Runs `body` on a copy of `base`, with the writer closed once it settles, as the memory adapter does. */
136
+ async function runWrite(boardId, base, slugTaken, body) {
137
+ const draft = cloneTables(base);
138
+ const log = { events: [] };
139
+ let open = true;
140
+ const guard = async () => {
141
+ await yieldTurn();
142
+ if (!open)
143
+ throw new OrbitError("write_closed");
144
+ };
145
+ try {
146
+ const result = await body(writer(boardId, draft, log, { slugTaken }, guard));
147
+ return { result, draft, log };
148
+ }
149
+ finally {
150
+ open = false;
151
+ }
152
+ }
153
+ /** Hands every event past the feed's cursor to its listeners, once and in order. */
154
+ function deliver(boardId, feed, snapshot) {
155
+ if (feed.cursor === null)
156
+ return;
157
+ for (const { s, event } of eventSeqs(snapshot)) {
158
+ if (s <= feed.cursor)
159
+ continue;
160
+ feed.cursor = s;
161
+ for (const listener of [...feed.listeners]) {
162
+ const change = changeOf(boardId, event);
163
+ queueMicrotask(() => {
164
+ listener(change);
165
+ });
166
+ }
167
+ }
168
+ }
169
+ function enqueue(boardId, feed, step) {
170
+ feed.chain = feed.chain
171
+ .then(step)
172
+ .then((snapshot) => {
173
+ if (snapshot && feeds.get(boardId) === feed)
174
+ deliver(boardId, feed, snapshot);
175
+ })
176
+ .catch(() => undefined);
177
+ }
178
+ const onNotice = (event) => {
179
+ const { board } = event.data;
180
+ const feed = feeds.get(board);
181
+ if (!feed)
182
+ return;
183
+ enqueue(board, feed, async () => (await load(board)).snapshot);
184
+ };
185
+ if (channel)
186
+ channel.onmessage = onNotice;
187
+ function startFeed(boardId) {
188
+ const feed = { listeners: new Set(), cursor: null, chain: Promise.resolve() };
189
+ // Made now, so it reads the board as it is when watch() is called; later commits are heard.
190
+ const start = transact(db, [HEADS], "readonly", (tx) => {
191
+ let seq = 0;
192
+ tx.objectStore(HEADS).get(boardId).onsuccess = (event) => {
193
+ seq = event.target.result?.seq ?? 0;
194
+ };
195
+ return () => seq;
196
+ });
197
+ feed.chain = start.then((seq) => {
198
+ feed.cursor = seq;
199
+ }, () => {
200
+ feed.cursor = 0;
201
+ });
202
+ feeds.set(boardId, feed);
203
+ return feed;
204
+ }
205
+ function boardStore(id) {
206
+ return {
207
+ id,
208
+ async read(body) {
209
+ const { snapshot } = await load(id);
210
+ return body(reader(() => snapshot.tables));
211
+ },
212
+ write(body) {
213
+ return locks.request(lockName(id), async () => {
214
+ const { snapshot, directory } = await load(id, true);
215
+ const slugTaken = (slug) => directory.some((entry) => entry.slug === slug && entry.id !== id);
216
+ const { result, draft } = await runWrite(id, snapshot.tables, slugTaken, body);
217
+ const changes = changesOf(id, snapshot, draft);
218
+ await commit(id, snapshot, changes, draft.board);
219
+ remember(id, changes.after);
220
+ const feed = feeds.get(id);
221
+ if (feed)
222
+ enqueue(id, feed, () => changes.after);
223
+ channel?.postMessage({ board: id, revision: changes.after.revision });
224
+ return result;
225
+ });
226
+ },
227
+ watch(listener) {
228
+ const feed = feeds.get(id) ?? startFeed(id);
229
+ feed.listeners.add(listener);
230
+ return () => {
231
+ feed.listeners.delete(listener);
232
+ if (feed.listeners.size === 0 && feeds.get(id) === feed)
233
+ feeds.delete(id);
234
+ };
235
+ }
236
+ };
237
+ }
238
+ const allListed = () => transact(db, [BOARDS], "readonly", (tx) => {
239
+ let entries = [];
240
+ tx.objectStore(BOARDS).getAll().onsuccess = (event) => {
241
+ entries = event.target.result;
242
+ };
243
+ return () => entries;
244
+ });
245
+ const allBoards = async () => made(await allListed());
246
+ const boards = {
247
+ async list() {
248
+ return directoryOrder(await allListed());
249
+ },
250
+ async get(id) {
251
+ return (await allBoards()).find((entry) => entry.id === id) ?? null;
252
+ },
253
+ async bySlug(slug) {
254
+ return (await allBoards()).find((entry) => entry.slug === slug) ?? null;
255
+ },
256
+ async latest() {
257
+ let best = null;
258
+ for (const entry of await allBoards()) {
259
+ if (best === null || (entry.last_active_at ?? "") >= (best.last_active_at ?? ""))
260
+ best = entry;
261
+ }
262
+ return best;
263
+ },
264
+ async create(board, seed) {
265
+ const existing = await allBoards();
266
+ const slugTaken = (slug) => existing.some((entry) => entry.slug === slug && entry.id !== board.id);
267
+ if (existing.some((entry) => entry.id === board.id))
268
+ throw new OrbitError("duplicate_id");
269
+ if (slugTaken(board.slug))
270
+ throw new OrbitError("board_slug_taken");
271
+ const base = emptyTables();
272
+ base.board = { ...board };
273
+ const empty = { revision: 0, seq: 0, tables: emptyTables(), seqs: new Map() };
274
+ const { draft } = await runWrite(board.id, base, slugTaken, seed);
275
+ const changes = changesOf(board.id, empty, draft);
276
+ const entry = {
277
+ id: board.id,
278
+ slug: draft.board?.slug ?? board.slug,
279
+ name: draft.board?.name ?? board.name,
280
+ db_path: "",
281
+ repo_url: draft.board?.repo_url ?? board.repo_url,
282
+ default_branch: draft.board?.default_branch ?? board.default_branch,
283
+ last_active_at: board.created_at,
284
+ created_at: board.created_at,
285
+ updated_at: draft.board?.updated_at ?? board.updated_at
286
+ };
287
+ await transact(db, [BOARDS, HEADS, ROWS], "readwrite", (tx, refuse) => {
288
+ const boardsStore = tx.objectStore(BOARDS);
289
+ boardsStore.getAll().onsuccess = (event) => {
290
+ const entries = event.target.result;
291
+ if (entries.some((other) => other.board.id === board.id)) {
292
+ refuse(new OrbitError("duplicate_id"));
293
+ return;
294
+ }
295
+ if (entries.some((other) => other.board.slug === entry.slug)) {
296
+ refuse(new OrbitError("board_slug_taken"));
297
+ return;
298
+ }
299
+ const n = entries.reduce((top, other) => Math.max(top, other.n), 0) + 1;
300
+ boardsStore.add({ n, board: entry }, board.id);
301
+ };
302
+ tx.objectStore(HEADS).get(board.id).onsuccess = (event) => {
303
+ // Rows left by a board removed with its data kept: the id is taken.
304
+ if (event.target.result) {
305
+ refuse(new OrbitError("duplicate_id"));
306
+ return;
307
+ }
308
+ tx.objectStore(HEADS).put({ board_id: board.id, revision: changes.after.revision, seq: changes.after.seq });
309
+ const rows = tx.objectStore(ROWS);
310
+ for (const { key, row } of changes.puts)
311
+ rows.put(row, key);
312
+ };
313
+ return () => undefined;
314
+ });
315
+ remember(board.id, changes.after);
316
+ return { ...entry };
317
+ },
318
+ touch(id, at) {
319
+ return transact(db, [BOARDS], "readwrite", (tx, refuse) => {
320
+ const store = tx.objectStore(BOARDS);
321
+ store.get(id).onsuccess = (event) => {
322
+ const listed = event.target.result;
323
+ if (!listed) {
324
+ refuse(new OrbitError("board_not_found"));
325
+ return;
326
+ }
327
+ store.put({ n: listed.n, board: { ...listed.board, last_active_at: at, updated_at: at } }, id);
328
+ };
329
+ return () => undefined;
330
+ });
331
+ },
332
+ remove(id, options = {}) {
333
+ return locks.request(lockName(id), async () => {
334
+ await transact(db, [BOARDS, HEADS, ROWS], "readwrite", (tx, refuse) => {
335
+ const store = tx.objectStore(BOARDS);
336
+ store.get(id).onsuccess = (event) => {
337
+ if (!event.target.result) {
338
+ refuse(new OrbitError("board_not_found"));
339
+ return;
340
+ }
341
+ store.delete(id);
342
+ if (options.keepData)
343
+ return;
344
+ tx.objectStore(HEADS).delete(id);
345
+ tx.objectStore(ROWS).delete(rowRange(id));
346
+ };
347
+ return () => undefined;
348
+ });
349
+ cache.delete(id);
350
+ return { removed: options.keepData ? [] : [`IndexedDB ${name}: board ${id}`] };
351
+ });
352
+ }
353
+ };
354
+ const TABLE_OF = { state: "states", ticket: "tickets", entry: "entries", relation: "relations" };
355
+ const store = {
356
+ boards,
357
+ blobs: createIndexedDbBlobStore(db),
358
+ async board(id) {
359
+ if (!(await boards.get(id)))
360
+ throw new OrbitError("board_not_found");
361
+ return boardStore(id);
362
+ },
363
+ locate(kind, id, hint) {
364
+ return transact(db, [BOARDS, ROWS], "readonly", (tx) => {
365
+ let order = [];
366
+ const holds = new Set();
367
+ tx.objectStore(BOARDS).getAll().onsuccess = (event) => {
368
+ order = directoryOrder(event.target.result).sort((a, b) => Number(b.id === hint) - Number(a.id === hint));
369
+ for (const entry of order) {
370
+ tx.objectStore(ROWS).count([entry.id, TABLE_OF[kind], id]).onsuccess = (countEvent) => {
371
+ if (countEvent.target.result > 0)
372
+ holds.add(entry.id);
373
+ };
374
+ }
375
+ };
376
+ return () => order.find((entry) => holds.has(entry.id)) ?? null;
377
+ });
378
+ },
379
+ relationsInto(boardId) {
380
+ return transact(db, [BOARDS, ROWS], "readonly", (tx) => {
381
+ const found = new Map();
382
+ let order = [];
383
+ tx.objectStore(BOARDS).getAll().onsuccess = (event) => {
384
+ order = directoryOrder(event.target.result);
385
+ for (const entry of order) {
386
+ tx.objectStore(ROWS).getAll(rowRange(entry.id, "crossBoardRelations")).onsuccess = (rowsEvent) => {
387
+ found.set(entry.id, rowsEvent.target.result);
388
+ };
389
+ }
390
+ };
391
+ return () => order.flatMap((entry) => (found.get(entry.id) ?? [])
392
+ .map((row) => ({ s: row.s, relation: row.v }))
393
+ .sort((a, b) => (a.relation.created_at < b.relation.created_at ? -1 : a.relation.created_at > b.relation.created_at ? 1 : a.s - b.s))
394
+ .filter(({ relation }) => relation.target_board_id === boardId)
395
+ .map(({ relation }) => ({ board_id: entry.id, relation })));
396
+ });
397
+ },
398
+ settings: {
399
+ get(key) {
400
+ return transact(db, [SETTINGS], "readonly", (tx) => {
401
+ let value = null;
402
+ tx.objectStore(SETTINGS).get(key).onsuccess = (event) => {
403
+ value = (event.target.result ?? null);
404
+ };
405
+ return () => value;
406
+ });
407
+ },
408
+ set(key, value) {
409
+ return transact(db, [SETTINGS], "readwrite", (tx) => {
410
+ tx.objectStore(SETTINGS).put(value, key);
411
+ return () => undefined;
412
+ });
413
+ }
414
+ },
415
+ backups: null,
416
+ async close() {
417
+ await yieldTurn();
418
+ feeds.clear();
419
+ channel?.close();
420
+ db.close();
421
+ }
422
+ };
423
+ return store;
424
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A board in memory and in rows: the memory adapter's tables (whose reader
3
+ * and writer the IndexedDB adapter reuses, so both keep the port's rules
4
+ * the same way), loaded from a board's rows, and the rows a write changed.
5
+ */
6
+ import type { Tables } from "../memory/board.ts";
7
+ import type { Row, RowKey } from "./database.ts";
8
+ /** One board as a tab holds it: its tables, the commit they are from, and each record's sequence number. */
9
+ export interface Snapshot {
10
+ revision: number;
11
+ /** The last sequence number given out on the board. */
12
+ seq: number;
13
+ tables: Tables;
14
+ /** `${table}\u0000${key}` to the record's sequence number. */
15
+ seqs: Map<string, number>;
16
+ }
17
+ /** A snapshot from a board's rows, as one getAll() returns them. */
18
+ export declare function snapshotOf(revision: number, seq: number, rows: readonly Row[]): Snapshot;
19
+ /** A copy a write can change freely: records are flat, so copying each record copies the board. */
20
+ export declare function cloneTables(tables: Tables): Tables;
21
+ /** What a write changed: rows to put and to delete, and the snapshot it leaves. */
22
+ export interface Changes {
23
+ puts: {
24
+ key: RowKey;
25
+ row: Row;
26
+ }[];
27
+ deletes: RowKey[];
28
+ after: Snapshot;
29
+ /** The board's own record changed (the directory follows it). */
30
+ boardChanged: boolean;
31
+ }
32
+ /**
33
+ * Compares a write's tables with the snapshot it started from. A record
34
+ * keeps its sequence number while it stays in place; one that is new, or
35
+ * that moved behind a record written after it, gets the next number, so
36
+ * the rows read back in the order the tables have them.
37
+ */
38
+ export declare function changesOf(boardId: string, before: Snapshot, draft: Tables): Changes;
39
+ /** The sequence number of each of the snapshot's events, in order. */
40
+ export declare function eventSeqs(snapshot: Snapshot): {
41
+ s: number;
42
+ event: Tables["events"][number];
43
+ }[];
@@ -0,0 +1,127 @@
1
+ /**
2
+ * A board in memory and in rows: the memory adapter's tables (whose reader
3
+ * and writer the IndexedDB adapter reuses, so both keep the port's rules
4
+ * the same way), loaded from a board's rows, and the rows a write changed.
5
+ */
6
+ import { emptyTables } from "../memory/board.js";
7
+ /** The tables that are lists of records, each in the order its records were written. */
8
+ const LISTS = ["states", "labels", "tickets", "ticketLabels", "comments", "relations", "crossBoardRelations", "entries", "verdicts", "attachments", "events"];
9
+ /** The board's own record and the search index are tables too, with no order. */
10
+ const BOARD = "board";
11
+ const SEARCH = "search";
12
+ /** A record's key within its table: its id, or for a label link its card and label. */
13
+ function keyOf(table, record) {
14
+ if (table === "ticketLabels") {
15
+ const link = record;
16
+ return `${link.ticket_id}\u0000${link.label_id}`;
17
+ }
18
+ return record.id;
19
+ }
20
+ const seqKey = (table, key) => `${table}\u0000${key}`;
21
+ /** A snapshot from a board's rows, as one getAll() returns them. */
22
+ export function snapshotOf(revision, seq, rows) {
23
+ const tables = emptyTables();
24
+ const seqs = new Map();
25
+ const lists = new Map();
26
+ for (const row of rows) {
27
+ if (row.t === BOARD)
28
+ tables.board = row.v;
29
+ else if (row.t === SEARCH) {
30
+ const { ticket_id, document } = row.v;
31
+ tables.search.set(ticket_id, document);
32
+ }
33
+ else {
34
+ const table = row.t;
35
+ let list = lists.get(table);
36
+ if (!list) {
37
+ list = [];
38
+ lists.set(table, list);
39
+ }
40
+ list.push({ s: row.s, v: row.v });
41
+ }
42
+ }
43
+ for (const [table, list] of lists) {
44
+ list.sort((a, b) => a.s - b.s);
45
+ tables[table] = list.map((entry) => entry.v);
46
+ for (const entry of list)
47
+ seqs.set(seqKey(table, keyOf(table, entry.v)), entry.s);
48
+ }
49
+ return { revision, seq, tables, seqs };
50
+ }
51
+ /** A copy a write can change freely: records are flat, so copying each record copies the board. */
52
+ export function cloneTables(tables) {
53
+ const copy = emptyTables();
54
+ copy.board = tables.board ? { ...tables.board } : null;
55
+ for (const table of LISTS)
56
+ copy[table] = tables[table].map((record) => ({ ...record }));
57
+ for (const [id, document] of tables.search)
58
+ copy.search.set(id, { ...document });
59
+ return copy;
60
+ }
61
+ function sameRecord(a, b) {
62
+ const keysA = Object.keys(a);
63
+ if (keysA.length !== Object.keys(b).length)
64
+ return false;
65
+ return keysA.every((key) => Object.hasOwn(b, key) && Object.is(a[key], b[key]));
66
+ }
67
+ /**
68
+ * Compares a write's tables with the snapshot it started from. A record
69
+ * keeps its sequence number while it stays in place; one that is new, or
70
+ * that moved behind a record written after it, gets the next number, so
71
+ * the rows read back in the order the tables have them.
72
+ */
73
+ export function changesOf(boardId, before, draft) {
74
+ const puts = [];
75
+ const deletes = [];
76
+ const seqs = new Map();
77
+ let seq = before.seq;
78
+ for (const table of LISTS) {
79
+ const old = new Map();
80
+ for (const record of before.tables[table])
81
+ old.set(keyOf(table, record), record);
82
+ let last = 0;
83
+ for (const record of draft[table]) {
84
+ const key = keyOf(table, record);
85
+ const kept = before.seqs.get(seqKey(table, key));
86
+ const previous = old.get(key);
87
+ old.delete(key);
88
+ let s;
89
+ let changed;
90
+ if (kept !== undefined && kept > last && previous !== undefined) {
91
+ s = kept;
92
+ changed = !sameRecord(previous, record);
93
+ }
94
+ else {
95
+ s = ++seq;
96
+ changed = true;
97
+ }
98
+ last = s;
99
+ seqs.set(seqKey(table, key), s);
100
+ if (changed)
101
+ puts.push({ key: [boardId, table, key], row: { t: table, s, v: record } });
102
+ }
103
+ for (const key of old.keys())
104
+ deletes.push([boardId, table, key]);
105
+ }
106
+ const boardChanged = before.tables.board === null || draft.board === null ? before.tables.board !== draft.board : !sameRecord(before.tables.board, draft.board);
107
+ if (boardChanged) {
108
+ if (draft.board)
109
+ puts.push({ key: [boardId, BOARD, ""], row: { t: BOARD, s: 0, v: draft.board } });
110
+ else
111
+ deletes.push([boardId, BOARD, ""]);
112
+ }
113
+ const oldDocuments = new Map(before.tables.search);
114
+ for (const [ticketId, document] of draft.search) {
115
+ const previous = oldDocuments.get(ticketId);
116
+ oldDocuments.delete(ticketId);
117
+ if (!previous || !sameRecord(previous, document))
118
+ puts.push({ key: [boardId, SEARCH, ticketId], row: { t: SEARCH, s: 0, v: { ticket_id: ticketId, document } } });
119
+ }
120
+ for (const ticketId of oldDocuments.keys())
121
+ deletes.push([boardId, SEARCH, ticketId]);
122
+ return { puts, deletes, boardChanged, after: { revision: before.revision + 1, seq, tables: draft, seqs } };
123
+ }
124
+ /** The sequence number of each of the snapshot's events, in order. */
125
+ export function eventSeqs(snapshot) {
126
+ return snapshot.tables.events.map((event) => ({ s: snapshot.seqs.get(seqKey("events", event.id)) ?? 0, event }));
127
+ }
@@ -0,0 +1,3 @@
1
+ /** Bytes in memory, by board and path: the reference BlobStore. */
2
+ import type { BlobStore } from "../../store/types.ts";
3
+ export declare function createMemoryBlobStore(): BlobStore;
@@ -0,0 +1,44 @@
1
+ /** Bytes in memory, by board and path: the reference BlobStore. */
2
+ import { OrbitError } from "../../core/errors.js";
3
+ import { checkBlobPath } from "../../store/blob-path.js";
4
+ const yieldTurn = () => Promise.resolve();
5
+ export function createMemoryBlobStore() {
6
+ const boards = new Map();
7
+ const of = (boardId) => {
8
+ let blobs = boards.get(boardId);
9
+ if (!blobs) {
10
+ blobs = new Map();
11
+ boards.set(boardId, blobs);
12
+ }
13
+ return blobs;
14
+ };
15
+ return {
16
+ async put(boardId, path, bytes, options = {}) {
17
+ await yieldTurn();
18
+ checkBlobPath(path);
19
+ const blobs = of(boardId);
20
+ if (options.exclusive && blobs.has(path))
21
+ throw new OrbitError("blob_exists");
22
+ blobs.set(path, bytes.slice());
23
+ },
24
+ async get(boardId, path) {
25
+ await yieldTurn();
26
+ checkBlobPath(path);
27
+ return boards.get(boardId)?.get(path)?.slice() ?? null;
28
+ },
29
+ async size(boardId, path) {
30
+ await yieldTurn();
31
+ checkBlobPath(path);
32
+ return boards.get(boardId)?.get(path)?.byteLength ?? null;
33
+ },
34
+ async remove(boardId, path) {
35
+ await yieldTurn();
36
+ checkBlobPath(path);
37
+ boards.get(boardId)?.delete(path);
38
+ },
39
+ async removeBoard(boardId) {
40
+ await yieldTurn();
41
+ boards.delete(boardId);
42
+ }
43
+ };
44
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * One board's records in memory, and the reader and writer over them. The
3
+ * tables are arrays in the order records were written, so "then by when it
4
+ * was written" is array order, and every sort here is stable.
5
+ */
6
+ import type { Attachment, Board, BoardEntry, Comment, CrossBoardRelation, Id, Label, OrbitEvent, Relation, ReviewVerdict, State, Ticket, TicketLabel } from "../../core/model.ts";
7
+ import type { BoardReader, BoardWriter, SearchDocument } from "../../store/types.ts";
8
+ export interface Tables {
9
+ board: Board | null;
10
+ states: State[];
11
+ labels: Label[];
12
+ tickets: Ticket[];
13
+ ticketLabels: TicketLabel[];
14
+ comments: Comment[];
15
+ relations: Relation[];
16
+ crossBoardRelations: CrossBoardRelation[];
17
+ entries: BoardEntry[];
18
+ verdicts: ReviewVerdict[];
19
+ attachments: Attachment[];
20
+ events: OrbitEvent[];
21
+ search: Map<Id, SearchDocument>;
22
+ }
23
+ export declare function emptyTables(): Tables;
24
+ /** What a writer reports to its store: the board's record changed, events appended. */
25
+ export interface WriteLog {
26
+ events: OrbitEvent[];
27
+ }
28
+ /** Checks a writer makes against the rest of the data: another board's slug. */
29
+ export interface WriteChecks {
30
+ slugTaken(slug: string): boolean;
31
+ }
32
+ /** Code-point order, as SQLite compares text. */
33
+ export declare function byCodePoint(a: string, b: string): number;
34
+ /** A reader over `tables()`: committed tables for a read, the draft for a write. */
35
+ export declare function reader(tables: () => Tables, ready?: () => Promise<void>): BoardReader;
36
+ /**
37
+ * A writer over a draft. Every check the port names is made here, in the
38
+ * order the port lists them, so a write that breaks one throws before it
39
+ * changes anything.
40
+ */
41
+ export declare function writer(boardId: Id, draft: Tables, log: WriteLog, checks: WriteChecks, guard: () => Promise<void>): BoardWriter;