@starlove/orbit 1.4.0 → 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.62905fcb.js +0 -952
  204. package/dist/full/js/app.62905fcb.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 -229
  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,77 @@
1
+ /**
2
+ * One board's database, as one store opens it: the BoardStore of the port.
3
+ *
4
+ * - Writes go one at a time through an in-process queue, and between
5
+ * processes through SQLite's write lock (`BEGIN IMMEDIATE`), taken with
6
+ * busy_timeout 0 and retried on a timer, so a lock held elsewhere never
7
+ * stops the thread. A body's reads see its own writes; a body that throws
8
+ * rolls everything back, events and index included.
9
+ * - Reads run on a small pool of connections, each read one transaction, so
10
+ * it sees one committed state (WAL lets reads and a write run side by side).
11
+ * Connections open when needed and close after a while idle.
12
+ * - Watchers hear this store's writes as each commits, and other processes'
13
+ * writes by polling `PRAGMA data_version`, then reading the events appended
14
+ * after the last one heard.
15
+ */
16
+ import type { Board, BoardChange, Id } from "../../core/model.ts";
17
+ import type { BoardStore, BoardWriter } from "../../store/types.ts";
18
+ import type { Params, Row } from "./database.ts";
19
+ import { reader } from "./records.ts";
20
+ /** The registry's side of a board's write: its listing brought in line, committed with the write. */
21
+ export interface Follow {
22
+ commit(): void;
23
+ rollback(): void;
24
+ }
25
+ /** What a board needs from the store that opened it. */
26
+ export interface BoardHost {
27
+ /** How long a write waits for a lock held elsewhere, in ms. */
28
+ readonly busyMs: number;
29
+ /** Whether a board other than `exceptId` has the slug. */
30
+ slugTaken(slug: string, exceptId: Id): boolean;
31
+ /** Starts updating the board's listing from its record; refuses a slug another board has. */
32
+ follow(board: Board): Promise<Follow>;
33
+ }
34
+ type Listener = (change: BoardChange) => void;
35
+ /** One board's database files, opened by one store. */
36
+ export declare class BoardFiles {
37
+ readonly id: Id;
38
+ readonly dbPath: string;
39
+ private readonly host;
40
+ private readonly writerConn;
41
+ private readonly lock;
42
+ private readonly idle;
43
+ private idleTimer;
44
+ private watching;
45
+ private fileIdentity;
46
+ private removed;
47
+ private closed;
48
+ private constructor();
49
+ /** Opens a board's database: refused untouched unless it is v2's, its index rebuilt once if its shape changed. */
50
+ static open(host: BoardHost, id: Id, dbPath: string, slug: string): Promise<BoardFiles>;
51
+ /** The documents stay; the table they live in is made again in its current shape. */
52
+ private rebuildIndex;
53
+ /** False once the board is removed, here or by another process (its file gone or made again). */
54
+ isLive(): boolean;
55
+ private live;
56
+ /** Async, so a board that's gone answers as a rejected read, as every other failure does. */
57
+ read<T>(body: (r: ReturnType<typeof reader>) => Promise<T>): Promise<T>;
58
+ /** One statement on a read connection, for the store's own lookups across boards. */
59
+ query(sql: string, params: Params): Row[];
60
+ private release;
61
+ write<T>(body: (w: BoardWriter) => Promise<T>): Promise<T>;
62
+ /** The board's first write: its own record, then what `seed` writes. Its listing is the store's to make. */
63
+ seed(board: Board, body: (w: BoardWriter) => Promise<void>): Promise<Board>;
64
+ /** One transaction: the body, the listing when the board's record changed, the commit, then the watchers. */
65
+ private transact;
66
+ watch(listener: Listener): () => void;
67
+ /** Runs `body` once this store's writes to the board are done, holding off new ones. */
68
+ exclusive<T>(body: () => Promise<T>): Promise<T>;
69
+ /** Lets go of the files after the board is gone; every later call answers board_not_found. */
70
+ markRemoved(): void;
71
+ /** Lets go of the files once this store's writes are done. */
72
+ close(): Promise<void>;
73
+ private shut;
74
+ /** The port's view of this board. */
75
+ store(): BoardStore;
76
+ }
77
+ export {};
@@ -0,0 +1,328 @@
1
+ /**
2
+ * One board's database, as one store opens it: the BoardStore of the port.
3
+ *
4
+ * - Writes go one at a time through an in-process queue, and between
5
+ * processes through SQLite's write lock (`BEGIN IMMEDIATE`), taken with
6
+ * busy_timeout 0 and retried on a timer, so a lock held elsewhere never
7
+ * stops the thread. A body's reads see its own writes; a body that throws
8
+ * rolls everything back, events and index included.
9
+ * - Reads run on a small pool of connections, each read one transaction, so
10
+ * it sees one committed state (WAL lets reads and a write run side by side).
11
+ * Connections open when needed and close after a while idle.
12
+ * - Watchers hear this store's writes as each commits, and other processes'
13
+ * writes by polling `PRAGMA data_version`, then reading the events appended
14
+ * after the last one heard.
15
+ */
16
+ import { statSync } from "node:fs";
17
+ import { OrbitError } from "../../core/errors.js";
18
+ import { changeOf } from "../../core/model.js";
19
+ import { checkFile, Connection, untilFree } from "./database.js";
20
+ import { Lock } from "./lock.js";
21
+ import { BOARD, EVENT, noWait, reader, toRecord, writer } from "./records.js";
22
+ import { SEARCH_INDEX_VERSION, SEARCH_TABLE } from "./schema.js";
23
+ /** How long other processes' writes take to reach a watcher, at most. */
24
+ const POLL_MS = 100;
25
+ /** Read connections kept open between reads, and how long they stay open unused. */
26
+ const IDLE_READERS = 4;
27
+ const IDLE_MS = 30_000;
28
+ /**
29
+ * Which file a path names now (its device and inode), so a board another
30
+ * process deleted, or deleted and made again, is noticed before the next read
31
+ * or write. Times aren't used: where a file system has no birth time, the one
32
+ * reported changes with every checkpoint.
33
+ */
34
+ function identity(path) {
35
+ const stat = statSync(path, { throwIfNoEntry: false });
36
+ return stat ? `${String(stat.dev)}:${String(stat.ino)}` : null;
37
+ }
38
+ /** The watchers of one board, and the connection that polls for other processes' writes. */
39
+ class Watching {
40
+ listeners = new Set();
41
+ files;
42
+ conn;
43
+ timer;
44
+ lastSeq;
45
+ dataVersion;
46
+ constructor(files) {
47
+ this.files = files;
48
+ this.conn = new Connection(files.dbPath, { queryOnly: true });
49
+ this.lastSeq = Number(this.conn.get("SELECT coalesce(max(seq), 0) AS seq FROM events")?.seq ?? 0);
50
+ this.dataVersion = this.version();
51
+ this.timer = setInterval(() => {
52
+ this.poll();
53
+ }, POLL_MS);
54
+ this.timer.unref();
55
+ }
56
+ version() {
57
+ return Number(this.conn.get("PRAGMA data_version")?.data_version ?? 0);
58
+ }
59
+ poll() {
60
+ if (!this.files.isLive())
61
+ return;
62
+ try {
63
+ const version = this.version();
64
+ if (version === this.dataVersion)
65
+ return;
66
+ this.dataVersion = version;
67
+ this.catchUp(this.conn);
68
+ }
69
+ catch {
70
+ // Busy or briefly unreadable: the next poll tries again.
71
+ }
72
+ }
73
+ /** This store's write just committed these events: hand them over, or read what came first if another process wrote in between. */
74
+ committed(events, c) {
75
+ const first = events[0];
76
+ if (!first)
77
+ return;
78
+ if (first.seq === this.lastSeq + 1) {
79
+ for (const { seq, event } of events)
80
+ this.deliver(seq, event);
81
+ }
82
+ else {
83
+ this.catchUp(c);
84
+ }
85
+ }
86
+ catchUp(c) {
87
+ for (const row of c.all(`SELECT seq, ${EVENT.columns.join(", ")} FROM events WHERE seq > ? ORDER BY seq`, [this.lastSeq])) {
88
+ this.deliver(Number(row.seq), toRecord(EVENT, row, this.files.id));
89
+ }
90
+ }
91
+ deliver(seq, event) {
92
+ if (seq <= this.lastSeq)
93
+ return;
94
+ this.lastSeq = seq;
95
+ for (const listener of [...this.listeners]) {
96
+ queueMicrotask(() => {
97
+ listener(changeOf(this.files.id, event));
98
+ });
99
+ }
100
+ }
101
+ stop() {
102
+ clearInterval(this.timer);
103
+ this.conn.close();
104
+ }
105
+ }
106
+ /** One board's database files, opened by one store. */
107
+ export class BoardFiles {
108
+ id;
109
+ dbPath;
110
+ host;
111
+ writerConn;
112
+ lock = new Lock();
113
+ idle = [];
114
+ idleTimer = null;
115
+ watching = null;
116
+ fileIdentity;
117
+ removed = false;
118
+ closed = false;
119
+ constructor(host, id, dbPath) {
120
+ this.host = host;
121
+ this.id = id;
122
+ this.dbPath = dbPath;
123
+ this.fileIdentity = identity(dbPath);
124
+ this.writerConn = new Connection(dbPath);
125
+ }
126
+ /** Opens a board's database: refused untouched unless it is v2's, its index rebuilt once if its shape changed. */
127
+ static async open(host, id, dbPath, slug) {
128
+ // The message names the board and the way out, never the path: agents read it too.
129
+ if (!checkFile(dbPath)) {
130
+ throw new OrbitError("board_file_missing", {
131
+ message: `Board "${slug}" has lost its database file. Restore the newest copy in the data folder's backups folder (the README's "Storage, backup, and moving machines" says how: its database, and the contents of its .files folder beside it), or delete the board in Settings.`
132
+ });
133
+ }
134
+ const files = new BoardFiles(host, id, dbPath);
135
+ try {
136
+ const version = files.writerConn.get("SELECT value FROM meta WHERE key = 'search_index_version'")?.value;
137
+ if (String(version) !== String(SEARCH_INDEX_VERSION))
138
+ await files.rebuildIndex();
139
+ }
140
+ catch (error) {
141
+ files.writerConn.close();
142
+ throw error;
143
+ }
144
+ return files;
145
+ }
146
+ /** The documents stay; the table they live in is made again in its current shape. */
147
+ rebuildIndex() {
148
+ return this.lock.run(async () => {
149
+ const c = this.writerConn;
150
+ await untilFree(() => {
151
+ c.exec("BEGIN IMMEDIATE");
152
+ }, Date.now() + this.host.busyMs);
153
+ try {
154
+ const documents = c.all("SELECT rowid, title, description, type, details, comments FROM search");
155
+ c.exec(`DROP TABLE search; ${SEARCH_TABLE}`);
156
+ for (const d of documents) {
157
+ c.run("INSERT INTO search (rowid, title, description, type, details, comments) VALUES (?, ?, ?, ?, ?, ?)", [d.rowid ?? null, d.title ?? "", d.description ?? "", d.type ?? "", d.details ?? "", d.comments ?? ""]);
158
+ }
159
+ c.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('search_index_version', ?)", [String(SEARCH_INDEX_VERSION)]);
160
+ c.exec("COMMIT");
161
+ }
162
+ catch (error) {
163
+ c.rollback();
164
+ throw error;
165
+ }
166
+ });
167
+ }
168
+ /** False once the board is removed, here or by another process (its file gone or made again). */
169
+ isLive() {
170
+ if (this.closed || this.removed)
171
+ return false;
172
+ if (identity(this.dbPath) !== this.fileIdentity)
173
+ this.removed = true;
174
+ return !this.removed;
175
+ }
176
+ live() {
177
+ if (this.closed)
178
+ throw new OrbitError("storage_failed", { message: "This store is closed." });
179
+ if (!this.isLive())
180
+ throw new OrbitError("board_not_found");
181
+ }
182
+ /** Async, so a board that's gone answers as a rejected read, as every other failure does. */
183
+ async read(body) {
184
+ this.live();
185
+ const c = this.idle.pop() ?? new Connection(this.dbPath, { queryOnly: true });
186
+ c.exec("BEGIN");
187
+ const done = () => {
188
+ c.finishRead();
189
+ this.release(c);
190
+ };
191
+ let result;
192
+ try {
193
+ result = body(reader({ c, boardId: this.id, before: noWait }));
194
+ }
195
+ catch (error) {
196
+ done();
197
+ throw error;
198
+ }
199
+ return result.finally(done);
200
+ }
201
+ /** One statement on a read connection, for the store's own lookups across boards. */
202
+ query(sql, params) {
203
+ this.live();
204
+ const c = this.idle.pop() ?? new Connection(this.dbPath, { queryOnly: true });
205
+ try {
206
+ return c.all(sql, params);
207
+ }
208
+ finally {
209
+ this.release(c);
210
+ }
211
+ }
212
+ release(c) {
213
+ if (this.closed || this.removed || this.idle.length >= IDLE_READERS) {
214
+ c.close();
215
+ return;
216
+ }
217
+ this.idle.push(c);
218
+ if (this.idleTimer)
219
+ clearTimeout(this.idleTimer);
220
+ this.idleTimer = setTimeout(() => {
221
+ for (const idle of this.idle.splice(0))
222
+ idle.close();
223
+ }, IDLE_MS);
224
+ this.idleTimer.unref();
225
+ }
226
+ write(body) {
227
+ return this.lock.run(() => this.transact(body, null));
228
+ }
229
+ /** The board's first write: its own record, then what `seed` writes. Its listing is the store's to make. */
230
+ seed(board, body) {
231
+ return this.lock.run(async () => {
232
+ let made = board;
233
+ await this.transact(async (w) => {
234
+ await body(w);
235
+ made = await w.board.get();
236
+ }, board);
237
+ return made;
238
+ });
239
+ }
240
+ /** One transaction: the body, the listing when the board's record changed, the commit, then the watchers. */
241
+ async transact(body, first) {
242
+ this.live();
243
+ const c = this.writerConn;
244
+ await untilFree(() => {
245
+ c.exec("BEGIN IMMEDIATE");
246
+ }, Date.now() + this.host.busyMs);
247
+ const log = { events: [], board: null };
248
+ let open = true;
249
+ const before = async () => {
250
+ await Promise.resolve();
251
+ if (!open)
252
+ throw new OrbitError("write_closed");
253
+ };
254
+ let follow = null;
255
+ try {
256
+ if (first)
257
+ c.run(BOARD.insert, BOARD.columns.map((column) => first[column]));
258
+ const checks = { slugTaken: (slug) => this.host.slugTaken(slug, this.id) };
259
+ let result;
260
+ try {
261
+ result = await body(writer({ c, boardId: this.id, before }, log, checks));
262
+ }
263
+ finally {
264
+ open = false;
265
+ }
266
+ if (log.board && !first)
267
+ follow = await this.host.follow(log.board);
268
+ c.exec("COMMIT");
269
+ follow?.commit();
270
+ if (log.events.length > 0)
271
+ this.watching?.committed(log.events, c);
272
+ return result;
273
+ }
274
+ catch (error) {
275
+ c.rollback();
276
+ follow?.rollback();
277
+ throw error;
278
+ }
279
+ }
280
+ watch(listener) {
281
+ this.live();
282
+ this.watching ??= new Watching(this);
283
+ const watching = this.watching;
284
+ watching.listeners.add(listener);
285
+ return () => {
286
+ watching.listeners.delete(listener);
287
+ if (watching.listeners.size === 0 && this.watching === watching) {
288
+ watching.stop();
289
+ this.watching = null;
290
+ }
291
+ };
292
+ }
293
+ /** Runs `body` once this store's writes to the board are done, holding off new ones. */
294
+ exclusive(body) {
295
+ return this.lock.run(body);
296
+ }
297
+ /** Lets go of the files after the board is gone; every later call answers board_not_found. */
298
+ markRemoved() {
299
+ this.removed = true;
300
+ this.shut();
301
+ }
302
+ /** Lets go of the files once this store's writes are done. */
303
+ close() {
304
+ return this.lock.run(() => {
305
+ this.closed = true;
306
+ this.shut();
307
+ return Promise.resolve();
308
+ });
309
+ }
310
+ shut() {
311
+ this.watching?.stop();
312
+ this.watching = null;
313
+ if (this.idleTimer)
314
+ clearTimeout(this.idleTimer);
315
+ for (const idle of this.idle.splice(0))
316
+ idle.close();
317
+ this.writerConn.close();
318
+ }
319
+ /** The port's view of this board. */
320
+ store() {
321
+ return {
322
+ id: this.id,
323
+ read: (body) => this.read(body),
324
+ write: (body) => this.write(body),
325
+ watch: (listener) => this.watch(listener)
326
+ };
327
+ }
328
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Connections to Orbit's SQLite files, and the rules every statement runs
3
+ * under: SQLite never waits on the server's thread (busy_timeout 0, retried
4
+ * here on a timer), a database's own error never reaches an answer (each is
5
+ * mapped to the port's codes), and a file without v2's version is never
6
+ * opened for writing.
7
+ */
8
+ import type { SQLInputValue, SQLOutputValue } from "node:sqlite";
9
+ import { OrbitError } from "../../core/errors.ts";
10
+ import type { ErrorCode } from "../../core/errors.ts";
11
+ export type Row = Record<string, SQLOutputValue>;
12
+ export type Params = readonly SQLInputValue[];
13
+ /** What a statement's failure means, where the schema alone can't say (a lane still holding cards). */
14
+ export interface Meaning {
15
+ /** The code for a foreign key failure; `invalid_reference` by default. */
16
+ foreignKey?: ErrorCode;
17
+ }
18
+ /** An OrbitError for anything a statement throws, the original kept as its cause. */
19
+ export declare function storageError(error: unknown, meaning?: Meaning): OrbitError;
20
+ /**
21
+ * Runs `attempt` until it isn't refused as busy, waiting on a timer between
22
+ * tries (1 ms, doubling to 50 ms), so the thread stays free for everything
23
+ * else. After `deadline` (a Date.now() value) the refusal stands: `storage_busy`.
24
+ */
25
+ export declare function untilFree<T>(attempt: () => T, deadline: number): Promise<T>;
26
+ /** One connection to one file, with each statement prepared once. */
27
+ export declare class Connection {
28
+ readonly path: string;
29
+ private readonly db;
30
+ private readonly prepared;
31
+ private closed;
32
+ constructor(path: string, options?: {
33
+ queryOnly?: boolean;
34
+ });
35
+ exec(sql: string): void;
36
+ private statement;
37
+ run(sql: string, params?: Params, meaning?: Meaning): {
38
+ changes: number;
39
+ lastInsertRowid: number;
40
+ };
41
+ get(sql: string, params?: Params, meaning?: Meaning): Row | undefined;
42
+ all(sql: string, params?: Params): Row[];
43
+ /**
44
+ * Ends a transaction left open by a failure; quiet if there is none (SQLite
45
+ * may have ended it with the failure). It asks SQLite by trying, which
46
+ * reads the same on every Node.
47
+ */
48
+ rollback(): void;
49
+ /** Ends a read's transaction. */
50
+ finishRead(): void;
51
+ close(): void;
52
+ }
53
+ /**
54
+ * Whether the file at `path` is one this build may open: true when it is v2's
55
+ * at this schema version, false when there is no file. Anything else is
56
+ * refused without being opened, so its bytes stay as they are: a file of 1.x
57
+ * or the first 2.0 (no version) with `data_needs_migration`, a newer
58
+ * schema's with `data_from_newer_orbit`.
59
+ */
60
+ export declare function checkFile(path: string): boolean;
61
+ /**
62
+ * Makes a database at `path` with `schema`, all at once: it is built under a
63
+ * temporary name and linked into place, so no other process ever sees a file
64
+ * without its version, and when two make the same file at once one wins and
65
+ * the other's attempt is discarded. True when this call made it.
66
+ */
67
+ export declare function createFile(path: string, schema: string): boolean;