code and doc cleanup
This commit is contained in:
+3
-21
@@ -1,5 +1,5 @@
|
||||
# meta-sort standalone Docker image
|
||||
# Includes: Redis, nginx, rclone, FFmpeg, Node.js backend, React UI, and Editor
|
||||
# Includes: Redis, nginx, rclone, FFmpeg, Node.js backend, and React UI
|
||||
#
|
||||
# Build standalone:
|
||||
# docker build -t meta-sort .
|
||||
@@ -24,20 +24,7 @@ RUN npm install
|
||||
COPY packages/meta-sort-ui/ ./
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Build Editor
|
||||
FROM node:21-alpine AS editor-builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy editor package and install
|
||||
COPY packages/meta-sort-editor/package.json ./
|
||||
RUN npm install
|
||||
|
||||
# Copy editor source and build
|
||||
COPY packages/meta-sort-editor/ ./
|
||||
RUN npm run build
|
||||
|
||||
# Stage 3: Build Backend using pnpm workspace
|
||||
# Stage 2: Build Backend using pnpm workspace
|
||||
FROM node:21-alpine AS backend-builder
|
||||
|
||||
# Install pnpm
|
||||
@@ -67,7 +54,7 @@ RUN pnpm --filter="@worph/async-utils" build || true
|
||||
# Build the core package
|
||||
RUN pnpm --filter="@meta-sort/core" build
|
||||
|
||||
# Stage 4: Runtime
|
||||
# Stage 3: Runtime
|
||||
FROM ubuntu:22.04
|
||||
|
||||
# Container registry metadata
|
||||
@@ -105,7 +92,6 @@ RUN curl https://rclone.org/install.sh | bash
|
||||
RUN mkdir -p \
|
||||
/app/backend \
|
||||
/app/ui \
|
||||
/app/editor \
|
||||
/data/watch \
|
||||
/data/cache \
|
||||
/data/redis \
|
||||
@@ -122,10 +108,6 @@ RUN mkdir -p \
|
||||
COPY --from=ui-builder /build/dist /app/ui
|
||||
RUN chmod -R 755 /app/ui
|
||||
|
||||
# Copy built Editor
|
||||
COPY --from=editor-builder /build/dist /app/editor
|
||||
RUN chmod -R 755 /app/editor
|
||||
|
||||
# Copy built backend with all dependencies
|
||||
COPY --from=backend-builder /build/packages/meta-sort-core/dist /app/backend/dist
|
||||
COPY --from=backend-builder /build/packages/meta-sort-core/package.json /app/backend/
|
||||
|
||||
@@ -361,8 +361,7 @@ async function discoverService(name: string): Promise<ServiceInfo | null> {
|
||||
│ │ ├── Watches /files/* for new media files ││
|
||||
│ │ ├── Extracts metadata → writes to KV (Redis in /meta-core/db) ││
|
||||
│ │ ├── Manages remote mounts INTO /files (NFS, SMB, rclone) ││
|
||||
│ │ ├── Leader election (spawns Redis if leader) ││
|
||||
│ │ └── Serves monitoring UI (meta-sort-editor) ││
|
||||
│ │ └── Leader election (spawns Redis if leader) ││
|
||||
│ │ ││
|
||||
│ │ [2] meta-fuse PROCESS-READ ││
|
||||
│ │ ├── Reads metadata from KV (Redis) ││
|
||||
@@ -665,9 +664,6 @@ meta-sort/
|
||||
│ │ │ └── index.ts # Entry point
|
||||
│ │ └── package.json
|
||||
│ │
|
||||
│ ├── meta-sort-editor/ # Metadata editor UI (@meta-sort/editor)
|
||||
│ │ └── ... # React/Vite app
|
||||
│ │
|
||||
│ ├── meta-sort-ui/ # Monitoring dashboard (@meta-sort/ui)
|
||||
│ │ └── ... # React/Vite app
|
||||
│ │
|
||||
|
||||
@@ -44,18 +44,6 @@ http {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Editor UI - SPA with base path /editor/
|
||||
location /editor/ {
|
||||
alias /app/editor/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /editor/index.html;
|
||||
}
|
||||
|
||||
# Redirect /editor to /editor/
|
||||
location = /editor {
|
||||
return 301 /editor/;
|
||||
}
|
||||
|
||||
# API routes
|
||||
location /api/ {
|
||||
proxy_pass http://api;
|
||||
|
||||
@@ -13,6 +13,8 @@ import { UnifiedProcessingStateManager } from '../logic/UnifiedProcessingStateMa
|
||||
import { DuplicateResult } from '../logic/DuplicateFinder.js';
|
||||
import type { IKVClient } from '../kv/IKVClient.js';
|
||||
import type { KVManager } from '../kv/KVManager.js';
|
||||
import type { ExtendedProcessingSnapshot, IKVClientWithPubSub } from '../types/ExtendedInterfaces.js';
|
||||
import { getErrorMessage } from '../types/ExtendedInterfaces.js';
|
||||
import { config } from '../config/EnvConfig.js';
|
||||
import type { PluginManager } from '../plugin-engine/PluginManager.js';
|
||||
import type { TaskScheduler } from '../plugin-engine/TaskScheduler.js';
|
||||
@@ -631,12 +633,12 @@ export class UnifiedAPIServer {
|
||||
// Get snapshot from state manager
|
||||
// Note: VFS-related features have been moved to meta-fuse
|
||||
// totalDone now uses state manager's count of fully processed files
|
||||
const snapshot = this.unifiedStateManager!.getSnapshot(0, this.fastQueueConcurrency, this.backgroundQueueConcurrency);
|
||||
const snapshot = this.unifiedStateManager!.getSnapshot(0, this.fastQueueConcurrency, this.backgroundQueueConcurrency) as ExtendedProcessingSnapshot;
|
||||
|
||||
// Add queue status if available
|
||||
if (this.getQueueStatus) {
|
||||
const queueStatus = this.getQueueStatus();
|
||||
(snapshot as any).queueStatus = queueStatus;
|
||||
snapshot.queueStatus = queueStatus;
|
||||
|
||||
// Add calculated fields that the UI needs (make API self-describing)
|
||||
// Pipeline: validation (preProcess) → lightProcessing (fast) → hashProcessing (background)
|
||||
@@ -659,7 +661,7 @@ export class UnifiedAPIServer {
|
||||
// Get gate status if container scheduler is available
|
||||
const gateStatus = this.containerPluginScheduler?.getGateStatus();
|
||||
|
||||
(snapshot as any).computed = {
|
||||
snapshot.computed = {
|
||||
// Pre-process queue (validation - quick extension checks)
|
||||
preProcessRunning,
|
||||
preProcessPending,
|
||||
@@ -693,7 +695,7 @@ export class UnifiedAPIServer {
|
||||
|
||||
// Add failed files count from metrics
|
||||
const metrics = performanceMetrics.getMetrics();
|
||||
(snapshot as any).totalFailed = metrics.totalFailedFiles || 0;
|
||||
snapshot.totalFailed = metrics.totalFailedFiles || 0;
|
||||
|
||||
return snapshot;
|
||||
});
|
||||
@@ -722,7 +724,7 @@ export class UnifiedAPIServer {
|
||||
// Trigger scan callback if available to pick up the file
|
||||
if (this.triggerScanCallback) {
|
||||
// Don't await - just trigger async
|
||||
this.triggerScanCallback().catch(() => {});
|
||||
this.triggerScanCallback().catch(err => console.warn('[API] Scan trigger failed during retry:', getErrorMessage(err)));
|
||||
}
|
||||
|
||||
return { status: 'ok', message: `File ${filePath} queued for retry` };
|
||||
@@ -744,7 +746,7 @@ export class UnifiedAPIServer {
|
||||
|
||||
// Trigger scan callback if available
|
||||
if (this.triggerScanCallback) {
|
||||
this.triggerScanCallback().catch(() => {});
|
||||
this.triggerScanCallback().catch(err => console.warn('[API] Scan trigger failed during retry-all:', getErrorMessage(err)));
|
||||
}
|
||||
|
||||
return { status: 'ok', message: `${queued} files queued for retry` };
|
||||
@@ -838,7 +840,7 @@ export class UnifiedAPIServer {
|
||||
this.app.get('/api/stats', async (request, reply) => {
|
||||
try {
|
||||
const hashIds = await this.kvClient!.getAllHashIds();
|
||||
const redis = (this.kvClient as any).getRedisClient?.();
|
||||
const redis = (this.kvClient as IKVClientWithPubSub).getRedisClient?.() as { info: (section: string) => Promise<string> } | undefined;
|
||||
|
||||
let memoryUsage = 'N/A';
|
||||
let memoryUsageBytes = 0;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { EventEmitter } from 'events';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { HashMeta } from '@metazla/meta-interface';
|
||||
import type { ExtendedHashMeta } from '../types/ExtendedInterfaces.js';
|
||||
import {
|
||||
IFuseAPI,
|
||||
VirtualDirectory,
|
||||
@@ -149,9 +150,10 @@ export class VirtualFileSystem extends EventEmitter implements IFuseAPI {
|
||||
let mtime: Date;
|
||||
let ctime: Date;
|
||||
|
||||
if (metadata && (metadata as any).sizeByte) {
|
||||
const extMeta = metadata as ExtendedHashMeta | undefined;
|
||||
if (extMeta && extMeta.sizeByte) {
|
||||
// Use cached metadata (fast path - no disk I/O)
|
||||
size = parseInt((metadata as any).sizeByte, 10);
|
||||
size = parseInt(String(extMeta.sizeByte), 10);
|
||||
// Use current time as placeholder for mtime/ctime (will be corrected on next scan)
|
||||
mtime = new Date();
|
||||
ctime = new Date();
|
||||
@@ -458,7 +460,8 @@ export class VirtualFileSystem extends EventEmitter implements IFuseAPI {
|
||||
// Only count actual media files (not directories, not metadata files)
|
||||
if (node.type === 'file' && node.sourcePath && node.metadata) {
|
||||
// Check if file has completed processing (both light and hash phases)
|
||||
if ((node.metadata as any).processingStatus === 'complete') {
|
||||
const extMeta = node.metadata as ExtendedHashMeta;
|
||||
if (extMeta.processingStatus === 'complete') {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ class MockProcessingStateManager {
|
||||
}
|
||||
|
||||
// Helpers for tests
|
||||
addPending(path: string): void {
|
||||
addDiscovered(path: string): void {
|
||||
this.pending.add(path);
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ describe('UnifiedAPIServer', function() {
|
||||
});
|
||||
|
||||
mockStateManager.addDone('/files/test/Test Movie.mp4');
|
||||
mockStateManager.addPending('/files/test/pending.mp4');
|
||||
mockStateManager.addDiscovered('/files/test/pending.mp4');
|
||||
|
||||
// Create VFS (no params needed - uses defaults)
|
||||
const vfs = new VirtualFileSystem();
|
||||
@@ -428,130 +428,6 @@ describe('UnifiedAPIServer', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Metadata API (File Browser)
|
||||
// =========================================================================
|
||||
|
||||
describe('Metadata API (/api/metadata/*)', function() {
|
||||
it('GET /api/metadata/hash-ids returns list of hash IDs', async function() {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/metadata/hash-ids'
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.equal(200);
|
||||
const body = JSON.parse(response.payload);
|
||||
expect(body).to.have.property('hashIds');
|
||||
expect(Array.isArray(body.hashIds)).to.be.true;
|
||||
expect(body.hashIds).to.include('testhash001');
|
||||
});
|
||||
|
||||
it('GET /api/metadata/list returns paginated file list', async function() {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/metadata/list?limit=10&offset=0'
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.equal(200);
|
||||
const body = JSON.parse(response.payload);
|
||||
expect(body).to.have.property('files');
|
||||
expect(body).to.have.property('total');
|
||||
expect(Array.isArray(body.files)).to.be.true;
|
||||
});
|
||||
|
||||
it('GET /api/metadata/:hashId returns file metadata', async function() {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/metadata/testhash001'
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.equal(200);
|
||||
const body = JSON.parse(response.payload);
|
||||
// API returns metadata flat (not wrapped in { hashId, metadata })
|
||||
expect(body).to.have.property('fileName', 'Test Movie.mp4');
|
||||
});
|
||||
|
||||
it('GET /api/metadata/:hashId returns 404 for unknown hash', async function() {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/metadata/unknownhash'
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.equal(404);
|
||||
});
|
||||
|
||||
it('GET /api/metadata/:hashId/property returns specific property', async function() {
|
||||
// The API uses getProperty with key format /file/{hashId}/{property}
|
||||
// We need to seed the property in the rawStore
|
||||
await mockKV.setProperty('/file/testhash001/videoType', 'movie');
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/metadata/testhash001/property?property=videoType'
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.equal(200);
|
||||
const body = JSON.parse(response.payload);
|
||||
expect(body).to.have.property('value', 'movie');
|
||||
});
|
||||
|
||||
it('PUT /api/metadata/:hashId updates metadata', async function() {
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/metadata/testhash001',
|
||||
payload: {
|
||||
customField: 'customValue'
|
||||
}
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.equal(200);
|
||||
|
||||
// Verify the update
|
||||
const verify = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/metadata/testhash001'
|
||||
});
|
||||
const body = JSON.parse(verify.payload);
|
||||
// Response is flat metadata
|
||||
expect(body).to.have.property('customField', 'customValue');
|
||||
});
|
||||
|
||||
it('DELETE /api/metadata/:hashId deletes file metadata', async function() {
|
||||
// First create a test entry
|
||||
mockKV.seedData('deleteme', { fileName: 'delete.mp4' });
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/metadata/deleteme'
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.equal(200);
|
||||
|
||||
// Verify deletion
|
||||
const verify = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/metadata/deleteme'
|
||||
});
|
||||
expect(verify.statusCode).to.equal(404);
|
||||
});
|
||||
|
||||
it('POST /api/metadata/search searches files', async function() {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/metadata/search',
|
||||
payload: {
|
||||
property: 'videoType',
|
||||
propertyValue: 'movie'
|
||||
}
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.equal(200);
|
||||
const body = JSON.parse(response.payload);
|
||||
expect(body).to.have.property('results');
|
||||
expect(Array.isArray(body.results)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Stats API (Dashboard)
|
||||
// =========================================================================
|
||||
@@ -694,35 +570,6 @@ describe('UnifiedAPIServer', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// KV Browser API (Editor)
|
||||
// =========================================================================
|
||||
|
||||
describe('KV Browser API (/api/kv/*)', function() {
|
||||
it('GET /api/kv/info returns database info', async function() {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/kv/info'
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.equal(200);
|
||||
const body = JSON.parse(response.payload);
|
||||
expect(body).to.have.property('fileCount');
|
||||
});
|
||||
|
||||
it('GET /api/kv/keys returns paginated keys', async function() {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/kv/keys?cursor=0&count=50'
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.equal(200);
|
||||
const body = JSON.parse(response.payload);
|
||||
expect(body).to.have.property('keys');
|
||||
expect(Array.isArray(body.keys)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Plugins API (Plugins Page)
|
||||
// =========================================================================
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
} from './types.js';
|
||||
import type { TaskQueueType } from '../plugin-engine/types.js';
|
||||
import type { IKVClient } from '../kv/IKVClient.js';
|
||||
import { hasPublish, type ExtendedContainerTask } from '../types/ExtendedInterfaces.js';
|
||||
import { config } from '../config/EnvConfig.js';
|
||||
|
||||
/**
|
||||
@@ -218,7 +219,7 @@ export class ContainerPluginScheduler extends EventEmitter {
|
||||
* to be notified when specific plugins complete for a file.
|
||||
*/
|
||||
private async publishPluginComplete(fileHash: string, pluginId: string, filePath: string): Promise<void> {
|
||||
if (!this.kvClient) {
|
||||
if (!this.kvClient || !hasPublish(this.kvClient)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -229,7 +230,7 @@ export class ContainerPluginScheduler extends EventEmitter {
|
||||
filePath,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
await (this.kvClient as any).publish('meta-sort:plugin:complete', message);
|
||||
await this.kvClient.publish('meta-sort:plugin:complete', message);
|
||||
} catch (error) {
|
||||
// Silent fail - pub/sub is optional
|
||||
}
|
||||
@@ -867,7 +868,7 @@ export class ContainerPluginScheduler extends EventEmitter {
|
||||
);
|
||||
|
||||
// Store existing metadata in task for dispatch
|
||||
(task as any).existingMeta = existingMeta;
|
||||
(task as ExtendedContainerTask).existingMeta = existingMeta;
|
||||
|
||||
this.enqueueTask(task);
|
||||
}
|
||||
|
||||
@@ -180,7 +180,6 @@ export class JellyfinNfo {
|
||||
existingData[targetRoot] = root;
|
||||
|
||||
if (hasChanged) {
|
||||
//const builder = new Builder({ headless: true, renderOpts: { 'pretty': true } });
|
||||
const builder = new Builder();
|
||||
// Convert the updated object back to XML
|
||||
try {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { promises as fs } from 'fs';
|
||||
import { hostname } from 'os';
|
||||
import { join } from 'path';
|
||||
import type { ServiceInfo } from './IKVClient.js';
|
||||
import { isNodeError } from '../types/ExtendedInterfaces.js';
|
||||
|
||||
interface ServiceDiscoveryConfig {
|
||||
/** Path to META_CORE_VOLUME (e.g., /meta-core) */
|
||||
@@ -126,7 +127,7 @@ export class ServiceDiscovery {
|
||||
await fs.writeFile(this.serviceFilePath, JSON.stringify(info, null, 2));
|
||||
} catch (error) {
|
||||
// If file was deleted, re-register
|
||||
if ((error as any).code === 'ENOENT') {
|
||||
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||
await this.register();
|
||||
await this.updateStatus('running');
|
||||
} else {
|
||||
@@ -200,7 +201,7 @@ export class ServiceDiscovery {
|
||||
|
||||
return service;
|
||||
} catch (error) {
|
||||
if ((error as any).code === 'ENOENT') {
|
||||
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||
return null; // Service not registered
|
||||
}
|
||||
throw error;
|
||||
@@ -228,7 +229,7 @@ export class ServiceDiscovery {
|
||||
|
||||
return services;
|
||||
} catch (error) {
|
||||
if ((error as any).code === 'ENOENT') {
|
||||
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||
return []; // Services directory doesn't exist yet
|
||||
}
|
||||
throw error;
|
||||
|
||||
@@ -82,13 +82,6 @@ export class UnifiedProcessingStateManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use addDiscovered instead
|
||||
*/
|
||||
addPending(filePath: string): void {
|
||||
this.addDiscovered(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move file from discovered to light processing
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,7 @@ import {VirtualFileSystem} from "../api/VirtualFileSystem.js";
|
||||
import {performanceMetrics} from "../metrics/PerformanceMetrics.js";
|
||||
import {UnifiedProcessingStateManager} from "./UnifiedProcessingStateManager.js";
|
||||
import type {IKVClient} from "../kv/IKVClient.js";
|
||||
import { hasPublish, type ExtendedFileAnalyzer, type ExtendedHashMeta } from "../types/ExtendedInterfaces.js";
|
||||
import PQueue from "p-queue";
|
||||
import os from 'os';
|
||||
import { TaskScheduler, createTaskScheduler, MetadataNodeKVStore } from "../plugin-engine/index.js";
|
||||
@@ -29,7 +30,6 @@ export class WatchedFileProcessor implements FileProcessorInterface {
|
||||
metaDataToFolderStruct = new MetaDataToFolderStruct(renamingRule);
|
||||
virtualFileSystem: VirtualFileSystem;
|
||||
unifiedStateManager = new UnifiedProcessingStateManager();
|
||||
//fileProcessor:FileAnalyzerInterface = new FileProcessor();//TODO only use for debug
|
||||
fileProcessor:FileAnalyzerInterface = new FileProcessorPiscina();
|
||||
duplicateFinder = new DuplicateFinder();
|
||||
duplicateResult: DuplicateResult | null = null;
|
||||
@@ -249,7 +249,7 @@ export class WatchedFileProcessor implements FileProcessorInterface {
|
||||
*/
|
||||
private async publishPluginComplete(fileHash: string, pluginId: string, filePath: string): Promise<void> {
|
||||
const kvClient = this.getKVClient();
|
||||
if (!kvClient || typeof (kvClient as any).publish !== 'function') {
|
||||
if (!kvClient || !hasPublish(kvClient)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ export class WatchedFileProcessor implements FileProcessorInterface {
|
||||
filePath,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
await (kvClient as any).publish('meta-sort:plugin:complete', message);
|
||||
await kvClient.publish('meta-sort:plugin:complete', message);
|
||||
} catch (error) {
|
||||
// Silent fail - pub/sub is optional
|
||||
}
|
||||
@@ -298,11 +298,7 @@ export class WatchedFileProcessor implements FileProcessorInterface {
|
||||
// Phase 1: Pre-process (midhash256 computation)
|
||||
// Phase 2: Plugin execution via TaskScheduler (fast/background queues)
|
||||
|
||||
// Debug: log queue state before adding
|
||||
console.log(`[DEBUG queueFile] Before add: size=${this.preProcessQueue.size}, pending=${this.preProcessQueue.pending}`);
|
||||
|
||||
await this.preProcessQueue.add(async () => {
|
||||
console.log(`[DEBUG queueFile] Task started: size=${this.preProcessQueue.size}, pending=${this.preProcessQueue.pending}`);
|
||||
try {
|
||||
// STATE: Start LIGHT processing
|
||||
if (this.unifiedStateManager) {
|
||||
@@ -314,7 +310,8 @@ export class WatchedFileProcessor implements FileProcessorInterface {
|
||||
const { computeMidHash256 } = await import('@metazla/meta-hash');
|
||||
|
||||
// Check cache first
|
||||
const indexLine = (this.fileProcessor as any).indexManager?.getCidForFile(
|
||||
const extAnalyzer = this.fileProcessor as ExtendedFileAnalyzer;
|
||||
const indexLine = extAnalyzer.indexManager?.getCidForFile(
|
||||
filePath,
|
||||
fileStats.size,
|
||||
fileStats.mtime.toISOString()
|
||||
@@ -331,7 +328,7 @@ export class WatchedFileProcessor implements FileProcessorInterface {
|
||||
performanceMetrics.recordCacheMiss('midhash256');
|
||||
|
||||
// Store in cache for future lookups
|
||||
(this.fileProcessor as any).indexManager?.addFileCid(
|
||||
extAnalyzer.indexManager?.addFileCid(
|
||||
filePath,
|
||||
fileStats.size,
|
||||
fileStats.mtime.toISOString(),
|
||||
@@ -570,7 +567,7 @@ export class WatchedFileProcessor implements FileProcessorInterface {
|
||||
for (const metadata of metadataList) {
|
||||
if (metadata && metadata.filePath) {
|
||||
// Mark as unverified - discovery will validate later
|
||||
(metadata as any)._lastVerified = 0;
|
||||
(metadata as ExtendedHashMeta)._lastVerified = 0;
|
||||
this.fileProcessor.getDatabase().set(metadata.filePath, metadata);
|
||||
loadedCount++;
|
||||
}
|
||||
|
||||
@@ -61,16 +61,12 @@ export class FileProcessor implements FileAnalyzerInterface{
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// Hash logic compute and get the hash - use cache etc...
|
||||
// --------------------------------------------
|
||||
const result = await this.fileProcessor.processFile(filePath,hash);
|
||||
//const result = await this.fileProcessor.processFile(filePath,params.hash);
|
||||
|
||||
//save the metadata to the cache or disk
|
||||
this.database.set(filePath, result.metadata);
|
||||
this.indexManager.addFileCid(filePath, stats.size, stats.mtime.toISOString(), result.metadata);
|
||||
//console.log(`File [${current}/${queueSize}] ${filePath} took ${result.time}ms`);
|
||||
} catch (e) {
|
||||
console.error(`Error processing file [No Thread] ${filePath}:`, e);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,6 @@ export class FileProcessorPiscina implements FileAnalyzerInterface{
|
||||
|
||||
this.piscina = new Piscina({
|
||||
maxThreads,
|
||||
//maxThreads: 1,//TODO note for DEBUG must be set to 1 to avoid memory issues
|
||||
filename:workerUrl
|
||||
});
|
||||
|
||||
|
||||
@@ -301,7 +301,11 @@ export class PluginCacheImpl implements PluginCache {
|
||||
try {
|
||||
const files = await fs.readdir(this.cacheDir);
|
||||
await Promise.all(
|
||||
files.map(file => fs.unlink(path.join(this.cacheDir, file)).catch(() => {}))
|
||||
files.map(file =>
|
||||
fs.unlink(path.join(this.cacheDir, file)).catch(err =>
|
||||
console.warn('[PluginCache] Failed to delete cache file:', file, err instanceof Error ? err.message : err)
|
||||
)
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* Extended Interfaces
|
||||
*
|
||||
* Type definitions that extend base interfaces with runtime-specific properties
|
||||
* that are not part of the core interface contracts. These extensions provide
|
||||
* type safety for implementation-specific features.
|
||||
*/
|
||||
|
||||
import type { HashMeta } from '@metazla/meta-interface';
|
||||
import type { IKVClient } from '../kv/IKVClient.js';
|
||||
import type { FileAnalyzerInterface } from '../logic/fileProcessor/FileAnalyzerInterface.js';
|
||||
import type { ContainerTask } from '../container-plugins/types.js';
|
||||
import type { UnifiedProcessingSnapshot } from '../logic/UnifiedProcessingStateManager.js';
|
||||
|
||||
// =============================================================================
|
||||
// IKVClient Extensions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* IKVClient with Redis pub/sub capabilities
|
||||
*
|
||||
* The base IKVClient interface doesn't include pub/sub methods since they're
|
||||
* Redis-specific. This extension provides type safety for Redis-based implementations.
|
||||
*/
|
||||
export interface IKVClientWithPubSub extends IKVClient {
|
||||
/**
|
||||
* Publish a message to a Redis channel
|
||||
*/
|
||||
publish(channel: string, message: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the underlying Redis client (implementation-specific)
|
||||
*/
|
||||
getRedisClient?(): unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a KVClient supports pub/sub
|
||||
*/
|
||||
export function hasPublish(client: IKVClient): client is IKVClientWithPubSub {
|
||||
return typeof (client as IKVClientWithPubSub).publish === 'function';
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HashMeta Extensions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Extended HashMeta with runtime fields added during processing
|
||||
*
|
||||
* These fields are added by the processing pipeline but aren't part of
|
||||
* the core HashMeta interface defined in meta-interface.
|
||||
*/
|
||||
export interface ExtendedHashMeta extends HashMeta {
|
||||
/** File size in bytes (string from plugins, number internally) */
|
||||
sizeByte?: string | number;
|
||||
|
||||
/** Current processing status */
|
||||
processingStatus?: 'pending' | 'processing' | 'complete' | 'failed';
|
||||
|
||||
/** Source file path */
|
||||
filePath?: string;
|
||||
|
||||
/** Last verification timestamp (0 = unverified, used during VFS rebuild) */
|
||||
_lastVerified?: number;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FileAnalyzerInterface Extensions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Index manager interface for file CID caching
|
||||
*/
|
||||
export interface IndexManager {
|
||||
/**
|
||||
* Get cached CID for a file based on path, size, and mtime
|
||||
*/
|
||||
getCidForFile(
|
||||
path: string,
|
||||
size: number,
|
||||
mtime: string
|
||||
): { cid_midhash256?: string } | undefined;
|
||||
|
||||
/**
|
||||
* Add a file CID to the cache
|
||||
*/
|
||||
addFileCid(
|
||||
path: string,
|
||||
size: number,
|
||||
mtime: string,
|
||||
cid: { cid_midhash256: string }
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended FileAnalyzerInterface with index manager
|
||||
*
|
||||
* FileProcessorPiscina exposes an indexManager property for CID caching
|
||||
* that isn't part of the base interface.
|
||||
*/
|
||||
export interface ExtendedFileAnalyzer extends FileAnalyzerInterface {
|
||||
/** Index manager for CID caching */
|
||||
indexManager?: IndexManager;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ContainerTask Extensions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Extended ContainerTask with runtime metadata
|
||||
*
|
||||
* During task dispatch, existing metadata is attached to the task for
|
||||
* the plugin to access.
|
||||
*/
|
||||
export interface ExtendedContainerTask extends ContainerTask {
|
||||
/** Existing metadata at time of dispatch */
|
||||
existingMeta?: Record<string, string>;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UnifiedProcessingSnapshot Extensions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Queue status structure from getQueueStatus()
|
||||
*/
|
||||
export interface QueueStatus {
|
||||
preProcessQueue: {
|
||||
size: number;
|
||||
pending: number;
|
||||
isPaused: boolean;
|
||||
};
|
||||
fastQueue: {
|
||||
size: number;
|
||||
pending: number;
|
||||
running: number;
|
||||
isPaused: boolean;
|
||||
};
|
||||
backgroundQueue: {
|
||||
size: number;
|
||||
pending: number;
|
||||
running: number;
|
||||
isPaused: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed processing status fields for UI
|
||||
*/
|
||||
export interface ComputedProcessingStatus {
|
||||
preProcessRunning: number;
|
||||
preProcessPending: number;
|
||||
preProcessPaused: boolean;
|
||||
|
||||
fastQueueRunning: number;
|
||||
fastQueuePending: number;
|
||||
fastQueueTotal: number;
|
||||
fastQueuePaused: boolean;
|
||||
|
||||
backgroundQueueRunning: number;
|
||||
backgroundQueuePending: number;
|
||||
backgroundQueueTotal: number;
|
||||
backgroundQueuePaused: boolean;
|
||||
|
||||
gateOpen: boolean;
|
||||
gateStatus: unknown;
|
||||
pipelinePaused: boolean;
|
||||
|
||||
// Legacy compatibility fields
|
||||
actualRunningHashWorkers: number;
|
||||
actualRunningLightWorkers: number;
|
||||
trueHashQueueSize: number;
|
||||
hashQueuePaused: boolean;
|
||||
lightQueuePaused: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended processing snapshot with queue status and computed fields
|
||||
*
|
||||
* The API adds queue status and computed fields to the base snapshot
|
||||
* for UI consumption.
|
||||
*/
|
||||
export interface ExtendedProcessingSnapshot extends UnifiedProcessingSnapshot {
|
||||
/** Queue status from pipeline */
|
||||
queueStatus?: QueueStatus;
|
||||
|
||||
/** Computed status fields for UI */
|
||||
computed?: ComputedProcessingStatus;
|
||||
|
||||
/** Total number of failed files */
|
||||
totalFailed?: number;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Error Type Guards
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Type guard for Node.js filesystem errors
|
||||
*
|
||||
* Use this instead of `(error as any).code` to safely check error codes.
|
||||
*/
|
||||
export function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
||||
return error instanceof Error && 'code' in error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error code if present, undefined otherwise
|
||||
*/
|
||||
export function getErrorCode(error: unknown): string | undefined {
|
||||
if (isNodeError(error)) {
|
||||
return error.code;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error message safely from unknown error type
|
||||
*/
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
@@ -44,17 +44,6 @@ function Welcome() {
|
||||
<h3>Plugins</h3>
|
||||
<p>Manage metadata plugins</p>
|
||||
</Link>
|
||||
|
||||
<a href="/editor/" className="link-card">
|
||||
<div className="link-icon">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Editor</h3>
|
||||
<p>Edit file metadata</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Generated
-28
@@ -132,34 +132,6 @@ importers:
|
||||
specifier: ^5.3.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/meta-sort-editor:
|
||||
dependencies:
|
||||
iso-639-3:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1([email protected])
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
specifier: ^18.2.48
|
||||
version: 18.3.27
|
||||
'@types/react-dom':
|
||||
specifier: ^18.2.18
|
||||
version: 18.3.7(@types/[email protected])
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.2.1
|
||||
version: 4.7.0([email protected](@types/[email protected]))
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^5.0.12
|
||||
version: 5.4.21(@types/[email protected])
|
||||
|
||||
packages/meta-sort-ui:
|
||||
dependencies:
|
||||
react:
|
||||
|
||||
@@ -3,4 +3,3 @@ packages:
|
||||
- 'packages/async-utils'
|
||||
- 'packages/meta-sort-core'
|
||||
- 'packages/meta-sort-ui'
|
||||
- 'packages/meta-sort-editor'
|
||||
|
||||
Reference in New Issue
Block a user