@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,621 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import type Database from "better-sqlite3";
|
|
5
|
+
import type { ContentItemBase } from "@digitalpaws/starr-types-core";
|
|
6
|
+
import type { EventBus, Logger, StarrEvent } from "../types/plugin.js";
|
|
7
|
+
import { env } from "node:process";
|
|
8
|
+
|
|
9
|
+
// ── Types ──────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/** Union of the v1 status values (§4). */
|
|
12
|
+
export type RequestStatus =
|
|
13
|
+
| "requested"
|
|
14
|
+
| "searching"
|
|
15
|
+
| "downloading"
|
|
16
|
+
| "downloaded"
|
|
17
|
+
| "importing"
|
|
18
|
+
| "imported"
|
|
19
|
+
| "failed";
|
|
20
|
+
|
|
21
|
+
/** The domain record as stored and returned to callers. */
|
|
22
|
+
export interface TrackedRequest {
|
|
23
|
+
requestId: string;
|
|
24
|
+
contentTypeId: string;
|
|
25
|
+
contentIdentity: string;
|
|
26
|
+
item: ContentItemBase;
|
|
27
|
+
ownerUserId: string;
|
|
28
|
+
watchers: string[];
|
|
29
|
+
status: RequestStatus;
|
|
30
|
+
statusDetail?: string;
|
|
31
|
+
chosenReleaseId?: string;
|
|
32
|
+
attempts: number;
|
|
33
|
+
libraryId?: string;
|
|
34
|
+
libraryName?: string;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
updatedAt: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Minimal data needed to insert a new tracked request. */
|
|
40
|
+
export interface TrackedRequestData {
|
|
41
|
+
requestId: string;
|
|
42
|
+
contentTypeId: string;
|
|
43
|
+
contentIdentity: string;
|
|
44
|
+
item: ContentItemBase;
|
|
45
|
+
ownerUserId: string;
|
|
46
|
+
libraryId?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A single captured spine event stored in the trace log. */
|
|
50
|
+
export interface TraceEntry {
|
|
51
|
+
id: number;
|
|
52
|
+
traceId: string;
|
|
53
|
+
requestId: string;
|
|
54
|
+
eventType: string;
|
|
55
|
+
/** JSON-serialized event envelope (envelope fields + payload). */
|
|
56
|
+
eventData: Record<string, unknown>;
|
|
57
|
+
timestamp: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Row mapping helper ─────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function rowToRequest(row: Record<string, unknown>): TrackedRequest {
|
|
63
|
+
return {
|
|
64
|
+
requestId: row.request_id as string,
|
|
65
|
+
contentTypeId: row.content_type_id as string,
|
|
66
|
+
contentIdentity: row.content_identity as string,
|
|
67
|
+
item: JSON.parse(row.item as string) as ContentItemBase,
|
|
68
|
+
ownerUserId: row.owner_user_id as string,
|
|
69
|
+
watchers: JSON.parse(row.watchers as string) as string[],
|
|
70
|
+
status: row.status as RequestStatus,
|
|
71
|
+
statusDetail: (row.status_detail as string) || undefined,
|
|
72
|
+
chosenReleaseId: (row.chosen_release_id as string) || undefined,
|
|
73
|
+
attempts: row.attempts as number,
|
|
74
|
+
libraryId: (row.library_id as string) || undefined,
|
|
75
|
+
createdAt: row.created_at as string,
|
|
76
|
+
updatedAt: row.updated_at as string,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Maximum number of trace entries retained per traceId.
|
|
82
|
+
* Set via STARR_TRACE_HISTORY_LIMIT env var; defaults to 50.
|
|
83
|
+
* Values ≤ 0 disable trace capture entirely.
|
|
84
|
+
*/
|
|
85
|
+
const TRACE_HISTORY_LIMIT = (() => {
|
|
86
|
+
const raw = env.STARR_TRACE_HISTORY_LIMIT;
|
|
87
|
+
if (raw === undefined || raw === "") return 50;
|
|
88
|
+
const n = Number(raw);
|
|
89
|
+
if (Number.isNaN(n)) {
|
|
90
|
+
// We don't have a logger at module scope — the state store
|
|
91
|
+
// constructor will log a warning if the env var is invalid.
|
|
92
|
+
return 50;
|
|
93
|
+
}
|
|
94
|
+
return Math.max(0, Math.floor(n));
|
|
95
|
+
})();
|
|
96
|
+
|
|
97
|
+
// ── StateStore class ───────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export class StateStore {
|
|
100
|
+
private db: Database.Database;
|
|
101
|
+
private bus: EventBus;
|
|
102
|
+
private logger: Logger;
|
|
103
|
+
|
|
104
|
+
// Prepared statements — created once in constructor
|
|
105
|
+
private stmtGetByRequestId: Database.Statement;
|
|
106
|
+
private stmtFindByIdentity: Database.Statement;
|
|
107
|
+
private stmtInsert: Database.Statement;
|
|
108
|
+
private stmtUpdateStatus: Database.Statement;
|
|
109
|
+
private stmtIncrementAttempts: Database.Statement;
|
|
110
|
+
private stmtAddWatcher: Database.Statement;
|
|
111
|
+
private stmtListByUser: Database.Statement;
|
|
112
|
+
private stmtInsertTrace: Database.Statement;
|
|
113
|
+
private stmtGetIdsByTraceId: Database.Statement;
|
|
114
|
+
private stmtGetTraceByRequestId: Database.Statement;
|
|
115
|
+
private stmtResetAttempts: Database.Statement;
|
|
116
|
+
private stmtResetForRetry: Database.Statement;
|
|
117
|
+
private stmtDeleteRequest: Database.Statement;
|
|
118
|
+
private stmtDeleteTraceByRequestId: Database.Statement;
|
|
119
|
+
|
|
120
|
+
constructor(
|
|
121
|
+
db: Database.Database,
|
|
122
|
+
bus: EventBus,
|
|
123
|
+
logger: Logger,
|
|
124
|
+
) {
|
|
125
|
+
this.db = db;
|
|
126
|
+
this.bus = bus;
|
|
127
|
+
this.logger = logger.child("state-store");
|
|
128
|
+
|
|
129
|
+
// Compile prepared statements once
|
|
130
|
+
this.stmtGetByRequestId = db.prepare(
|
|
131
|
+
"SELECT * FROM tracked_requests WHERE request_id = @requestId",
|
|
132
|
+
);
|
|
133
|
+
this.stmtFindByIdentity = db.prepare(
|
|
134
|
+
`SELECT * FROM tracked_requests
|
|
135
|
+
WHERE content_identity = @identity AND status != 'failed'
|
|
136
|
+
ORDER BY created_at DESC
|
|
137
|
+
LIMIT 1`,
|
|
138
|
+
);
|
|
139
|
+
this.stmtInsert = db.prepare(
|
|
140
|
+
`INSERT INTO tracked_requests
|
|
141
|
+
(request_id, content_type_id, content_identity, item,
|
|
142
|
+
owner_user_id, watchers, status, attempts, library_id, created_at, updated_at)
|
|
143
|
+
VALUES
|
|
144
|
+
(@requestId, @contentTypeId, @contentIdentity, @item,
|
|
145
|
+
@ownerUserId, '[]', 'requested', 0,
|
|
146
|
+
@libraryId,
|
|
147
|
+
datetime('now'), datetime('now'))`,
|
|
148
|
+
);
|
|
149
|
+
this.stmtUpdateStatus = db.prepare(
|
|
150
|
+
`UPDATE tracked_requests
|
|
151
|
+
SET status = @status,
|
|
152
|
+
status_detail = @detail,
|
|
153
|
+
chosen_release_id = COALESCE(@chosenReleaseId, chosen_release_id),
|
|
154
|
+
updated_at = datetime('now')
|
|
155
|
+
WHERE request_id = @requestId`,
|
|
156
|
+
);
|
|
157
|
+
this.stmtIncrementAttempts = db.prepare(
|
|
158
|
+
`UPDATE tracked_requests
|
|
159
|
+
SET attempts = attempts + 1, updated_at = datetime('now')
|
|
160
|
+
WHERE request_id = @requestId`,
|
|
161
|
+
);
|
|
162
|
+
this.stmtAddWatcher = db.prepare(
|
|
163
|
+
`UPDATE tracked_requests
|
|
164
|
+
SET watchers = @watchers, updated_at = datetime('now')
|
|
165
|
+
WHERE request_id = @requestId`,
|
|
166
|
+
);
|
|
167
|
+
this.stmtListByUser = db.prepare(
|
|
168
|
+
`SELECT * FROM tracked_requests
|
|
169
|
+
WHERE owner_user_id = @userId
|
|
170
|
+
OR watchers LIKE @pattern
|
|
171
|
+
ORDER BY created_at DESC`,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Trace log — prepared statements
|
|
175
|
+
this.stmtInsertTrace = db.prepare(
|
|
176
|
+
`INSERT INTO trace_log (trace_id, request_id, event_type, event_data, timestamp)
|
|
177
|
+
VALUES (@traceId, @requestId, @eventType, @eventData, @timestamp)`,
|
|
178
|
+
);
|
|
179
|
+
this.stmtGetIdsByTraceId = db.prepare(
|
|
180
|
+
`SELECT id FROM trace_log
|
|
181
|
+
WHERE trace_id = @traceId
|
|
182
|
+
ORDER BY id ASC`,
|
|
183
|
+
);
|
|
184
|
+
this.stmtGetTraceByRequestId = db.prepare(
|
|
185
|
+
`SELECT * FROM trace_log
|
|
186
|
+
WHERE request_id = @requestId
|
|
187
|
+
ORDER BY id ASC`,
|
|
188
|
+
);
|
|
189
|
+
this.stmtResetAttempts = db.prepare(
|
|
190
|
+
`UPDATE tracked_requests
|
|
191
|
+
SET attempts = 0, updated_at = datetime('now')
|
|
192
|
+
WHERE request_id = @requestId`,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
this.stmtResetForRetry = db.prepare(
|
|
196
|
+
`UPDATE tracked_requests
|
|
197
|
+
SET status = 'requested',
|
|
198
|
+
status_detail = @detail,
|
|
199
|
+
chosen_release_id = NULL,
|
|
200
|
+
attempts = 0,
|
|
201
|
+
updated_at = datetime('now')
|
|
202
|
+
WHERE request_id = @requestId`,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
this.stmtDeleteRequest = db.prepare(
|
|
206
|
+
`DELETE FROM tracked_requests WHERE request_id = @requestId`,
|
|
207
|
+
);
|
|
208
|
+
this.stmtDeleteTraceByRequestId = db.prepare(
|
|
209
|
+
`DELETE FROM trace_log WHERE request_id = @requestId`,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Subscribe to acquisition spine events
|
|
213
|
+
this.subscribe();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Public API ───────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
/** Retrieve a single tracked request by its domain id. */
|
|
219
|
+
getByRequestId(requestId: string): TrackedRequest | undefined {
|
|
220
|
+
const row = this.stmtGetByRequestId.get({ requestId }) as
|
|
221
|
+
| Record<string, unknown>
|
|
222
|
+
| undefined;
|
|
223
|
+
return row ? rowToRequest(row) : undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Find a non-failed tracked request with the given content identity.
|
|
228
|
+
* Used by request intake for de-duplication (DU1).
|
|
229
|
+
* Returns the most recently created match, or undefined.
|
|
230
|
+
*/
|
|
231
|
+
findByContentIdentity(contentIdentity: string): TrackedRequest | undefined {
|
|
232
|
+
const row = this.stmtFindByIdentity.get({ identity: contentIdentity }) as
|
|
233
|
+
| Record<string, unknown>
|
|
234
|
+
| undefined;
|
|
235
|
+
return row ? rowToRequest(row) : undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Insert a brand-new tracked request with status ``requested``.
|
|
240
|
+
* Called by request intake after de-dup check (DU3).
|
|
241
|
+
*/
|
|
242
|
+
insert(data: TrackedRequestData): void {
|
|
243
|
+
this.stmtInsert.run({
|
|
244
|
+
requestId: data.requestId,
|
|
245
|
+
contentTypeId: data.contentTypeId,
|
|
246
|
+
contentIdentity: data.contentIdentity,
|
|
247
|
+
item: JSON.stringify(data.item),
|
|
248
|
+
ownerUserId: data.ownerUserId,
|
|
249
|
+
libraryId: data.libraryId ?? null,
|
|
250
|
+
});
|
|
251
|
+
this.logger.debug("tracked request created", {
|
|
252
|
+
requestId: data.requestId,
|
|
253
|
+
contentIdentity: data.contentIdentity,
|
|
254
|
+
libraryId: data.libraryId,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Add a userId to the watchers array of an existing request.
|
|
260
|
+
* Called by request intake when re-attaching to an existing request (DU2).
|
|
261
|
+
* No-op if the user is already the owner or a watcher.
|
|
262
|
+
*/
|
|
263
|
+
addWatcher(requestId: string, userId: string): void {
|
|
264
|
+
const req = this.getByRequestId(requestId);
|
|
265
|
+
if (!req) {
|
|
266
|
+
this.logger.warn("addWatcher: unknown requestId", { requestId });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (req.ownerUserId === userId || req.watchers.includes(userId)) {
|
|
270
|
+
return; // already attached
|
|
271
|
+
}
|
|
272
|
+
const updated = [...req.watchers, userId];
|
|
273
|
+
this.stmtAddWatcher.run({
|
|
274
|
+
requestId,
|
|
275
|
+
watchers: JSON.stringify(updated),
|
|
276
|
+
});
|
|
277
|
+
this.logger.debug("watcher added", { requestId, userId });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Return the ordered trace history for a tracked request.
|
|
282
|
+
*
|
|
283
|
+
* Returns an array of TraceEntry, oldest first. Returns an empty array
|
|
284
|
+
* if the requestId is unknown or has no captured events.
|
|
285
|
+
*
|
|
286
|
+
* This is the data surface for the admin "activity" view (DBG2).
|
|
287
|
+
* The UI (Task 022) renders the returned data; this task only provides
|
|
288
|
+
* the query.
|
|
289
|
+
*/
|
|
290
|
+
getTraceLog(requestId: string): TraceEntry[] {
|
|
291
|
+
const rows = this.stmtGetTraceByRequestId.all({ requestId }) as Array<
|
|
292
|
+
Record<string, unknown>
|
|
293
|
+
>;
|
|
294
|
+
return rows.map((row) => ({
|
|
295
|
+
id: row.id as number,
|
|
296
|
+
traceId: row.trace_id as string,
|
|
297
|
+
requestId: row.request_id as string,
|
|
298
|
+
eventType: row.event_type as string,
|
|
299
|
+
eventData: JSON.parse(row.event_data as string) as Record<string, unknown>,
|
|
300
|
+
timestamp: row.timestamp as string,
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── Retry ───────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Reset a tracked request to 'requested' for a manual retry.
|
|
308
|
+
*
|
|
309
|
+
* Clears chosenReleaseId, resets attempts to 0, sets status to
|
|
310
|
+
* 'requested' with a detail message. This is called from the
|
|
311
|
+
* retry endpoint before re-emitting content.requested.
|
|
312
|
+
*
|
|
313
|
+
* Uses a transaction for atomicity.
|
|
314
|
+
* Emits item.updated so the UI reflects the change immediately.
|
|
315
|
+
*/
|
|
316
|
+
resetAndRetry(requestId: string): void {
|
|
317
|
+
const txn = this.db.transaction(() => {
|
|
318
|
+
// Reset status, clear chosenReleaseId, reset attempts in one statement
|
|
319
|
+
this.stmtResetForRetry.run({
|
|
320
|
+
requestId,
|
|
321
|
+
detail: "Retrying — restarted by user",
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
txn();
|
|
325
|
+
|
|
326
|
+
// Emit item.updated so the UI updates live
|
|
327
|
+
this.bus.emit("item.updated", {
|
|
328
|
+
requestId,
|
|
329
|
+
status: "requested",
|
|
330
|
+
detail: "Retrying — restarted by user",
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
this.logger.info("request reset for retry", { requestId });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── Delete ──────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Permanently delete a tracked request and its associated
|
|
340
|
+
* trace-log entries. This is unconditional (any status).
|
|
341
|
+
*
|
|
342
|
+
* Returns true if a row was deleted, false if the requestId
|
|
343
|
+
* did not exist.
|
|
344
|
+
*/
|
|
345
|
+
deleteRequest(requestId: string): boolean {
|
|
346
|
+
const txn = this.db.transaction(() => {
|
|
347
|
+
// 1. Delete trace log entries for this request
|
|
348
|
+
this.stmtDeleteTraceByRequestId.run({ requestId });
|
|
349
|
+
|
|
350
|
+
// 2. Delete the request itself
|
|
351
|
+
const info = this.stmtDeleteRequest.run({ requestId });
|
|
352
|
+
return info.changes > 0;
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const deleted = txn();
|
|
356
|
+
|
|
357
|
+
if (deleted) {
|
|
358
|
+
this.logger.info("request deleted", { requestId });
|
|
359
|
+
} else {
|
|
360
|
+
this.logger.warn("deleteRequest: requestId not found", { requestId });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Note: we deliberately do NOT emit item.updated here.
|
|
364
|
+
// Deletion is handled on the UI side by removing the item from
|
|
365
|
+
// the local store and/or navigating away.
|
|
366
|
+
|
|
367
|
+
return deleted;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Return ALL tracked requests (admin only).
|
|
372
|
+
*/
|
|
373
|
+
getLibraryPath(requestId: string): string | undefined {
|
|
374
|
+
const row = this.db
|
|
375
|
+
.prepare("SELECT library_path FROM library_paths WHERE request_id = ?")
|
|
376
|
+
.get(requestId) as { library_path: string } | undefined;
|
|
377
|
+
return row?.library_path;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
getAllRequests(): TrackedRequest[] {
|
|
381
|
+
const rows = this.db
|
|
382
|
+
.prepare("SELECT * FROM tracked_requests ORDER BY created_at DESC")
|
|
383
|
+
.all() as any[];
|
|
384
|
+
return rows.map((row: any) => rowToRequest(row));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* List tracked requests visible to a given user (owner or watcher).
|
|
389
|
+
* Used by the UI to show the request list (UI-ST2).
|
|
390
|
+
*/
|
|
391
|
+
listByUser(userId: string): TrackedRequest[] {
|
|
392
|
+
// The LIKE pattern searches for the userId inside the JSON watchers array.
|
|
393
|
+
// e.g. '%"admin"%' matches ["admin"] or ["alice","admin"].
|
|
394
|
+
const rows = this.stmtListByUser.all({
|
|
395
|
+
userId,
|
|
396
|
+
pattern: `%"${userId}"%`,
|
|
397
|
+
}) as Array<Record<string, unknown>>;
|
|
398
|
+
return rows.map(rowToRequest);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── Trace capture ────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Capture a spine event into the trace log. Stores the full event data
|
|
405
|
+
* (envelope fields + payload) as JSON, then prunes if the per-traceId
|
|
406
|
+
* count exceeds TRACE_HISTORY_LIMIT.
|
|
407
|
+
*
|
|
408
|
+
* This is a no-op if TRACE_HISTORY_LIMIT is 0 (trace capture disabled).
|
|
409
|
+
*/
|
|
410
|
+
private captureTraceEvent(event: StarrEvent, requestId: string): void {
|
|
411
|
+
if (TRACE_HISTORY_LIMIT === 0) return;
|
|
412
|
+
|
|
413
|
+
const eventData = {
|
|
414
|
+
eventId: event.id,
|
|
415
|
+
traceId: event.traceId,
|
|
416
|
+
correlationId: event.correlationId,
|
|
417
|
+
source: event.source,
|
|
418
|
+
schemaVersion: event.schemaVersion,
|
|
419
|
+
timestamp: event.timestamp,
|
|
420
|
+
payload: event.payload,
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// 1. Insert the new entry
|
|
424
|
+
this.stmtInsertTrace.run({
|
|
425
|
+
traceId: event.traceId,
|
|
426
|
+
requestId,
|
|
427
|
+
eventType: event.type,
|
|
428
|
+
eventData: JSON.stringify(eventData),
|
|
429
|
+
timestamp: event.timestamp,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// 2. Fetch all entry ids for this traceId, oldest first
|
|
433
|
+
const rows = this.stmtGetIdsByTraceId.all({
|
|
434
|
+
traceId: event.traceId,
|
|
435
|
+
}) as Array<{ id: number }>;
|
|
436
|
+
|
|
437
|
+
// 3. Prune if over the limit — delete the oldest excess entries
|
|
438
|
+
if (rows.length > TRACE_HISTORY_LIMIT) {
|
|
439
|
+
const excess = rows.length - TRACE_HISTORY_LIMIT;
|
|
440
|
+
const deleteIds = rows.slice(0, excess).map((r) => r.id);
|
|
441
|
+
|
|
442
|
+
// Use a one-off prepared statement for deletes
|
|
443
|
+
const deleteStmt = this.db.prepare("DELETE FROM trace_log WHERE id = ?");
|
|
444
|
+
for (const id of deleteIds) {
|
|
445
|
+
deleteStmt.run(id);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
this.logger.debug("trace pruned", {
|
|
449
|
+
traceId: event.traceId,
|
|
450
|
+
deleted: excess,
|
|
451
|
+
remaining: TRACE_HISTORY_LIMIT,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ── Event subscription ───────────────────────────────────
|
|
457
|
+
|
|
458
|
+
private subscribe(): void {
|
|
459
|
+
this.bus.on("release.found", this.onReleaseFound.bind(this));
|
|
460
|
+
this.bus.on("download.started", this.onDownloadStarted.bind(this));
|
|
461
|
+
this.bus.on("download.completed", this.onDownloadCompleted.bind(this));
|
|
462
|
+
this.bus.on("download.failed", this.onDownloadFailed.bind(this));
|
|
463
|
+
this.bus.on("request.failed", this.onRequestFailed.bind(this));
|
|
464
|
+
this.bus.on("import.started", this.onImportStarted.bind(this));
|
|
465
|
+
this.bus.on("import.completed", this.onImportCompleted.bind(this));
|
|
466
|
+
this.bus.on("import.failed", this.onImportFailed.bind(this));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private onReleaseFound(event: StarrEvent): void {
|
|
470
|
+
const { requestId } = event.payload as { requestId: string };
|
|
471
|
+
const req = this.getByRequestId(requestId);
|
|
472
|
+
if (!req) {
|
|
473
|
+
this.logger.warn("release.found for unknown requestId", { requestId });
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
this.captureTraceEvent(event, requestId);
|
|
477
|
+
// Transition: requested → searching (first candidate arrived)
|
|
478
|
+
if (req.status === "requested") {
|
|
479
|
+
this.updateStatus(requestId, "searching");
|
|
480
|
+
}
|
|
481
|
+
// If already searching (or beyond), just more candidates — no status change.
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private onDownloadStarted(event: StarrEvent): void {
|
|
485
|
+
const payload = event.payload as {
|
|
486
|
+
requestId: string;
|
|
487
|
+
release?: { id?: string; title?: string };
|
|
488
|
+
};
|
|
489
|
+
const req = this.getByRequestId(payload.requestId);
|
|
490
|
+
if (!req) {
|
|
491
|
+
this.logger.warn("download.started for unknown requestId", {
|
|
492
|
+
requestId: payload.requestId,
|
|
493
|
+
});
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
this.captureTraceEvent(event, payload.requestId);
|
|
497
|
+
|
|
498
|
+
// Increment the display-only attempts counter (SM4)
|
|
499
|
+
this.incrementAttempts(payload.requestId);
|
|
500
|
+
|
|
501
|
+
// Transition: searching → downloading (defensive: also from 'requested')
|
|
502
|
+
if (req.status === "searching" || req.status === "requested") {
|
|
503
|
+
const releaseId = payload.release?.id ?? payload.release?.title ?? undefined;
|
|
504
|
+
this.updateStatus(payload.requestId, "downloading", undefined, releaseId);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private onDownloadCompleted(event: StarrEvent): void {
|
|
509
|
+
const { requestId } = event.payload as { requestId: string };
|
|
510
|
+
const req = this.getByRequestId(requestId);
|
|
511
|
+
if (!req) {
|
|
512
|
+
this.logger.warn("download.completed for unknown requestId", { requestId });
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
this.captureTraceEvent(event, requestId);
|
|
516
|
+
// Terminal success — only if not already terminal
|
|
517
|
+
if (req.status !== "failed" && req.status !== "downloaded" && req.status !== "imported") {
|
|
518
|
+
this.updateStatus(requestId, "downloaded");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private onDownloadFailed(event: StarrEvent): void {
|
|
523
|
+
const { requestId } = event.payload as { requestId: string };
|
|
524
|
+
const req = this.getByRequestId(requestId);
|
|
525
|
+
if (!req) {
|
|
526
|
+
this.logger.warn("download.failed for unknown requestId", { requestId });
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
this.captureTraceEvent(event, requestId);
|
|
530
|
+
// Transient failure: go back to searching, NEVER to failed (SM1)
|
|
531
|
+
if (req.status === "downloading") {
|
|
532
|
+
this.updateStatus(
|
|
533
|
+
requestId,
|
|
534
|
+
"searching",
|
|
535
|
+
`retrying (attempt ${req.attempts})`,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private onImportStarted(event: StarrEvent): void {
|
|
541
|
+
const { requestId } = event.payload as { requestId: string };
|
|
542
|
+
const req = this.getByRequestId(requestId);
|
|
543
|
+
if (!req) return;
|
|
544
|
+
if (req.status === "downloaded") {
|
|
545
|
+
this.updateStatus(requestId, "importing");
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private onImportCompleted(event: StarrEvent): void {
|
|
550
|
+
const { requestId } = event.payload as { requestId: string };
|
|
551
|
+
const req = this.getByRequestId(requestId);
|
|
552
|
+
if (!req) return;
|
|
553
|
+
if (req.status === "importing") {
|
|
554
|
+
this.updateStatus(requestId, "imported");
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private onImportFailed(event: StarrEvent): void {
|
|
559
|
+
const payload = event.payload as { requestId: string; error?: string };
|
|
560
|
+
const req = this.getByRequestId(payload.requestId);
|
|
561
|
+
if (!req) return;
|
|
562
|
+
if (req.status === "importing") {
|
|
563
|
+
this.updateStatus(payload.requestId, "downloaded",
|
|
564
|
+
`Import failed: ${payload.error ?? "unknown error"}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private onRequestFailed(event: StarrEvent): void {
|
|
569
|
+
const payload = event.payload as { requestId: string; reason?: string };
|
|
570
|
+
const req = this.getByRequestId(payload.requestId);
|
|
571
|
+
if (!req) {
|
|
572
|
+
this.logger.warn("request.failed for unknown requestId", {
|
|
573
|
+
requestId: payload.requestId,
|
|
574
|
+
});
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
this.captureTraceEvent(event, payload.requestId);
|
|
578
|
+
// Terminal failure — the ONLY path to 'failed' (SM2, SP1a)
|
|
579
|
+
if (req.status !== "failed" && req.status !== "downloaded" && req.status !== "imported") {
|
|
580
|
+
this.updateStatus(payload.requestId, "failed", payload.reason);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ── Internal helpers ──────────────────────────────────────
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Update status, optional detail, and optional chosenReleaseId.
|
|
588
|
+
* Emits item.updated on every call (the caller guards transitions,
|
|
589
|
+
* so duplicate emissions are not a concern).
|
|
590
|
+
*/
|
|
591
|
+
private updateStatus(
|
|
592
|
+
requestId: string,
|
|
593
|
+
status: RequestStatus,
|
|
594
|
+
detail?: string,
|
|
595
|
+
chosenReleaseId?: string,
|
|
596
|
+
): void {
|
|
597
|
+
this.stmtUpdateStatus.run({
|
|
598
|
+
requestId,
|
|
599
|
+
status,
|
|
600
|
+
detail: detail ?? null,
|
|
601
|
+
chosenReleaseId: chosenReleaseId ?? null,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Emit item.updated — the single event the UI relies on (SP2, UI-ST1)
|
|
605
|
+
const payload: { requestId: string; status: string; detail?: string } = {
|
|
606
|
+
requestId,
|
|
607
|
+
status,
|
|
608
|
+
};
|
|
609
|
+
if (detail !== undefined) {
|
|
610
|
+
payload.detail = detail;
|
|
611
|
+
}
|
|
612
|
+
this.bus.emit("item.updated", payload);
|
|
613
|
+
|
|
614
|
+
this.logger.debug("status updated", { requestId, status, detail });
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Increment the attempts counter by 1. Called on download.started (SM4). */
|
|
618
|
+
private incrementAttempts(requestId: string): void {
|
|
619
|
+
this.stmtIncrementAttempts.run({ requestId });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import type { TraceEntry, StateStore } from "./state-store.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Module-level reference to the state store, set by the host bootstrap.
|
|
8
|
+
* This avoids circular imports between route modules and the host.
|
|
9
|
+
*/
|
|
10
|
+
let stateStore: StateStore | null = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Initialize the trace-store wrapper with a reference to the StateStore.
|
|
14
|
+
* Called from main.ts after the web server and state store are created.
|
|
15
|
+
*/
|
|
16
|
+
export function initTraceStore(store: StateStore): void {
|
|
17
|
+
stateStore = store;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Retrieve trace events for a given requestId.
|
|
22
|
+
* Returns an empty array if the state store isn't available or no events exist.
|
|
23
|
+
*/
|
|
24
|
+
export function getTraceEvents(requestId: string): TraceEntry[] {
|
|
25
|
+
if (!stateStore) return [];
|
|
26
|
+
return stateStore.getTraceLog(requestId);
|
|
27
|
+
}
|