@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.
- 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.62905fcb.js +0 -952
- package/dist/full/js/app.62905fcb.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 -229
- 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,285 @@
|
|
|
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 { randomUUID } from "node:crypto";
|
|
9
|
+
import { closeSync, linkSync, openSync, readSync, renameSync, rmSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
12
|
+
import { DatabaseSync } from "node:sqlite";
|
|
13
|
+
import { isOrbitError, OrbitError } from "../../core/errors.js";
|
|
14
|
+
import { APPLICATION_ID, SCHEMA_VERSION } from "./schema.js";
|
|
15
|
+
/**
|
|
16
|
+
* The port's code for each UNIQUE constraint in the schema, as SQLite names
|
|
17
|
+
* it ("UNIQUE constraint failed: states.name"). A repeated record id is
|
|
18
|
+
* `duplicate_id` everywhere.
|
|
19
|
+
*/
|
|
20
|
+
const UNIQUE_CODES = {
|
|
21
|
+
"boards.slug": "board_slug_taken",
|
|
22
|
+
"states.name": "state_name_taken",
|
|
23
|
+
"labels.name": "label_name_taken",
|
|
24
|
+
"tickets.number": "ticket_number_taken",
|
|
25
|
+
"relations.source_ticket_id, relations.target_ticket_id, relations.type": "relation_exists",
|
|
26
|
+
"cross_board_relations.source_ticket_id, cross_board_relations.target_board_id, cross_board_relations.target_ticket_id, cross_board_relations.type": "relation_exists"
|
|
27
|
+
};
|
|
28
|
+
const SQLITE_BUSY = 5;
|
|
29
|
+
const SQLITE_LOCKED = 6;
|
|
30
|
+
const SQLITE_FULL = 13;
|
|
31
|
+
const SQLITE_CONSTRAINT_FOREIGNKEY = 787;
|
|
32
|
+
const SQLITE_CONSTRAINT_PRIMARYKEY = 1555;
|
|
33
|
+
const SQLITE_CONSTRAINT_UNIQUE = 2067;
|
|
34
|
+
/** SQLite's code from its message, for a Node whose errors don't carry `errcode`. */
|
|
35
|
+
function codeFromMessage(error) {
|
|
36
|
+
const message = error instanceof Error ? error.message : "";
|
|
37
|
+
if (/database (table )?is locked|database is busy/.test(message))
|
|
38
|
+
return SQLITE_BUSY;
|
|
39
|
+
if (message.includes("database or disk is full"))
|
|
40
|
+
return SQLITE_FULL;
|
|
41
|
+
if (message.includes("FOREIGN KEY constraint failed"))
|
|
42
|
+
return SQLITE_CONSTRAINT_FOREIGNKEY;
|
|
43
|
+
if (message.startsWith("UNIQUE constraint failed"))
|
|
44
|
+
return SQLITE_CONSTRAINT_UNIQUE;
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/** An OrbitError for anything a statement throws, the original kept as its cause. */
|
|
48
|
+
export function storageError(error, meaning = {}) {
|
|
49
|
+
if (isOrbitError(error))
|
|
50
|
+
return error;
|
|
51
|
+
const code = error?.errcode ?? codeFromMessage(error);
|
|
52
|
+
if (typeof code !== "number")
|
|
53
|
+
return new OrbitError("storage_failed", { cause: error });
|
|
54
|
+
const primary = code & 0xff;
|
|
55
|
+
if (primary === SQLITE_BUSY || primary === SQLITE_LOCKED)
|
|
56
|
+
return new OrbitError("storage_busy", { cause: error });
|
|
57
|
+
if (primary === SQLITE_FULL)
|
|
58
|
+
return new OrbitError("storage_full", { cause: error });
|
|
59
|
+
if (code === SQLITE_CONSTRAINT_FOREIGNKEY)
|
|
60
|
+
return new OrbitError(meaning.foreignKey ?? "invalid_reference", { cause: error });
|
|
61
|
+
if (code === SQLITE_CONSTRAINT_UNIQUE || code === SQLITE_CONSTRAINT_PRIMARYKEY) {
|
|
62
|
+
const columns = error.message.replace(/^UNIQUE constraint failed: /, "");
|
|
63
|
+
const known = UNIQUE_CODES[columns] ?? (/^\w+\.id$/.test(columns) ? "duplicate_id" : undefined);
|
|
64
|
+
if (known)
|
|
65
|
+
return new OrbitError(known, { cause: error });
|
|
66
|
+
}
|
|
67
|
+
return new OrbitError("storage_failed", { cause: error });
|
|
68
|
+
}
|
|
69
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
70
|
+
/**
|
|
71
|
+
* Runs `attempt` until it isn't refused as busy, waiting on a timer between
|
|
72
|
+
* tries (1 ms, doubling to 50 ms), so the thread stays free for everything
|
|
73
|
+
* else. After `deadline` (a Date.now() value) the refusal stands: `storage_busy`.
|
|
74
|
+
*/
|
|
75
|
+
export async function untilFree(attempt, deadline) {
|
|
76
|
+
let delay = 1;
|
|
77
|
+
for (;;) {
|
|
78
|
+
try {
|
|
79
|
+
return attempt();
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const mapped = storageError(error);
|
|
83
|
+
if (mapped.code !== "storage_busy" || Date.now() + delay > deadline)
|
|
84
|
+
throw mapped;
|
|
85
|
+
await sleep(delay);
|
|
86
|
+
delay = Math.min(delay * 2, 50);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** One connection to one file, with each statement prepared once. */
|
|
91
|
+
export class Connection {
|
|
92
|
+
path;
|
|
93
|
+
db;
|
|
94
|
+
prepared = new Map();
|
|
95
|
+
closed = false;
|
|
96
|
+
constructor(path, options = {}) {
|
|
97
|
+
this.path = path;
|
|
98
|
+
try {
|
|
99
|
+
this.db = new DatabaseSync(path);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
throw storageError(error);
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
this.db.exec(`PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 0; PRAGMA synchronous = NORMAL;${options.queryOnly ? " PRAGMA query_only = ON;" : ""}`);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
this.db.close();
|
|
109
|
+
throw storageError(error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
exec(sql) {
|
|
113
|
+
try {
|
|
114
|
+
this.db.exec(sql);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
throw storageError(error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
statement(sql) {
|
|
121
|
+
let statement = this.prepared.get(sql);
|
|
122
|
+
if (!statement) {
|
|
123
|
+
statement = this.db.prepare(sql);
|
|
124
|
+
this.prepared.set(sql, statement);
|
|
125
|
+
}
|
|
126
|
+
return statement;
|
|
127
|
+
}
|
|
128
|
+
run(sql, params = [], meaning) {
|
|
129
|
+
try {
|
|
130
|
+
const result = this.statement(sql).run(...params);
|
|
131
|
+
return { changes: Number(result.changes), lastInsertRowid: Number(result.lastInsertRowid) };
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
throw storageError(error, meaning);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
get(sql, params = [], meaning) {
|
|
138
|
+
try {
|
|
139
|
+
return this.statement(sql).get(...params);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
throw storageError(error, meaning);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
all(sql, params = []) {
|
|
146
|
+
try {
|
|
147
|
+
return this.statement(sql).all(...params);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
throw storageError(error);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Ends a transaction left open by a failure; quiet if there is none (SQLite
|
|
155
|
+
* may have ended it with the failure). It asks SQLite by trying, which
|
|
156
|
+
* reads the same on every Node.
|
|
157
|
+
*/
|
|
158
|
+
rollback() {
|
|
159
|
+
if (this.closed)
|
|
160
|
+
return;
|
|
161
|
+
try {
|
|
162
|
+
this.db.exec("ROLLBACK");
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// No transaction was open.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Ends a read's transaction. */
|
|
169
|
+
finishRead() {
|
|
170
|
+
if (this.closed)
|
|
171
|
+
return;
|
|
172
|
+
try {
|
|
173
|
+
this.db.exec("COMMIT");
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
this.rollback();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
close() {
|
|
180
|
+
if (this.closed)
|
|
181
|
+
return;
|
|
182
|
+
this.closed = true;
|
|
183
|
+
this.prepared.clear();
|
|
184
|
+
this.db.close();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// The files themselves
|
|
188
|
+
/** The header fields that say whose file it is (SQLite's file format: offsets 60 and 68, big-endian). */
|
|
189
|
+
function readHeader(path) {
|
|
190
|
+
let fd;
|
|
191
|
+
try {
|
|
192
|
+
fd = openSync(path, "r");
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (error.code === "ENOENT")
|
|
196
|
+
return null;
|
|
197
|
+
throw new OrbitError("storage_failed", { cause: error });
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const header = Buffer.alloc(100);
|
|
201
|
+
const length = readSync(fd, header, 0, 100, 0);
|
|
202
|
+
const sqlite = length === 100 && header.toString("latin1", 0, 16) === "SQLite format 3\0";
|
|
203
|
+
return { sqlite, userVersion: sqlite ? header.readInt32BE(60) : 0, applicationId: sqlite ? header.readInt32BE(68) : 0 };
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
closeSync(fd);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const OLDER_DATA = "This data folder holds boards from an older Orbit (1.x, or 2.0 before its rebuild), which this Orbit doesn't open. Nothing in it was changed. Keep it as it is: ";
|
|
210
|
+
/**
|
|
211
|
+
* The way out for a folder of older data. The older Orbit's own default,
|
|
212
|
+
* ~/.orbit (os.homedir() reads HOME, as Orbit does), takes the short form,
|
|
213
|
+
* which reads it into this Orbit's default folder.
|
|
214
|
+
*/
|
|
215
|
+
function migrationNeeded(path) {
|
|
216
|
+
const older = join(homedir(), ".orbit");
|
|
217
|
+
if (resolve(path).startsWith(older + sep))
|
|
218
|
+
return `${OLDER_DATA}with DATA_DIR unset, \`orbit migrate\` brings its boards into ~/.starscape/orbit, the folder this Orbit uses, and orbit run then opens them.`;
|
|
219
|
+
return `${OLDER_DATA}\`orbit migrate <this folder> --to <a new folder>\` brings its boards into a new folder, and DATA_DIR then points there.`;
|
|
220
|
+
}
|
|
221
|
+
const NEWER_ORBIT = "This data folder was written by a newer Orbit. Nothing in it was changed. Update Orbit to open it.";
|
|
222
|
+
/**
|
|
223
|
+
* Whether the file at `path` is one this build may open: true when it is v2's
|
|
224
|
+
* at this schema version, false when there is no file. Anything else is
|
|
225
|
+
* refused without being opened, so its bytes stay as they are: a file of 1.x
|
|
226
|
+
* or the first 2.0 (no version) with `data_needs_migration`, a newer
|
|
227
|
+
* schema's with `data_from_newer_orbit`.
|
|
228
|
+
*/
|
|
229
|
+
export function checkFile(path) {
|
|
230
|
+
const header = readHeader(path);
|
|
231
|
+
if (header === null)
|
|
232
|
+
return false;
|
|
233
|
+
if (!header.sqlite)
|
|
234
|
+
throw new OrbitError("storage_failed", { message: `${basename(path)} in this data folder isn't a database.` });
|
|
235
|
+
if (header.applicationId !== APPLICATION_ID || header.userVersion < 1)
|
|
236
|
+
throw new OrbitError("data_needs_migration", { message: migrationNeeded(path) });
|
|
237
|
+
if (header.userVersion > SCHEMA_VERSION)
|
|
238
|
+
throw new OrbitError("data_from_newer_orbit", { message: NEWER_ORBIT });
|
|
239
|
+
// v2 has one schema version so far; a later one upgrades older v2 files here instead.
|
|
240
|
+
if (header.userVersion < SCHEMA_VERSION)
|
|
241
|
+
throw new OrbitError("data_needs_migration", { message: migrationNeeded(path) });
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Makes a database at `path` with `schema`, all at once: it is built under a
|
|
246
|
+
* temporary name and linked into place, so no other process ever sees a file
|
|
247
|
+
* without its version, and when two make the same file at once one wins and
|
|
248
|
+
* the other's attempt is discarded. True when this call made it.
|
|
249
|
+
*/
|
|
250
|
+
export function createFile(path, schema) {
|
|
251
|
+
const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.creating`);
|
|
252
|
+
try {
|
|
253
|
+
const db = new DatabaseSync(temporary);
|
|
254
|
+
try {
|
|
255
|
+
db.exec(`PRAGMA application_id = ${String(APPLICATION_ID)}; PRAGMA user_version = ${String(SCHEMA_VERSION)}; PRAGMA journal_mode = WAL;`);
|
|
256
|
+
db.exec(`BEGIN; ${schema} COMMIT;`);
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
db.close();
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
linkSync(temporary, path);
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
const code = error.code;
|
|
267
|
+
if (code === "EEXIST")
|
|
268
|
+
return false;
|
|
269
|
+
if (code !== "EPERM" && code !== "ENOTSUP" && code !== "EXDEV")
|
|
270
|
+
throw error;
|
|
271
|
+
// A file system without hard links: a rename, checked first.
|
|
272
|
+
if (readHeader(path) !== null)
|
|
273
|
+
return false;
|
|
274
|
+
renameSync(temporary, path);
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
throw storageError(error);
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
for (const suffix of ["", "-wal", "-shm", "-journal"])
|
|
283
|
+
rmSync(`${temporary}${suffix}`, { force: true });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs bodies one at a time, in the order they asked: the in-process half of
|
|
3
|
+
* keeping a board's writes one at a time (SQLite's write lock is the other,
|
|
4
|
+
* between processes).
|
|
5
|
+
*/
|
|
6
|
+
export declare class Lock {
|
|
7
|
+
private tail;
|
|
8
|
+
/** Waits for every earlier holder, then returns the function that lets go. */
|
|
9
|
+
acquire(): Promise<() => void>;
|
|
10
|
+
run<T>(body: () => Promise<T>): Promise<T>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs bodies one at a time, in the order they asked: the in-process half of
|
|
3
|
+
* keeping a board's writes one at a time (SQLite's write lock is the other,
|
|
4
|
+
* between processes).
|
|
5
|
+
*/
|
|
6
|
+
export class Lock {
|
|
7
|
+
tail = Promise.resolve();
|
|
8
|
+
/** Waits for every earlier holder, then returns the function that lets go. */
|
|
9
|
+
async acquire() {
|
|
10
|
+
let release = () => undefined;
|
|
11
|
+
const next = new Promise((resolve) => {
|
|
12
|
+
release = resolve;
|
|
13
|
+
});
|
|
14
|
+
const previous = this.tail;
|
|
15
|
+
this.tail = previous.then(() => next);
|
|
16
|
+
await previous;
|
|
17
|
+
let released = false;
|
|
18
|
+
return () => {
|
|
19
|
+
if (released)
|
|
20
|
+
return;
|
|
21
|
+
released = true;
|
|
22
|
+
release();
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async run(body) {
|
|
26
|
+
const release = await this.acquire();
|
|
27
|
+
try {
|
|
28
|
+
return await body();
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
release();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One board's records in SQL: the port's BoardReader and BoardWriter over a
|
|
3
|
+
* connection. Each method is one statement, prepared once per connection, and
|
|
4
|
+
* reads that serve a whole board return every row at once. Records come back
|
|
5
|
+
* built field for field in the order src/core/model.ts lists them, with the
|
|
6
|
+
* board's id added where a record names its board (board.db holds one board,
|
|
7
|
+
* so its rows don't repeat it).
|
|
8
|
+
*/
|
|
9
|
+
import type { Board, CrossBoardRelation, Id, OrbitEvent } from "../../core/model.ts";
|
|
10
|
+
import type { BoardReader, BoardWriter } from "../../store/types.ts";
|
|
11
|
+
import type { Connection, Row } from "./database.ts";
|
|
12
|
+
/** A kind of record: its table, its fields in the model's order, and the statements every kind shares. */
|
|
13
|
+
interface Kind<T> {
|
|
14
|
+
table: string;
|
|
15
|
+
fields: readonly (keyof T & string)[];
|
|
16
|
+
/** The fields stored as columns: all but `board_id`. */
|
|
17
|
+
columns: readonly string[];
|
|
18
|
+
select: string;
|
|
19
|
+
insert: string;
|
|
20
|
+
}
|
|
21
|
+
export declare const BOARD: Kind<Board>;
|
|
22
|
+
export declare const CROSS: Kind<CrossBoardRelation>;
|
|
23
|
+
export declare const EVENT: Kind<OrbitEvent>;
|
|
24
|
+
/** The record a row holds, built in the model's field order. */
|
|
25
|
+
export declare function toRecord<T>(of: Kind<T>, row: Row, boardId: Id): T;
|
|
26
|
+
/** What a reader or writer works over. */
|
|
27
|
+
export interface Over {
|
|
28
|
+
c: Connection;
|
|
29
|
+
boardId: Id;
|
|
30
|
+
/** Awaited before each call: the writer's check that its write is still open. */
|
|
31
|
+
before: () => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
/** For readers, which have nothing to check before a call. */
|
|
34
|
+
export declare const noWait: () => Promise<void>;
|
|
35
|
+
/** The port's BoardReader over a connection. */
|
|
36
|
+
export declare function reader({ c, boardId, before }: Over): BoardReader;
|
|
37
|
+
/** What a write did that its store acts on once it commits. */
|
|
38
|
+
export interface WriteLog {
|
|
39
|
+
/** Events appended, with their place in the board's history. */
|
|
40
|
+
events: {
|
|
41
|
+
seq: number;
|
|
42
|
+
event: OrbitEvent;
|
|
43
|
+
}[];
|
|
44
|
+
/** The board's own record as the write left it, when the write changed it. */
|
|
45
|
+
board: Board | null;
|
|
46
|
+
}
|
|
47
|
+
/** Checks a writer makes against the rest of the data folder. */
|
|
48
|
+
export interface WriteChecks {
|
|
49
|
+
/** Another board has this slug. */
|
|
50
|
+
slugTaken(slug: string): boolean;
|
|
51
|
+
}
|
|
52
|
+
/** The port's BoardWriter over a connection in an open transaction. */
|
|
53
|
+
export declare function writer(over: Over, log: WriteLog, checks: WriteChecks): BoardWriter;
|
|
54
|
+
export {};
|