| 116 | } |
| 117 | |
| 118 | export class ViewerServer { |
| 119 | private server: http.Server | null = null; |
| 120 | private readonly store: SqliteStore; |
| 121 | private readonly embedder: Embedder; |
| 122 | private readonly port: number; |
| 123 | private readonly log: Logger; |
| 124 | private readonly dataDir: string; |
| 125 | private readonly authFile: string; |
| 126 | private readonly auth: AuthState; |
| 127 | private readonly ctx?: PluginContext; |
| 128 | private readonly cookieName: string; |
| 129 | private readonly defaultHubPort: number; |
| 130 | private readonly branding?: ViewerBranding; |
| 131 | |
| 132 | private static readonly SESSION_TTL = 24 * 60 * 60 * 1000; |
| 133 | private static readonly PLUGIN_VERSION: string = (() => { |
| 134 | try { |
| 135 | const pkgPath = path.resolve(__dirname, "../../package.json"); |
| 136 | return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version ?? "unknown"; |
| 137 | } catch { |
| 138 | return "unknown"; |
| 139 | } |
| 140 | })(); |
| 141 | private resetToken: string; |
| 142 | private migrationRunning = false; |
| 143 | private migrationAbort = false; |
| 144 | private migrationState: MigrationStateSnapshot = createInitialMigrationState(); |
| 145 | private migrationSSEClients: http.ServerResponse[] = []; |
| 146 | |
| 147 | private ppRunning = false; |
| 148 | private ppAbort = false; |
| 149 | private ppState: { running: boolean; done: boolean; stopped: boolean; processed: number; total: number; tasksCreated: number; skillsCreated: number; errors: number; skippedSessions: number; totalSessions: number } = |
| 150 | { running: false, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0, skippedSessions: 0, totalSessions: 0 }; |
| 151 | private ppSSEClients: http.ServerResponse[] = []; |
| 152 | |
| 153 | private notifSSEClients: http.ServerResponse[] = []; |
| 154 | private notifPollTimer?: ReturnType<typeof setInterval>; |
| 155 | private lastKnownNotifCount = 0; |
| 156 | private hubHeartbeatTimer?: ReturnType<typeof setInterval>; |
| 157 | private static readonly HUB_HEARTBEAT_INTERVAL_MS = 45_000; |
| 158 | private static readonly STALE_TASK_TIMEOUT_MS = 4 * 60 * 60 * 1000; |
| 159 | private staleFinalizeRunning = false; |
| 160 | |
| 161 | constructor(opts: ViewerServerOptions) { |
| 162 | this.store = opts.store; |
| 163 | this.embedder = opts.embedder; |
| 164 | this.port = opts.port; |
| 165 | this.log = opts.log; |
| 166 | this.dataDir = opts.dataDir; |
| 167 | this.ctx = opts.ctx; |
| 168 | this.authFile = path.join(opts.dataDir, "viewer-auth.json"); |
| 169 | this.auth = { passwordHash: null, sessions: new Map() }; |
| 170 | this.cookieName = `memos_token_${opts.port}`; |
| 171 | this.defaultHubPort = opts.defaultHubPort ?? 18800; |
| 172 | this.branding = opts.branding; |
| 173 | this.resetToken = crypto.randomBytes(16).toString("hex"); |
| 174 | this.loadAuth(); |
| 175 | } |
nothing calls this directly
no test coverage detected