@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,405 @@
1
+ // The drive, as a dependency graph.
2
+ //
3
+ // This is what `createServer` used to do in 474 lines of statements whose order
4
+ // was the dependency graph, held in the author's head and re-derived by anyone
5
+ // reading it. Three things were maintained by hand there and are derived here:
6
+ //
7
+ // • BUILD ORDER. sqlite before metadata before kv before issues before vfs,
8
+ // because that is what the code said, not because anything checked. Now each
9
+ // provider names what it needs and the container walks it — and refuses a
10
+ // cycle by name rather than producing `undefined`.
11
+ // • TEARDOWN ORDER. `close()` was a hand-written list that had to stay in
12
+ // agreement with the build order and silently didn't have to. The container
13
+ // disposes in reverse construction order, so a resource is always torn down
14
+ // before the things it was built from.
15
+ // • LAZINESS. `await sqlite.init()`, `await vfs.init()`, `await plugins.init()`
16
+ // ran at construction whether or not anything used them. They now run on
17
+ // first use, ordered by need.
18
+ //
19
+ // Every provider still accepts an injected instance, which is the property the
20
+ // whole test suite rests on: `createServer({ storage: new MemoryStorage() })`
21
+ // means the same thing it always did.
22
+
23
+ import {
24
+ StorageBackend, MemoryStorage, FilesystemStorage, S3Storage,
25
+ MetadataStore, MemoryStore, SqliteStore,
26
+ SearchService, EmbeddingProvider, LocalHashEmbedding, HttpEmbedding,
27
+ SearchTransformer, ParsingSearchTransformer, WorkersAiSearchTransformer,
28
+ VectorStore, MemoryVectorStore, QdrantVectorStore, VectorizeVectorStore, SqliteVectorStore,
29
+ KeywordStore, MemoryKeywordStore, SqliteKeywordStore,
30
+ IndexerRegistry,
31
+ IdentityProvider, JwtIdentityProvider, HeaderIdentityProvider, AnonymousIdentityProvider,
32
+ cloudflareAccess,
33
+ KeyValueStore, MemoryKV, SqliteKV,
34
+ SqliteProvider, LocalSqliteProvider,
35
+ SidecarService, NotificationCenter, WebPushService,
36
+ CollectionService,
37
+ PluginService, PackageStore, StoragePackageStore, SqlitePluginInstallStore,
38
+ IndexerRuntime, InProcessIndexerRuntime, PluginIndexers,
39
+ TaskRegistry, IssueRegistry,
40
+ Vfs, TroveError,
41
+ resolveAuthDiscovery,
42
+ SignedUrls, resolveUrlSecret,
43
+ } from '@3sln/trove/core';
44
+ import { Provider } from '@3sln/ngin';
45
+ import { need } from '../lazy.js';
46
+
47
+ // Injected instance or driver config — the duality the whole server is built on,
48
+ // and the only thing a provider has to decide before it builds anything.
49
+ const resolve = (value, BaseClass, build) =>
50
+ (value instanceof BaseClass ? value : build(value || {}));
51
+
52
+ export function buildStorage(cfg) {
53
+ switch (cfg.driver) {
54
+ case 's3': return new S3Storage(cfg.s3);
55
+ case 'filesystem': return new FilesystemStorage({ root: cfg.root });
56
+ case 'memory': default: return new MemoryStorage();
57
+ }
58
+ }
59
+
60
+ function buildIdentity(cfg) {
61
+ switch (cfg.driver) {
62
+ // Cloudflare Access, configured by team name alone. Everything else about Access —
63
+ // the JWKS path, the issuer, and (since managed OAuth) the authorization server an
64
+ // agent signs in at — is that same domain, and writing it into three settings that
65
+ // must agree is three chances to get it wrong.
66
+ case 'cloudflare-access':
67
+ return new JwtIdentityProvider(cloudflareAccess({ ...(cfg.access || {}), ...(cfg.jwt || {}) }));
68
+ case 'jwt': return new JwtIdentityProvider(cfg.jwt || {});
69
+ case 'header': return new HeaderIdentityProvider(cfg.header || {});
70
+ case 'anonymous': case undefined: case null: return new AnonymousIdentityProvider();
71
+ default:
72
+ // A typo must NOT open the drive. `TROVE_AUTH=JWT` (wrong case), `oidc`,
73
+ // `cloudflare_access` all fell through to anonymous — which also skipped the
74
+ // per-driver branch that reads TROVE_AUTH_REQUIRED, and silenced the boot warning
75
+ // (it only fires when the driver string IS 'anonymous'). The result was a
76
+ // world-readable, world-writable drive with a clean log, and an MCP endpoint that
77
+ // dropped its own auth requirement because it sniffs the provider's class name.
78
+ throw TroveError.invalid(
79
+ `Unknown TROVE_AUTH driver "${cfg.driver}" — expected one of: anonymous, jwt, header, cloudflare-access`,
80
+ );
81
+ }
82
+ }
83
+
84
+ async function buildVectorStore(cfg, dimensions, sqlite) {
85
+ switch (cfg.driver) {
86
+ case 'qdrant': return new QdrantVectorStore({ dimensions, ...cfg.qdrant });
87
+ case 'vectorize': return new VectorizeVectorStore({ dimensions, binding: cfg.binding, ...cfg.vectorize });
88
+ case 'sqlite': {
89
+ // sqlite-vec is an optional native dependency: absent on an unsupported platform
90
+ // or a `--omit=optional` install. Degrade loudly to memory rather than refusing to
91
+ // start — keyword search still works, and the warning says what was lost.
92
+ const store = await SqliteVectorStore.open({ provider: sqlite, dimensions });
93
+ if (store) return store;
94
+ console.warn(
95
+ '[trove] sqlite-vec is not loadable here — semantic search is falling back to an '
96
+ + 'IN-MEMORY index that is rebuilt from scratch on every restart. Install the '
97
+ + 'optional `sqlite-vec` dependency, or set TROVE_VECTOR to an external store.',
98
+ );
99
+ return new MemoryVectorStore({ dimensions });
100
+ }
101
+ case 'memory': default: return new MemoryVectorStore({ dimensions });
102
+ }
103
+ }
104
+
105
+ /**
106
+ * The whole graph.
107
+ *
108
+ * @param {object} config the same config object `createServer` takes
109
+ * @param {{closing: () => boolean}} lifecycleState
110
+ */
111
+ export function coreProviders(config, lifecycleState) {
112
+ return {
113
+ // Config as a dependency rather than a captured variable, so a provider that
114
+ // reads it has to say so.
115
+ config: Provider.fromSingleton(config),
116
+
117
+ // Shutdown, likewise. Long work has to be able to ask whether the server is
118
+ // going down, and closing over a `let` made that invisible.
119
+ lifecycle: Provider.fromSingleton({
120
+ get closing() { return lifecycleState.closing; },
121
+ }),
122
+
123
+ // WHO RUNS LONG WORK.
124
+ //
125
+ // A scan or a reindex outlives the request that asked for it, and where it
126
+ // runs is a deployment fact: in this process on Node and Bun, in a Durable
127
+ // Object on Workers, because an isolate cannot own work that outlives a
128
+ // response. That is the two-domain split — request and background — and it
129
+ // is not a Workers concession. On a long-lived process the two domains
130
+ // happen to share a process, which is the coincidence; Workers only makes
131
+ // the seam visible by forcing it.
132
+ //
133
+ // So a route asks for `backgroundWork` and says what it wants started. It
134
+ // does not know, and must not know, which process obliges.
135
+ //
136
+ // Late-bound through `lifecycleState`, which is the one honest circularity
137
+ // here: dispatching needs the engine that owns this container, so it cannot
138
+ // be a constructor dependency of something inside it.
139
+ backgroundWork: Provider.fromSingleton({
140
+ beginScan: (collectionId, opts) => lifecycleState.background.beginScan(collectionId, opts),
141
+ beginReindex: (opts) => lifecycleState.background.beginReindex(opts),
142
+ }),
143
+
144
+ storage: Provider.fromLazySingleton(
145
+ () => resolve(config.storage ?? config.vfs?.storage, StorageBackend, buildStorage),
146
+ ),
147
+
148
+ // One shared SQLite provider (a keyed pool) for metadata, kv, and per-plugin
149
+ // scopes. Always present so plugin storage works regardless of the metadata
150
+ // backend. Injectable, so a Worker supplies a D1-backed one instead.
151
+ sqlite: Provider.fromLazySingleton(
152
+ async () => {
153
+ const provider = resolve(config.sqlite, SqliteProvider, () => new LocalSqliteProvider({
154
+ path: config.sqlite?.path
155
+ || (config.metadata?.driver === 'sqlite' ? config.metadata.path : null)
156
+ || ':memory:',
157
+ }));
158
+ await provider.init();
159
+ return provider;
160
+ },
161
+ (provider) => provider.close?.(),
162
+ ),
163
+
164
+ metadata: Provider.fromLazySingleton(
165
+ async (deps) => {
166
+ const { sqlite } = await need(deps, ['sqlite']);
167
+ return resolve(config.metadata ?? config.vfs?.metadata, MetadataStore, (cfg) =>
168
+ (cfg.driver === 'sqlite'
169
+ ? new SqliteStore({ provider: sqlite, key: 'metadata' })
170
+ : new MemoryStore()));
171
+ },
172
+ null,
173
+ { deps: ['sqlite'] },
174
+ ),
175
+
176
+ embeddings: Provider.fromLazySingleton(
177
+ () => resolve(config.embeddings, EmbeddingProvider, (cfg) =>
178
+ (cfg.driver === 'http'
179
+ ? new HttpEmbedding(cfg.http)
180
+ : new LocalHashEmbedding({ dimensions: cfg.dimensions ?? 256 }))),
181
+ ),
182
+
183
+ search: Provider.fromLazySingleton(
184
+ async (deps) => {
185
+ const { sqlite, embeddings } = await need(deps, ['sqlite', 'embeddings']);
186
+ if (config.search instanceof SearchService) return config.search;
187
+ // Where the index lives follows the SQLite provider, not the config: the
188
+ // question is whether there is somewhere durable to write, and only the
189
+ // provider knows. A file-backed provider gets a SQLite index that survives a
190
+ // restart; an in-memory one gets the memory stores, because an index in an
191
+ // ephemeral database is strictly worse than one in memory — it looks
192
+ // persistent until the restart that proves it isn't. An explicit driver wins.
193
+ const driverFor = (cfg) =>
194
+ (cfg?.driver ? cfg : { ...cfg, driver: sqlite.durable ? 'sqlite' : 'memory' });
195
+ return new SearchService({
196
+ embeddings,
197
+ vectorStore: await resolve(config.vectorStore, VectorStore, (cfg) =>
198
+ buildVectorStore(driverFor(cfg), embeddings.dimensions, sqlite)),
199
+ keywordStore: await resolve(config.keywordStore, KeywordStore, (cfg) =>
200
+ (driverFor(cfg).driver === 'sqlite'
201
+ ? SqliteKeywordStore.open({ provider: sqlite })
202
+ : new MemoryKeywordStore())),
203
+ });
204
+ },
205
+ null,
206
+ { deps: ['sqlite', 'embeddings'] },
207
+ ),
208
+
209
+ indexers: Provider.fromLazySingleton(() => resolve(config.indexers, IndexerRegistry, () => new IndexerRegistry())),
210
+
211
+ // Raw query → { semanticText, tagFilters }. Default parses the `#tag` grammar;
212
+ // inject one (Workers AI) for LLM-assisted query understanding.
213
+ searchTransformer: Provider.fromLazySingleton(() => resolve(config.searchTransformer, SearchTransformer, (cfg) =>
214
+ ((cfg?.driver === 'workers-ai' || config?.ai)
215
+ ? new WorkersAiSearchTransformer({ ai: cfg?.ai || config?.ai, model: cfg?.model, run: cfg?.run })
216
+ : new ParsingSearchTransformer()))),
217
+
218
+ // BYO IdP. Default anonymous (single shared user) so a zero-config run works;
219
+ // production injects a JwtIdentityProvider.
220
+ identity: Provider.fromLazySingleton(() => resolve(config.identity, IdentityProvider, buildIdentity)),
221
+
222
+ // Where an unauthenticated client is sent — ONE answer for the whole drive,
223
+ // used by the JSON API's 401s and by MCP's discovery alike.
224
+ auth: Provider.fromLazySingleton(() => resolveAuthDiscovery(config)),
225
+
226
+ // Shared KV (subscriptions, inboxes, leases). Co-located in the main database
227
+ // when there is one, so it actually persists.
228
+ kv: Provider.fromLazySingleton(
229
+ async (deps) => {
230
+ const { sqlite, metadata } = await need(deps, ['sqlite', 'metadata']);
231
+ const store = resolve(config.kv, KeyValueStore, (cfg) =>
232
+ ((cfg?.driver === 'sqlite' || metadata instanceof SqliteStore)
233
+ ? new SqliteKV({ provider: sqlite, key: 'kv' })
234
+ : new MemoryKV()));
235
+ await store.init?.();
236
+ return store;
237
+ },
238
+ null,
239
+ { deps: ['sqlite', 'metadata'] },
240
+ ),
241
+
242
+ // Signing for URLs that carry their own authorization — an <img src>, a <video src>,
243
+ // an indexer handing a file to an external API. See docs/design/signed-urls.md.
244
+ //
245
+ // The secret is configured, or generated once and kept in the KV store. Generated is
246
+ // safe rather than merely convenient BECAUSE it lives in the KV store: a per-process
247
+ // random secret would work perfectly on one machine and reject half the URLs in
248
+ // flight the moment a second instance answered a request.
249
+ signedUrls: Provider.fromLazySingleton(
250
+ async (deps) => {
251
+ const { kv } = await need(deps, ['kv']);
252
+ if (config.signedUrls instanceof SignedUrls) return config.signedUrls;
253
+ return new SignedUrls({ secret: await resolveUrlSecret({ configured: config.urlSecret, kv }) });
254
+ },
255
+ null,
256
+ { deps: ['kv'] },
257
+ ),
258
+
259
+ // The two halves of "tell the user what's going on", split by lifetime:
260
+ //
261
+ // tasks in-flight work, in memory, per-process. A reindex that was running
262
+ // when the server stopped is not running now — forgetting it is correct.
263
+ // issues standing problems, in the KV store, durable. A file that failed to
264
+ // index is STILL unindexed after a restart, so this one has to survive.
265
+ //
266
+ // They meet at the retry: a failure raises an issue, retrying it starts a task,
267
+ // and the task succeeding clears the issue.
268
+ tasks: Provider.fromLazySingleton(() => resolve(config.tasks, TaskRegistry, () => new TaskRegistry())),
269
+
270
+ issues: Provider.fromLazySingleton(
271
+ async (deps) => {
272
+ const { kv } = await need(deps, ['kv']);
273
+ return resolve(config.issues, IssueRegistry, () => new IssueRegistry({ kv }));
274
+ },
275
+ null,
276
+ { deps: ['kv'] },
277
+ ),
278
+
279
+ push: Provider.fromLazySingleton(() => resolve(config.push, WebPushService, () =>
280
+ (config.vapid?.publicKey && config.vapid?.privateKey
281
+ ? new WebPushService({
282
+ publicKey: config.vapid.publicKey,
283
+ privateKey: config.vapid.privateKey,
284
+ subject: config.vapid.subject || 'mailto:admin@example.com',
285
+ })
286
+ : null))),
287
+
288
+ notifications: Provider.fromLazySingleton(
289
+ async (deps) => {
290
+ const { kv, push } = await need(deps, ['kv', 'push']);
291
+ const center = new NotificationCenter({ kv, push, flushIntervalMs: config.mentionFlushMs ?? 30_000 });
292
+ if (config.startFlusher !== false) center.start();
293
+ return center;
294
+ },
295
+ // Stopping the flusher used to be a line in close() that had to remember this
296
+ // existed. Now it is attached to the thing it stops.
297
+ (center) => center.stop(),
298
+ { deps: ['kv', 'push'] },
299
+ ),
300
+
301
+ sidecar: Provider.fromLazySingleton(
302
+ async (deps) => {
303
+ const { storage, issues, notifications } = await need(deps, ['storage', 'issues', 'notifications']);
304
+ return resolve(config.sidecar, SidecarService, () => new SidecarService({
305
+ storage,
306
+ issues,
307
+ onMentions: (mentions) =>
308
+ notifications.enqueue(mentions).catch((e) => console.error('enqueue mentions failed', e)),
309
+ }));
310
+ },
311
+ (sidecar) => sidecar.dispose?.(),
312
+ { deps: ['storage', 'issues', 'notifications'] },
313
+ ),
314
+
315
+ // The ownership + permission boundary; each collection is a store config.
316
+ // `config.collections === false` disables it (single open storage, no ACLs),
317
+ // and the resource is then null — a provider is allowed to provide nothing.
318
+ collections: Provider.fromLazySingleton(
319
+ async (deps) => {
320
+ if (config.collections === false) return null;
321
+ const { kv, storage } = await need(deps, ['kv', 'storage']);
322
+ return resolve(config.collections, CollectionService, () => new CollectionService({
323
+ kv,
324
+ storageFactory: (storeConfig) => buildStorage(storeConfig),
325
+ admins: config.admins || [],
326
+ creatorRoles: config.creatorRoles || [],
327
+ defaultOpen: config.defaultOpen !== false,
328
+ // Record the primary driver on 'default', but reuse its live instance.
329
+ defaultStore: (config.storage && !(config.storage instanceof StorageBackend))
330
+ ? config.storage
331
+ : { driver: config.storageDriver || 'memory' },
332
+ storageOverrides: { default: storage },
333
+ }));
334
+ },
335
+ null,
336
+ { deps: ['kv', 'storage'] },
337
+ ),
338
+
339
+ vfs: Provider.fromLazySingleton(
340
+ async (deps) => {
341
+ const r = await need(deps, [
342
+ 'storage', 'metadata', 'search', 'indexers', 'sidecar', 'collections',
343
+ 'searchTransformer', 'issues', 'signedUrls',
344
+ ]);
345
+ const vfs = new Vfs({
346
+ ...r,
347
+ maxUploadBytes: config.maxUploadBytes ?? null,
348
+ // Where this server can be reached from outside, for the URLs that leave the
349
+ // browser (an indexer hands one to an external API). Absent, those are refused
350
+ // rather than handed out as links to nowhere.
351
+ publicUrl: config.publicUrl || '',
352
+ });
353
+ await vfs.init();
354
+ return vfs;
355
+ },
356
+ null,
357
+ { deps: ['storage', 'metadata', 'search', 'indexers', 'sidecar', 'collections', 'searchTransformer', 'issues', 'signedUrls'] },
358
+ ),
359
+
360
+ // Bulk plugin package blobs. Defaults to the primary storage under a prefix;
361
+ // TROVE_PACKAGE_STORE points it elsewhere.
362
+ packageStore: Provider.fromLazySingleton(
363
+ async (deps) => {
364
+ const { storage } = await need(deps, ['storage']);
365
+ return resolve(config.packages, PackageStore, () =>
366
+ new StoragePackageStore(config.packageStore ? buildStorage(config.packageStore) : storage));
367
+ },
368
+ null,
369
+ { deps: ['storage'] },
370
+ ),
371
+
372
+ // Server indexer sub-packages run through a pluggable runtime. The default is
373
+ // the in-process (trusted) runner; a deployment swaps in an isolate runtime.
374
+ indexerRuntime: Provider.fromLazySingleton(() =>
375
+ (config.serverIndexers === false
376
+ ? null
377
+ : resolve(config.indexerRuntime, IndexerRuntime, () => new InProcessIndexerRuntime()))),
378
+
379
+ plugins: Provider.fromLazySingleton(
380
+ async (deps) => {
381
+ const r = await need(deps, ['vfs', 'sqlite', 'packageStore', 'indexerRuntime', 'collections']);
382
+ const service = new PluginService({
383
+ packages: r.packageStore,
384
+ installs: new SqlitePluginInstallStore({ provider: r.sqlite }),
385
+ indexers: r.indexerRuntime
386
+ ? new PluginIndexers({ vfs: r.vfs, runtime: r.indexerRuntime, packages: r.packageStore })
387
+ : null,
388
+ // Who may install a plugin. With no ACL layer configured there is no admin
389
+ // list to consult and any authenticated caller qualifies — decided from
390
+ // config, not from whether `collections` happens to be here, so a graph that
391
+ // failed to build cannot silently promote everyone.
392
+ isAdmin: (principal) => (config.collections === false
393
+ ? !!principal
394
+ : r.collections.isAdmin(principal)),
395
+ maxPackageBytes: config.maxUploadBytes ?? undefined,
396
+ strict: config.enforcePluginCaps === true,
397
+ });
398
+ await service.init();
399
+ return service;
400
+ },
401
+ null,
402
+ { deps: ['vfs', 'sqlite', 'packageStore', 'indexerRuntime', 'collections'] },
403
+ ),
404
+ };
405
+ }
@@ -0,0 +1,67 @@
1
+ // The scan's dependencies, as ngin providers.
2
+ //
3
+ // The one provider here that is not part of the drive's backbone: a claim is
4
+ // obtained per scan, not once per process. Its lifetime is exactly an action's
5
+ // lease — taken before the work, given back after, given back even if the work
6
+ // throws — which is `Provider.obtain`/`release` verbatim, and replaces a
7
+ // hand-rolled try/finally that had already gone wrong once by releasing early.
8
+
9
+ import { Provider } from '@3sln/ngin';
10
+
11
+ /** Namespace for both the resume cursor and the claim on a collection. */
12
+ export const SCAN_NS = 'scan-cursor';
13
+ /**
14
+ * How long a claim stays valid unrenewed. Long enough that a slow list() does
15
+ * not lose it, short enough that a holder that died — an isolate evicted, a
16
+ * container killed — does not block the collection for long.
17
+ */
18
+ export const LEASE_MS = 60_000;
19
+
20
+ /**
21
+ * The right to scan one collection.
22
+ *
23
+ * A scan writes a resume cursor, so two running at once means last-writer-wins
24
+ * on that cursor and a slice of the bucket is silently never reached. The guard
25
+ * has to live where every process can see it, which is why this is a lease in
26
+ * the metadata store rather than a flag in memory.
27
+ *
28
+ * `obtain` does NOT throw when the claim is taken. Another scan already running
29
+ * is an ordinary answer to "may I scan this", not an exception — and a resource
30
+ * that reports `held: false` keeps the release path identical either way.
31
+ */
32
+ export class ScanClaimProvider extends Provider {
33
+ static deps = ['kv'];
34
+
35
+ constructor({ kv }) {
36
+ super();
37
+ this.kv = kv;
38
+ }
39
+
40
+ async obtain({ collectionId = 'default', ttlMs = LEASE_MS } = {}) {
41
+ // Held for the claim's lifetime, not just this call: renewing and releasing
42
+ // both need it, and they happen after `obtain` has returned.
43
+ const kv = await this.kv.obtain();
44
+ const token = await kv.acquire(SCAN_NS, collectionId, ttlMs);
45
+ return {
46
+ collectionId,
47
+ held: !!token,
48
+ _kv: kv,
49
+ _token: token,
50
+ /** False means the claim was lost — stop, something else owns this now. */
51
+ renew: () => (token ? kv.renew(SCAN_NS, collectionId, token, ttlMs) : Promise.resolve(false)),
52
+ readCursor: async () => (token ? (await kv.get(SCAN_NS, collectionId))?.cursor || null : null),
53
+ writeCursor: (cursor) =>
54
+ (cursor
55
+ ? kv.set(SCAN_NS, collectionId, { cursor, at: Date.now() })
56
+ : kv.delete(SCAN_NS, collectionId).catch(() => {})),
57
+ };
58
+ }
59
+
60
+ async release(claim) {
61
+ if (!claim) return;
62
+ if (claim._token) {
63
+ await claim._kv.release(SCAN_NS, claim.collectionId, claim._token).catch(() => {});
64
+ }
65
+ this.kv.release(claim._kv);
66
+ }
67
+ }