@3sln/trove 0.0.16 → 0.0.18

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 (29) hide show
  1. package/package.json +1 -1
  2. package/packages/core/src/index.js +1 -1
  3. package/packages/core/src/indexers/registry.js +11 -0
  4. package/packages/core/src/plugins/index.js +44 -6
  5. package/packages/core/src/plugins/indexers.js +52 -0
  6. package/packages/core/src/plugins/runtime.js +101 -4
  7. package/packages/core/src/plugins/workerLoaderRuntime.js +160 -0
  8. package/packages/core/src/sidecar/document.js +60 -2
  9. package/packages/core/src/sidecar/index.js +28 -1
  10. package/packages/core/src/vfs.js +14 -3
  11. package/packages/plugin-sdk/src/browser.js +257 -26
  12. package/packages/plugin-sdk/src/protocol.js +9 -0
  13. package/packages/server/src/adapters/worker-tasks.js +11 -0
  14. package/packages/server/src/engine/providers/core.js +12 -5
  15. package/packages/server/src/index.js +90 -0
  16. package/packages/server/src/routes.js +92 -2
  17. package/packages/web/dist/assets/main-b9jd0fyt.js +742 -0
  18. package/packages/web/dist/assets/{main-wpfmmbfd.js.map → main-b9jd0fyt.js.map} +13 -12
  19. package/packages/web/dist/index.html +1 -1
  20. package/packages/web/dist/sw.js +33 -3
  21. package/packages/web/src/bl/fileType.js +8 -0
  22. package/packages/web/src/bl/launcher.js +15 -0
  23. package/packages/web/src/platform/index.js +8 -0
  24. package/packages/web/src/platform/itemData.js +161 -0
  25. package/packages/web/src/platform/navigation.js +16 -2
  26. package/packages/web/src/platform/pluginRpc.js +55 -0
  27. package/packages/web/src/ui/components/launcher.js +1 -1
  28. package/packages/web/src/ui/components/views/index.js +14 -1
  29. package/packages/web/dist/assets/main-wpfmmbfd.js +0 -511
@@ -373,6 +373,29 @@ export function createRouter() {
373
373
  return { node: handle.node };
374
374
  });
375
375
 
376
+ // The same question with no collection in the path, for a caller that has an id.
377
+ //
378
+ // `api.stat(ref)` builds this URL whenever it is not given a collection, which is the
379
+ // normal case — an id and a `trove:` URI each name themselves, so there is nothing to
380
+ // scope. The route did not exist, so every such call answered "No such route": that is
381
+ // what broke `ctx.files.blob()` for plugins, and with it every viewer that reads its own
382
+ // file. The audiobook player showed it twice over — no cover art, and no sample tables
383
+ // to stream from — and on the drive it simply said "No such route" and stopped.
384
+ //
385
+ // A NAME is refused here rather than guessed at. A name is only unique inside a
386
+ // collection, so resolving one without saying which collection is a question with more
387
+ // than one right answer.
388
+ r.get('/api/items/resolve', [], async (ctx) => {
389
+ const ref = ctx.query.id || ctx.query.uri;
390
+ if (!ref) {
391
+ throw TroveError.invalid(ctx.query.name
392
+ ? 'a name is only unique within a collection — resolve it at /api/collections/:collection/items/resolve'
393
+ : 'id or uri is required');
394
+ }
395
+ const handle = await ctx.access.node(ref, 'read');
396
+ return { node: handle.node };
397
+ });
398
+
376
399
  // What links to this item — the inverse of the links its own content declares, and
377
400
  // what replaces "which folder is it in?".
378
401
  r.get('/api/items/backlinks', ['collections'], async (ctx) => {
@@ -854,6 +877,24 @@ export function createRouter() {
854
877
  admin: ctx.collections.isAdmin(ctx.principal),
855
878
  }));
856
879
 
880
+ /**
881
+ * Which plugin scope this caller may write, from `?scope=`.
882
+ *
883
+ * A contribution URI names a plugin, and the caller must have it installed — otherwise
884
+ * "scoped per plugin" is a naming convention rather than a boundary, and one plugin
885
+ * could read or overwrite another's state on every item in the drive.
886
+ */
887
+ async function requireScope(ctx) {
888
+ const scope = String(ctx.query.scope || '').trim();
889
+ if (!scope) throw TroveError.invalid('scope is required');
890
+ const pluginId = parseContribUri(scope)?.pluginId ?? (scope.startsWith('trove+plugin:') ? scope.slice('trove+plugin:'.length) : null);
891
+ if (!pluginId) throw TroveError.invalid('scope must be a trove+plugin: URI');
892
+ requirePlugins(ctx.plugins);
893
+ const installed = await ctx.plugins.get(ctx.principal, pluginId);
894
+ if (!installed) throw TroveError.forbidden(`"${pluginId}" is not installed for this account`);
895
+ return `trove+plugin:${pluginId}`;
896
+ }
897
+
857
898
  // --- conversations, tags, sidecar (per file) -------------------------------
