@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.
Files changed (162) hide show
  1. package/README.md +1227 -0
  2. package/package.json +75 -0
  3. package/packages/core/src/collections/index.js +249 -0
  4. package/packages/core/src/errors.js +186 -0
  5. package/packages/core/src/identity/discovery.js +210 -0
  6. package/packages/core/src/identity/index.js +188 -0
  7. package/packages/core/src/identity/jwt.js +199 -0
  8. package/packages/core/src/index.js +104 -0
  9. package/packages/core/src/indexers/contribution.js +115 -0
  10. package/packages/core/src/indexers/registry.js +162 -0
  11. package/packages/core/src/indexing.js +340 -0
  12. package/packages/core/src/issues.js +150 -0
  13. package/packages/core/src/kv.js +0 -0
  14. package/packages/core/src/links.js +141 -0
  15. package/packages/core/src/metadata/cursor.js +73 -0
  16. package/packages/core/src/metadata/interface.js +244 -0
  17. package/packages/core/src/metadata/memory.js +270 -0
  18. package/packages/core/src/metadata/sqlite.js +412 -0
  19. package/packages/core/src/notifications/index.js +139 -0
  20. package/packages/core/src/notifications/webpush.js +217 -0
  21. package/packages/core/src/plugins/contributions.js +177 -0
  22. package/packages/core/src/plugins/identity.js +98 -0
  23. package/packages/core/src/plugins/index.js +225 -0
  24. package/packages/core/src/plugins/indexers.js +142 -0
  25. package/packages/core/src/plugins/installStore.js +134 -0
  26. package/packages/core/src/plugins/package.js +102 -0
  27. package/packages/core/src/plugins/packageStore.js +61 -0
  28. package/packages/core/src/plugins/runtime.js +101 -0
  29. package/packages/core/src/plugins/sql.js +52 -0
  30. package/packages/core/src/retry.js +74 -0
  31. package/packages/core/src/scan.js +302 -0
  32. package/packages/core/src/search/embeddings.js +128 -0
  33. package/packages/core/src/search/index.js +200 -0
  34. package/packages/core/src/search/keywordStore.js +107 -0
  35. package/packages/core/src/search/sqliteStores.js +455 -0
  36. package/packages/core/src/search/tagMatch.js +59 -0
  37. package/packages/core/src/search/transformer.js +195 -0
  38. package/packages/core/src/search/vectorStore.js +274 -0
  39. package/packages/core/src/search/vectorize.js +249 -0
  40. package/packages/core/src/sidecar/document.js +213 -0
  41. package/packages/core/src/sidecar/index.js +174 -0
  42. package/packages/core/src/sidecar/manager.js +239 -0
  43. package/packages/core/src/sidecar/store.js +46 -0
  44. package/packages/core/src/signedUrls.js +170 -0
  45. package/packages/core/src/sqlite-d1.js +162 -0
  46. package/packages/core/src/sqlite-driver.js +42 -0
  47. package/packages/core/src/sqlite.js +162 -0
  48. package/packages/core/src/storage/filesystem.js +283 -0
  49. package/packages/core/src/storage/interface.js +222 -0
  50. package/packages/core/src/storage/memory.js +113 -0
  51. package/packages/core/src/storage/prefixed.js +75 -0
  52. package/packages/core/src/storage/s3.js +316 -0
  53. package/packages/core/src/storage/s3sigv4.js +185 -0
  54. package/packages/core/src/tasks.js +228 -0
  55. package/packages/core/src/uploads.js +386 -0
  56. package/packages/core/src/util.js +125 -0
  57. package/packages/core/src/vfs.js +666 -0
  58. package/packages/plugin-sdk/src/browser.js +316 -0
  59. package/packages/plugin-sdk/src/index.js +32 -0
  60. package/packages/plugin-sdk/src/protocol.js +59 -0
  61. package/packages/plugin-sdk/src/rpc.js +95 -0
  62. package/packages/server/src/adapters/bun.js +78 -0
  63. package/packages/server/src/adapters/node.js +115 -0
  64. package/packages/server/src/adapters/staticAssets.js +123 -0
  65. package/packages/server/src/adapters/webDist.js +70 -0
  66. package/packages/server/src/adapters/worker-tasks.js +206 -0
  67. package/packages/server/src/adapters/worker.js +159 -0
  68. package/packages/server/src/cachePolicy.js +34 -0
  69. package/packages/server/src/engine/README.md +88 -0
  70. package/packages/server/src/engine/actions/scanCollection.js +114 -0
  71. package/packages/server/src/engine/index.js +95 -0
  72. package/packages/server/src/engine/lazy.js +25 -0
  73. package/packages/server/src/engine/providers/access.js +363 -0
  74. package/packages/server/src/engine/providers/core.js +405 -0
  75. package/packages/server/src/engine/providers/scan.js +67 -0
  76. package/packages/server/src/index.js +698 -0
  77. package/packages/server/src/manifest.js +98 -0
  78. package/packages/server/src/mcp/auth.js +40 -0
  79. package/packages/server/src/mcp/index.js +213 -0
  80. package/packages/server/src/mcp/protocol.js +181 -0
  81. package/packages/server/src/mcp/tools.js +351 -0
  82. package/packages/server/src/router.js +229 -0
  83. package/packages/server/src/routes.js +1066 -0
  84. package/packages/server/src/scope.js +43 -0
  85. package/packages/web/dist/assets/chunk-4xqbzebh.js +5 -0
  86. package/packages/web/dist/assets/chunk-4xqbzebh.js.map +9 -0
  87. package/packages/web/dist/assets/chunk-h05bxfbs.js +5 -0
  88. package/packages/web/dist/assets/chunk-h05bxfbs.js.map +10 -0
  89. package/packages/web/dist/assets/main-4cxs7prw.js +356 -0
  90. package/packages/web/dist/assets/main-4cxs7prw.js.map +103 -0
  91. package/packages/web/dist/assets/styles-kcx1x337.css +1 -0
  92. package/packages/web/dist/icon.svg +11 -0
  93. package/packages/web/dist/index.html +16 -0
  94. package/packages/web/dist/sql-wasm.wasm +0 -0
  95. package/packages/web/dist/sw.js +186 -0
  96. package/packages/web/src/bl/actions.js +410 -0
  97. package/packages/web/src/bl/activity.js +306 -0
  98. package/packages/web/src/bl/commands.js +274 -0
  99. package/packages/web/src/bl/fileType.js +49 -0
  100. package/packages/web/src/bl/index.js +70 -0
  101. package/packages/web/src/bl/links.js +54 -0
  102. package/packages/web/src/bl/offline.js +268 -0
  103. package/packages/web/src/bl/openers.js +71 -0
  104. package/packages/web/src/bl/pluginInstall.js +59 -0
  105. package/packages/web/src/bl/services.js +143 -0
  106. package/packages/web/src/bl/social.js +234 -0
  107. package/packages/web/src/bl/tagQuery.js +44 -0
  108. package/packages/web/src/main.js +10 -0
  109. package/packages/web/src/platform/api.js +529 -0
  110. package/packages/web/src/platform/commands.js +89 -0
  111. package/packages/web/src/platform/context.js +77 -0
  112. package/packages/web/src/platform/contributions.js +156 -0
  113. package/packages/web/src/platform/index.js +150 -0
  114. package/packages/web/src/platform/keybindings.js +199 -0
  115. package/packages/web/src/platform/mediaUrls.js +137 -0
  116. package/packages/web/src/platform/navigation.js +131 -0
  117. package/packages/web/src/platform/notifications.js +50 -0
  118. package/packages/web/src/platform/overlay.js +81 -0
  119. package/packages/web/src/platform/pluginClientDb.js +132 -0
  120. package/packages/web/src/platform/pluginDock.js +141 -0
  121. package/packages/web/src/platform/pluginFrames.js +194 -0
  122. package/packages/web/src/platform/pluginHost.js +648 -0
  123. package/packages/web/src/platform/pluginMedia.js +62 -0
  124. package/packages/web/src/platform/pluginModules.js +90 -0
  125. package/packages/web/src/platform/pluginNet.js +71 -0
  126. package/packages/web/src/platform/pluginPackage.js +247 -0
  127. package/packages/web/src/platform/pluginRpc.js +377 -0
  128. package/packages/web/src/platform/pluginSigning.js +168 -0
  129. package/packages/web/src/platform/pluginStore.js +67 -0
  130. package/packages/web/src/platform/settings.js +101 -0
  131. package/packages/web/src/platform/spatialNav.js +286 -0
  132. package/packages/web/src/platform/viewport.js +123 -0
  133. package/packages/web/src/platform/voice.js +133 -0
  134. package/packages/web/src/platform/voiceSearch.js +155 -0
  135. package/packages/web/src/platform/whenclause.js +162 -0
  136. package/packages/web/src/platform/workbench.js +156 -0
  137. package/packages/web/src/runtime.js +73 -0
  138. package/packages/web/src/styles.css +1382 -0
  139. package/packages/web/src/ui/components/activityBar.js +35 -0
  140. package/packages/web/src/ui/components/activityPanel.js +132 -0
  141. package/packages/web/src/ui/components/commandPalette.js +154 -0
  142. package/packages/web/src/ui/components/editorArea.js +75 -0
  143. package/packages/web/src/ui/components/launcher.js +392 -0
  144. package/packages/web/src/ui/components/openers/index.js +212 -0
  145. package/packages/web/src/ui/components/openers/markdown.js +222 -0
  146. package/packages/web/src/ui/components/overlays.js +255 -0
  147. package/packages/web/src/ui/components/phoneChrome.js +188 -0
  148. package/packages/web/src/ui/components/pluginReview.js +151 -0
  149. package/packages/web/src/ui/components/pluginsView.js +120 -0
  150. package/packages/web/src/ui/components/settingsView.js +258 -0
  151. package/packages/web/src/ui/components/social.js +290 -0
  152. package/packages/web/src/ui/components/statusBar.js +198 -0
  153. package/packages/web/src/ui/components/views/grid.js +115 -0
  154. package/packages/web/src/ui/components/views/index.js +155 -0
  155. package/packages/web/src/ui/components/views/list.js +50 -0
  156. package/packages/web/src/ui/components/views/parts.js +58 -0
  157. package/packages/web/src/ui/compositions/workbench.js +125 -0
  158. package/packages/web/src/ui/format.js +33 -0
  159. package/packages/web/src/ui/icon.js +81 -0
  160. package/packages/web/src/ui/media.js +114 -0
  161. package/packages/web/src/ui/sanitize.js +86 -0
  162. 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
+ }