redmine-context 1.0.0
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/LICENSE +21 -0
- package/README.md +449 -0
- package/dist/bundle/index.d.ts +5 -0
- package/dist/bundle/index.js +5 -0
- package/dist/bundle/json.d.ts +90 -0
- package/dist/bundle/json.js +266 -0
- package/dist/bundle/markdown.d.ts +75 -0
- package/dist/bundle/markdown.js +294 -0
- package/dist/bundle/search-list.d.ts +43 -0
- package/dist/bundle/search-list.js +53 -0
- package/dist/bundle/stable-stringify.d.ts +26 -0
- package/dist/bundle/stable-stringify.js +50 -0
- package/dist/cache/contract.d.ts +157 -0
- package/dist/cache/contract.js +0 -0
- package/dist/cache/disk-index.d.ts +82 -0
- package/dist/cache/disk-index.js +220 -0
- package/dist/cache/disk.d.ts +133 -0
- package/dist/cache/disk.js +313 -0
- package/dist/cache/gc.d.ts +78 -0
- package/dist/cache/gc.js +123 -0
- package/dist/cache/get-or-compute.d.ts +36 -0
- package/dist/cache/get-or-compute.js +52 -0
- package/dist/cache/index.d.ts +9 -0
- package/dist/cache/index.js +8 -0
- package/dist/cache/keys.d.ts +76 -0
- package/dist/cache/keys.js +78 -0
- package/dist/cache/memory.d.ts +48 -0
- package/dist/cache/memory.js +110 -0
- package/dist/cache-first.d.ts +127 -0
- package/dist/cache-first.js +227 -0
- package/dist/client/errors.d.ts +33 -0
- package/dist/client/errors.js +49 -0
- package/dist/client/http.d.ts +110 -0
- package/dist/client/http.js +207 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +5 -0
- package/dist/client/issues.d.ts +71 -0
- package/dist/client/issues.js +100 -0
- package/dist/client/search.d.ts +58 -0
- package/dist/client/search.js +81 -0
- package/dist/config/credentials.d.ts +247 -0
- package/dist/config/credentials.js +427 -0
- package/dist/config/doctor.d.ts +123 -0
- package/dist/config/doctor.js +260 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.js +6 -0
- package/dist/config/keyring.d.ts +96 -0
- package/dist/config/keyring.js +158 -0
- package/dist/config/login.d.ts +97 -0
- package/dist/config/login.js +189 -0
- package/dist/config/settings.d.ts +94 -0
- package/dist/config/settings.js +140 -0
- package/dist/contract.d.ts +173 -0
- package/dist/contract.js +27 -0
- package/dist/core.d.ts +1 -0
- package/dist/core.js +8 -0
- package/dist/extract/audio-extractor.d.ts +105 -0
- package/dist/extract/audio-extractor.js +156 -0
- package/dist/extract/audio.d.ts +126 -0
- package/dist/extract/audio.js +184 -0
- package/dist/extract/dispatcher.d.ts +132 -0
- package/dist/extract/dispatcher.js +115 -0
- package/dist/extract/download.d.ts +111 -0
- package/dist/extract/download.js +261 -0
- package/dist/extract/duration.d.ts +106 -0
- package/dist/extract/duration.js +148 -0
- package/dist/extract/ffmpeg.d.ts +56 -0
- package/dist/extract/ffmpeg.js +95 -0
- package/dist/extract/gguf.d.ts +137 -0
- package/dist/extract/gguf.js +215 -0
- package/dist/extract/index.d.ts +19 -0
- package/dist/extract/index.js +19 -0
- package/dist/extract/magic.d.ts +80 -0
- package/dist/extract/magic.js +282 -0
- package/dist/extract/ooxml.d.ts +131 -0
- package/dist/extract/ooxml.js +336 -0
- package/dist/extract/pdf.d.ts +147 -0
- package/dist/extract/pdf.js +322 -0
- package/dist/extract/queue.d.ts +167 -0
- package/dist/extract/queue.js +217 -0
- package/dist/extract/subprocess.d.ts +145 -0
- package/dist/extract/subprocess.js +181 -0
- package/dist/extract/tesseract.d.ts +153 -0
- package/dist/extract/tesseract.js +321 -0
- package/dist/extract/video-extractor.d.ts +84 -0
- package/dist/extract/video-extractor.js +89 -0
- package/dist/extract/video.d.ts +198 -0
- package/dist/extract/video.js +418 -0
- package/dist/extract/which.d.ts +63 -0
- package/dist/extract/which.js +72 -0
- package/dist/extract/whisper-extract.d.ts +211 -0
- package/dist/extract/whisper-extract.js +323 -0
- package/dist/extract/whisper.d.ts +50 -0
- package/dist/extract/whisper.js +67 -0
- package/dist/extract/zip.d.ts +50 -0
- package/dist/extract/zip.js +165 -0
- package/dist/extract-issue-attachments.d.ts +82 -0
- package/dist/extract-issue-attachments.js +156 -0
- package/dist/fetch-attachment-text.d.ts +113 -0
- package/dist/fetch-attachment-text.js +155 -0
- package/dist/fetch-issue-bundle.d.ts +81 -0
- package/dist/fetch-issue-bundle.js +93 -0
- package/dist/fetch-issue-search.d.ts +74 -0
- package/dist/fetch-issue-search.js +120 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +67 -0
- package/dist/normalize/collections.d.ts +52 -0
- package/dist/normalize/collections.js +170 -0
- package/dist/normalize/helpers.d.ts +35 -0
- package/dist/normalize/helpers.js +59 -0
- package/dist/normalize/index.d.ts +2 -0
- package/dist/normalize/index.js +2 -0
- package/dist/normalize/issue.d.ts +34 -0
- package/dist/normalize/issue.js +154 -0
- package/dist/surfaces/cli/commands.d.ts +61 -0
- package/dist/surfaces/cli/commands.js +262 -0
- package/dist/surfaces/cli/main.d.ts +32 -0
- package/dist/surfaces/cli/main.js +210 -0
- package/dist/surfaces/cli/prompts.d.ts +66 -0
- package/dist/surfaces/cli/prompts.js +147 -0
- package/dist/surfaces/cli/tty.d.ts +27 -0
- package/dist/surfaces/cli/tty.js +35 -0
- package/dist/surfaces/cli/types.d.ts +39 -0
- package/dist/surfaces/cli/types.js +7 -0
- package/dist/surfaces/mcp/server.d.ts +171 -0
- package/dist/surfaces/mcp/server.js +427 -0
- package/dist/surfaces/tui/app.d.ts +55 -0
- package/dist/surfaces/tui/app.js +180 -0
- package/dist/surfaces/tui/attachment-status.d.ts +79 -0
- package/dist/surfaces/tui/attachment-status.js +113 -0
- package/dist/surfaces/tui/components/breadcrumb.d.ts +7 -0
- package/dist/surfaces/tui/components/breadcrumb.js +31 -0
- package/dist/surfaces/tui/components/gradient-text.d.ts +23 -0
- package/dist/surfaces/tui/components/gradient-text.js +75 -0
- package/dist/surfaces/tui/components/scroll-view.d.ts +25 -0
- package/dist/surfaces/tui/components/scroll-view.js +77 -0
- package/dist/surfaces/tui/components/spinner.d.ts +12 -0
- package/dist/surfaces/tui/components/spinner.js +39 -0
- package/dist/surfaces/tui/components/text-input.d.ts +45 -0
- package/dist/surfaces/tui/components/text-input.js +114 -0
- package/dist/surfaces/tui/format-file-size.d.ts +24 -0
- package/dist/surfaces/tui/format-file-size.js +43 -0
- package/dist/surfaces/tui/glyphs.d.ts +47 -0
- package/dist/surfaces/tui/glyphs.js +84 -0
- package/dist/surfaces/tui/hooks/use-auth-guard.d.ts +39 -0
- package/dist/surfaces/tui/hooks/use-auth-guard.js +135 -0
- package/dist/surfaces/tui/hooks/use-doctor-status.d.ts +64 -0
- package/dist/surfaces/tui/hooks/use-doctor-status.js +123 -0
- package/dist/surfaces/tui/hooks/use-escape-interceptor.d.ts +25 -0
- package/dist/surfaces/tui/hooks/use-escape-interceptor.js +65 -0
- package/dist/surfaces/tui/hooks/use-exit-guard.d.ts +18 -0
- package/dist/surfaces/tui/hooks/use-exit-guard.js +66 -0
- package/dist/surfaces/tui/hooks/use-export-bundle.d.ts +62 -0
- package/dist/surfaces/tui/hooks/use-export-bundle.js +100 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +61 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.js +132 -0
- package/dist/surfaces/tui/hooks/use-issue-search.d.ts +71 -0
- package/dist/surfaces/tui/hooks/use-issue-search.js +168 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +44 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.js +82 -0
- package/dist/surfaces/tui/hooks/use-media-binaries.d.ts +24 -0
- package/dist/surfaces/tui/hooks/use-media-binaries.js +44 -0
- package/dist/surfaces/tui/hooks/use-my-issues.d.ts +66 -0
- package/dist/surfaces/tui/hooks/use-my-issues.js +151 -0
- package/dist/surfaces/tui/hooks/use-onboarding-callbacks.d.ts +11 -0
- package/dist/surfaces/tui/hooks/use-onboarding-callbacks.js +104 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +52 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.js +90 -0
- package/dist/surfaces/tui/index.d.ts +50 -0
- package/dist/surfaces/tui/index.js +158 -0
- package/dist/surfaces/tui/instance.d.ts +37 -0
- package/dist/surfaces/tui/instance.js +36 -0
- package/dist/surfaces/tui/job-registry.d.ts +113 -0
- package/dist/surfaces/tui/job-registry.js +123 -0
- package/dist/surfaces/tui/job-status.d.ts +45 -0
- package/dist/surfaces/tui/job-status.js +81 -0
- package/dist/surfaces/tui/navigation.d.ts +74 -0
- package/dist/surfaces/tui/navigation.js +87 -0
- package/dist/surfaces/tui/palettes.d.ts +38 -0
- package/dist/surfaces/tui/palettes.js +244 -0
- package/dist/surfaces/tui/screen.d.ts +30 -0
- package/dist/surfaces/tui/screen.js +51 -0
- package/dist/surfaces/tui/screens/about.d.ts +2 -0
- package/dist/surfaces/tui/screens/about.js +35 -0
- package/dist/surfaces/tui/screens/appearance.d.ts +2 -0
- package/dist/surfaces/tui/screens/appearance.js +74 -0
- package/dist/surfaces/tui/screens/config.d.ts +2 -0
- package/dist/surfaces/tui/screens/config.js +82 -0
- package/dist/surfaces/tui/screens/doctor.d.ts +2 -0
- package/dist/surfaces/tui/screens/doctor.js +109 -0
- package/dist/surfaces/tui/screens/export.d.ts +2 -0
- package/dist/surfaces/tui/screens/export.js +168 -0
- package/dist/surfaces/tui/screens/home-selection.d.ts +70 -0
- package/dist/surfaces/tui/screens/home-selection.js +80 -0
- package/dist/surfaces/tui/screens/home.d.ts +2 -0
- package/dist/surfaces/tui/screens/home.js +200 -0
- package/dist/surfaces/tui/screens/issue-detail.d.ts +6 -0
- package/dist/surfaces/tui/screens/issue-detail.js +182 -0
- package/dist/surfaces/tui/screens/jobs.d.ts +7 -0
- package/dist/surfaces/tui/screens/jobs.js +89 -0
- package/dist/surfaces/tui/screens/loaded-issue-context.d.ts +49 -0
- package/dist/surfaces/tui/screens/loaded-issue-context.js +57 -0
- package/dist/surfaces/tui/screens/onboarding/api-key.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/api-key.js +69 -0
- package/dist/surfaces/tui/screens/onboarding/login.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/login.js +50 -0
- package/dist/surfaces/tui/screens/onboarding/mode.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/mode.js +42 -0
- package/dist/surfaces/tui/screens/onboarding/onboarding-context.d.ts +221 -0
- package/dist/surfaces/tui/screens/onboarding/onboarding-context.js +131 -0
- package/dist/surfaces/tui/screens/onboarding/success.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/success.js +41 -0
- package/dist/surfaces/tui/screens/onboarding/url.d.ts +24 -0
- package/dist/surfaces/tui/screens/onboarding/url.js +84 -0
- package/dist/surfaces/tui/screens/onboarding/validating.d.ts +1 -0
- package/dist/surfaces/tui/screens/onboarding/validating.js +86 -0
- package/dist/surfaces/tui/screens/welcome.d.ts +6 -0
- package/dist/surfaces/tui/screens/welcome.js +95 -0
- package/dist/surfaces/tui/status-color.d.ts +21 -0
- package/dist/surfaces/tui/status-color.js +23 -0
- package/dist/surfaces/tui/symbols.d.ts +231 -0
- package/dist/surfaces/tui/symbols.js +14 -0
- package/dist/surfaces/tui/terminal-colors.d.ts +29 -0
- package/dist/surfaces/tui/terminal-colors.js +39 -0
- package/dist/surfaces/tui/theme.d.ts +156 -0
- package/dist/surfaces/tui/theme.js +86 -0
- package/dist/surfaces/tui/truncate.d.ts +31 -0
- package/dist/surfaces/tui/truncate.js +81 -0
- package/package.json +93 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `getOrCompute` — padrão de acesso ao cache dos extratores (M3-04, ADR-004).
|
|
3
|
+
*
|
|
4
|
+
* Encapsula o fluxo canônico "leia; se faltar, extraia sob lock; grave" que TODOS
|
|
5
|
+
* os extratores do M3/M4 usarão, garantindo que uma extração CARA rode UMA única
|
|
6
|
+
* vez mesmo sob concorrência na mesma chave. O helper depende apenas do contrato
|
|
7
|
+
* {@link CacheStore} (lock por chave + get/put), logo funciona com QUALQUER
|
|
8
|
+
* implementação — memória ou disco — sem alteração.
|
|
9
|
+
*
|
|
10
|
+
* Duas checagens complementares evitam recomputar:
|
|
11
|
+
* - fast-path: um `get` ANTES do lock resolve o caso comum (hit) sem serializar;
|
|
12
|
+
* - double-check: após ADQUIRIR o lock, um novo `get` cobre a corrida em que outro
|
|
13
|
+
* waiter da mesma chave já gravou o valor enquanto esperávamos — nesse caso
|
|
14
|
+
* retornamos sem recomputar.
|
|
15
|
+
*
|
|
16
|
+
* POLÍTICA DE ERRO (decisão desta issue): se `compute` lançar, o erro propaga ao
|
|
17
|
+
* DONO do lock (quem chamou `compute`); nada é gravado. Os waiters, ao adquirirem
|
|
18
|
+
* o lock em seguida, veem o miss no double-check e RECOMPUTAM — não herdam a falha
|
|
19
|
+
* do dono. Isso mantém a falha localizada e permite recuperação transparente.
|
|
20
|
+
*/
|
|
21
|
+
import type { CacheKey, CacheStore } from './contract.js';
|
|
22
|
+
/**
|
|
23
|
+
* Retorna o valor cacheado de `key`, computando-o sob lock caso ausente.
|
|
24
|
+
*
|
|
25
|
+
* @typeParam V - Tipo do valor cacheado.
|
|
26
|
+
* @param store - Backend de cache (memória ou disco) — ver {@link CacheStore}.
|
|
27
|
+
* @param key - Chave de qualquer camada (ADR-004).
|
|
28
|
+
* @param compute - Produz o valor quando há cache-miss; roda no máximo uma vez por
|
|
29
|
+
* vencedor do lock. Sob concorrência na mesma chave, apenas o dono a executa.
|
|
30
|
+
* @returns O valor cacheado (existente ou recém-computado).
|
|
31
|
+
* @throws Propaga qualquer erro lançado por `compute` ao chamador que a executou;
|
|
32
|
+
* nesse caso nada é gravado e os demais waiters recomputam.
|
|
33
|
+
* @example
|
|
34
|
+
* const extracao = await getOrCompute(store, key, () => extrairAudio(anexo));
|
|
35
|
+
*/
|
|
36
|
+
export declare function getOrCompute<V>(store: CacheStore<V>, key: CacheKey, compute: () => Promise<V>): Promise<V>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `getOrCompute` — padrão de acesso ao cache dos extratores (M3-04, ADR-004).
|
|
3
|
+
*
|
|
4
|
+
* Encapsula o fluxo canônico "leia; se faltar, extraia sob lock; grave" que TODOS
|
|
5
|
+
* os extratores do M3/M4 usarão, garantindo que uma extração CARA rode UMA única
|
|
6
|
+
* vez mesmo sob concorrência na mesma chave. O helper depende apenas do contrato
|
|
7
|
+
* {@link CacheStore} (lock por chave + get/put), logo funciona com QUALQUER
|
|
8
|
+
* implementação — memória ou disco — sem alteração.
|
|
9
|
+
*
|
|
10
|
+
* Duas checagens complementares evitam recomputar:
|
|
11
|
+
* - fast-path: um `get` ANTES do lock resolve o caso comum (hit) sem serializar;
|
|
12
|
+
* - double-check: após ADQUIRIR o lock, um novo `get` cobre a corrida em que outro
|
|
13
|
+
* waiter da mesma chave já gravou o valor enquanto esperávamos — nesse caso
|
|
14
|
+
* retornamos sem recomputar.
|
|
15
|
+
*
|
|
16
|
+
* POLÍTICA DE ERRO (decisão desta issue): se `compute` lançar, o erro propaga ao
|
|
17
|
+
* DONO do lock (quem chamou `compute`); nada é gravado. Os waiters, ao adquirirem
|
|
18
|
+
* o lock em seguida, veem o miss no double-check e RECOMPUTAM — não herdam a falha
|
|
19
|
+
* do dono. Isso mantém a falha localizada e permite recuperação transparente.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Retorna o valor cacheado de `key`, computando-o sob lock caso ausente.
|
|
23
|
+
*
|
|
24
|
+
* @typeParam V - Tipo do valor cacheado.
|
|
25
|
+
* @param store - Backend de cache (memória ou disco) — ver {@link CacheStore}.
|
|
26
|
+
* @param key - Chave de qualquer camada (ADR-004).
|
|
27
|
+
* @param compute - Produz o valor quando há cache-miss; roda no máximo uma vez por
|
|
28
|
+
* vencedor do lock. Sob concorrência na mesma chave, apenas o dono a executa.
|
|
29
|
+
* @returns O valor cacheado (existente ou recém-computado).
|
|
30
|
+
* @throws Propaga qualquer erro lançado por `compute` ao chamador que a executou;
|
|
31
|
+
* nesse caso nada é gravado e os demais waiters recomputam.
|
|
32
|
+
* @example
|
|
33
|
+
* const extracao = await getOrCompute(store, key, () => extrairAudio(anexo));
|
|
34
|
+
*/
|
|
35
|
+
export async function getOrCompute(store, key, compute) {
|
|
36
|
+
// Fast-path: hit comum não precisa serializar no lock.
|
|
37
|
+
const cached = await store.get(key);
|
|
38
|
+
if (cached !== undefined) {
|
|
39
|
+
return cached;
|
|
40
|
+
}
|
|
41
|
+
return store.lock(key, async () => {
|
|
42
|
+
// Double-check: outro waiter da mesma chave pode ter gravado enquanto
|
|
43
|
+
// esperávamos o lock — reusa o resultado em vez de recomputar.
|
|
44
|
+
const existing = await store.get(key);
|
|
45
|
+
if (existing !== undefined) {
|
|
46
|
+
return existing;
|
|
47
|
+
}
|
|
48
|
+
const value = await compute();
|
|
49
|
+
await store.put(key, value);
|
|
50
|
+
return value;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const MODULE_NAME: "cache";
|
|
2
|
+
export { DEFAULT_STALE_LOCK_TTL_MS, instanceHash, serializeCacheKey, type AttachmentCacheKey, type CacheKey, type CacheStore, type CacheStoreOptions, type ExtractorParams, type GcContext, type GcHook, type IssueCacheKey, } from './contract.js';
|
|
3
|
+
export { getOrCompute } from './get-or-compute.js';
|
|
4
|
+
export { InMemoryCacheStore } from './memory.js';
|
|
5
|
+
export { DiskCacheStore, defaultCacheDir, type DiskCacheStoreOptions } from './disk.js';
|
|
6
|
+
export { buildAttachmentKey, deriveAttachmentDigest, type BuildAttachmentKeyInput, type ExtractorConfig, } from './keys.js';
|
|
7
|
+
export { DiskCacheIndex, INDEX_FILE_NAME } from './disk-index.js';
|
|
8
|
+
export type { CacheIndexEntry, CacheEntryType } from './disk-index.js';
|
|
9
|
+
export { DEFAULT_MAX_BYTES, DEFAULT_MAX_ORIGINAL_FRACTION, planGc, resolveEvictionTarget, type GcCandidate, type GcDecision, type GcPolicyOptions, } from './gc.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const MODULE_NAME = 'cache';
|
|
2
|
+
export { DEFAULT_STALE_LOCK_TTL_MS, instanceHash, serializeCacheKey, } from './contract.js';
|
|
3
|
+
export { getOrCompute } from './get-or-compute.js';
|
|
4
|
+
export { InMemoryCacheStore } from './memory.js';
|
|
5
|
+
export { DiskCacheStore, defaultCacheDir } from './disk.js';
|
|
6
|
+
export { buildAttachmentKey, deriveAttachmentDigest, } from './keys.js';
|
|
7
|
+
export { DiskCacheIndex, INDEX_FILE_NAME } from './disk-index.js';
|
|
8
|
+
export { DEFAULT_MAX_BYTES, DEFAULT_MAX_ORIGINAL_FRACTION, planGc, resolveEvictionTarget, } from './gc.js';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder de alto nível da chave attachment-level (M3-03, ADR-004).
|
|
3
|
+
*
|
|
4
|
+
* Monta a {@link AttachmentCacheKey} correta a partir do modelo `Attachment` do
|
|
5
|
+
* contrato ({@link Attachment}) mais a configuração do extrator
|
|
6
|
+
* ({@link ExtractorConfig}), escondendo das camadas superiores dois detalhes:
|
|
7
|
+
*
|
|
8
|
+
* 1. o cálculo do `instance_hash` a partir da URL da instância
|
|
9
|
+
* ({@link instanceHash}); e
|
|
10
|
+
* 2. a origem do `digest` — usa `attachment.digest` quando presente e, como
|
|
11
|
+
* FALLBACK para Redmine < 4.x (que não expõe `digest`), deriva um digest
|
|
12
|
+
* DETERMINÍSTICO de `(id, filesize, created_on)`.
|
|
13
|
+
*
|
|
14
|
+
* A chave attachment-level NÃO depende de `updated_on` da issue: o builder sequer
|
|
15
|
+
* o recebe. Assim, editar a issue (novo `updated_on`, comentários) invalida só a
|
|
16
|
+
* camada de issue e reaproveita 100% das extrações caras dos anexos imutáveis.
|
|
17
|
+
*
|
|
18
|
+
* Sanitização hex "por construção": o digest de fallback é o SHA-256 hex do
|
|
19
|
+
* material derivado — 64 chars `[0-9a-f]`. Ele satisfaz o padrão hex que o
|
|
20
|
+
* {@link DiskCacheStore} usa ao montar `<id>-<digest8>/`, atravessando o caminho
|
|
21
|
+
* hex direto (sem o re-hash de escape reservado a valores não-hex/maliciosos).
|
|
22
|
+
*/
|
|
23
|
+
import type { Attachment } from '../contract.js';
|
|
24
|
+
import { type AttachmentCacheKey, type ExtractorParams } from './contract.js';
|
|
25
|
+
/**
|
|
26
|
+
* Configuração do extrator que participa da identidade da extração (ADR-004).
|
|
27
|
+
* Trocar qualquer campo invalida corretamente a entrada de cache do anexo.
|
|
28
|
+
*/
|
|
29
|
+
export interface ExtractorConfig {
|
|
30
|
+
/** Versão do extrator (mapeada para `extractorVersion` na chave). */
|
|
31
|
+
version: string;
|
|
32
|
+
/** Modelo usado (ex.: `whisper-large-v3`). */
|
|
33
|
+
model: string;
|
|
34
|
+
/** Parâmetros escalares do extrator — ver {@link ExtractorParams}. */
|
|
35
|
+
params: ExtractorParams;
|
|
36
|
+
}
|
|
37
|
+
/** Entrada de {@link buildAttachmentKey}. */
|
|
38
|
+
export interface BuildAttachmentKeyInput {
|
|
39
|
+
/** URL base da instância Redmine — vira `instanceHash` na chave. */
|
|
40
|
+
instanceUrl: string;
|
|
41
|
+
/** Anexo do contrato; `digest` pode faltar em Redmine < 4.x. */
|
|
42
|
+
attachment: Attachment;
|
|
43
|
+
/** Configuração do extrator — ver {@link ExtractorConfig}. */
|
|
44
|
+
extractor: ExtractorConfig;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Deriva um digest DETERMINÍSTICO de um anexo sem `digest` (fallback Redmine
|
|
48
|
+
* < 4.x): SHA-256 hex de `(id, filesize, created_on)` separados por NUL.
|
|
49
|
+
*
|
|
50
|
+
* O resultado é hex puro (64 chars), estável entre execuções e distinto sempre
|
|
51
|
+
* que qualquer um dos três campos muda — servindo como identidade de conteúdo
|
|
52
|
+
* "boa o suficiente" quando o hash real do anexo não está disponível.
|
|
53
|
+
*
|
|
54
|
+
* @param attachment - Anexo do contrato.
|
|
55
|
+
* @returns Digest hex (SHA-256, 64 chars).
|
|
56
|
+
* @example
|
|
57
|
+
* deriveAttachmentDigest({ id: 7, filesize: 12345, created_on: '2026-07-20T00:00:00Z', ... });
|
|
58
|
+
*/
|
|
59
|
+
export declare function deriveAttachmentDigest(attachment: Attachment): string;
|
|
60
|
+
/**
|
|
61
|
+
* Monta a {@link AttachmentCacheKey} de um anexo para uma configuração de
|
|
62
|
+
* extrator (ADR-004). Usa `attachment.digest` quando presente; caso ausente
|
|
63
|
+
* (`undefined` ou `""`, típico de Redmine < 4.x), cai no fallback determinístico
|
|
64
|
+
* de {@link deriveAttachmentDigest}.
|
|
65
|
+
*
|
|
66
|
+
* @param input - Ver {@link BuildAttachmentKeyInput}.
|
|
67
|
+
* @returns Chave attachment-level pronta para o {@link CacheStore}.
|
|
68
|
+
* @throws {TypeError} Se `instanceUrl` não for uma URL válida (via {@link instanceHash}).
|
|
69
|
+
* @example
|
|
70
|
+
* const key = buildAttachmentKey({
|
|
71
|
+
* instanceUrl: 'https://redmine.example',
|
|
72
|
+
* attachment,
|
|
73
|
+
* extractor: { version: '1.0.0', model: 'whisper-large-v3', params: { language: 'pt' } },
|
|
74
|
+
* });
|
|
75
|
+
*/
|
|
76
|
+
export declare function buildAttachmentKey({ instanceUrl, attachment, extractor, }: BuildAttachmentKeyInput): AttachmentCacheKey;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder de alto nível da chave attachment-level (M3-03, ADR-004).
|
|
3
|
+
*
|
|
4
|
+
* Monta a {@link AttachmentCacheKey} correta a partir do modelo `Attachment` do
|
|
5
|
+
* contrato ({@link Attachment}) mais a configuração do extrator
|
|
6
|
+
* ({@link ExtractorConfig}), escondendo das camadas superiores dois detalhes:
|
|
7
|
+
*
|
|
8
|
+
* 1. o cálculo do `instance_hash` a partir da URL da instância
|
|
9
|
+
* ({@link instanceHash}); e
|
|
10
|
+
* 2. a origem do `digest` — usa `attachment.digest` quando presente e, como
|
|
11
|
+
* FALLBACK para Redmine < 4.x (que não expõe `digest`), deriva um digest
|
|
12
|
+
* DETERMINÍSTICO de `(id, filesize, created_on)`.
|
|
13
|
+
*
|
|
14
|
+
* A chave attachment-level NÃO depende de `updated_on` da issue: o builder sequer
|
|
15
|
+
* o recebe. Assim, editar a issue (novo `updated_on`, comentários) invalida só a
|
|
16
|
+
* camada de issue e reaproveita 100% das extrações caras dos anexos imutáveis.
|
|
17
|
+
*
|
|
18
|
+
* Sanitização hex "por construção": o digest de fallback é o SHA-256 hex do
|
|
19
|
+
* material derivado — 64 chars `[0-9a-f]`. Ele satisfaz o padrão hex que o
|
|
20
|
+
* {@link DiskCacheStore} usa ao montar `<id>-<digest8>/`, atravessando o caminho
|
|
21
|
+
* hex direto (sem o re-hash de escape reservado a valores não-hex/maliciosos).
|
|
22
|
+
*/
|
|
23
|
+
import { createHash } from 'node:crypto';
|
|
24
|
+
import { instanceHash } from './contract.js';
|
|
25
|
+
/**
|
|
26
|
+
* Separador NUL entre os campos do material de derivação do digest de fallback.
|
|
27
|
+
* ` ` (NUL) nunca ocorre em inteiros nem em timestamps ISO, evitando que a
|
|
28
|
+
* concatenação de campos vizinhos colida (ex.: `(1, 23)` vs `(12, 3)`).
|
|
29
|
+
*/
|
|
30
|
+
const FALLBACK_FIELD_SEPARATOR = '\0';
|
|
31
|
+
/**
|
|
32
|
+
* Deriva um digest DETERMINÍSTICO de um anexo sem `digest` (fallback Redmine
|
|
33
|
+
* < 4.x): SHA-256 hex de `(id, filesize, created_on)` separados por NUL.
|
|
34
|
+
*
|
|
35
|
+
* O resultado é hex puro (64 chars), estável entre execuções e distinto sempre
|
|
36
|
+
* que qualquer um dos três campos muda — servindo como identidade de conteúdo
|
|
37
|
+
* "boa o suficiente" quando o hash real do anexo não está disponível.
|
|
38
|
+
*
|
|
39
|
+
* @param attachment - Anexo do contrato.
|
|
40
|
+
* @returns Digest hex (SHA-256, 64 chars).
|
|
41
|
+
* @example
|
|
42
|
+
* deriveAttachmentDigest({ id: 7, filesize: 12345, created_on: '2026-07-20T00:00:00Z', ... });
|
|
43
|
+
*/
|
|
44
|
+
export function deriveAttachmentDigest(attachment) {
|
|
45
|
+
const material = [attachment.id, attachment.filesize, attachment.created_on].join(FALLBACK_FIELD_SEPARATOR);
|
|
46
|
+
return createHash('sha256').update(material).digest('hex');
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Monta a {@link AttachmentCacheKey} de um anexo para uma configuração de
|
|
50
|
+
* extrator (ADR-004). Usa `attachment.digest` quando presente; caso ausente
|
|
51
|
+
* (`undefined` ou `""`, típico de Redmine < 4.x), cai no fallback determinístico
|
|
52
|
+
* de {@link deriveAttachmentDigest}.
|
|
53
|
+
*
|
|
54
|
+
* @param input - Ver {@link BuildAttachmentKeyInput}.
|
|
55
|
+
* @returns Chave attachment-level pronta para o {@link CacheStore}.
|
|
56
|
+
* @throws {TypeError} Se `instanceUrl` não for uma URL válida (via {@link instanceHash}).
|
|
57
|
+
* @example
|
|
58
|
+
* const key = buildAttachmentKey({
|
|
59
|
+
* instanceUrl: 'https://redmine.example',
|
|
60
|
+
* attachment,
|
|
61
|
+
* extractor: { version: '1.0.0', model: 'whisper-large-v3', params: { language: 'pt' } },
|
|
62
|
+
* });
|
|
63
|
+
*/
|
|
64
|
+
export function buildAttachmentKey({ instanceUrl, attachment, extractor, }) {
|
|
65
|
+
// Presente = definido e não-vazio; qualquer outra forma cai no fallback.
|
|
66
|
+
const digest = attachment.digest !== undefined && attachment.digest !== ''
|
|
67
|
+
? attachment.digest
|
|
68
|
+
: deriveAttachmentDigest(attachment);
|
|
69
|
+
return {
|
|
70
|
+
kind: 'attachment',
|
|
71
|
+
instanceHash: instanceHash(instanceUrl),
|
|
72
|
+
attachmentId: attachment.id,
|
|
73
|
+
digest,
|
|
74
|
+
extractorVersion: extractor.version,
|
|
75
|
+
model: extractor.model,
|
|
76
|
+
params: extractor.params,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Implementação de REFERÊNCIA em memória do {@link CacheStore} (M3-01, ADR-004).
|
|
3
|
+
*
|
|
4
|
+
* Serve a dois propósitos: (1) fixar o comportamento esperado que a suíte de
|
|
5
|
+
* contrato valida e (2) ser um backend real usável em testes de camadas
|
|
6
|
+
* superiores. A implementação em disco (issues seguintes) satisfará o mesmo
|
|
7
|
+
* contrato e passará na mesma suíte.
|
|
8
|
+
*
|
|
9
|
+
* O lock por chave é promise-based: cada chave mantém a cauda de uma fila
|
|
10
|
+
* encadeada de promises; um novo pretendente espera a cauda atual antes de
|
|
11
|
+
* executar sua seção crítica. Um teto de "stale lock" evita deadlock permanente
|
|
12
|
+
* caso um detentor nunca libere.
|
|
13
|
+
*/
|
|
14
|
+
import { type CacheKey, type CacheStore, type CacheStoreOptions } from './contract.js';
|
|
15
|
+
/**
|
|
16
|
+
* {@link CacheStore} em memória (referência do contrato ADR-004). Não persiste
|
|
17
|
+
* entre processos; o isolamento por instância se dá pela própria chave.
|
|
18
|
+
*
|
|
19
|
+
* @typeParam V - Tipo do valor cacheado.
|
|
20
|
+
*/
|
|
21
|
+
export declare class InMemoryCacheStore<V = unknown> implements CacheStore<V> {
|
|
22
|
+
private readonly entries;
|
|
23
|
+
/** Cauda da fila de locks por chave serializada. */
|
|
24
|
+
private readonly lockTails;
|
|
25
|
+
private readonly logger;
|
|
26
|
+
private readonly onGc;
|
|
27
|
+
private readonly staleLockTtlMs;
|
|
28
|
+
/** @param options - Ver {@link CacheStoreOptions}. */
|
|
29
|
+
constructor(options?: CacheStoreOptions);
|
|
30
|
+
/** @inheritdoc */
|
|
31
|
+
get(key: CacheKey): Promise<V | undefined>;
|
|
32
|
+
/** @inheritdoc */
|
|
33
|
+
put(key: CacheKey, value: V): Promise<void>;
|
|
34
|
+
/** @inheritdoc */
|
|
35
|
+
invalidate(key: CacheKey): Promise<void>;
|
|
36
|
+
/** @inheritdoc */
|
|
37
|
+
lock<T>(key: CacheKey, critical: () => Promise<T>): Promise<T>;
|
|
38
|
+
/** @inheritdoc */
|
|
39
|
+
gc(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Espera o detentor anterior liberar, com teto de stale lock: se o detentor
|
|
42
|
+
* exceder `staleLockTtlMs`, assume-se que ele morreu e a aquisição prossegue
|
|
43
|
+
* (com aviso). `staleLockTtlMs <= 0` desabilita o teto (espera indefinida).
|
|
44
|
+
*/
|
|
45
|
+
private awaitHolder;
|
|
46
|
+
/** Aciona o hook de GC, se configurado, com a origem informada. */
|
|
47
|
+
private runGc;
|
|
48
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Implementação de REFERÊNCIA em memória do {@link CacheStore} (M3-01, ADR-004).
|
|
3
|
+
*
|
|
4
|
+
* Serve a dois propósitos: (1) fixar o comportamento esperado que a suíte de
|
|
5
|
+
* contrato valida e (2) ser um backend real usável em testes de camadas
|
|
6
|
+
* superiores. A implementação em disco (issues seguintes) satisfará o mesmo
|
|
7
|
+
* contrato e passará na mesma suíte.
|
|
8
|
+
*
|
|
9
|
+
* O lock por chave é promise-based: cada chave mantém a cauda de uma fila
|
|
10
|
+
* encadeada de promises; um novo pretendente espera a cauda atual antes de
|
|
11
|
+
* executar sua seção crítica. Um teto de "stale lock" evita deadlock permanente
|
|
12
|
+
* caso um detentor nunca libere.
|
|
13
|
+
*/
|
|
14
|
+
import { DEFAULT_STALE_LOCK_TTL_MS, serializeCacheKey, } from './contract.js';
|
|
15
|
+
/** Logger no-op: usado quando nenhum logger é injetado. Nunca `console.*`. */
|
|
16
|
+
const noopLogger = { warn: () => undefined };
|
|
17
|
+
/**
|
|
18
|
+
* {@link CacheStore} em memória (referência do contrato ADR-004). Não persiste
|
|
19
|
+
* entre processos; o isolamento por instância se dá pela própria chave.
|
|
20
|
+
*
|
|
21
|
+
* @typeParam V - Tipo do valor cacheado.
|
|
22
|
+
*/
|
|
23
|
+
export class InMemoryCacheStore {
|
|
24
|
+
entries = new Map();
|
|
25
|
+
/** Cauda da fila de locks por chave serializada. */
|
|
26
|
+
lockTails = new Map();
|
|
27
|
+
logger;
|
|
28
|
+
onGc;
|
|
29
|
+
staleLockTtlMs;
|
|
30
|
+
/** @param options - Ver {@link CacheStoreOptions}. */
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.logger = options.logger ?? noopLogger;
|
|
33
|
+
this.onGc = options.onGc;
|
|
34
|
+
this.staleLockTtlMs = options.staleLockTtlMs ?? DEFAULT_STALE_LOCK_TTL_MS;
|
|
35
|
+
}
|
|
36
|
+
/** @inheritdoc */
|
|
37
|
+
async get(key) {
|
|
38
|
+
return this.entries.get(serializeCacheKey(key));
|
|
39
|
+
}
|
|
40
|
+
/** @inheritdoc */
|
|
41
|
+
async put(key, value) {
|
|
42
|
+
this.entries.set(serializeCacheKey(key), value);
|
|
43
|
+
await this.runGc('put');
|
|
44
|
+
}
|
|
45
|
+
/** @inheritdoc */
|
|
46
|
+
async invalidate(key) {
|
|
47
|
+
this.entries.delete(serializeCacheKey(key));
|
|
48
|
+
}
|
|
49
|
+
/** @inheritdoc */
|
|
50
|
+
async lock(key, critical) {
|
|
51
|
+
const serial = serializeCacheKey(key);
|
|
52
|
+
const previous = this.lockTails.get(serial);
|
|
53
|
+
// Encadeia esta aquisição na cauda: o próximo pretendente esperará `current`.
|
|
54
|
+
let release;
|
|
55
|
+
const current = new Promise((resolve) => {
|
|
56
|
+
release = resolve;
|
|
57
|
+
});
|
|
58
|
+
this.lockTails.set(serial, current);
|
|
59
|
+
if (previous !== undefined) {
|
|
60
|
+
await this.awaitHolder(previous, serial);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return await critical();
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
release();
|
|
67
|
+
// Só limpa o mapa se ninguém entrou na fila depois de nós (evita vazar
|
|
68
|
+
// chaves) — caso contrário a cauda ainda é necessária para o próximo.
|
|
69
|
+
if (this.lockTails.get(serial) === current) {
|
|
70
|
+
this.lockTails.delete(serial);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** @inheritdoc */
|
|
75
|
+
async gc() {
|
|
76
|
+
await this.runGc('manual');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Espera o detentor anterior liberar, com teto de stale lock: se o detentor
|
|
80
|
+
* exceder `staleLockTtlMs`, assume-se que ele morreu e a aquisição prossegue
|
|
81
|
+
* (com aviso). `staleLockTtlMs <= 0` desabilita o teto (espera indefinida).
|
|
82
|
+
*/
|
|
83
|
+
async awaitHolder(previous, serial) {
|
|
84
|
+
if (this.staleLockTtlMs <= 0) {
|
|
85
|
+
await previous;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let timer;
|
|
89
|
+
const staleTimeout = new Promise((resolve) => {
|
|
90
|
+
timer = setTimeout(() => resolve('stale'), this.staleLockTtlMs);
|
|
91
|
+
});
|
|
92
|
+
const outcome = await Promise.race([
|
|
93
|
+
previous.then(() => 'released'),
|
|
94
|
+
staleTimeout,
|
|
95
|
+
]);
|
|
96
|
+
if (timer !== undefined) {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
}
|
|
99
|
+
if (outcome === 'stale') {
|
|
100
|
+
this.logger.warn(`Lock estagnado para a chave ${serial} após ${this.staleLockTtlMs}ms; prosseguindo.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Aciona o hook de GC, se configurado, com a origem informada. */
|
|
104
|
+
async runGc(reason) {
|
|
105
|
+
if (this.onGc === undefined) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await this.onGc({ reason, entryCount: this.entries.size });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leitura CACHE-FIRST não-bloqueante das extrações de anexos (M4-11, ADR-004).
|
|
3
|
+
*
|
|
4
|
+
* O núcleo do modo cache-first do MCP (#70): dado uma issue JÁ normalizada, para
|
|
5
|
+
* cada anexo com extrator registrado LÊ o cache attachment-level e devolve
|
|
6
|
+
* IMEDIATAMENTE o que já está pronto (`done`/`text`). Quando a extração de um
|
|
7
|
+
* anexo pesado (vídeo/áudio) ainda NÃO está no cache, NÃO espera a extração
|
|
8
|
+
* terminar: marca o anexo como {@link ExtractionStatus} `processing` com
|
|
9
|
+
* metadados e, opcionalmente, dispara o job de extração em SEGUNDO PLANO via a
|
|
10
|
+
* fila (`runQueue`, #67) — fire-and-forget. Assim nenhuma chamada MCP bloqueia
|
|
11
|
+
* aguardando mídia (resposta imediata; a continuação que completa na 2ª chamada
|
|
12
|
+
* é a #71).
|
|
13
|
+
*
|
|
14
|
+
* A leitura de cache (índice/`extraction.json`) é barata (ms); só a COMPUTAÇÃO da
|
|
15
|
+
* extração é cara — e é justamente ela que este módulo evita no caminho síncrono.
|
|
16
|
+
* A chave attachment-level é montada EXATAMENTE como no caminho bloqueante
|
|
17
|
+
* ({@link extractIssueAttachments}) — mesmos `probableMime`/`toExtractorConfig` —
|
|
18
|
+
* garantindo cache-hit coerente com o que o job de background grava.
|
|
19
|
+
*
|
|
20
|
+
* Fronteira ADR-005: módulo de core (pipeline de extração). Não importa de
|
|
21
|
+
* `src/surfaces/**` e nunca usa `console.*` — avisos vão pelo {@link Logger}.
|
|
22
|
+
*/
|
|
23
|
+
import { type AttachmentCacheKey, type CacheStore } from './cache/index.js';
|
|
24
|
+
import type { Logger } from './client/index.js';
|
|
25
|
+
import type { Attachment, ExtractionResult, Issue } from './contract.js';
|
|
26
|
+
import { type Extractor, type ExtractorRegistry } from './extract/index.js';
|
|
27
|
+
/**
|
|
28
|
+
* Constrói um {@link ExtractionResult} `processing` — o anexo existe e TEM
|
|
29
|
+
* extrator, mas a extração (cara) ainda não está no cache. Sinaliza à superfície
|
|
30
|
+
* que o texto virá em uma consulta futura (#71), sem bloquear a atual.
|
|
31
|
+
*
|
|
32
|
+
* @returns Resultado `processing` com motivo/dica legíveis (sem texto).
|
|
33
|
+
*/
|
|
34
|
+
export declare function processingResult(): ExtractionResult;
|
|
35
|
+
/** Alvo de uma extração pendente descoberta no caminho cache-first (cache-miss). */
|
|
36
|
+
export interface BackgroundExtractionTarget {
|
|
37
|
+
/** Anexo cujo texto ainda não está no cache. */
|
|
38
|
+
readonly attachment: Attachment;
|
|
39
|
+
/** Extrator resolvido pelo MIME provável do anexo. */
|
|
40
|
+
readonly extractor: Extractor;
|
|
41
|
+
/** Chave attachment-level já montada (idêntica à do caminho bloqueante). */
|
|
42
|
+
readonly key: AttachmentCacheKey;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Dispatcher de extração em SEGUNDO PLANO (#70/#71). Recebe um anexo com
|
|
46
|
+
* cache-miss e DEVE retornar imediatamente (fire-and-forget): jamais aguarda a
|
|
47
|
+
* extração. Injetável para testes (um fake determinístico que não espera de fato).
|
|
48
|
+
*/
|
|
49
|
+
export type BackgroundExtractor = (target: BackgroundExtractionTarget) => void;
|
|
50
|
+
/** Opções de {@link extractIssueAttachmentsCacheFirst}. */
|
|
51
|
+
export interface CacheFirstExtractionOptions {
|
|
52
|
+
/** URL base da instância — compõe o `instance_hash` da chave (ADR-004). */
|
|
53
|
+
instanceUrl: string;
|
|
54
|
+
/** Registry consultado para achar o extrator do MIME provável de cada anexo. */
|
|
55
|
+
registry: ExtractorRegistry;
|
|
56
|
+
/** Store de cache attachment-level (memória ou disco) — apenas LIDO aqui. */
|
|
57
|
+
store: CacheStore<ExtractionResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Dispatcher opcional do job em background (#71). Chamado UMA vez por anexo com
|
|
60
|
+
* cache-miss. Ausente = apenas sinaliza `processing` (o #70 não exige disparar).
|
|
61
|
+
*/
|
|
62
|
+
background?: BackgroundExtractor;
|
|
63
|
+
/** Logger para avisos (ex.: falha ao ler o cache); default no-op. Nunca `console.*`. */
|
|
64
|
+
logger?: Logger;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Lê o cache das extrações de TODOS os anexos com extrator registrado de uma
|
|
68
|
+
* issue, SEM computar nenhuma extração (não-bloqueante).
|
|
69
|
+
*
|
|
70
|
+
* Para cada anexo:
|
|
71
|
+
* - sem extrator para o MIME provável → nem entra no mapa (igual ao bloqueante);
|
|
72
|
+
* - cache-hit → o {@link ExtractionResult} pronto (texto imediato);
|
|
73
|
+
* - cache-miss → `processing` + (opcional) dispara o job de background.
|
|
74
|
+
*
|
|
75
|
+
* @param issue - Issue normalizada cujos anexos serão consultados no cache.
|
|
76
|
+
* @param options - Ver {@link CacheFirstExtractionOptions}.
|
|
77
|
+
* @returns `Map<attachmentId, ExtractionResult>` só com anexos que têm extrator.
|
|
78
|
+
* @example
|
|
79
|
+
* const map = await extractIssueAttachmentsCacheFirst(issue, {
|
|
80
|
+
* instanceUrl, registry, store, background,
|
|
81
|
+
* });
|
|
82
|
+
*/
|
|
83
|
+
export declare function extractIssueAttachmentsCacheFirst(issue: Issue, options: CacheFirstExtractionOptions): Promise<Map<number, ExtractionResult>>;
|
|
84
|
+
/**
|
|
85
|
+
* Computa a extração de UM anexo em background. Recebe o alvo do cache-miss e o
|
|
86
|
+
* `AbortSignal` da fila (#69); deve resolver com o {@link ExtractionResult}
|
|
87
|
+
* gravado no cache (ou reportar falha por rejeição, isolada pela fila).
|
|
88
|
+
*/
|
|
89
|
+
export type BackgroundCompute = (target: BackgroundExtractionTarget, signal: AbortSignal | undefined) => Promise<ExtractionResult>;
|
|
90
|
+
/** Opções de {@link makeQueueBackgroundExtractor}. */
|
|
91
|
+
export interface QueueBackgroundOptions {
|
|
92
|
+
/** Logger para avisos de jobs que falharam/abortaram (ADR-005; sem `console.*`). */
|
|
93
|
+
logger?: Logger;
|
|
94
|
+
/** Sinal de cancelamento propagado à fila e aos jobs (#69). */
|
|
95
|
+
signal?: AbortSignal;
|
|
96
|
+
/**
|
|
97
|
+
* Store attachment-level onde o resultado do job de background é PERSISTIDO ao
|
|
98
|
+
* concluir (#71), sob a MESMA chave que a leitura cache-first busca. Fecha o
|
|
99
|
+
* loop do #70: a 2ª chamada (pós-conclusão) acerta o cache e devolve o resultado
|
|
100
|
+
* completo — e uma FALHA vira `failed` PERSISTIDO, nunca `processing` eterno.
|
|
101
|
+
* Ausente = comportamento #70 (fire-and-forget sem persistência de fechamento).
|
|
102
|
+
*/
|
|
103
|
+
store?: CacheStore<ExtractionResult>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Cria um {@link BackgroundExtractor} respaldado pela fila de jobs (#67): cada
|
|
107
|
+
* anexo com cache-miss vira UM {@link QueueJob} rodado por `runQueue`, cujas
|
|
108
|
+
* transições são consumidas de forma DESACOPLADA (fire-and-forget). O dispatcher
|
|
109
|
+
* retorna imediatamente — o trabalho caro acontece na fila, fora do caminho da
|
|
110
|
+
* resposta MCP. O `AbortSignal` da fila é propagado ao `compute` via
|
|
111
|
+
* {@link JobContext.signal} (#69), pronto para o kill de subprocesso do #68.
|
|
112
|
+
*
|
|
113
|
+
* DEDUP DE IN-FLIGHT (#71): o dispatcher mantém um registro dos jobs em andamento
|
|
114
|
+
* POR CHAVE attachment-level. Uma 2ª chamada para um anexo cujo job ainda processa
|
|
115
|
+
* NÃO dispara um job duplicado — reusa o que já corre. O registro é limpo quando o
|
|
116
|
+
* job conclui (via `finally`), liberando futuras re-extrações (ex.: cache invalidado).
|
|
117
|
+
*
|
|
118
|
+
* @param compute - Executa a extração real do anexo — ver {@link BackgroundCompute}.
|
|
119
|
+
* @param options - Logger/signal/store opcionais — ver {@link QueueBackgroundOptions}.
|
|
120
|
+
* @returns Um dispatcher fire-and-forget para injetar em {@link CacheFirstExtractionOptions.background}.
|
|
121
|
+
* @example
|
|
122
|
+
* const background = makeQueueBackgroundExtractor(async (target) => {
|
|
123
|
+
* const map = await extractIssueAttachments(http, single, { store, registry, instanceUrl });
|
|
124
|
+
* return map.get(target.attachment.id) ?? processingResult();
|
|
125
|
+
* }, { store });
|
|
126
|
+
*/
|
|
127
|
+
export declare function makeQueueBackgroundExtractor(compute: BackgroundCompute, options?: QueueBackgroundOptions): BackgroundExtractor;
|