858
899
  // The :id is a file node id; the sidecar is that file's CRDT document.
859
900
 
@@ -863,6 +904,39 @@ export function createRouter() {
863
904
  return (await ctx.access.node(ctx.params.id, 'read')).view();
864
905
  });
865
906
 
907
+ // A PLUGIN's own key/value data for this item — a listening position, a last page.
908
+ //
909
+ // The scope is `?scope=` and the route does NOT trust it on its own: `requireScope`
910
+ // checks the caller owns that plugin id, the same gate `/api/index/:indexerId` applies
911
+ // to contributions. Without it any caller could write into any plugin's namespace, and
912
+ // "scoped" would be a naming convention rather than a boundary.
913
+ //
914
+ // `read` to read and `write` to write, on the ITEM — this is data about someone's use of
915
+ // a file, so it follows the file's permissions.
916
+ r.get('/api/items/:id/data', ['sidecar'], async (ctx) => {
917
+ const scope = await requireScope(ctx);
918
+ await ctx.access.node(ctx.params.id, 'read');
919
+ return { scope, data: await ctx.sidecar.data(ctx.params.id, scope) };
920
+ });
921
+
922
+ r.post('/api/items/:id/data', ['sidecar'], async (ctx) => {
923
+ const scope = await requireScope(ctx);
924
+ requirePrincipal(ctx.principal);
925
+ await ctx.access.node(ctx.params.id, 'write');
926
+ const b = await body(ctx.req);
927
+ // A BATCH, because the client flushes what it queued while offline and one request
928
+ // per key would make a reconnect a thundering herd against one sidecar document.
929
+ const entries = Array.isArray(b.entries) ? b.entries : [{ key: b.key, value: b.value, remove: b.remove }];
930
+ for (const e of entries) {
931
+ if (!e?.key) continue;
932
+ if (e.remove) await ctx.sidecar.removeData(ctx.params.id, scope, e.key, ctx.principal);
933
+ else await ctx.sidecar.setData(ctx.params.id, scope, e.key, e.value, ctx.principal);
934
+ }
935
+ // The merged view back, so a client that was offline learns what won without a
936
+ // second round trip — which is the whole shape of "write, then reconcile".
937
+ return { scope, data: await ctx.sidecar.data(ctx.params.id, scope) };
938
+ });
939
+
866
940
  r.post('/api/items/:id/comments', [], async (ctx) => {
867
941
  requirePrincipal(ctx.principal);
868
942
  const node = await ctx.access.node(ctx.params.id, 'write');
@@ -966,12 +1040,28 @@ export function createRouter() {
966
1040
  // Install: upload the raw package zip; grants via ?grants=files,storage. The server
967
1041
  // re-parses + validates and gates on scope (admin for server indexers / shared
968
1042
  // resources), then stores the blob (deduped by digest) + the install record.
969
- r.post('/api/plugins/install', ['plugins'], async ({ plugins, principal, req, query }) => {
1043
+ r.post('/api/plugins/install', ['plugins', 'backgroundWork'], async ({ plugins, principal, req, query, backgroundWork }) => {
970
1044
  requirePlugins(plugins);
971
1045
  requirePrincipal(principal);
972
1046
  const bytes = await readBytesCapped(req, plugins.maxPackageBytes || 32 * 1024 * 1024);
973
1047
  const grants = query.grants ? String(query.grants).split(',').map((s) => s.trim()).filter(Boolean) : undefined;
974
- return { install: await plugins.install({ principal, bytes, grants }) };
1048
+ const install = await plugins.install({ principal, bytes, grants });
1049
+
1050
+ // A new indexer only earns its keep over files that are ALREADY here, and re-reading
1051
+ // them is not work an install request can finish — so it is scheduled, not awaited.
1052
+ // Skipped when the deployment cannot run them: `indexersSkipped` says so, and a task
1053
+ // that indexes nothing is worse than no task, because it reports success.
1054
+ let backfill = null;
1055
+ if (install.indexers?.length && !install.indexersSkipped) {
1056
+ const { task } = await backgroundWork.beginBackfill({
1057
+ indexerIds: install.indexers.map((i) => i.id),
1058
+ reason: `Indexing existing files for ${install.pluginId}`,
1059
+ });
1060
+ backfill = task;
1061
+ }
1062
+ // The task rides back with the install so a client can watch it rather than
1063
+ // discovering later that its drive is quietly re-indexing.
1064
+ return { install, backfill };
975
1065
  }, { cost: 'install' });
976
1066
 
977
1067
  // List this account's server-installed plugins (for cross-device sync).