fix: show live Laya analysis progress (#21)
Track the Laya identity phase separately from catalog projection progress and expose phase-specific counters through the live status API and Settings presentation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -14,8 +14,11 @@ const LIVE_PROJECTION_COLUMNS = {
|
||||
};
|
||||
|
||||
const CATALOG_PROJECTION_STATE_COLUMNS = {
|
||||
phase: "TEXT NOT NULL DEFAULT 'idle'",
|
||||
processed_count: 'INTEGER NOT NULL DEFAULT 0',
|
||||
total_count: 'INTEGER NOT NULL DEFAULT 0'
|
||||
total_count: 'INTEGER NOT NULL DEFAULT 0',
|
||||
laya_processed_count: 'INTEGER NOT NULL DEFAULT 0',
|
||||
laya_total_count: 'INTEGER NOT NULL DEFAULT 0'
|
||||
};
|
||||
|
||||
const LIVE_PROJECTION_SCHEMA_SQL = `
|
||||
@ -40,8 +43,11 @@ const LIVE_PROJECTION_SCHEMA_SQL = `
|
||||
completed_at INTEGER,
|
||||
input_hash TEXT,
|
||||
projected_count INTEGER NOT NULL DEFAULT 0,
|
||||
phase TEXT NOT NULL DEFAULT 'idle',
|
||||
processed_count INTEGER NOT NULL DEFAULT 0,
|
||||
total_count INTEGER NOT NULL DEFAULT 0,
|
||||
laya_processed_count INTEGER NOT NULL DEFAULT 0,
|
||||
laya_total_count INTEGER NOT NULL DEFAULT 0,
|
||||
error TEXT
|
||||
);
|
||||
`;
|
||||
|
||||
@ -13,8 +13,11 @@ function serializeState(state) {
|
||||
completedAt: state.completed_at,
|
||||
inputHash: state.input_hash,
|
||||
projectedCount: state.projected_count,
|
||||
phase: state.phase,
|
||||
processed: state.processed_count,
|
||||
total: state.total_count,
|
||||
layaProcessed: state.laya_processed_count,
|
||||
layaTotal: state.laya_total_count,
|
||||
error: state.error
|
||||
};
|
||||
}
|
||||
|
||||
@ -141,21 +141,26 @@ function identityCandidatePairs(items) {
|
||||
return pairs;
|
||||
}
|
||||
|
||||
async function collectIdentityHints(items, mode, layaClient) {
|
||||
async function collectIdentityHints(items, mode, layaClient, options = {}) {
|
||||
if (mode === 'algo' || !layaClient) return new Map();
|
||||
const pairs = identityCandidatePairs(items);
|
||||
const total = Math.ceil(pairs.length / 64);
|
||||
let processed = 0;
|
||||
const hints = new Map();
|
||||
options.onProgress?.({ processed, total });
|
||||
for (let index = 0; index < pairs.length; index += 64) {
|
||||
const results = await layaClient.identityHintBatch(pairs.slice(index, index + 64));
|
||||
results.forEach(result => hints.set(result.id, result));
|
||||
processed += 1;
|
||||
options.onProgress?.({ processed, total });
|
||||
}
|
||||
return hints;
|
||||
}
|
||||
|
||||
async function collectOptionalIdentityHints(items, mode, layaClient) {
|
||||
async function collectOptionalIdentityHints(items, mode, layaClient, options = {}) {
|
||||
if (mode === 'algo') return new Map();
|
||||
try {
|
||||
return await collectIdentityHints(items, mode, layaClient);
|
||||
return await collectIdentityHints(items, mode, layaClient, options);
|
||||
} catch (error) {
|
||||
console.warn('[LiveProjection] Laya identity hints unavailable; continuing deterministically');
|
||||
return new Map();
|
||||
@ -177,20 +182,33 @@ function projectionState(database = null) {
|
||||
database ||= defaultDatabase();
|
||||
return database.prepare('SELECT * FROM catalog_projection_state WHERE projection = ?').get('live') || {
|
||||
projection: 'live', status: 'missing', started_at: null, completed_at: null,
|
||||
input_hash: null, projected_count: 0, processed_count: 0, total_count: 0, error: null
|
||||
input_hash: null, projected_count: 0, phase: 'idle', processed_count: 0, total_count: 0,
|
||||
laya_processed_count: 0, laya_total_count: 0, error: null
|
||||
};
|
||||
}
|
||||
|
||||
function updateProjectionProgress(database, phase, progress = {}) {
|
||||
database.prepare(`
|
||||
UPDATE catalog_projection_state SET phase = ?,
|
||||
laya_processed_count = COALESCE(?, laya_processed_count),
|
||||
laya_total_count = COALESCE(?, laya_total_count)
|
||||
WHERE projection = 'live'
|
||||
`).run(phase, progress.layaProcessed ?? null, progress.layaTotal ?? null);
|
||||
}
|
||||
|
||||
function markProjectionBuilding(database = null) {
|
||||
database ||= defaultDatabase();
|
||||
const total = database.prepare("SELECT COUNT(*) AS count FROM playlist_items WHERE type = 'live'").get().count;
|
||||
const startedAt = Date.now();
|
||||
database.prepare(`
|
||||
INSERT INTO catalog_projection_state
|
||||
(projection, status, started_at, completed_at, input_hash, projected_count, processed_count, total_count, error)
|
||||
VALUES ('live', 'building', ?, NULL, NULL, 0, 0, ?, NULL)
|
||||
(projection, status, started_at, completed_at, input_hash, projected_count, phase,
|
||||
processed_count, total_count, laya_processed_count, laya_total_count, error)
|
||||
VALUES ('live', 'building', ?, NULL, NULL, 0, 'preparing', 0, ?, 0, 0, NULL)
|
||||
ON CONFLICT(projection) DO UPDATE SET status = 'building', started_at = excluded.started_at,
|
||||
completed_at = NULL, processed_count = 0, total_count = excluded.total_count, error = NULL
|
||||
completed_at = NULL, phase = 'preparing', processed_count = 0,
|
||||
total_count = excluded.total_count, laya_processed_count = 0,
|
||||
laya_total_count = 0, error = NULL
|
||||
`).run(startedAt, total);
|
||||
return projectionState(database);
|
||||
}
|
||||
@ -212,9 +230,11 @@ function loadProjectionItems(database) {
|
||||
function rebuild(database = null, options = {}) {
|
||||
database ||= defaultDatabase();
|
||||
const items = options.items || loadProjectionItems(database);
|
||||
if (!options.stateAlreadyBuilding) markProjectionBuilding(database);
|
||||
updateProjectionProgress(database, 'projecting');
|
||||
const groups = projectCatalog(items, { mode: options.mode, identityHints: options.identityHints });
|
||||
const inputHash = catalogInputHash(items);
|
||||
if (!options.stateAlreadyBuilding) markProjectionBuilding(database);
|
||||
updateProjectionProgress(database, 'writing');
|
||||
|
||||
try {
|
||||
const apply = database.transaction(() => {
|
||||
@ -243,7 +263,7 @@ function rebuild(database = null, options = {}) {
|
||||
}
|
||||
}
|
||||
database.prepare(`
|
||||
UPDATE catalog_projection_state SET status = 'success', completed_at = ?,
|
||||
UPDATE catalog_projection_state SET status = 'success', phase = 'complete', completed_at = ?,
|
||||
input_hash = ?, projected_count = ?, processed_count = ?, total_count = ?, error = NULL
|
||||
WHERE projection = 'live'
|
||||
`).run(Date.now(), inputHash, groups.length, items.length, items.length);
|
||||
@ -254,7 +274,7 @@ function rebuild(database = null, options = {}) {
|
||||
const diagnosticCode = /^SQLITE_[A-Z_]+$/.test(error.code || '') ? error.code : 'INTERNAL';
|
||||
console.error(`[LiveProjection] Rebuild transaction failed (${diagnosticCode}, ${error.name || 'Error'})`);
|
||||
database.prepare(`
|
||||
UPDATE catalog_projection_state SET status = 'error', completed_at = ?, error = ?
|
||||
UPDATE catalog_projection_state SET status = 'error', phase = 'failed', completed_at = ?, error = ?
|
||||
WHERE projection = 'live'
|
||||
`).run(Date.now(), `Live catalog projection failed [${diagnosticCode}]`);
|
||||
throw error;
|
||||
@ -269,7 +289,12 @@ async function rebuildWithSignals(database = null, options = {}) {
|
||||
let identityHints = new Map();
|
||||
if (mode !== 'algo') {
|
||||
const layaClient = options.layaClient || new (require('./layaService').LayaService)();
|
||||
identityHints = await collectOptionalIdentityHints(items, mode, layaClient);
|
||||
identityHints = await collectOptionalIdentityHints(items, mode, layaClient, {
|
||||
onProgress: ({ processed, total }) => updateProjectionProgress(database, 'laya_identity', {
|
||||
layaProcessed: processed,
|
||||
layaTotal: total
|
||||
})
|
||||
});
|
||||
}
|
||||
return rebuild(database, { ...options, items, mode, identityHints });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user