@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.
- package/Dockerfile +10 -8
- package/README.md +146 -164
- package/dist/full/csp.txt +1 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-subset.woff2 +0 -0
- package/dist/full/fonts/JetBrains_Mono/README.txt +5 -0
- package/dist/full/fonts/Mona_Sans/MonaSans-subset.woff2 +0 -0
- package/{public/fonts/Open_Sans → dist/full/fonts/Mona_Sans}/OFL.txt +93 -93
- package/dist/full/fonts/Mona_Sans/README.txt +10 -0
- package/{public/fonts/JetBrains_Mono → dist/full/fonts/Newsreader}/OFL.txt +93 -93
- package/dist/full/fonts/Newsreader/README.txt +8 -0
- package/dist/full/fonts/Newsreader/newsreader.woff2 +0 -0
- package/dist/full/index.html +36 -653
- package/dist/full/js/app.BmuponPJ.js +32 -0
- package/dist/full/js/app.BmuponPJ.js.map +1 -0
- package/dist/full/js/chunk.1fJGQGbW.js +2 -0
- package/dist/full/js/chunk.1fJGQGbW.js.map +1 -0
- package/dist/full/js/chunk.5D7JO2zp.js +2 -0
- package/dist/full/js/chunk.5D7JO2zp.js.map +1 -0
- package/dist/full/js/chunk.BrNn_D79.js +2 -0
- package/dist/full/js/chunk.BrNn_D79.js.map +1 -0
- package/dist/full/js/chunk.CokJXIqM.js +2 -0
- package/dist/full/js/chunk.CokJXIqM.js.map +1 -0
- package/dist/full/js/chunk.D_Haqy6R.js +2 -0
- package/dist/full/js/chunk.D_Haqy6R.js.map +1 -0
- package/dist/full/js/chunk.RO0jsH6I.js +15 -0
- package/dist/full/js/chunk.RO0jsH6I.js.map +1 -0
- package/dist/full/js/chunk.dqWOEtBM.js +2 -0
- package/dist/full/js/chunk.dqWOEtBM.js.map +1 -0
- package/dist/full/js/warm.f260bbbc.js +15 -0
- package/dist/full/orbits-favicon.png +0 -0
- package/dist/full/orbits-logo-bg-wide.png +0 -0
- package/dist/full/orbits-logo-bg.png +0 -0
- package/dist/full/orbits-logo.png +0 -0
- package/dist/full/styles.2b154ffe.css +1 -0
- package/dist/full/vendor/starlove-ui/theme-init.js +66 -0
- package/dist/full/version.json +2 -2
- package/docs/AGENT_GUIDE.md +114 -0
- package/docs/UPDATING.md +249 -64
- package/lib/adapters/indexeddb/blobs.d.ts +3 -0
- package/lib/adapters/indexeddb/blobs.js +54 -0
- package/lib/adapters/indexeddb/database.d.ts +60 -0
- package/lib/adapters/indexeddb/database.js +126 -0
- package/lib/adapters/indexeddb/index.d.ts +5 -0
- package/lib/adapters/indexeddb/index.js +3 -0
- package/lib/adapters/indexeddb/locks.d.ts +14 -0
- package/lib/adapters/indexeddb/locks.js +40 -0
- package/lib/adapters/indexeddb/store.d.ts +40 -0
- package/lib/adapters/indexeddb/store.js +424 -0
- package/lib/adapters/indexeddb/tables.d.ts +43 -0
- package/lib/adapters/indexeddb/tables.js +127 -0
- package/lib/adapters/memory/blobs.d.ts +3 -0
- package/lib/adapters/memory/blobs.js +44 -0
- package/lib/adapters/memory/board.d.ts +41 -0
- package/lib/adapters/memory/board.js +521 -0
- package/lib/adapters/memory/index.d.ts +2 -0
- package/lib/adapters/memory/index.js +2 -0
- package/lib/adapters/memory/search.d.ts +6 -0
- package/lib/adapters/memory/search.js +30 -0
- package/lib/adapters/memory/store.d.ts +34 -0
- package/lib/adapters/memory/store.js +266 -0
- package/lib/adapters/sqlite/backups.d.ts +53 -0
- package/lib/adapters/sqlite/backups.js +232 -0
- package/lib/adapters/sqlite/blobs.d.ts +14 -0
- package/lib/adapters/sqlite/blobs.js +109 -0
- package/lib/adapters/sqlite/board.d.ts +77 -0
- package/lib/adapters/sqlite/board.js +328 -0
- package/lib/adapters/sqlite/database.d.ts +67 -0
- package/lib/adapters/sqlite/database.js +285 -0
- package/lib/adapters/sqlite/index.d.ts +3 -0
- package/lib/adapters/sqlite/index.js +2 -0
- package/lib/adapters/sqlite/lock.d.ts +11 -0
- package/lib/adapters/sqlite/lock.js +34 -0
- package/lib/adapters/sqlite/records.d.ts +54 -0
- package/lib/adapters/sqlite/records.js +546 -0
- package/lib/adapters/sqlite/schema.d.ts +54 -0
- package/lib/adapters/sqlite/schema.js +276 -0
- package/lib/adapters/sqlite/store.d.ts +39 -0
- package/lib/adapters/sqlite/store.js +385 -0
- package/lib/browser/in-page.d.ts +31 -0
- package/lib/browser/in-page.js +46 -0
- package/lib/browser/old-preview.d.ts +13 -0
- package/lib/browser/old-preview.js +80 -0
- package/lib/browser/preview-board.d.ts +8 -0
- package/lib/browser/preview-board.js +69 -0
- package/lib/cli/main.d.ts +7 -0
- package/lib/cli/main.js +343 -0
- package/lib/cli/orbit.d.ts +8 -0
- package/lib/cli/orbit.js +25 -0
- package/lib/client/http.d.ts +38 -0
- package/lib/client/http.js +334 -0
- package/lib/client/local.d.ts +26 -0
- package/lib/client/local.js +190 -0
- package/lib/client/object-url.d.ts +8 -0
- package/lib/client/object-url.js +21 -0
- package/lib/client/types.d.ts +268 -0
- package/lib/client/types.js +8 -0
- package/lib/commands/agent-tools.d.ts +31 -0
- package/lib/commands/agent-tools.js +100 -0
- package/lib/commands/index.d.ts +56 -0
- package/lib/commands/index.js +298 -0
- package/lib/commands/parse.d.ts +73 -0
- package/lib/commands/parse.js +231 -0
- package/lib/commands/present.d.ts +134 -0
- package/lib/commands/present.js +198 -0
- package/lib/commands/session.d.ts +85 -0
- package/lib/commands/session.js +138 -0
- package/lib/commands/verbs.d.ts +35 -0
- package/lib/commands/verbs.js +842 -0
- package/lib/core/actors.d.ts +18 -0
- package/lib/core/actors.js +45 -0
- package/lib/core/agent.d.ts +128 -0
- package/lib/core/agent.js +323 -0
- package/lib/core/boards.d.ts +113 -0
- package/lib/core/boards.js +288 -0
- package/lib/core/bootstrap.d.ts +47 -0
- package/lib/core/bootstrap.js +100 -0
- package/lib/core/bytes.d.ts +10 -0
- package/lib/core/bytes.js +52 -0
- package/lib/core/card-paths.d.ts +25 -0
- package/lib/core/card-paths.js +44 -0
- package/lib/core/context.d.ts +60 -0
- package/lib/core/context.js +42 -0
- package/lib/core/cross-board.d.ts +81 -0
- package/lib/core/cross-board.js +228 -0
- package/lib/core/describe.d.ts +21 -0
- package/lib/core/describe.js +81 -0
- package/lib/core/errors.d.ts +159 -0
- package/lib/core/errors.js +160 -0
- package/lib/core/files.d.ts +79 -0
- package/lib/core/files.js +262 -0
- package/lib/core/history.d.ts +20 -0
- package/lib/core/history.js +53 -0
- package/lib/core/image-types.d.ts +16 -0
- package/lib/core/image-types.js +36 -0
- package/lib/core/model.d.ts +263 -0
- package/lib/core/model.js +34 -0
- package/lib/core/nebula-link.d.ts +14 -0
- package/lib/core/nebula-link.js +39 -0
- package/lib/core/relations.d.ts +58 -0
- package/lib/core/relations.js +112 -0
- package/lib/core/search.d.ts +38 -0
- package/lib/core/search.js +92 -0
- package/lib/core/sessions.d.ts +44 -0
- package/lib/core/sessions.js +158 -0
- package/lib/core/snapshot-schema.d.ts +295 -0
- package/lib/core/snapshot-schema.js +82 -0
- package/lib/core/snapshots.d.ts +75 -0
- package/lib/core/snapshots.js +409 -0
- package/lib/core/states.d.ts +25 -0
- package/lib/core/states.js +126 -0
- package/lib/core/text.d.ts +40 -0
- package/lib/core/text.js +113 -0
- package/lib/core/tickets.d.ts +69 -0
- package/lib/core/tickets.js +311 -0
- package/lib/core/trello.d.ts +17 -0
- package/lib/core/trello.js +269 -0
- package/lib/core/verdicts.d.ts +28 -0
- package/lib/core/verdicts.js +92 -0
- package/lib/core/views.d.ts +95 -0
- package/lib/core/views.js +186 -0
- package/lib/http/app.d.ts +46 -0
- package/lib/http/app.js +491 -0
- package/lib/mcp-server.d.ts +10 -0
- package/lib/mcp-server.js +120 -0
- package/lib/node/assets.d.ts +7 -0
- package/lib/node/assets.js +68 -0
- package/lib/node/data-folder.d.ts +21 -0
- package/lib/node/data-folder.js +34 -0
- package/lib/node/env.d.ts +38 -0
- package/lib/node/env.js +49 -0
- package/lib/node/host-check.d.ts +24 -0
- package/lib/node/host-check.js +53 -0
- package/lib/node/host-services.d.ts +9 -0
- package/lib/node/host-services.js +150 -0
- package/lib/node/local.d.ts +22 -0
- package/lib/node/local.js +61 -0
- package/lib/node/migrate.d.ts +53 -0
- package/lib/node/migrate.js +635 -0
- package/lib/node/package.d.ts +7 -0
- package/lib/node/package.js +14 -0
- package/lib/node/repo-board.d.ts +49 -0
- package/lib/node/repo-board.js +245 -0
- package/lib/node/serve.d.ts +8 -0
- package/lib/node/serve.js +106 -0
- package/lib/orbit.d.ts +47 -0
- package/lib/orbit.js +44 -0
- package/lib/quiet-sqlite-warning.d.ts +1 -0
- package/lib/quiet-sqlite-warning.js +18 -0
- package/lib/store/blob-path.d.ts +6 -0
- package/lib/store/blob-path.js +12 -0
- package/lib/store/conformance.d.ts +52 -0
- package/lib/store/conformance.js +1449 -0
- package/lib/store/search-words.d.ts +10 -0
- package/lib/store/search-words.js +19 -0
- package/lib/store/types.d.ts +452 -0
- package/lib/store/types.js +34 -0
- package/llms.txt +63 -0
- package/package.json +109 -25
- package/src/cli/orbit.js +11 -658
- package/src/mcp-server.js +16 -876
- package/SKILL-ORBIT.md +0 -151
- package/build.mjs +0 -210
- package/dist/full/js/app.4e6fbe20.js +0 -952
- package/dist/full/js/app.4e6fbe20.js.map +0 -7
- package/dist/full/styles.4e60d824.css +0 -1
- package/docs/AGENT_PROTOCOL.md +0 -424
- package/docs/MCP_SETUP.md +0 -54
- package/docs/ORBIT_DISPATCH.md +0 -280
- package/docs/SS-Orbit-Dark.png +0 -0
- package/docs/SS-Orbit-Light.png +0 -0
- package/docs/mcp-client.example.json +0 -13
- package/docs/setup/orbit-init-wizard.md +0 -174
- package/public/apple-touch-icon.png +0 -0
- package/public/cursors/live_arrow.cur +0 -0
- package/public/cursors/live_link.cur +0 -0
- package/public/cursors/live_move.cur +0 -0
- package/public/cursors/live_select.cur +0 -0
- package/public/cursors/live_unavail.cur +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/OFL.txt +0 -93
- package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/OFL.txt +0 -93
- package/public/fonts/Inter/README.txt +0 -118
- package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/README.txt +0 -79
- package/public/fonts/OpenRing-Regular.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/README.txt +0 -100
- package/public/fonts/Orbitron/OFL.txt +0 -93
- package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/public/fonts/Orbitron/README.txt +0 -68
- package/public/fonts/Rajdhani/OFL.txt +0 -93
- package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/public/fonts/runescape-uf.ttf +0 -0
- package/public/index.html +0 -769
- package/public/js/actor-labels.js +0 -65
- package/public/js/api.js +0 -39
- package/public/js/app.js +0 -45
- package/public/js/artifact-drawer.js +0 -79
- package/public/js/board-export.js +0 -161
- package/public/js/board-menu.js +0 -374
- package/public/js/card-actions.js +0 -545
- package/public/js/config.js +0 -20
- package/public/js/create-card.js +0 -190
- package/public/js/dom.js +0 -19
- package/public/js/drawer.js +0 -273
- package/public/js/font-preference.js +0 -230
- package/public/js/format.js +0 -433
- package/public/js/help-menu.js +0 -31
- package/public/js/icons.js +0 -16
- package/public/js/kanban.js +0 -694
- package/public/js/local-backend.js +0 -1015
- package/public/js/main.js +0 -161
- package/public/js/modal.js +0 -33
- package/public/js/motion-preference.js +0 -64
- package/public/js/panel.js +0 -58
- package/public/js/router.js +0 -134
- package/public/js/search.js +0 -56
- package/public/js/settings-tabs.js +0 -7
- package/public/js/settings.js +0 -1634
- package/public/js/sse.js +0 -159
- package/public/js/starscape.js +0 -332
- package/public/js/state.js +0 -129
- package/public/js/text-editing.js +0 -128
- package/public/js/theme-preference.js +0 -95
- package/public/js/ticket-detail.js +0 -1516
- package/public/js/ticket-typeahead.js +0 -49
- package/public/js/toast.js +0 -89
- package/public/js/topbar-notes.js +0 -155
- package/public/js/transport.js +0 -22
- package/public/js/typeahead.js +0 -206
- package/public/js/unread.js +0 -121
- package/public/js/update-check.js +0 -58
- package/public/js/url-routes.js +0 -94
- package/public/orbits-favicon.png +0 -0
- package/public/orbits-logo-bg-wide.png +0 -0
- package/public/orbits-logo-bg.png +0 -0
- package/public/orbits-logo.png +0 -0
- package/public/styles.css +0 -7709
- package/src/core/agent.js +0 -387
- package/src/core/agents-md.js +0 -109
- package/src/core/artifacts.js +0 -149
- package/src/core/attachments.js +0 -238
- package/src/core/auth.js +0 -85
- package/src/core/backups.js +0 -142
- package/src/core/board.js +0 -148
- package/src/core/boards.js +0 -409
- package/src/core/bootstrap.js +0 -170
- package/src/core/css-bundle.js +0 -30
- package/src/core/db.js +0 -315
- package/src/core/delete-board.js +0 -163
- package/src/core/dispatch.js +0 -1007
- package/src/core/events.js +0 -314
- package/src/core/http.js +0 -115
- package/src/core/migrate.js +0 -198
- package/src/core/paths.js +0 -55
- package/src/core/provision-repo-board.js +0 -119
- package/src/core/queries.js +0 -246
- package/src/core/registry.js +0 -211
- package/src/core/relations.js +0 -163
- package/src/core/review-verdicts.js +0 -138
- package/src/core/router.js +0 -556
- package/src/core/search.js +0 -183
- package/src/core/seed.js +0 -234
- package/src/core/snapshots.js +0 -692
- package/src/core/states.js +0 -167
- package/src/core/system-picker.js +0 -109
- package/src/core/tickets.js +0 -400
- package/src/core/util.js +0 -106
- package/src/core/version-check.js +0 -154
- package/src/mcp/orbit-client.js +0 -287
- 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;
|