@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.
- package/package.json +1 -1
- package/packages/core/src/index.js +1 -1
- package/packages/core/src/indexers/registry.js +11 -0
- package/packages/core/src/plugins/index.js +44 -6
- package/packages/core/src/plugins/indexers.js +52 -0
- package/packages/core/src/plugins/runtime.js +101 -4
- package/packages/core/src/plugins/workerLoaderRuntime.js +160 -0
- package/packages/core/src/sidecar/document.js +60 -2
- package/packages/core/src/sidecar/index.js +28 -1
- package/packages/core/src/vfs.js +14 -3
- package/packages/plugin-sdk/src/browser.js +257 -26
- package/packages/plugin-sdk/src/protocol.js +9 -0
- package/packages/server/src/adapters/worker-tasks.js +11 -0
- package/packages/server/src/engine/providers/core.js +12 -5
- package/packages/server/src/index.js +90 -0
- package/packages/server/src/routes.js +92 -2
- package/packages/web/dist/assets/main-b9jd0fyt.js +742 -0
- package/packages/web/dist/assets/{main-wpfmmbfd.js.map → main-b9jd0fyt.js.map} +13 -12
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +33 -3
- package/packages/web/src/bl/fileType.js +8 -0
- package/packages/web/src/bl/launcher.js +15 -0
- package/packages/web/src/platform/index.js +8 -0
- package/packages/web/src/platform/itemData.js +161 -0
- package/packages/web/src/platform/navigation.js +16 -2
- package/packages/web/src/platform/pluginRpc.js +55 -0
- package/packages/web/src/ui/components/launcher.js +1 -1
- package/packages/web/src/ui/components/views/index.js +14 -1
- 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
|
-
|
|
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).
|