@3sln/trove 0.0.2
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/README.md +1227 -0
- package/package.json +75 -0
- package/packages/core/src/collections/index.js +249 -0
- package/packages/core/src/errors.js +186 -0
- package/packages/core/src/identity/discovery.js +210 -0
- package/packages/core/src/identity/index.js +188 -0
- package/packages/core/src/identity/jwt.js +199 -0
- package/packages/core/src/index.js +104 -0
- package/packages/core/src/indexers/contribution.js +115 -0
- package/packages/core/src/indexers/registry.js +162 -0
- package/packages/core/src/indexing.js +340 -0
- package/packages/core/src/issues.js +150 -0
- package/packages/core/src/kv.js +0 -0
- package/packages/core/src/links.js +141 -0
- package/packages/core/src/metadata/cursor.js +73 -0
- package/packages/core/src/metadata/interface.js +244 -0
- package/packages/core/src/metadata/memory.js +270 -0
- package/packages/core/src/metadata/sqlite.js +412 -0
- package/packages/core/src/notifications/index.js +139 -0
- package/packages/core/src/notifications/webpush.js +217 -0
- package/packages/core/src/plugins/contributions.js +177 -0
- package/packages/core/src/plugins/identity.js +98 -0
- package/packages/core/src/plugins/index.js +225 -0
- package/packages/core/src/plugins/indexers.js +142 -0
- package/packages/core/src/plugins/installStore.js +134 -0
- package/packages/core/src/plugins/package.js +102 -0
- package/packages/core/src/plugins/packageStore.js +61 -0
- package/packages/core/src/plugins/runtime.js +101 -0
- package/packages/core/src/plugins/sql.js +52 -0
- package/packages/core/src/retry.js +74 -0
- package/packages/core/src/scan.js +302 -0
- package/packages/core/src/search/embeddings.js +128 -0
- package/packages/core/src/search/index.js +200 -0
- package/packages/core/src/search/keywordStore.js +107 -0
- package/packages/core/src/search/sqliteStores.js +455 -0
- package/packages/core/src/search/tagMatch.js +59 -0
- package/packages/core/src/search/transformer.js +195 -0
- package/packages/core/src/search/vectorStore.js +274 -0
- package/packages/core/src/search/vectorize.js +249 -0
- package/packages/core/src/sidecar/document.js +213 -0
- package/packages/core/src/sidecar/index.js +174 -0
- package/packages/core/src/sidecar/manager.js +239 -0
- package/packages/core/src/sidecar/store.js +46 -0
- package/packages/core/src/signedUrls.js +170 -0
- package/packages/core/src/sqlite-d1.js +162 -0
- package/packages/core/src/sqlite-driver.js +42 -0
- package/packages/core/src/sqlite.js +162 -0
- package/packages/core/src/storage/filesystem.js +283 -0
- package/packages/core/src/storage/interface.js +222 -0
- package/packages/core/src/storage/memory.js +113 -0
- package/packages/core/src/storage/prefixed.js +75 -0
- package/packages/core/src/storage/s3.js +316 -0
- package/packages/core/src/storage/s3sigv4.js +185 -0
- package/packages/core/src/tasks.js +228 -0
- package/packages/core/src/uploads.js +386 -0
- package/packages/core/src/util.js +125 -0
- package/packages/core/src/vfs.js +666 -0
- package/packages/plugin-sdk/src/browser.js +316 -0
- package/packages/plugin-sdk/src/index.js +32 -0
- package/packages/plugin-sdk/src/protocol.js +59 -0
- package/packages/plugin-sdk/src/rpc.js +95 -0
- package/packages/server/src/adapters/bun.js +78 -0
- package/packages/server/src/adapters/node.js +115 -0
- package/packages/server/src/adapters/staticAssets.js +123 -0
- package/packages/server/src/adapters/webDist.js +70 -0
- package/packages/server/src/adapters/worker-tasks.js +206 -0
- package/packages/server/src/adapters/worker.js +159 -0
- package/packages/server/src/cachePolicy.js +34 -0
- package/packages/server/src/engine/README.md +88 -0
- package/packages/server/src/engine/actions/scanCollection.js +114 -0
- package/packages/server/src/engine/index.js +95 -0
- package/packages/server/src/engine/lazy.js +25 -0
- package/packages/server/src/engine/providers/access.js +363 -0
- package/packages/server/src/engine/providers/core.js +405 -0
- package/packages/server/src/engine/providers/scan.js +67 -0
- package/packages/server/src/index.js +698 -0
- package/packages/server/src/manifest.js +98 -0
- package/packages/server/src/mcp/auth.js +40 -0
- package/packages/server/src/mcp/index.js +213 -0
- package/packages/server/src/mcp/protocol.js +181 -0
- package/packages/server/src/mcp/tools.js +351 -0
- package/packages/server/src/router.js +229 -0
- package/packages/server/src/routes.js +1066 -0
- package/packages/server/src/scope.js +43 -0
- package/packages/web/dist/assets/chunk-4xqbzebh.js +5 -0
- package/packages/web/dist/assets/chunk-4xqbzebh.js.map +9 -0
- package/packages/web/dist/assets/chunk-h05bxfbs.js +5 -0
- package/packages/web/dist/assets/chunk-h05bxfbs.js.map +10 -0
- package/packages/web/dist/assets/main-4cxs7prw.js +356 -0
- package/packages/web/dist/assets/main-4cxs7prw.js.map +103 -0
- package/packages/web/dist/assets/styles-kcx1x337.css +1 -0
- package/packages/web/dist/icon.svg +11 -0
- package/packages/web/dist/index.html +16 -0
- package/packages/web/dist/sql-wasm.wasm +0 -0
- package/packages/web/dist/sw.js +186 -0
- package/packages/web/src/bl/actions.js +410 -0
- package/packages/web/src/bl/activity.js +306 -0
- package/packages/web/src/bl/commands.js +274 -0
- package/packages/web/src/bl/fileType.js +49 -0
- package/packages/web/src/bl/index.js +70 -0
- package/packages/web/src/bl/links.js +54 -0
- package/packages/web/src/bl/offline.js +268 -0
- package/packages/web/src/bl/openers.js +71 -0
- package/packages/web/src/bl/pluginInstall.js +59 -0
- package/packages/web/src/bl/services.js +143 -0
- package/packages/web/src/bl/social.js +234 -0
- package/packages/web/src/bl/tagQuery.js +44 -0
- package/packages/web/src/main.js +10 -0
- package/packages/web/src/platform/api.js +529 -0
- package/packages/web/src/platform/commands.js +89 -0
- package/packages/web/src/platform/context.js +77 -0
- package/packages/web/src/platform/contributions.js +156 -0
- package/packages/web/src/platform/index.js +150 -0
- package/packages/web/src/platform/keybindings.js +199 -0
- package/packages/web/src/platform/mediaUrls.js +137 -0
- package/packages/web/src/platform/navigation.js +131 -0
- package/packages/web/src/platform/notifications.js +50 -0
- package/packages/web/src/platform/overlay.js +81 -0
- package/packages/web/src/platform/pluginClientDb.js +132 -0
- package/packages/web/src/platform/pluginDock.js +141 -0
- package/packages/web/src/platform/pluginFrames.js +194 -0
- package/packages/web/src/platform/pluginHost.js +648 -0
- package/packages/web/src/platform/pluginMedia.js +62 -0
- package/packages/web/src/platform/pluginModules.js +90 -0
- package/packages/web/src/platform/pluginNet.js +71 -0
- package/packages/web/src/platform/pluginPackage.js +247 -0
- package/packages/web/src/platform/pluginRpc.js +377 -0
- package/packages/web/src/platform/pluginSigning.js +168 -0
- package/packages/web/src/platform/pluginStore.js +67 -0
- package/packages/web/src/platform/settings.js +101 -0
- package/packages/web/src/platform/spatialNav.js +286 -0
- package/packages/web/src/platform/viewport.js +123 -0
- package/packages/web/src/platform/voice.js +133 -0
- package/packages/web/src/platform/voiceSearch.js +155 -0
- package/packages/web/src/platform/whenclause.js +162 -0
- package/packages/web/src/platform/workbench.js +156 -0
- package/packages/web/src/runtime.js +73 -0
- package/packages/web/src/styles.css +1382 -0
- package/packages/web/src/ui/components/activityBar.js +35 -0
- package/packages/web/src/ui/components/activityPanel.js +132 -0
- package/packages/web/src/ui/components/commandPalette.js +154 -0
- package/packages/web/src/ui/components/editorArea.js +75 -0
- package/packages/web/src/ui/components/launcher.js +392 -0
- package/packages/web/src/ui/components/openers/index.js +212 -0
- package/packages/web/src/ui/components/openers/markdown.js +222 -0
- package/packages/web/src/ui/components/overlays.js +255 -0
- package/packages/web/src/ui/components/phoneChrome.js +188 -0
- package/packages/web/src/ui/components/pluginReview.js +151 -0
- package/packages/web/src/ui/components/pluginsView.js +120 -0
- package/packages/web/src/ui/components/settingsView.js +258 -0
- package/packages/web/src/ui/components/social.js +290 -0
- package/packages/web/src/ui/components/statusBar.js +198 -0
- package/packages/web/src/ui/components/views/grid.js +115 -0
- package/packages/web/src/ui/components/views/index.js +155 -0
- package/packages/web/src/ui/components/views/list.js +50 -0
- package/packages/web/src/ui/components/views/parts.js +58 -0
- package/packages/web/src/ui/compositions/workbench.js +125 -0
- package/packages/web/src/ui/format.js +33 -0
- package/packages/web/src/ui/icon.js +81 -0
- package/packages/web/src/ui/media.js +114 -0
- package/packages/web/src/ui/sanitize.js +86 -0
- package/packages/web/src/workbench.js +205 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// What may be cached, and for how long.
|
|
2
|
+
//
|
|
3
|
+
// Its own module, with no imports at all, because it is needed in two places that
|
|
4
|
+
// cannot import each other: the Node/Bun static file server (which needs `node:path`
|
|
5
|
+
// and so cannot load on Workers) and the runtime-agnostic request path in index.js
|
|
6
|
+
// (which must load on Workers, where the assets come from a binding instead).
|
|
7
|
+
//
|
|
8
|
+
// The rule the build enforces and this describes: everything under /assets/ is
|
|
9
|
+
// content-addressed, so the filename changes whenever the bytes do and the URL can be
|
|
10
|
+
// kept forever. Everything else — index.html, sw.js, the manifest, the icon — keeps a
|
|
11
|
+
// stable name and must be revalidated on every load.
|
|
12
|
+
//
|
|
13
|
+
// Getting this backwards is what turns a deploy into a blank page. Marking a mutable,
|
|
14
|
+
// stable-named URL `immutable` hands a browser a cached entry point it will not check
|
|
15
|
+
// again, so it goes on importing hashed modules from a build that no longer exists.
|
|
16
|
+
// Leaving the content-addressed tree unmarked is the cheaper mistake and was the one in
|
|
17
|
+
// force: the responses carried no Cache-Control at all, so the hashing bought nothing.
|
|
18
|
+
|
|
19
|
+
/** Kept in step with the `naming` patterns in packages/web/build.mjs. */
|
|
20
|
+
export const IMMUTABLE_PREFIX = '/assets/';
|
|
21
|
+
|
|
22
|
+
/** A year, and never revalidated — safe only because the name changes with the bytes. */
|
|
23
|
+
export const IMMUTABLE = 'public, max-age=31536000, immutable';
|
|
24
|
+
|
|
25
|
+
/** Cacheable, but check with the server before every use. */
|
|
26
|
+
export const REVALIDATE = 'no-cache';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} pathname request path, already decoded
|
|
30
|
+
* @returns {string} a Cache-Control value
|
|
31
|
+
*/
|
|
32
|
+
export function cacheControlFor(pathname) {
|
|
33
|
+
return pathname.startsWith(IMMUTABLE_PREFIX) ? IMMUTABLE : REVALIDATE;
|
|
34
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Spike: the backend as an ngin engine
|
|
2
|
+
|
|
3
|
+
One route — the collection scan — rewritten as a provider graph and an action,
|
|
4
|
+
to find out whether the backend reads better this way before forty of them are
|
|
5
|
+
touched. **Not merged into the architecture yet**: `createServer` still builds
|
|
6
|
+
everything else by hand.
|
|
7
|
+
|
|
8
|
+
## The check that makes it worth attempting
|
|
9
|
+
|
|
10
|
+
`beginScan` keeps the signature it always had, so `createServer` stays a facade
|
|
11
|
+
and every existing test drives HTTP exactly as before. 409 tests passed before
|
|
12
|
+
the rewrite and 409 passed after, which is the only useful definition of "this
|
|
13
|
+
changed no behaviour". A migration that cannot be verified that way should not
|
|
14
|
+
be started.
|
|
15
|
+
|
|
16
|
+
## What it replaced
|
|
17
|
+
|
|
18
|
+
78 lines inside `createServer`, which had accreted three things:
|
|
19
|
+
|
|
20
|
+
| Hand-rolled | What it actually was |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `{ task, alreadyRunning, done }` | a feed — `started` now, `result` later |
|
|
23
|
+
| `task.progress()` + clients polling `/api/tasks` | a feed, with a second mechanism bolted on |
|
|
24
|
+
| a `try/finally` releasing the scan claim | a resource lifetime — and it released too early the first time it was written |
|
|
25
|
+
|
|
26
|
+
## What came out better
|
|
27
|
+
|
|
28
|
+
**Dependencies are declared.** `ScanCollection.deps` is `['vfs', 'tasks',
|
|
29
|
+
'lifecycle']` plus a `claim` carrying its collection. Today every route receives
|
|
30
|
+
one 18-key context object, which is a service locator: nothing records what a
|
|
31
|
+
route uses, so nothing stops it reaching for more. A declared list is checkable,
|
|
32
|
+
and `engine-scan.test.js` checks it.
|
|
33
|
+
|
|
34
|
+
**The claim is a resource, not a discipline.** `ScanClaimProvider.obtain` takes
|
|
35
|
+
the lease, `release` gives it back, and the container calls `release` whether the
|
|
36
|
+
action returns, throws, or is aborted. Two of those paths are tested; before,
|
|
37
|
+
they were a `finally` block that had already been written wrong once.
|
|
38
|
+
|
|
39
|
+
**Cancellation has one shape.** `signal.aborted` (the caller gave up),
|
|
40
|
+
`handle.cancelled` (someone clicked Cancel) and `lifecycle.closing` (the server
|
|
41
|
+
is going down) are three different events that all mean stop, and they now read
|
|
42
|
+
as three lines in one place. The caller-gave-up case did not exist before.
|
|
43
|
+
|
|
44
|
+
**Shutdown stopped being a closure variable.** `lifecycle` is a dependency, so an
|
|
45
|
+
action that must stop when the server is going down has to say so.
|
|
46
|
+
|
|
47
|
+
## What it cost, and what to watch
|
|
48
|
+
|
|
49
|
+
**A dispatch costs a turn of the event loop.** `dispatch()` returns
|
|
50
|
+
synchronously and the action runs on a later macrotask, so `beginScan` awaits a
|
|
51
|
+
`started` event rather than having the task record in hand. Microseconds — but
|
|
52
|
+
it moved a real boundary. The `alreadyRunning` answer used to be decided before
|
|
53
|
+
`beginScan` returned; now the claim is taken one turn later. Two scan requests
|
|
54
|
+
arriving in the same tick can therefore both run, back to back, instead of the
|
|
55
|
+
second being turned away. That is wasteful, not incorrect — the claim still
|
|
56
|
+
guarantees they never overlap, which is the property that protects the resume
|
|
57
|
+
cursor — but two tests were asserting the old timing and had to be given a
|
|
58
|
+
storage backend that genuinely blocks so the overlap is real.
|
|
59
|
+
|
|
60
|
+
**`next()` semantics need care at the seam.** `done` was first written as
|
|
61
|
+
`feed.next(['result'])`, which the abort contract correctly pre-empts — aborting
|
|
62
|
+
rejects anything waiting. But `done` is the work's own completion, not a caller
|
|
63
|
+
who gave up, and an aborted scan still produces a partial result with
|
|
64
|
+
`stopped: true` that a resumable scan needs to report. Naming `abort` in the wait
|
|
65
|
+
opts out of the pre-emption. Worth knowing before forty routes are written
|
|
66
|
+
against it.
|
|
67
|
+
|
|
68
|
+
**Half the graph is still singletons.** `vfs`, `tasks` and `kv` are wrapped
|
|
69
|
+
instances rather than providers that build their own resources. That is the
|
|
70
|
+
honest first step — the graph moves under the container a piece at a time — but
|
|
71
|
+
it means this spike does not yet demonstrate the container's teardown ordering
|
|
72
|
+
or lazy initialisation, which is where the remaining value of `createServer`'s
|
|
73
|
+
474 lines actually is.
|
|
74
|
+
|
|
75
|
+
## Not done here
|
|
76
|
+
|
|
77
|
+
- The two-domain split (request vs background) as a first-class idea rather than
|
|
78
|
+
a Workers concession.
|
|
79
|
+
- `backgroundWork` as a provider with local and Durable Object implementations.
|
|
80
|
+
The seam exists already as `config.background`; it has not been renamed.
|
|
81
|
+
- Anything about queries. One-shot HTTP reads through a boot/kill subscription
|
|
82
|
+
store is the wrong shape; inside the Durable Object it is the right one.
|
|
83
|
+
|
|
84
|
+
## Running it
|
|
85
|
+
|
|
86
|
+
The spike needs ngin with `DispatchFeed.next`/`abort`
|
|
87
|
+
([3sln/ngin#2](https://github.com/3sln/ngin/pull/2)), which is unpublished. Until
|
|
88
|
+
it lands, `node_modules/@3sln/ngin` must point at a checkout of that branch.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Reconcile a collection against the bytes actually in its store.
|
|
2
|
+
//
|
|
3
|
+
// SPIKE. The same work `beginScan` did by hand, as an ngin action — which is
|
|
4
|
+
// the interesting comparison, because the hand-rolled version had grown three
|
|
5
|
+
// things an action gets for free:
|
|
6
|
+
//
|
|
7
|
+
// • a `{ task, alreadyRunning, done }` return shape, invented because a route
|
|
8
|
+
// must answer immediately with something watchable while the work runs on.
|
|
9
|
+
// That is a feed: `started` now, `result` later.
|
|
10
|
+
// • progress reported by poking a TaskRegistry, which clients then poll. That
|
|
11
|
+
// is a feed too — and one an SSE endpoint could subscribe to directly.
|
|
12
|
+
// • a try/finally releasing the claim, which released too early the first
|
|
13
|
+
// time it was written. The container does it, and cannot get it wrong.
|
|
14
|
+
|
|
15
|
+
import { Action } from '@3sln/ngin';
|
|
16
|
+
|
|
17
|
+
export class ScanCollection extends Action {
|
|
18
|
+
static deps = ['vfs', 'tasks', 'lifecycle'];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} opts
|
|
22
|
+
* @param {string} [opts.collectionId]
|
|
23
|
+
* @param {string} [opts.reason] shown under the task title
|
|
24
|
+
* @param {number|null} [opts.deadlineMs] budget for one slice; null runs to the end
|
|
25
|
+
*/
|
|
26
|
+
constructor({ collectionId = 'default', reason = null, deadlineMs = null } = {}) {
|
|
27
|
+
super();
|
|
28
|
+
this.collectionId = collectionId;
|
|
29
|
+
this.reason = reason;
|
|
30
|
+
this.deadlineMs = deadlineMs;
|
|
31
|
+
// Instance deps carry options to the provider — this is how the claim knows
|
|
32
|
+
// which collection it is claiming.
|
|
33
|
+
this.deps = { claim: { collectionId } };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async execute({ vfs, tasks, lifecycle, claim }, { dispatchFeed, signal }) {
|
|
37
|
+
if (!claim.held) {
|
|
38
|
+
// Someone else is scanning this collection. Say so and stop; this is an
|
|
39
|
+
// ordinary answer, not a failure, so the dispatch completes normally.
|
|
40
|
+
emit(dispatchFeed, 'started', { task: null, alreadyRunning: true });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { task, done } = tasks.begin(
|
|
45
|
+
{
|
|
46
|
+
kind: 'scan',
|
|
47
|
+
title: `Scanning “${this.collectionId}” for outside changes`,
|
|
48
|
+
detail: this.reason,
|
|
49
|
+
unit: 'objects',
|
|
50
|
+
collectionId: this.collectionId,
|
|
51
|
+
cancellable: true,
|
|
52
|
+
},
|
|
53
|
+
(handle) => this.#scan({ vfs, lifecycle, claim, handle, dispatchFeed, signal }),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// The route is waiting on this: it has the record to answer with long before
|
|
57
|
+
// there is a result to report.
|
|
58
|
+
emit(dispatchFeed, 'started', { task, alreadyRunning: false });
|
|
59
|
+
emit(dispatchFeed, 'result', { result: await done });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async #scan({ vfs, lifecycle, claim, handle, dispatchFeed, signal }) {
|
|
63
|
+
// A budget, when the caller has one. On Workers a request has a CPU ceiling
|
|
64
|
+
// measured in seconds and a bucket has none at all, so "run until done" is
|
|
65
|
+
// not a thing that can be promised — this runs a slice and remembers where
|
|
66
|
+
// it got to.
|
|
67
|
+
const until = this.deadlineMs ? Date.now() + this.deadlineMs : null;
|
|
68
|
+
const cursor = await claim.readCursor();
|
|
69
|
+
|
|
70
|
+
// A scan with no deadline runs for minutes, well past the claim. Renew as it
|
|
71
|
+
// goes, and treat a failed renewal as a stop: something else owns this
|
|
72
|
+
// collection now, and carrying on would put us back in the race the claim
|
|
73
|
+
// exists to prevent.
|
|
74
|
+
let lost = false;
|
|
75
|
+
let renewedAt = Date.now();
|
|
76
|
+
|
|
77
|
+
const result = await vfs.scanCollection(this.collectionId, {
|
|
78
|
+
cursor,
|
|
79
|
+
shouldStop: () =>
|
|
80
|
+
lifecycle.closing
|
|
81
|
+
|| signal.aborted // the caller gave up — see DispatchFeed.abort
|
|
82
|
+
|| handle.cancelled // …or clicked Cancel on the task
|
|
83
|
+
|| lost
|
|
84
|
+
|| (until != null && Date.now() > until),
|
|
85
|
+
// The store cannot say how many objects it holds without listing them, so
|
|
86
|
+
// this is honestly indeterminate: a count that rises, with no total to
|
|
87
|
+
// divide it by.
|
|
88
|
+
onProgress: ({ scanned, adopted, refreshed }) => {
|
|
89
|
+
handle.progress({
|
|
90
|
+
done: scanned,
|
|
91
|
+
detail: adopted || refreshed ? `${adopted} new, ${refreshed} changed` : null,
|
|
92
|
+
});
|
|
93
|
+
// The same numbers on the feed. Nothing consumes these yet — the task
|
|
94
|
+
// registry is still what /api/tasks reports from — but this is the shape
|
|
95
|
+
// that makes streaming progress possible without a second mechanism.
|
|
96
|
+
emit(dispatchFeed, 'progress', { scanned, adopted, refreshed });
|
|
97
|
+
if (Date.now() - renewedAt < 20_000) return;
|
|
98
|
+
renewedAt = Date.now();
|
|
99
|
+
claim.renew().then((ok) => { if (!ok) lost = true; }).catch(() => {});
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Record the resume point, or clear it once a pass completes — but only
|
|
104
|
+
// while we still hold the claim. Writing a cursor we no longer own is
|
|
105
|
+
// exactly the clobber the claim is here to stop.
|
|
106
|
+
if (lost || !(await claim.renew())) return { ...result, lostLease: true };
|
|
107
|
+
await claim.writeCursor(result.nextCursor);
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function emit(feed, type, detail) {
|
|
113
|
+
feed.dispatchEvent(Object.assign(new Event(type), detail));
|
|
114
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// The drive as an ngin engine.
|
|
2
|
+
//
|
|
3
|
+
// `createServer` is a facade over this: it builds the container, obtains the
|
|
4
|
+
// backbone, and wires the HTTP surface on top. That layering is what makes the
|
|
5
|
+
// migration checkable — every existing test calls `createServer` and drives
|
|
6
|
+
// HTTP, so if they still pass, the rewrite changed no behaviour.
|
|
7
|
+
|
|
8
|
+
import { Engine } from '@3sln/ngin';
|
|
9
|
+
import { coreProviders } from './providers/core.js';
|
|
10
|
+
import { ScanClaimProvider } from './providers/scan.js';
|
|
11
|
+
import {
|
|
12
|
+
NodeAccessProvider, CollectionAccessProvider, UploadAccessProvider,
|
|
13
|
+
SystemNodeProvider, SystemCollectionProvider,
|
|
14
|
+
} from './providers/access.js';
|
|
15
|
+
import { ScanCollection } from './actions/scanCollection.js';
|
|
16
|
+
|
|
17
|
+
export { ScanCollection, ScanClaimProvider };
|
|
18
|
+
export { buildStorage } from './providers/core.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Every provider the drive has.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} config the same config object `createServer` takes
|
|
24
|
+
* @param {{closing: boolean}} lifecycleState mutable; `close()` flips it
|
|
25
|
+
*/
|
|
26
|
+
export function driveProviders(config, lifecycleState) {
|
|
27
|
+
return {
|
|
28
|
+
...coreProviders(config, lifecycleState),
|
|
29
|
+
claim: ScanClaimProvider,
|
|
30
|
+
// Authorization as something you hold rather than something you checked —
|
|
31
|
+
// see providers/access.js. Denial happens during the lease, so an action
|
|
32
|
+
// that may not act never runs at all.
|
|
33
|
+
node: NodeAccessProvider,
|
|
34
|
+
collection: CollectionAccessProvider,
|
|
35
|
+
upload: UploadAccessProvider,
|
|
36
|
+
// The background domain's grant, named separately so declaring it is a
|
|
37
|
+
// visible decision rather than an option someone passed.
|
|
38
|
+
systemNode: SystemNodeProvider,
|
|
39
|
+
systemCollection: SystemCollectionProvider,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @returns {Engine} */
|
|
44
|
+
export function createDriveEngine(config = {}, lifecycleState = { closing: false }) {
|
|
45
|
+
return new Engine({ providers: driveProviders(config, lifecycleState) });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The names `createServer` hands back, and therefore obtains up front. */
|
|
49
|
+
export const BACKBONE = [
|
|
50
|
+
'storage', 'sqlite', 'metadata', 'kv', 'tasks', 'issues', 'notifications',
|
|
51
|
+
'sidecar', 'collections', 'identity', 'auth', 'search', 'vfs', 'plugins',
|
|
52
|
+
'lifecycle',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/** The shape `beginScan` has always returned, so no caller has to change. */
|
|
56
|
+
const NOTHING_SCANNED = {
|
|
57
|
+
scanned: 0, adopted: 0, refreshed: 0, orphaned: 0, skipped: 0,
|
|
58
|
+
failed: 0, unaddressable: 0, stopped: false, nextCursor: null, resumed: false,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `beginScan(collectionId, opts) -> { task, alreadyRunning, done, feed }`,
|
|
63
|
+
* backed by a dispatch.
|
|
64
|
+
*
|
|
65
|
+
* @param {Engine} engine
|
|
66
|
+
*/
|
|
67
|
+
export function scanStarter(engine) {
|
|
68
|
+
return async function beginScan(collectionId = 'default', { reason, deadlineMs = null } = {}) {
|
|
69
|
+
const feed = engine.dispatch(new ScanCollection({ collectionId, reason, deadlineMs }));
|
|
70
|
+
|
|
71
|
+
// `started` carries the task record. Awaiting it costs one turn of the event
|
|
72
|
+
// loop and is what lets the route answer with something watchable.
|
|
73
|
+
const started = await feed.next(['started']);
|
|
74
|
+
if (started.alreadyRunning) {
|
|
75
|
+
return {
|
|
76
|
+
task: null,
|
|
77
|
+
alreadyRunning: true,
|
|
78
|
+
done: Promise.resolve({ alreadyRunning: true, ...NOTHING_SCANNED }),
|
|
79
|
+
feed,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// `abort` is named so the wait is not pre-empted by aborting. That rule is
|
|
84
|
+
// for a caller who gave up; this promise is the work's own completion, and
|
|
85
|
+
// an aborted scan still produces a result — a partial one, with
|
|
86
|
+
// `stopped: true` — which is exactly what a resumable scan needs to report.
|
|
87
|
+
const done = feed.next(['result', 'abort']).then((event) => {
|
|
88
|
+
if (event.type === 'result') return event.result;
|
|
89
|
+
// No result means the action never reached the end of a scan: it threw.
|
|
90
|
+
throw event.error ?? event.reason;
|
|
91
|
+
});
|
|
92
|
+
done.catch(() => {});
|
|
93
|
+
return { task: started.task, alreadyRunning: false, done, feed };
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Obtaining several backbone resources at once.
|
|
2
|
+
//
|
|
3
|
+
// The lazy-singleton provider that used to live here is `Provider.fromLazySingleton`
|
|
4
|
+
// in ngin now (3sln/ngin#3) — it was the one shape the library was missing, and
|
|
5
|
+
// every resource in this drive's graph is that shape.
|
|
6
|
+
//
|
|
7
|
+
// What is left is Trove's own convenience, and it is only safe because of what
|
|
8
|
+
// that shape guarantees.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Obtain several dependency providers at once.
|
|
12
|
+
*
|
|
13
|
+
* NOT released, deliberately: everything in the core graph is a lazy singleton
|
|
14
|
+
* whose `release` is a no-op, and a resource built from these holds them for its
|
|
15
|
+
* own lifetime — releasing would be a lie about a lease nobody is keeping.
|
|
16
|
+
* Anything pooled or ref-counted must be leased properly instead, which is why
|
|
17
|
+
* ScanClaimProvider does its own obtain/release rather than coming through here.
|
|
18
|
+
*/
|
|
19
|
+
export async function need(providers, names) {
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const name of names) {
|
|
22
|
+
out[name] = await providers[name].obtain();
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|