@digitalpaws/starr 0.0.1
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.rst +75 -0
- package/data/.nfs.811bbb0d.43c34.4 +0 -0
- package/data/.nfs.81ba4cd0.ffff87804.4 +0 -0
- package/data/.nfs.82d127f7.fffff5704.4 +0 -0
- package/data/.nfs.82d127fe.fffff5704.4 +0 -0
- package/data/.nfs.82d159a5.fffff5aa4.4 +0 -0
- package/data/.nfs.82d159aa.fffff5aa4.4 +0 -0
- package/data/.nfs.82d15a9c.fffff5bb4.4 +0 -0
- package/data/.nfs.82d15aa1.fffff5bb4.4 +0 -0
- package/data/.nfs.82d15c65.fffff5c24.4 +0 -0
- package/data/.nfs.82d1852c.fffff5e24.4 +0 -0
- package/data/.nfs.82d18536.fffff5e24.4 +0 -0
- package/data/.nfs.82d18dbe.fffff5e44.4 +0 -0
- package/data/.nfs.82d18dc6.fffff5e44.4 +0 -0
- package/data/.nfs.8309a3f5.fffff82f4.4 +0 -0
- package/data/.nfs.8309a3fe.fffff82f4.4 +0 -0
- package/data/.nfs.8314122e.fffffb044.4 +0 -0
- package/data/.nfs.83141236.fffffb044.4 +0 -0
- package/data/.nfs.833c4fe9.fffffbc34.4 +0 -0
- package/data/.nfs.833c4ff3.fffffbc34.4 +0 -0
- package/data/logs/starr.log +66244 -0
- package/data/starr.db +0 -0
- package/data/starr.db-shm +0 -0
- package/data/starr.db-wal +0 -0
- package/data/starr.jwt +1 -0
- package/data/starr.key +1 -0
- package/dist/auth/auth-service.d.ts +42 -0
- package/dist/auth/auth-service.d.ts.map +1 -0
- package/dist/auth/auth-service.js +226 -0
- package/dist/auth/auth-service.js.map +1 -0
- package/dist/auth/http-server.d.ts +16 -0
- package/dist/auth/http-server.d.ts.map +1 -0
- package/dist/auth/http-server.js +207 -0
- package/dist/auth/http-server.js.map +1 -0
- package/dist/auth/jwt-secret.d.ts +6 -0
- package/dist/auth/jwt-secret.d.ts.map +1 -0
- package/dist/auth/jwt-secret.js +51 -0
- package/dist/auth/jwt-secret.js.map +1 -0
- package/dist/auth/oidc/oidc-flow.d.ts +60 -0
- package/dist/auth/oidc/oidc-flow.d.ts.map +1 -0
- package/dist/auth/oidc/oidc-flow.js +201 -0
- package/dist/auth/oidc/oidc-flow.js.map +1 -0
- package/dist/auth/oidc/oidc-provider-service.d.ts +59 -0
- package/dist/auth/oidc/oidc-provider-service.d.ts.map +1 -0
- package/dist/auth/oidc/oidc-provider-service.js +124 -0
- package/dist/auth/oidc/oidc-provider-service.js.map +1 -0
- package/dist/auth/oidc/user-service.d.ts +67 -0
- package/dist/auth/oidc/user-service.d.ts.map +1 -0
- package/dist/auth/oidc/user-service.js +177 -0
- package/dist/auth/oidc/user-service.js.map +1 -0
- package/dist/auth/rate-limiter.d.ts +31 -0
- package/dist/auth/rate-limiter.d.ts.map +1 -0
- package/dist/auth/rate-limiter.js +70 -0
- package/dist/auth/rate-limiter.js.map +1 -0
- package/dist/config/config-service.d.ts +83 -0
- package/dist/config/config-service.d.ts.map +1 -0
- package/dist/config/config-service.js +309 -0
- package/dist/config/config-service.js.map +1 -0
- package/dist/config/validate.d.ts +12 -0
- package/dist/config/validate.d.ts.map +1 -0
- package/dist/config/validate.js +140 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/content-types/registry.d.ts +75 -0
- package/dist/content-types/registry.d.ts.map +1 -0
- package/dist/content-types/registry.js +171 -0
- package/dist/content-types/registry.js.map +1 -0
- package/dist/datastore/db.d.ts +9 -0
- package/dist/datastore/db.d.ts.map +1 -0
- package/dist/datastore/db.js +27 -0
- package/dist/datastore/db.js.map +1 -0
- package/dist/datastore/encrypt.d.ts +9 -0
- package/dist/datastore/encrypt.d.ts.map +1 -0
- package/dist/datastore/encrypt.js +84 -0
- package/dist/datastore/encrypt.js.map +1 -0
- package/dist/datastore/migrations.d.ts +10 -0
- package/dist/datastore/migrations.d.ts.map +1 -0
- package/dist/datastore/migrations.js +340 -0
- package/dist/datastore/migrations.js.map +1 -0
- package/dist/datastore/storage.d.ts +4 -0
- package/dist/datastore/storage.d.ts.map +1 -0
- package/dist/datastore/storage.js +60 -0
- package/dist/datastore/storage.js.map +1 -0
- package/dist/events/event-bus.d.ts +15 -0
- package/dist/events/event-bus.d.ts.map +1 -0
- package/dist/events/event-bus.js +140 -0
- package/dist/events/event-bus.js.map +1 -0
- package/dist/events/spine.d.ts +31 -0
- package/dist/events/spine.d.ts.map +1 -0
- package/dist/events/spine.js +68 -0
- package/dist/events/spine.js.map +1 -0
- package/dist/events/trace.d.ts +24 -0
- package/dist/events/trace.d.ts.map +1 -0
- package/dist/events/trace.js +33 -0
- package/dist/events/trace.js.map +1 -0
- package/dist/host.d.ts +65 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +94 -0
- package/dist/host.js.map +1 -0
- package/dist/library/importer.d.ts +29 -0
- package/dist/library/importer.d.ts.map +1 -0
- package/dist/library/importer.js +145 -0
- package/dist/library/importer.js.map +1 -0
- package/dist/library/library-cache.d.ts +46 -0
- package/dist/library/library-cache.d.ts.map +1 -0
- package/dist/library/library-cache.js +152 -0
- package/dist/library/library-cache.js.map +1 -0
- package/dist/library/library-config.d.ts +12 -0
- package/dist/library/library-config.d.ts.map +1 -0
- package/dist/library/library-config.js +10 -0
- package/dist/library/library-config.js.map +1 -0
- package/dist/library/library-scanner.d.ts +62 -0
- package/dist/library/library-scanner.d.ts.map +1 -0
- package/dist/library/library-scanner.js +336 -0
- package/dist/library/library-scanner.js.map +1 -0
- package/dist/library/library-service.d.ts +65 -0
- package/dist/library/library-service.d.ts.map +1 -0
- package/dist/library/library-service.js +197 -0
- package/dist/library/library-service.js.map +1 -0
- package/dist/library/metadata-matcher.d.ts +56 -0
- package/dist/library/metadata-matcher.d.ts.map +1 -0
- package/dist/library/metadata-matcher.js +207 -0
- package/dist/library/metadata-matcher.js.map +1 -0
- package/dist/library/naming.d.ts +8 -0
- package/dist/library/naming.d.ts.map +1 -0
- package/dist/library/naming.js +47 -0
- package/dist/library/naming.js.map +1 -0
- package/dist/library/scan-controller.d.ts +95 -0
- package/dist/library/scan-controller.d.ts.map +1 -0
- package/dist/library/scan-controller.js +379 -0
- package/dist/library/scan-controller.js.map +1 -0
- package/dist/library/source-resolver.d.ts +17 -0
- package/dist/library/source-resolver.d.ts.map +1 -0
- package/dist/library/source-resolver.js +75 -0
- package/dist/library/source-resolver.js.map +1 -0
- package/dist/logging/logger.d.ts +3 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +107 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/main.d.ts +36 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +229 -0
- package/dist/main.js.map +1 -0
- package/dist/plugins/context.d.ts +10 -0
- package/dist/plugins/context.d.ts.map +1 -0
- package/dist/plugins/context.js +110 -0
- package/dist/plugins/context.js.map +1 -0
- package/dist/plugins/discovery.d.ts +2 -0
- package/dist/plugins/discovery.d.ts.map +1 -0
- package/dist/plugins/discovery.js +32 -0
- package/dist/plugins/discovery.js.map +1 -0
- package/dist/plugins/loader.d.ts +9 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +245 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/state/request-intake.d.ts +26 -0
- package/dist/state/request-intake.d.ts.map +1 -0
- package/dist/state/request-intake.js +74 -0
- package/dist/state/request-intake.js.map +1 -0
- package/dist/state/state-store.d.ts +146 -0
- package/dist/state/state-store.d.ts.map +1 -0
- package/dist/state/state-store.js +465 -0
- package/dist/state/state-store.js.map +1 -0
- package/dist/state/trace-store.d.ts +12 -0
- package/dist/state/trace-store.d.ts.map +1 -0
- package/dist/state/trace-store.js +24 -0
- package/dist/state/trace-store.js.map +1 -0
- package/dist/types/plugin.d.ts +93 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/plugin.js +4 -0
- package/dist/types/plugin.js.map +1 -0
- package/dist/web/index.d.ts +10 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +153 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/routes/activity.d.ts +6 -0
- package/dist/web/routes/activity.d.ts.map +1 -0
- package/dist/web/routes/activity.js +31 -0
- package/dist/web/routes/activity.js.map +1 -0
- package/dist/web/routes/auth.d.ts +6 -0
- package/dist/web/routes/auth.d.ts.map +1 -0
- package/dist/web/routes/auth.js +142 -0
- package/dist/web/routes/auth.js.map +1 -0
- package/dist/web/routes/config.d.ts +7 -0
- package/dist/web/routes/config.d.ts.map +1 -0
- package/dist/web/routes/config.js +204 -0
- package/dist/web/routes/config.js.map +1 -0
- package/dist/web/routes/content-types.d.ts +6 -0
- package/dist/web/routes/content-types.d.ts.map +1 -0
- package/dist/web/routes/content-types.js +29 -0
- package/dist/web/routes/content-types.js.map +1 -0
- package/dist/web/routes/discovery.d.ts +6 -0
- package/dist/web/routes/discovery.d.ts.map +1 -0
- package/dist/web/routes/discovery.js +111 -0
- package/dist/web/routes/discovery.js.map +1 -0
- package/dist/web/routes/library-import.d.ts +6 -0
- package/dist/web/routes/library-import.d.ts.map +1 -0
- package/dist/web/routes/library-import.js +120 -0
- package/dist/web/routes/library-import.js.map +1 -0
- package/dist/web/routes/library-management.d.ts +6 -0
- package/dist/web/routes/library-management.d.ts.map +1 -0
- package/dist/web/routes/library-management.js +120 -0
- package/dist/web/routes/library-management.js.map +1 -0
- package/dist/web/routes/library.d.ts +6 -0
- package/dist/web/routes/library.d.ts.map +1 -0
- package/dist/web/routes/library.js +28 -0
- package/dist/web/routes/library.js.map +1 -0
- package/dist/web/routes/oidc.d.ts +9 -0
- package/dist/web/routes/oidc.d.ts.map +1 -0
- package/dist/web/routes/oidc.js +203 -0
- package/dist/web/routes/oidc.js.map +1 -0
- package/dist/web/routes/requests.d.ts +6 -0
- package/dist/web/routes/requests.d.ts.map +1 -0
- package/dist/web/routes/requests.js +183 -0
- package/dist/web/routes/requests.js.map +1 -0
- package/dist/web/routes/sse.d.ts +6 -0
- package/dist/web/routes/sse.d.ts.map +1 -0
- package/dist/web/routes/sse.js +54 -0
- package/dist/web/routes/sse.js.map +1 -0
- package/package.json +33 -0
- package/package.json.license +2 -0
- package/src/auth/auth-service.ts +333 -0
- package/src/auth/http-server.ts +249 -0
- package/src/auth/jwt-secret.ts +65 -0
- package/src/auth/oidc/oidc-flow.ts +266 -0
- package/src/auth/oidc/oidc-provider-service.ts +174 -0
- package/src/auth/oidc/user-service.ts +227 -0
- package/src/auth/rate-limiter.ts +85 -0
- package/src/config/config-service.ts +434 -0
- package/src/config/validate.ts +153 -0
- package/src/content-types/registry.ts +194 -0
- package/src/datastore/db.ts +38 -0
- package/src/datastore/encrypt.ts +107 -0
- package/src/datastore/migrations.ts +367 -0
- package/src/datastore/storage.ts +83 -0
- package/src/events/event-bus.ts +180 -0
- package/src/events/spine.ts +87 -0
- package/src/events/trace.ts +38 -0
- package/src/host.ts +130 -0
- package/src/library/importer.ts +174 -0
- package/src/library/library-cache.ts +212 -0
- package/src/library/library-config.ts +22 -0
- package/src/library/library-scanner.ts +404 -0
- package/src/library/library-service.ts +282 -0
- package/src/library/metadata-matcher.ts +285 -0
- package/src/library/naming.ts +59 -0
- package/src/library/scan-controller.ts +490 -0
- package/src/library/source-resolver.ts +92 -0
- package/src/logging/logger.ts +115 -0
- package/src/main.ts +364 -0
- package/src/plugins/context.ts +138 -0
- package/src/plugins/discovery.ts +29 -0
- package/src/plugins/loader.ts +309 -0
- package/src/state/request-intake.ts +101 -0
- package/src/state/state-store.ts +621 -0
- package/src/state/trace-store.ts +27 -0
- package/src/types/plugin.ts +110 -0
- package/src/web/index.ts +182 -0
- package/src/web/routes/activity.ts +41 -0
- package/src/web/routes/auth.ts +170 -0
- package/src/web/routes/config.ts +282 -0
- package/src/web/routes/content-types.ts +42 -0
- package/src/web/routes/discovery.ts +156 -0
- package/src/web/routes/library-import.ts +196 -0
- package/src/web/routes/library-management.ts +140 -0
- package/src/web/routes/library.ts +46 -0
- package/src/web/routes/oidc.ts +260 -0
- package/src/web/routes/requests.ts +250 -0
- package/src/web/routes/sse.ts +92 -0
- package/tests/auth.test.ts +510 -0
- package/tests/config-route.test.ts +345 -0
- package/tests/config.test.ts +612 -0
- package/tests/content-types.test.ts +376 -0
- package/tests/datastore.test.ts +345 -0
- package/tests/e2e.test.ts +430 -0
- package/tests/event-bus.test.ts +925 -0
- package/tests/host.test.ts +449 -0
- package/tests/library-cache.test.ts +171 -0
- package/tests/library-scanner.test.ts +177 -0
- package/tests/library.test.ts +210 -0
- package/tests/loader-context.test.ts +644 -0
- package/tests/logger-jsonl.test.ts +154 -0
- package/tests/metadata-matcher.test.ts +198 -0
- package/tests/oidc.test.ts +218 -0
- package/tests/scan-controller.test.ts +328 -0
- package/tests/source-resolver.test.ts +100 -0
- package/tests/starr-video-integration.test.ts +153 -0
- package/tests/state-store.test.ts +700 -0
- package/tests/trace-history.test.ts +356 -0
- package/tsconfig.json +8 -0
- package/tsconfig.json.license +2 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { createContext } from "./context.js";
|
|
8
|
+
import { discoverPluginDirs } from "./discovery.js";
|
|
9
|
+
import type { PluginRuntime, Starr } from "../host.js";
|
|
10
|
+
import type { RegisterFn } from "../types/plugin.js";
|
|
11
|
+
function withTimeout<T>(
|
|
12
|
+
promise: Promise<T>,
|
|
13
|
+
ms: number,
|
|
14
|
+
label: string,
|
|
15
|
+
): Promise<T> {
|
|
16
|
+
return Promise.race([
|
|
17
|
+
promise,
|
|
18
|
+
new Promise<T>((_, reject) =>
|
|
19
|
+
setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms),
|
|
20
|
+
),
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── Plugin registry (used for isolated reload) ────────────────────────
|
|
25
|
+
|
|
26
|
+
interface PluginRecord {
|
|
27
|
+
dir: string;
|
|
28
|
+
ctx: ReturnType<typeof createContext>;
|
|
29
|
+
runtime: PluginRuntime;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const pluginRegistry = new Map<string, PluginRecord>();
|
|
33
|
+
|
|
34
|
+
export async function loadPlugins(starr: Starr): Promise<void> {
|
|
35
|
+
const dirs = await discoverPluginDirs(starr.options.pluginsDir);
|
|
36
|
+
starr.logger.info("discovered plugins", {
|
|
37
|
+
count: dirs.length,
|
|
38
|
+
dirs: dirs.map((d) => path.basename(d)),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const db = starr.getDatabase();
|
|
42
|
+
if (!db) throw new Error("datastore not initialized; call openDatastore first");
|
|
43
|
+
|
|
44
|
+
// ── Pass 1: Discover plugin IDs ──────────────────────────
|
|
45
|
+
// Trial registration to get canonical plugin IDs before querying instances.
|
|
46
|
+
const pluginIdMap = new Map<string, { dir: string; entryUrl: string }>();
|
|
47
|
+
|
|
48
|
+
for (const dir of dirs) {
|
|
49
|
+
let pkg: { main: string };
|
|
50
|
+
try {
|
|
51
|
+
const pkgRaw = await readFile(path.join(dir, "package.json"), "utf8");
|
|
52
|
+
pkg = JSON.parse(pkgRaw) as { main: string };
|
|
53
|
+
} catch {
|
|
54
|
+
starr.logger.warn("skipping dir: no package.json", { dir });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const entryUrl = pathToFileURL(path.resolve(dir, pkg.main)).href;
|
|
58
|
+
|
|
59
|
+
// Trial context (no real config service)
|
|
60
|
+
const trialCtx = createContext(starr, dir, db, undefined);
|
|
61
|
+
try {
|
|
62
|
+
const mod = await import(entryUrl);
|
|
63
|
+
const register = mod.default as RegisterFn;
|
|
64
|
+
if (typeof register !== "function") {
|
|
65
|
+
starr.logger.warn("skipping dir: no default export function", { dir });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
await register(trialCtx);
|
|
69
|
+
const meta = trialCtx.getMeta();
|
|
70
|
+
if (meta) {
|
|
71
|
+
pluginIdMap.set(meta.id, { dir, entryUrl });
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
starr.logger.warn("trial registration failed for dir (will retry with real instances)", { dir, error: String(err) });
|
|
75
|
+
// Guess pluginId from directory basename
|
|
76
|
+
const guessedId = path.basename(dir);
|
|
77
|
+
pluginIdMap.set(guessedId, { dir, entryUrl });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Clean up trial registrations — roll back any content types ──
|
|
82
|
+
// that were registered during Pass 1, so Pass 2 can register them
|
|
83
|
+
// with the real context without triggering duplicate errors.
|
|
84
|
+
for (const pluginId of pluginIdMap.keys()) {
|
|
85
|
+
starr.contentTypeRegistry.rollbackPlugin(pluginId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Pass 2: Load instances ───────────────────────────────
|
|
89
|
+
const seenIds = new Set<string>();
|
|
90
|
+
const lifecycles: PluginRuntime[] = [];
|
|
91
|
+
|
|
92
|
+
for (const [pluginId, info] of pluginIdMap) {
|
|
93
|
+
// Get instance IDs from DB (or "default" if none)
|
|
94
|
+
const instanceRows = db
|
|
95
|
+
.prepare("SELECT DISTINCT instance_id FROM plugin_config WHERE plugin_id = ?")
|
|
96
|
+
.all(pluginId) as Array<{ instance_id: string }>;
|
|
97
|
+
const instanceIds = instanceRows.length > 0
|
|
98
|
+
? instanceRows.map(r => r.instance_id)
|
|
99
|
+
: ["default"];
|
|
100
|
+
|
|
101
|
+
for (const instanceId of instanceIds) {
|
|
102
|
+
let ctx: ReturnType<typeof createContext> | undefined;
|
|
103
|
+
try {
|
|
104
|
+
ctx = createContext(starr, info.dir, db, starr.configService ?? undefined, instanceId);
|
|
105
|
+
const mod = await import(info.entryUrl);
|
|
106
|
+
const register = mod.default as RegisterFn;
|
|
107
|
+
if (typeof register !== "function") throw new Error("default export is not a function");
|
|
108
|
+
const lifecycle = (await register(ctx)) ?? {};
|
|
109
|
+
const meta = ctx.getMeta();
|
|
110
|
+
if (!meta) throw new Error("register did not call ctx.define");
|
|
111
|
+
|
|
112
|
+
const fullInstanceId = `${meta.id}::${instanceId}`;
|
|
113
|
+
if (seenIds.has(fullInstanceId)) {
|
|
114
|
+
throw new Error(`duplicate instance: ${fullInstanceId}`);
|
|
115
|
+
}
|
|
116
|
+
seenIds.add(fullInstanceId);
|
|
117
|
+
|
|
118
|
+
const runtime: PluginRuntime = {
|
|
119
|
+
meta: { ...meta, id: fullInstanceId },
|
|
120
|
+
lifecycle,
|
|
121
|
+
startOrder: -1,
|
|
122
|
+
};
|
|
123
|
+
lifecycles.push(runtime);
|
|
124
|
+
pluginRegistry.set(fullInstanceId, { dir: info.dir, ctx: ctx!, runtime });
|
|
125
|
+
} catch (err) {
|
|
126
|
+
starr.logger.error("instance registration failed", {
|
|
127
|
+
pluginId, instanceId, error: String(err),
|
|
128
|
+
});
|
|
129
|
+
if (ctx) {
|
|
130
|
+
ctx.unsubscribeAll();
|
|
131
|
+
ctx.deleteAllStorage().catch(e =>
|
|
132
|
+
starr.logger.error("storage rollback failed", { pluginId, instanceId, error: String(e) }),
|
|
133
|
+
);
|
|
134
|
+
const meta = ctx.getMeta();
|
|
135
|
+
if (meta) {
|
|
136
|
+
starr.contentTypeRegistry.rollbackPlugin(meta.id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Start pass
|
|
144
|
+
let order = 0;
|
|
145
|
+
for (const runtime of lifecycles) {
|
|
146
|
+
if (runtime.lifecycle.start) {
|
|
147
|
+
try {
|
|
148
|
+
await withTimeout(
|
|
149
|
+
Promise.resolve(runtime.lifecycle.start()),
|
|
150
|
+
30_000,
|
|
151
|
+
`start(${runtime.meta.id})`,
|
|
152
|
+
);
|
|
153
|
+
runtime.startOrder = order++;
|
|
154
|
+
} catch (err) {
|
|
155
|
+
starr.logger.error("plugin start failed", {
|
|
156
|
+
id: runtime.meta.id,
|
|
157
|
+
error: String(err),
|
|
158
|
+
});
|
|
159
|
+
runtime.startOrder = -1;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
runtime.startOrder = order++;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
starr.setRuntimes(lifecycles);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Reload a single plugin in isolation: stop → unregister → re-import → start.
|
|
171
|
+
* In multi-instance mode, all instances of the plugin are reloaded.
|
|
172
|
+
*/
|
|
173
|
+
export async function reloadPlugin(
|
|
174
|
+
starr: Starr,
|
|
175
|
+
pluginId: string,
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
// Find all instances matching this pluginId (pluginId::instanceId or plain pluginId)
|
|
178
|
+
const records: Array<{ fullId: string; record: PluginRecord }> = [];
|
|
179
|
+
for (const [fullId, record] of pluginRegistry) {
|
|
180
|
+
if (fullId === pluginId || fullId.startsWith(`${pluginId}::`)) {
|
|
181
|
+
records.push({ fullId, record });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (records.length === 0) {
|
|
186
|
+
starr.logger.warn("reload requested for unknown plugin", { pluginId });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
starr.logger.info("reloading plugin instances", { pluginId, count: records.length });
|
|
191
|
+
|
|
192
|
+
// 1. Stop all instances
|
|
193
|
+
for (const { fullId, record } of records) {
|
|
194
|
+
if (record.runtime.lifecycle.stop) {
|
|
195
|
+
try {
|
|
196
|
+
await withTimeout(
|
|
197
|
+
Promise.resolve(record.runtime.lifecycle.stop()),
|
|
198
|
+
10_000,
|
|
199
|
+
`reload-stop(${fullId})`,
|
|
200
|
+
);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
starr.logger.error("plugin stop failed during reload", {
|
|
203
|
+
pluginId: fullId,
|
|
204
|
+
error: String(err),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
record.ctx.unsubscribeAll();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 2. Remove from runtimes list
|
|
212
|
+
const runtimes = [...starr.getRuntimes()].filter(
|
|
213
|
+
(r) => !records.some(({ fullId }) => r.meta.id === fullId),
|
|
214
|
+
);
|
|
215
|
+
starr.setRuntimes(runtimes);
|
|
216
|
+
|
|
217
|
+
// 3. For each instance, re-import and start
|
|
218
|
+
for (const { fullId, record } of records) {
|
|
219
|
+
const pkgRaw = await readFile(
|
|
220
|
+
path.join(record.dir, "package.json"), "utf8",
|
|
221
|
+
);
|
|
222
|
+
const pkg = JSON.parse(pkgRaw) as { main: string };
|
|
223
|
+
const entryUrl = pathToFileURL(
|
|
224
|
+
path.resolve(record.dir, pkg.main),
|
|
225
|
+
).href;
|
|
226
|
+
const cacheBustUrl = `${entryUrl}?reload=${Date.now()}`;
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const mod = await import(cacheBustUrl);
|
|
230
|
+
const register = mod.default as RegisterFn;
|
|
231
|
+
if (typeof register !== "function") {
|
|
232
|
+
throw new Error("default export is not a function");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const db = starr.getDatabase();
|
|
236
|
+
if (!db) throw new Error("datastore not available");
|
|
237
|
+
|
|
238
|
+
// Extract instanceId from fullId (pluginId::instanceId)
|
|
239
|
+
const instanceId = fullId.includes("::") ? fullId.split("::")[1] : "default";
|
|
240
|
+
|
|
241
|
+
const newCtx = createContext(
|
|
242
|
+
starr,
|
|
243
|
+
record.dir,
|
|
244
|
+
db,
|
|
245
|
+
starr.configService ?? undefined,
|
|
246
|
+
instanceId,
|
|
247
|
+
);
|
|
248
|
+
const lifecycle = (await register(newCtx)) ?? {};
|
|
249
|
+
const meta = newCtx.getMeta();
|
|
250
|
+
if (!meta) throw new Error("register did not call ctx.define");
|
|
251
|
+
|
|
252
|
+
const startOrder = runtimes.length;
|
|
253
|
+
if (lifecycle.start) {
|
|
254
|
+
try {
|
|
255
|
+
await withTimeout(
|
|
256
|
+
Promise.resolve(lifecycle.start()),
|
|
257
|
+
30_000,
|
|
258
|
+
`reload-start(${fullId})`,
|
|
259
|
+
);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
starr.logger.error("plugin start failed during reload", {
|
|
262
|
+
pluginId: fullId,
|
|
263
|
+
error: String(err),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const newRuntime: PluginRuntime = { meta: { ...meta, id: fullId }, lifecycle, startOrder };
|
|
269
|
+
pluginRegistry.set(fullId, {
|
|
270
|
+
dir: record.dir,
|
|
271
|
+
ctx: newCtx,
|
|
272
|
+
runtime: newRuntime,
|
|
273
|
+
});
|
|
274
|
+
runtimes.push(newRuntime);
|
|
275
|
+
|
|
276
|
+
starr.logger.info("plugin instance reloaded", { pluginId: fullId });
|
|
277
|
+
} catch (err) {
|
|
278
|
+
starr.logger.error("plugin instance reload failed", {
|
|
279
|
+
pluginId: fullId,
|
|
280
|
+
error: String(err),
|
|
281
|
+
});
|
|
282
|
+
pluginRegistry.delete(fullId);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
starr.setRuntimes(runtimes);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function stopPlugins(starr: Starr): Promise<void> {
|
|
290
|
+
const runtimes = [...starr.getRuntimes()]
|
|
291
|
+
.filter((r) => r.startOrder >= 0)
|
|
292
|
+
.sort((a, b) => b.startOrder - a.startOrder);
|
|
293
|
+
for (const runtime of runtimes) {
|
|
294
|
+
if (runtime.lifecycle.stop) {
|
|
295
|
+
try {
|
|
296
|
+
await withTimeout(
|
|
297
|
+
Promise.resolve(runtime.lifecycle.stop()),
|
|
298
|
+
10_000,
|
|
299
|
+
`stop(${runtime.meta.id})`,
|
|
300
|
+
);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
starr.logger.error("plugin stop failed", {
|
|
303
|
+
id: runtime.meta.id,
|
|
304
|
+
error: String(err),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import type { ContentItemBase } from "@digitalpaws/starr-types-core";
|
|
6
|
+
import type { EventBus, Logger } from "../types/plugin.js";
|
|
7
|
+
import { computeContentIdentity, ContentTypeRegistryImpl } from "../content-types/registry.js";
|
|
8
|
+
import { StateStore } from "./state-store.js";
|
|
9
|
+
import type { LibraryService } from "../library/library-service.js";
|
|
10
|
+
|
|
11
|
+
export interface RequestIntakeInput {
|
|
12
|
+
/** The user making the request (v1: always "admin"). */
|
|
13
|
+
userId: string;
|
|
14
|
+
/** The content item selected from browse/discovery. */
|
|
15
|
+
item: ContentItemBase;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Process a user request for content.
|
|
20
|
+
*
|
|
21
|
+
* 1. Compute the item's contentIdentity via the content-type registry.
|
|
22
|
+
* 2. Look for an existing non-failed TrackedRequest with that identity (DU1).
|
|
23
|
+
* 3. If found: add the user as a watcher and return the existing requestId (DU2).
|
|
24
|
+
* 4. If not found (or prior is failed): create a new TrackedRequest in status
|
|
25
|
+
* "requested", then emit "content.requested" to start the pipeline (DU3).
|
|
26
|
+
*
|
|
27
|
+
* Resolves target library for the request via LibraryService (if available).
|
|
28
|
+
*
|
|
29
|
+
* @returns The requestId (either existing or newly created).
|
|
30
|
+
*/
|
|
31
|
+
export function requestIntake(
|
|
32
|
+
input: RequestIntakeInput,
|
|
33
|
+
store: StateStore,
|
|
34
|
+
registry: ContentTypeRegistryImpl,
|
|
35
|
+
bus: EventBus,
|
|
36
|
+
logger: Logger,
|
|
37
|
+
libraryService?: LibraryService,
|
|
38
|
+
): string {
|
|
39
|
+
// 1. Compute content identity
|
|
40
|
+
const contentIdentity = computeContentIdentity(input.item, registry);
|
|
41
|
+
logger.debug("request intake: computed identity", {
|
|
42
|
+
contentTypeId: input.item.contentTypeId,
|
|
43
|
+
contentIdentity,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// 2. Look for existing non-failed request (DU1)
|
|
47
|
+
const existing = store.findByContentIdentity(contentIdentity);
|
|
48
|
+
|
|
49
|
+
if (existing) {
|
|
50
|
+
// 3. Found — attach as watcher (DU2)
|
|
51
|
+
logger.debug("request intake: attaching to existing request", {
|
|
52
|
+
existingRequestId: existing.requestId,
|
|
53
|
+
existingStatus: existing.status,
|
|
54
|
+
});
|
|
55
|
+
store.addWatcher(existing.requestId, input.userId);
|
|
56
|
+
return existing.requestId;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 4. Not found (or only failed exists) — create new (DU3)
|
|
60
|
+
const requestId = randomUUID();
|
|
61
|
+
|
|
62
|
+
// Determine target library (FR-RIR-01)
|
|
63
|
+
let libraryId: string | undefined;
|
|
64
|
+
if (libraryService) {
|
|
65
|
+
const resolved = libraryService.resolveForContentType(input.item.contentTypeId);
|
|
66
|
+
if (resolved) {
|
|
67
|
+
libraryId = resolved.libraryId;
|
|
68
|
+
logger.debug("request intake: resolved library", {
|
|
69
|
+
contentTypeId: input.item.contentTypeId,
|
|
70
|
+
libraryId,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
logger.info("request intake: creating new tracked request", {
|
|
76
|
+
requestId,
|
|
77
|
+
contentTypeId: input.item.contentTypeId,
|
|
78
|
+
contentIdentity,
|
|
79
|
+
libraryId,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
store.insert({
|
|
83
|
+
requestId,
|
|
84
|
+
contentTypeId: input.item.contentTypeId,
|
|
85
|
+
contentIdentity,
|
|
86
|
+
item: input.item,
|
|
87
|
+
ownerUserId: input.userId,
|
|
88
|
+
libraryId,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Emit content.requested to start the acquisition pipeline
|
|
92
|
+
bus.emit("content.requested", {
|
|
93
|
+
requestId,
|
|
94
|
+
userId: input.userId,
|
|
95
|
+
contentTypeId: input.item.contentTypeId,
|
|
96
|
+
item: input.item,
|
|
97
|
+
libraryId,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return requestId;
|
|
101
|
+
}
|