Add adapter timeouts and drop unreachable spec-merge code

- Barcode- und OCR-Adapter erhalten je eine eigene, ueberschreibbare
  Zeitobergrenze (deps.barcodeTimeoutMs / deps.ocrTimeoutMs). Ein
  Adapter, der nie einloest, blockiert recognize() damit nicht mehr
  dauerhaft und faellt stattdessen auf den Ersatzwert zurueck, genau
  wie bei einem geworfenen Fehler. Kein offener Timer bleibt zurueck.
- mergeCompatibleSpecs entfernt: vertraegliche verwertbare
  Barcode-Treffer sind wegen der Kopplung von partNumber und den
  uebrigen Feldern ohnehin identisch, der erste Treffer genuegt.
  Test entsprechend umbenannt, um das tatsaechlich gepruefte
  Verhalten zu benennen statt eine nicht mehr vorhandene
  Auffuell-Semantik zu behaupten.
- Kommentar in pn-tables.js von einer inhaltlichen Wiederholung
  befreit.
This commit is contained in:
vchuser
2026-07-28 15:36:52 +02:00
parent 4099687707
commit be437a5fc5
3 changed files with 69 additions and 25 deletions
+44 -22
View File
@@ -2,31 +2,40 @@ import { emptySpec, isUsable, specsCompatible } from './spec.js';
import { decodePartNumber } from './pn-decoder.js';
import { extractFields } from './ocr-extract.js';
async function safely(fn, fallback) {
// Barcode-Dekodierung ist schnell; ein Adapter, der laenger braucht, gilt
// als gescheitert. Grosszuegig genug, dass ein normaler Ablauf sie nie reisst.
const DEFAULT_BARCODE_TIMEOUT_MS = 1500;
// Texterkennung kann auf einem Handy durchaus einige Sekunden dauern - die
// Obergrenze muss so grosszuegig sein, dass ein normaler Ablauf auf einem
// langsamen Handy sie nie erreicht.
const DEFAULT_OCR_TIMEOUT_MS = 20000;
const TIMED_OUT = Symbol('timed-out');
/**
* Wettlauf zwischen dem uebergebenen Versprechen und einem Zeitgeber. Ist der
* Zeitgeber schneller, wird TIMED_OUT geliefert, ohne dass der urspruengliche
* Aufruf abgebrochen wird - er darf spaeter noch einloesen, wir warten nur
* nicht mehr darauf. Der Zeitgeber wird in jedem Fall wieder aufgeraeumt,
* damit kein offener Timer den Node-Prozess am Beenden hindert.
*/
function withTimeout(promise, ms) {
let timer;
const timeout = new Promise((resolve) => {
timer = setTimeout(() => resolve(TIMED_OUT), ms);
});
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}
async function safely(fn, fallback, timeoutMs) {
try {
return await fn();
const result = await withTimeout(fn(), timeoutMs);
return result === TIMED_OUT ? fallback : result;
} catch {
return fallback;
}
}
/**
* Fuehrt mehrere untereinander vertraegliche Specs zusammen: ein spaeterer
* Treffer fuellt nur Felder, die noch offen sind, ueberschreibt aber nie
* ein bereits gesetztes Feld.
*/
function mergeCompatibleSpecs(specs) {
const merged = { ...specs[0] };
for (let i = 1; i < specs.length; i++) {
for (const field of Object.keys(merged)) {
if (merged[field] === null && specs[i][field] !== null) {
merged[field] = specs[i][field];
}
}
}
return merged;
}
/** Prueft, ob alle Specs paarweise vertraeglich sind. */
function allCompatible(specs) {
for (let i = 0; i < specs.length; i++) {
@@ -39,10 +48,17 @@ function allCompatible(specs) {
/**
* Barcode zuerst, OCR nur als Rueckfallebene.
* @param {object} deps.decodeBarcodes und deps.runOcr sind erforderlich.
* deps.barcodeTimeoutMs und deps.ocrTimeoutMs sind optional und ueberschreiben
* die eingebauten Obergrenzen - fuer Tests, die einen haengenden Adapter
* simulieren, ohne tatsaechlich zu warten.
* @returns {Promise<{spec: object, source: 'barcode'|'ocr'|'none', confidence: 'green'|'yellow'|'red'}>}
*/
export async function recognize(frame, deps) {
const decodedCodes = await safely(() => deps.decodeBarcodes(frame), []);
const barcodeTimeoutMs = deps.barcodeTimeoutMs ?? DEFAULT_BARCODE_TIMEOUT_MS;
const ocrTimeoutMs = deps.ocrTimeoutMs ?? DEFAULT_OCR_TIMEOUT_MS;
const decodedCodes = await safely(() => deps.decodeBarcodes(frame), [], barcodeTimeoutMs);
const codes = Array.isArray(decodedCodes) ? decodedCodes : [];
let best = emptySpec();
@@ -62,13 +78,19 @@ export async function recognize(frame, deps) {
}
if (usable.length > 1) {
if (allCompatible(usable)) {
return { spec: mergeCompatibleSpecs(usable), source: 'barcode', confidence: 'green' };
// Vertraeglichkeit unter verwertbaren Barcode-Treffern setzt eine
// uebereinstimmende Teilenummer voraus (decodePartNumber setzt sie fuer
// jede nicht-leere Eingabe, specsCompatible vergleicht sie mit) - und
// da alle uebrigen Felder aus derselben Teilenummer abgeleitet werden,
// sind vertraegliche Treffer ohnehin gleich. Es gibt nichts aufzufuellen,
// der erste Treffer genuegt.
return { spec: usable[0], source: 'barcode', confidence: 'green' };
}
// Mehrdeutigkeit zwischen verwertbaren Barcodes: nicht raten, Nutzer entscheidet.
return { spec: emptySpec(), source: 'none', confidence: 'red' };
}
const ocrResult = await safely(() => deps.runOcr(frame), '');
const ocrResult = await safely(() => deps.runOcr(frame), '', ocrTimeoutMs);
const text = typeof ocrResult === 'string' ? ocrResult : '';
const fromOcr = extractFields(text);
+1 -2
View File
@@ -6,8 +6,7 @@
// Systematik und muessen vor produktivem Einsatz gegen reale Module bestaetigt
// werden - siehe Task 14. Ein falscher Eintrag faellt auf dem Barcode-Weg
// nicht automatisch auf, weil dort keine Texterkennung laeuft, gegen die er
// widersprechen koennte - die Eintraege muessen deshalb gegen reale Module
// geprueft werden.
// widersprechen koennte.
export const VENDOR_TABLES = [
{
+24 -1
View File
@@ -115,7 +115,7 @@ test('mehrere unvertraegliche Barcodes ergeben rot statt eines ungeprueften erst
assert.equal(ocrAufgerufen, false, 'bei mehrdeutigem Barcode-Ergebnis soll nicht geraten werden');
});
test('mehrere untereinander vertraegliche Barcodes werden zusammengefuehrt und bleiben gruen', async () => {
test('mehrere untereinander vertraegliche Barcodes ergeben gruen (der erste Treffer entscheidet, es gibt nichts zusammenzufuehren)', async () => {
let ocrAufgerufen = false;
const result = await recognize({}, {
// Zwei Lesungen desselben Etiketts (z.B. Mehrfachtreffer durch
@@ -132,3 +132,26 @@ test('mehrere untereinander vertraegliche Barcodes werden zusammengefuehrt und b
assert.equal(result.spec.speed, 'PC4-2400');
assert.equal(ocrAufgerufen, false, 'OCR darf bei gruenem Mehrfachtreffer nicht laufen');
});
// --- Beanstandung 1: haengender Adapter darf die Erkennung nicht dauerhaft blockieren ---
test('haengender Barcode-Adapter blockiert nicht dauerhaft, OCR uebernimmt', { timeout: 500 }, async () => {
const result = await recognize({}, {
decodeBarcodes: () => new Promise(() => {}), // loest nie ein und wirft nie
runOcr: async () => OCR_TEXT,
barcodeTimeoutMs: 20,
});
assert.equal(result.source, 'ocr');
assert.equal(result.confidence, 'yellow');
assert.equal(result.spec.capacityGb, 64);
});
test('haengender OCR-Adapter blockiert nicht dauerhaft, Erkennung ergibt rot', { timeout: 500 }, async () => {
const result = await recognize({}, {
decodeBarcodes: async () => [],
runOcr: () => new Promise(() => {}), // loest nie ein und wirft nie
ocrTimeoutMs: 20,
});
assert.equal(result.source, 'none');
assert.equal(result.confidence, 'red');
});