@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,424 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* The IndexedDB adapter: Orbit's storage port in a browser, shared by every
|
|
4
|
+
* tab of the origin. `openIndexedDbStore()` gives the store and, as its
|
|
5
|
+
* `blobs`, the blob store of the same database.
|
|
6
|
+
*
|
|
7
|
+
* - **Writes.** An IndexedDB transaction commits by itself once its code
|
|
8
|
+
* waits for anything else, so a write can't be one. A write takes the
|
|
9
|
+
* board's exclusive lock (a Web Lock, held across tabs), runs its body
|
|
10
|
+
* against a copy of the board in memory (the memory adapter's writer, so
|
|
11
|
+
* its reads see its own changes and the port's checks are the same), then
|
|
12
|
+
* commits what changed in one readwrite transaction. A body that throws
|
|
13
|
+
* commits nothing. Card numbers are given under the lock.
|
|
14
|
+
* - **Reads.** A read loads the board in one readonly transaction, which is
|
|
15
|
+
* one committed state, and runs its body against it. It takes no lock, so
|
|
16
|
+
* a read never waits for a write. A tab keeps the board it last loaded and
|
|
17
|
+
* reuses it while the board's revision is unchanged.
|
|
18
|
+
* - **watch().** This tab's commits reach its watchers at once; other tabs'
|
|
19
|
+
* through a BroadcastChannel, after which the board is read again and the
|
|
20
|
+
* events past the last one heard are delivered, in order, once.
|
|
21
|
+
* - **Search** filters a board's documents in memory, as the memory adapter
|
|
22
|
+
* does: a board's documents are few.
|
|
23
|
+
* - **Backups:** none (`backups` is null); people export a board instead.
|
|
24
|
+
*/
|
|
25
|
+
import { OrbitError } from "../../core/errors.js";
|
|
26
|
+
import { changeOf } from "../../core/model.js";
|
|
27
|
+
import { byCodePoint, emptyTables, reader, writer } from "../memory/board.js";
|
|
28
|
+
import { createIndexedDbBlobStore } from "./blobs.js";
|
|
29
|
+
import { BOARDS, HEADS, ROWS, SETTINGS, openDatabase, rowRange, transact } from "./database.js";
|
|
30
|
+
import { createLocalLocks, webLocks } from "./locks.js";
|
|
31
|
+
import { changesOf, cloneTables, eventSeqs, snapshotOf } from "./tables.js";
|
|
32
|
+
const yieldTurn = () => Promise.resolve();
|
|
33
|
+
/** Listings in the order boards were made. */
|
|
34
|
+
const made = (listed) => [...listed].sort((a, b) => a.n - b.n).map((entry) => entry.board);
|
|
35
|
+
/** The directory's order: by name, then by when each was made. */
|
|
36
|
+
const directoryOrder = (listed) => made(listed).sort((a, b) => byCodePoint(a.name, b.name));
|
|
37
|
+
export async function openIndexedDbStore(options = {}) {
|
|
38
|
+
const name = options.name ?? "orbit";
|
|
39
|
+
const factory = options.indexedDB ?? globalThis.indexedDB;
|
|
40
|
+
const locks = options.locks ?? webLocks() ?? createLocalLocks();
|
|
41
|
+
const db = await openDatabase(factory, name);
|
|
42
|
+
const makeChannel = options.channel === undefined ? (typeof BroadcastChannel === "undefined" ? null : (channelName) => new BroadcastChannel(channelName)) : options.channel;
|
|
43
|
+
const channel = makeChannel ? makeChannel(`orbit-indexeddb:${name}`) : null;
|
|
44
|
+
const lockName = (boardId) => `orbit-indexeddb:${name}:board:${boardId}`;
|
|
45
|
+
/** The board as this tab last saw it. */
|
|
46
|
+
const cache = new Map();
|
|
47
|
+
const feeds = new Map();
|
|
48
|
+
const remember = (boardId, snapshot) => {
|
|
49
|
+
const known = cache.get(boardId);
|
|
50
|
+
if (!known || known.revision < snapshot.revision)
|
|
51
|
+
cache.set(boardId, snapshot);
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* The board's committed state, in one transaction: this tab's copy when
|
|
55
|
+
* the revision matches, else its rows. With `directory`, every listing too.
|
|
56
|
+
*/
|
|
57
|
+
function load(boardId, withDirectory = false) {
|
|
58
|
+
return transact(db, [BOARDS, HEADS, ROWS], "readonly", (tx, refuse) => {
|
|
59
|
+
let snapshot = null;
|
|
60
|
+
let directory = [];
|
|
61
|
+
tx.objectStore(BOARDS).get(boardId).onsuccess = (event) => {
|
|
62
|
+
if (!event.target.result)
|
|
63
|
+
refuse(new OrbitError("board_not_found"));
|
|
64
|
+
};
|
|
65
|
+
if (withDirectory) {
|
|
66
|
+
tx.objectStore(BOARDS).getAll().onsuccess = (event) => {
|
|
67
|
+
directory = made(event.target.result);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
tx.objectStore(HEADS).get(boardId).onsuccess = (event) => {
|
|
71
|
+
const head = event.target.result;
|
|
72
|
+
if (!head) {
|
|
73
|
+
refuse(new OrbitError("board_not_found"));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const known = cache.get(boardId);
|
|
77
|
+
if (known && known.revision === head.revision) {
|
|
78
|
+
snapshot = known;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
tx.objectStore(ROWS).getAll(rowRange(boardId)).onsuccess = (rowsEvent) => {
|
|
82
|
+
snapshot = snapshotOf(head.revision, head.seq, rowsEvent.target.result);
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
return () => {
|
|
86
|
+
if (!snapshot)
|
|
87
|
+
throw new OrbitError("board_not_found");
|
|
88
|
+
remember(boardId, snapshot);
|
|
89
|
+
return { snapshot, directory };
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/** Writes a write's changes in one transaction, if the board is still where the write found it. */
|
|
94
|
+
function commit(boardId, before, changes, board) {
|
|
95
|
+
return transact(db, [BOARDS, HEADS, ROWS], "readwrite", (tx, refuse) => {
|
|
96
|
+
const boards = tx.objectStore(BOARDS);
|
|
97
|
+
const rows = tx.objectStore(ROWS);
|
|
98
|
+
tx.objectStore(HEADS).get(boardId).onsuccess = (event) => {
|
|
99
|
+
const head = event.target.result;
|
|
100
|
+
if (!head) {
|
|
101
|
+
refuse(new OrbitError("board_not_found"));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// The lock makes this impossible; a page without shared locks would otherwise lose a write.
|
|
105
|
+
if (head.revision !== before.revision) {
|
|
106
|
+
refuse(new OrbitError("storage_busy", { message: "Another tab changed the board during this write." }));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const next = { board_id: boardId, revision: changes.after.revision, seq: changes.after.seq };
|
|
110
|
+
tx.objectStore(HEADS).put(next);
|
|
111
|
+
for (const key of changes.deletes)
|
|
112
|
+
rows.delete(key);
|
|
113
|
+
for (const { key, row } of changes.puts)
|
|
114
|
+
rows.put(row, key);
|
|
115
|
+
};
|
|
116
|
+
boards.getAll().onsuccess = (event) => {
|
|
117
|
+
const entries = event.target.result;
|
|
118
|
+
const listed = entries.find((candidate) => candidate.board.id === boardId);
|
|
119
|
+
if (!listed) {
|
|
120
|
+
refuse(new OrbitError("board_not_found"));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!changes.boardChanged || !board)
|
|
124
|
+
return;
|
|
125
|
+
if (entries.some((other) => other.board.id !== boardId && other.board.slug === board.slug)) {
|
|
126
|
+
refuse(new OrbitError("board_slug_taken"));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const entry = listed.board;
|
|
130
|
+
boards.put({ n: listed.n, board: { ...entry, slug: board.slug, name: board.name, repo_url: board.repo_url, default_branch: board.default_branch, updated_at: board.updated_at } }, boardId);
|
|
131
|
+
};
|
|
132
|
+
return () => undefined;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** Runs `body` on a copy of `base`, with the writer closed once it settles, as the memory adapter does. */
|
|
136
|
+
async function runWrite(boardId, base, slugTaken, body) {
|
|
137
|
+
const draft = cloneTables(base);
|
|
138
|
+
const log = { events: [] };
|
|
139
|
+
let open = true;
|
|
140
|
+
const guard = async () => {
|
|
141
|
+
await yieldTurn();
|
|
142
|
+
if (!open)
|
|
143
|
+
throw new OrbitError("write_closed");
|
|
144
|
+
};
|
|
145
|
+
try {
|
|
146
|
+
const result = await body(writer(boardId, draft, log, { slugTaken }, guard));
|
|
147
|
+
return { result, draft, log };
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
open = false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Hands every event past the feed's cursor to its listeners, once and in order. */
|
|
154
|
+
function deliver(boardId, feed, snapshot) {
|
|
155
|
+
if (feed.cursor === null)
|
|
156
|
+
return;
|
|
157
|
+
for (const { s, event } of eventSeqs(snapshot)) {
|
|
158
|
+
if (s <= feed.cursor)
|
|
159
|
+
continue;
|
|
160
|
+
feed.cursor = s;
|
|
161
|
+
for (const listener of [...feed.listeners]) {
|
|
162
|
+
const change = changeOf(boardId, event);
|
|
163
|
+
queueMicrotask(() => {
|
|
164
|
+
listener(change);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function enqueue(boardId, feed, step) {
|
|
170
|
+
feed.chain = feed.chain
|
|
171
|
+
.then(step)
|
|
172
|
+
.then((snapshot) => {
|
|
173
|
+
if (snapshot && feeds.get(boardId) === feed)
|
|
174
|
+
deliver(boardId, feed, snapshot);
|
|
175
|
+
})
|
|
176
|
+
.catch(() => undefined);
|
|
177
|
+
}
|
|
178
|
+
const onNotice = (event) => {
|
|
179
|
+
const { board } = event.data;
|
|
180
|
+
const feed = feeds.get(board);
|
|
181
|
+
if (!feed)
|
|
182
|
+
return;
|
|
183
|
+
enqueue(board, feed, async () => (await load(board)).snapshot);
|
|
184
|
+
};
|
|
185
|
+
if (channel)
|
|
186
|
+
channel.onmessage = onNotice;
|
|
187
|
+
function startFeed(boardId) {
|
|
188
|
+
const feed = { listeners: new Set(), cursor: null, chain: Promise.resolve() };
|
|
189
|
+
// Made now, so it reads the board as it is when watch() is called; later commits are heard.
|
|
190
|
+
const start = transact(db, [HEADS], "readonly", (tx) => {
|
|
191
|
+
let seq = 0;
|
|
192
|
+
tx.objectStore(HEADS).get(boardId).onsuccess = (event) => {
|
|
193
|
+
seq = event.target.result?.seq ?? 0;
|
|
194
|
+
};
|
|
195
|
+
return () => seq;
|
|
196
|
+
});
|
|
197
|
+
feed.chain = start.then((seq) => {
|
|
198
|
+
feed.cursor = seq;
|
|
199
|
+
}, () => {
|
|
200
|
+
feed.cursor = 0;
|
|
201
|
+
});
|
|
202
|
+
feeds.set(boardId, feed);
|
|
203
|
+
return feed;
|
|
204
|
+
}
|
|
205
|
+
function boardStore(id) {
|
|
206
|
+
return {
|
|
207
|
+
id,
|
|
208
|
+
async read(body) {
|
|
209
|
+
const { snapshot } = await load(id);
|
|
210
|
+
return body(reader(() => snapshot.tables));
|
|
211
|
+
},
|
|
212
|
+
write(body) {
|
|
213
|
+
return locks.request(lockName(id), async () => {
|
|
214
|
+
const { snapshot, directory } = await load(id, true);
|
|
215
|
+
const slugTaken = (slug) => directory.some((entry) => entry.slug === slug && entry.id !== id);
|
|
216
|
+
const { result, draft } = await runWrite(id, snapshot.tables, slugTaken, body);
|
|
217
|
+
const changes = changesOf(id, snapshot, draft);
|
|
218
|
+
await commit(id, snapshot, changes, draft.board);
|
|
219
|
+
remember(id, changes.after);
|
|
220
|
+
const feed = feeds.get(id);
|
|
221
|
+
if (feed)
|
|
222
|
+
enqueue(id, feed, () => changes.after);
|
|
223
|
+
channel?.postMessage({ board: id, revision: changes.after.revision });
|
|
224
|
+
return result;
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
watch(listener) {
|
|
228
|
+
const feed = feeds.get(id) ?? startFeed(id);
|
|
229
|
+
feed.listeners.add(listener);
|
|
230
|
+
return () => {
|
|
231
|
+
feed.listeners.delete(listener);
|
|
232
|
+
if (feed.listeners.size === 0 && feeds.get(id) === feed)
|
|
233
|
+
feeds.delete(id);
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const allListed = () => transact(db, [BOARDS], "readonly", (tx) => {
|
|
239
|
+
let entries = [];
|
|
240
|
+
tx.objectStore(BOARDS).getAll().onsuccess = (event) => {
|
|
241
|
+
entries = event.target.result;
|
|
242
|
+
};
|
|
243
|
+
return () => entries;
|
|
244
|
+
});
|
|
245
|
+
const allBoards = async () => made(await allListed());
|
|
246
|
+
const boards = {
|
|
247
|
+
async list() {
|
|
248
|
+
return directoryOrder(await allListed());
|
|
249
|
+
},
|
|
250
|
+
async get(id) {
|
|
251
|
+
return (await allBoards()).find((entry) => entry.id === id) ?? null;
|
|
252
|
+
},
|
|
253
|
+
async bySlug(slug) {
|
|
254
|
+
return (await allBoards()).find((entry) => entry.slug === slug) ?? null;
|
|
255
|
+
},
|
|
256
|
+
async latest() {
|
|
257
|
+
let best = null;
|
|
258
|
+
for (const entry of await allBoards()) {
|
|
259
|
+
if (best === null || (entry.last_active_at ?? "") >= (best.last_active_at ?? ""))
|
|
260
|
+
best = entry;
|
|
261
|
+
}
|
|
262
|
+
return best;
|
|
263
|
+
},
|
|
264
|
+
async create(board, seed) {
|
|
265
|
+
const existing = await allBoards();
|
|
266
|
+
const slugTaken = (slug) => existing.some((entry) => entry.slug === slug && entry.id !== board.id);
|
|
267
|
+
if (existing.some((entry) => entry.id === board.id))
|
|
268
|
+
throw new OrbitError("duplicate_id");
|
|
269
|
+
if (slugTaken(board.slug))
|
|
270
|
+
throw new OrbitError("board_slug_taken");
|
|
271
|
+
const base = emptyTables();
|
|
272
|
+
base.board = { ...board };
|
|
273
|
+
const empty = { revision: 0, seq: 0, tables: emptyTables(), seqs: new Map() };
|
|
274
|
+
const { draft } = await runWrite(board.id, base, slugTaken, seed);
|
|
275
|
+
const changes = changesOf(board.id, empty, draft);
|
|
276
|
+
const entry = {
|
|
277
|
+
id: board.id,
|
|
278
|
+
slug: draft.board?.slug ?? board.slug,
|
|
279
|
+
name: draft.board?.name ?? board.name,
|
|
280
|
+
db_path: "",
|
|
281
|
+
repo_url: draft.board?.repo_url ?? board.repo_url,
|
|
282
|
+
default_branch: draft.board?.default_branch ?? board.default_branch,
|
|
283
|
+
last_active_at: board.created_at,
|
|
284
|
+
created_at: board.created_at,
|
|
285
|
+
updated_at: draft.board?.updated_at ?? board.updated_at
|
|
286
|
+
};
|
|
287
|
+
await transact(db, [BOARDS, HEADS, ROWS], "readwrite", (tx, refuse) => {
|
|
288
|
+
const boardsStore = tx.objectStore(BOARDS);
|
|
289
|
+
boardsStore.getAll().onsuccess = (event) => {
|
|
290
|
+
const entries = event.target.result;
|
|
291
|
+
if (entries.some((other) => other.board.id === board.id)) {
|
|
292
|
+
refuse(new OrbitError("duplicate_id"));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (entries.some((other) => other.board.slug === entry.slug)) {
|
|
296
|
+
refuse(new OrbitError("board_slug_taken"));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const n = entries.reduce((top, other) => Math.max(top, other.n), 0) + 1;
|
|
300
|
+
boardsStore.add({ n, board: entry }, board.id);
|
|
301
|
+
};
|
|
302
|
+
tx.objectStore(HEADS).get(board.id).onsuccess = (event) => {
|
|
303
|
+
// Rows left by a board removed with its data kept: the id is taken.
|
|
304
|
+
if (event.target.result) {
|
|
305
|
+
refuse(new OrbitError("duplicate_id"));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
tx.objectStore(HEADS).put({ board_id: board.id, revision: changes.after.revision, seq: changes.after.seq });
|
|
309
|
+
const rows = tx.objectStore(ROWS);
|
|
310
|
+
for (const { key, row } of changes.puts)
|
|
311
|
+
rows.put(row, key);
|
|
312
|
+
};
|
|
313
|
+
return () => undefined;
|
|
314
|
+
});
|
|
315
|
+
remember(board.id, changes.after);
|
|
316
|
+
return { ...entry };
|
|
317
|
+
},
|
|
318
|
+
touch(id, at) {
|
|
319
|
+
return transact(db, [BOARDS], "readwrite", (tx, refuse) => {
|
|
320
|
+
const store = tx.objectStore(BOARDS);
|
|
321
|
+
store.get(id).onsuccess = (event) => {
|
|
322
|
+
const listed = event.target.result;
|
|
323
|
+
if (!listed) {
|
|
324
|
+
refuse(new OrbitError("board_not_found"));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
store.put({ n: listed.n, board: { ...listed.board, last_active_at: at, updated_at: at } }, id);
|
|
328
|
+
};
|
|
329
|
+
return () => undefined;
|
|
330
|
+
});
|
|
331
|
+
},
|
|
332
|
+
remove(id, options = {}) {
|
|
333
|
+
return locks.request(lockName(id), async () => {
|
|
334
|
+
await transact(db, [BOARDS, HEADS, ROWS], "readwrite", (tx, refuse) => {
|
|
335
|
+
const store = tx.objectStore(BOARDS);
|
|
336
|
+
store.get(id).onsuccess = (event) => {
|
|
337
|
+
if (!event.target.result) {
|
|
338
|
+
refuse(new OrbitError("board_not_found"));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
store.delete(id);
|
|
342
|
+
if (options.keepData)
|
|
343
|
+
return;
|
|
344
|
+
tx.objectStore(HEADS).delete(id);
|
|
345
|
+
tx.objectStore(ROWS).delete(rowRange(id));
|
|
346
|
+
};
|
|
347
|
+
return () => undefined;
|
|
348
|
+
});
|
|
349
|
+
cache.delete(id);
|
|
350
|
+
return { removed: options.keepData ? [] : [`IndexedDB ${name}: board ${id}`] };
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const TABLE_OF = { state: "states", ticket: "tickets", entry: "entries", relation: "relations" };
|
|
355
|
+
const store = {
|
|
356
|
+
boards,
|
|
357
|
+
blobs: createIndexedDbBlobStore(db),
|
|
358
|
+
async board(id) {
|
|
359
|
+
if (!(await boards.get(id)))
|
|
360
|
+
throw new OrbitError("board_not_found");
|
|
361
|
+
return boardStore(id);
|
|
362
|
+
},
|
|
363
|
+
locate(kind, id, hint) {
|
|
364
|
+
return transact(db, [BOARDS, ROWS], "readonly", (tx) => {
|
|
365
|
+
let order = [];
|
|
366
|
+
const holds = new Set();
|
|
367
|
+
tx.objectStore(BOARDS).getAll().onsuccess = (event) => {
|
|
368
|
+
order = directoryOrder(event.target.result).sort((a, b) => Number(b.id === hint) - Number(a.id === hint));
|
|
369
|
+
for (const entry of order) {
|
|
370
|
+
tx.objectStore(ROWS).count([entry.id, TABLE_OF[kind], id]).onsuccess = (countEvent) => {
|
|
371
|
+
if (countEvent.target.result > 0)
|
|
372
|
+
holds.add(entry.id);
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
return () => order.find((entry) => holds.has(entry.id)) ?? null;
|
|
377
|
+
});
|
|
378
|
+
},
|
|
379
|
+
relationsInto(boardId) {
|
|
380
|
+
return transact(db, [BOARDS, ROWS], "readonly", (tx) => {
|
|
381
|
+
const found = new Map();
|
|
382
|
+
let order = [];
|
|
383
|
+
tx.objectStore(BOARDS).getAll().onsuccess = (event) => {
|
|
384
|
+
order = directoryOrder(event.target.result);
|
|
385
|
+
for (const entry of order) {
|
|
386
|
+
tx.objectStore(ROWS).getAll(rowRange(entry.id, "crossBoardRelations")).onsuccess = (rowsEvent) => {
|
|
387
|
+
found.set(entry.id, rowsEvent.target.result);
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
return () => order.flatMap((entry) => (found.get(entry.id) ?? [])
|
|
392
|
+
.map((row) => ({ s: row.s, relation: row.v }))
|
|
393
|
+
.sort((a, b) => (a.relation.created_at < b.relation.created_at ? -1 : a.relation.created_at > b.relation.created_at ? 1 : a.s - b.s))
|
|
394
|
+
.filter(({ relation }) => relation.target_board_id === boardId)
|
|
395
|
+
.map(({ relation }) => ({ board_id: entry.id, relation })));
|
|
396
|
+
});
|
|
397
|
+
},
|
|
398
|
+
settings: {
|
|
399
|
+
get(key) {
|
|
400
|
+
return transact(db, [SETTINGS], "readonly", (tx) => {
|
|
401
|
+
let value = null;
|
|
402
|
+
tx.objectStore(SETTINGS).get(key).onsuccess = (event) => {
|
|
403
|
+
value = (event.target.result ?? null);
|
|
404
|
+
};
|
|
405
|
+
return () => value;
|
|
406
|
+
});
|
|
407
|
+
},
|
|
408
|
+
set(key, value) {
|
|
409
|
+
return transact(db, [SETTINGS], "readwrite", (tx) => {
|
|
410
|
+
tx.objectStore(SETTINGS).put(value, key);
|
|
411
|
+
return () => undefined;
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
backups: null,
|
|
416
|
+
async close() {
|
|
417
|
+
await yieldTurn();
|
|
418
|
+
feeds.clear();
|
|
419
|
+
channel?.close();
|
|
420
|
+
db.close();
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
return store;
|
|
424
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A board in memory and in rows: the memory adapter's tables (whose reader
|
|
3
|
+
* and writer the IndexedDB adapter reuses, so both keep the port's rules
|
|
4
|
+
* the same way), loaded from a board's rows, and the rows a write changed.
|
|
5
|
+
*/
|
|
6
|
+
import type { Tables } from "../memory/board.ts";
|
|
7
|
+
import type { Row, RowKey } from "./database.ts";
|
|
8
|
+
/** One board as a tab holds it: its tables, the commit they are from, and each record's sequence number. */
|
|
9
|
+
export interface Snapshot {
|
|
10
|
+
revision: number;
|
|
11
|
+
/** The last sequence number given out on the board. */
|
|
12
|
+
seq: number;
|
|
13
|
+
tables: Tables;
|
|
14
|
+
/** `${table}\u0000${key}` to the record's sequence number. */
|
|
15
|
+
seqs: Map<string, number>;
|
|
16
|
+
}
|
|
17
|
+
/** A snapshot from a board's rows, as one getAll() returns them. */
|
|
18
|
+
export declare function snapshotOf(revision: number, seq: number, rows: readonly Row[]): Snapshot;
|
|
19
|
+
/** A copy a write can change freely: records are flat, so copying each record copies the board. */
|
|
20
|
+
export declare function cloneTables(tables: Tables): Tables;
|
|
21
|
+
/** What a write changed: rows to put and to delete, and the snapshot it leaves. */
|
|
22
|
+
export interface Changes {
|
|
23
|
+
puts: {
|
|
24
|
+
key: RowKey;
|
|
25
|
+
row: Row;
|
|
26
|
+
}[];
|
|
27
|
+
deletes: RowKey[];
|
|
28
|
+
after: Snapshot;
|
|
29
|
+
/** The board's own record changed (the directory follows it). */
|
|
30
|
+
boardChanged: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Compares a write's tables with the snapshot it started from. A record
|
|
34
|
+
* keeps its sequence number while it stays in place; one that is new, or
|
|
35
|
+
* that moved behind a record written after it, gets the next number, so
|
|
36
|
+
* the rows read back in the order the tables have them.
|
|
37
|
+
*/
|
|
38
|
+
export declare function changesOf(boardId: string, before: Snapshot, draft: Tables): Changes;
|
|
39
|
+
/** The sequence number of each of the snapshot's events, in order. */
|
|
40
|
+
export declare function eventSeqs(snapshot: Snapshot): {
|
|
41
|
+
s: number;
|
|
42
|
+
event: Tables["events"][number];
|
|
43
|
+
}[];
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A board in memory and in rows: the memory adapter's tables (whose reader
|
|
3
|
+
* and writer the IndexedDB adapter reuses, so both keep the port's rules
|
|
4
|
+
* the same way), loaded from a board's rows, and the rows a write changed.
|
|
5
|
+
*/
|
|
6
|
+
import { emptyTables } from "../memory/board.js";
|
|
7
|
+
/** The tables that are lists of records, each in the order its records were written. */
|
|
8
|
+
const LISTS = ["states", "labels", "tickets", "ticketLabels", "comments", "relations", "crossBoardRelations", "entries", "verdicts", "attachments", "events"];
|
|
9
|
+
/** The board's own record and the search index are tables too, with no order. */
|
|
10
|
+
const BOARD = "board";
|
|
11
|
+
const SEARCH = "search";
|
|
12
|
+
/** A record's key within its table: its id, or for a label link its card and label. */
|
|
13
|
+
function keyOf(table, record) {
|
|
14
|
+
if (table === "ticketLabels") {
|
|
15
|
+
const link = record;
|
|
16
|
+
return `${link.ticket_id}\u0000${link.label_id}`;
|
|
17
|
+
}
|
|
18
|
+
return record.id;
|
|
19
|
+
}
|
|
20
|
+
const seqKey = (table, key) => `${table}\u0000${key}`;
|
|
21
|
+
/** A snapshot from a board's rows, as one getAll() returns them. */
|
|
22
|
+
export function snapshotOf(revision, seq, rows) {
|
|
23
|
+
const tables = emptyTables();
|
|
24
|
+
const seqs = new Map();
|
|
25
|
+
const lists = new Map();
|
|
26
|
+
for (const row of rows) {
|
|
27
|
+
if (row.t === BOARD)
|
|
28
|
+
tables.board = row.v;
|
|
29
|
+
else if (row.t === SEARCH) {
|
|
30
|
+
const { ticket_id, document } = row.v;
|
|
31
|
+
tables.search.set(ticket_id, document);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const table = row.t;
|
|
35
|
+
let list = lists.get(table);
|
|
36
|
+
if (!list) {
|
|
37
|
+
list = [];
|
|
38
|
+
lists.set(table, list);
|
|
39
|
+
}
|
|
40
|
+
list.push({ s: row.s, v: row.v });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const [table, list] of lists) {
|
|
44
|
+
list.sort((a, b) => a.s - b.s);
|
|
45
|
+
tables[table] = list.map((entry) => entry.v);
|
|
46
|
+
for (const entry of list)
|
|
47
|
+
seqs.set(seqKey(table, keyOf(table, entry.v)), entry.s);
|
|
48
|
+
}
|
|
49
|
+
return { revision, seq, tables, seqs };
|
|
50
|
+
}
|
|
51
|
+
/** A copy a write can change freely: records are flat, so copying each record copies the board. */
|
|
52
|
+
export function cloneTables(tables) {
|
|
53
|
+
const copy = emptyTables();
|
|
54
|
+
copy.board = tables.board ? { ...tables.board } : null;
|
|
55
|
+
for (const table of LISTS)
|
|
56
|
+
copy[table] = tables[table].map((record) => ({ ...record }));
|
|
57
|
+
for (const [id, document] of tables.search)
|
|
58
|
+
copy.search.set(id, { ...document });
|
|
59
|
+
return copy;
|
|
60
|
+
}
|
|
61
|
+
function sameRecord(a, b) {
|
|
62
|
+
const keysA = Object.keys(a);
|
|
63
|
+
if (keysA.length !== Object.keys(b).length)
|
|
64
|
+
return false;
|
|
65
|
+
return keysA.every((key) => Object.hasOwn(b, key) && Object.is(a[key], b[key]));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Compares a write's tables with the snapshot it started from. A record
|
|
69
|
+
* keeps its sequence number while it stays in place; one that is new, or
|
|
70
|
+
* that moved behind a record written after it, gets the next number, so
|
|
71
|
+
* the rows read back in the order the tables have them.
|
|
72
|
+
*/
|
|
73
|
+
export function changesOf(boardId, before, draft) {
|
|
74
|
+
const puts = [];
|
|
75
|
+
const deletes = [];
|
|
76
|
+
const seqs = new Map();
|
|
77
|
+
let seq = before.seq;
|
|
78
|
+
for (const table of LISTS) {
|
|
79
|
+
const old = new Map();
|
|
80
|
+
for (const record of before.tables[table])
|
|
81
|
+
old.set(keyOf(table, record), record);
|
|
82
|
+
let last = 0;
|
|
83
|
+
for (const record of draft[table]) {
|
|
84
|
+
const key = keyOf(table, record);
|
|
85
|
+
const kept = before.seqs.get(seqKey(table, key));
|
|
86
|
+
const previous = old.get(key);
|
|
87
|
+
old.delete(key);
|
|
88
|
+
let s;
|
|
89
|
+
let changed;
|
|
90
|
+
if (kept !== undefined && kept > last && previous !== undefined) {
|
|
91
|
+
s = kept;
|
|
92
|
+
changed = !sameRecord(previous, record);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
s = ++seq;
|
|
96
|
+
changed = true;
|
|
97
|
+
}
|
|
98
|
+
last = s;
|
|
99
|
+
seqs.set(seqKey(table, key), s);
|
|
100
|
+
if (changed)
|
|
101
|
+
puts.push({ key: [boardId, table, key], row: { t: table, s, v: record } });
|
|
102
|
+
}
|
|
103
|
+
for (const key of old.keys())
|
|
104
|
+
deletes.push([boardId, table, key]);
|
|
105
|
+
}
|
|
106
|
+
const boardChanged = before.tables.board === null || draft.board === null ? before.tables.board !== draft.board : !sameRecord(before.tables.board, draft.board);
|
|
107
|
+
if (boardChanged) {
|
|
108
|
+
if (draft.board)
|
|
109
|
+
puts.push({ key: [boardId, BOARD, ""], row: { t: BOARD, s: 0, v: draft.board } });
|
|
110
|
+
else
|
|
111
|
+
deletes.push([boardId, BOARD, ""]);
|
|
112
|
+
}
|
|
113
|
+
const oldDocuments = new Map(before.tables.search);
|
|
114
|
+
for (const [ticketId, document] of draft.search) {
|
|
115
|
+
const previous = oldDocuments.get(ticketId);
|
|
116
|
+
oldDocuments.delete(ticketId);
|
|
117
|
+
if (!previous || !sameRecord(previous, document))
|
|
118
|
+
puts.push({ key: [boardId, SEARCH, ticketId], row: { t: SEARCH, s: 0, v: { ticket_id: ticketId, document } } });
|
|
119
|
+
}
|
|
120
|
+
for (const ticketId of oldDocuments.keys())
|
|
121
|
+
deletes.push([boardId, SEARCH, ticketId]);
|
|
122
|
+
return { puts, deletes, boardChanged, after: { revision: before.revision + 1, seq, tables: draft, seqs } };
|
|
123
|
+
}
|
|
124
|
+
/** The sequence number of each of the snapshot's events, in order. */
|
|
125
|
+
export function eventSeqs(snapshot) {
|
|
126
|
+
return snapshot.tables.events.map((event) => ({ s: snapshot.seqs.get(seqKey("events", event.id)) ?? 0, event }));
|
|
127
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Bytes in memory, by board and path: the reference BlobStore. */
|
|
2
|
+
import { OrbitError } from "../../core/errors.js";
|
|
3
|
+
import { checkBlobPath } from "../../store/blob-path.js";
|
|
4
|
+
const yieldTurn = () => Promise.resolve();
|
|
5
|
+
export function createMemoryBlobStore() {
|
|
6
|
+
const boards = new Map();
|
|
7
|
+
const of = (boardId) => {
|
|
8
|
+
let blobs = boards.get(boardId);
|
|
9
|
+
if (!blobs) {
|
|
10
|
+
blobs = new Map();
|
|
11
|
+
boards.set(boardId, blobs);
|
|
12
|
+
}
|
|
13
|
+
return blobs;
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
async put(boardId, path, bytes, options = {}) {
|
|
17
|
+
await yieldTurn();
|
|
18
|
+
checkBlobPath(path);
|
|
19
|
+
const blobs = of(boardId);
|
|
20
|
+
if (options.exclusive && blobs.has(path))
|
|
21
|
+
throw new OrbitError("blob_exists");
|
|
22
|
+
blobs.set(path, bytes.slice());
|
|
23
|
+
},
|
|
24
|
+
async get(boardId, path) {
|
|
25
|
+
await yieldTurn();
|
|
26
|
+
checkBlobPath(path);
|
|
27
|
+
return boards.get(boardId)?.get(path)?.slice() ?? null;
|
|
28
|
+
},
|
|
29
|
+
async size(boardId, path) {
|
|
30
|
+
await yieldTurn();
|
|
31
|
+
checkBlobPath(path);
|
|
32
|
+
return boards.get(boardId)?.get(path)?.byteLength ?? null;
|
|
33
|
+
},
|
|
34
|
+
async remove(boardId, path) {
|
|
35
|
+
await yieldTurn();
|
|
36
|
+
checkBlobPath(path);
|
|
37
|
+
boards.get(boardId)?.delete(path);
|
|
38
|
+
},
|
|
39
|
+
async removeBoard(boardId) {
|
|
40
|
+
await yieldTurn();
|
|
41
|
+
boards.delete(boardId);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One board's records in memory, and the reader and writer over them. The
|
|
3
|
+
* tables are arrays in the order records were written, so "then by when it
|
|
4
|
+
* was written" is array order, and every sort here is stable.
|
|
5
|
+
*/
|
|
6
|
+
import type { Attachment, Board, BoardEntry, Comment, CrossBoardRelation, Id, Label, OrbitEvent, Relation, ReviewVerdict, State, Ticket, TicketLabel } from "../../core/model.ts";
|
|
7
|
+
import type { BoardReader, BoardWriter, SearchDocument } from "../../store/types.ts";
|
|
8
|
+
export interface Tables {
|
|
9
|
+
board: Board | null;
|
|
10
|
+
states: State[];
|
|
11
|
+
labels: Label[];
|
|
12
|
+
tickets: Ticket[];
|
|
13
|
+
ticketLabels: TicketLabel[];
|
|
14
|
+
comments: Comment[];
|
|
15
|
+
relations: Relation[];
|
|
16
|
+
crossBoardRelations: CrossBoardRelation[];
|
|
17
|
+
entries: BoardEntry[];
|
|
18
|
+
verdicts: ReviewVerdict[];
|
|
19
|
+
attachments: Attachment[];
|
|
20
|
+
events: OrbitEvent[];
|
|
21
|
+
search: Map<Id, SearchDocument>;
|
|
22
|
+
}
|
|
23
|
+
export declare function emptyTables(): Tables;
|
|
24
|
+
/** What a writer reports to its store: the board's record changed, events appended. */
|
|
25
|
+
export interface WriteLog {
|
|
26
|
+
events: OrbitEvent[];
|
|
27
|
+
}
|
|
28
|
+
/** Checks a writer makes against the rest of the data: another board's slug. */
|
|
29
|
+
export interface WriteChecks {
|
|
30
|
+
slugTaken(slug: string): boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Code-point order, as SQLite compares text. */
|
|
33
|
+
export declare function byCodePoint(a: string, b: string): number;
|
|
34
|
+
/** A reader over `tables()`: committed tables for a read, the draft for a write. */
|
|
35
|
+
export declare function reader(tables: () => Tables, ready?: () => Promise<void>): BoardReader;
|
|
36
|
+
/**
|
|
37
|
+
* A writer over a draft. Every check the port names is made here, in the
|
|
38
|
+
* order the port lists them, so a write that breaks one throws before it
|
|
39
|
+
* changes anything.
|
|
40
|
+
*/
|
|
41
|
+
export declare function writer(boardId: Id, draft: Tables, log: WriteLog, checks: WriteChecks, guard: () => Promise<void>): BoardWriter;
|