Implements Phases 1-8 of the TFTSR implementation plan. Rust backend (Tauri 2.x, src-tauri/): - Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama - PII detection engine: 11 regex patterns with overlap resolution - SQLCipher AES-256 encrypted database with 10 versioned migrations - 28 Tauri IPC commands for triage, analysis, document, and system ops - Ollama: hardware probe, model recommendations, pull/delete with events - RCA and blameless post-mortem Markdown document generators - PDF export via printpdf - Audit log: SHA-256 hash of every external data send - Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2) Frontend (React 18 + TypeScript + Vite, src/): - 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings - 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives - 3 Zustand stores: session, settings (persisted), history - Type-safe tauriCommands.ts matching Rust backend types exactly - 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs) DevOps: - .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push - .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload Verified: - cargo check: zero errors - tsc --noEmit: zero errors - vitest run: 13/13 unit tests passing Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
118 lines
4.0 KiB
JavaScript
118 lines
4.0 KiB
JavaScript
'use strict'
|
|
|
|
const diagnosticsChannel = require('node:diagnostics_channel')
|
|
const util = require('../core/util')
|
|
const DeduplicationHandler = require('../handler/deduplication-handler')
|
|
const { normalizeHeaders, makeCacheKey, makeDeduplicationKey } = require('../util/cache.js')
|
|
|
|
const pendingRequestsChannel = diagnosticsChannel.channel('undici:request:pending-requests')
|
|
|
|
/**
|
|
* @param {import('../../types/interceptors.d.ts').default.DeduplicateInterceptorOpts} [opts]
|
|
* @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
|
|
*/
|
|
module.exports = (opts = {}) => {
|
|
const {
|
|
methods = ['GET'],
|
|
skipHeaderNames = [],
|
|
excludeHeaderNames = [],
|
|
maxBufferSize = 5 * 1024 * 1024
|
|
} = opts
|
|
|
|
if (typeof opts !== 'object' || opts === null) {
|
|
throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`)
|
|
}
|
|
|
|
if (!Array.isArray(methods)) {
|
|
throw new TypeError(`expected opts.methods to be an array, got ${typeof methods}`)
|
|
}
|
|
|
|
for (const method of methods) {
|
|
if (!util.safeHTTPMethods.includes(method)) {
|
|
throw new TypeError(`expected opts.methods to only contain safe HTTP methods, got ${method}`)
|
|
}
|
|
}
|
|
|
|
if (!Array.isArray(skipHeaderNames)) {
|
|
throw new TypeError(`expected opts.skipHeaderNames to be an array, got ${typeof skipHeaderNames}`)
|
|
}
|
|
|
|
if (!Array.isArray(excludeHeaderNames)) {
|
|
throw new TypeError(`expected opts.excludeHeaderNames to be an array, got ${typeof excludeHeaderNames}`)
|
|
}
|
|
|
|
if (!Number.isFinite(maxBufferSize) || maxBufferSize <= 0) {
|
|
throw new TypeError(`expected opts.maxBufferSize to be a positive finite number, got ${maxBufferSize}`)
|
|
}
|
|
|
|
// Convert to lowercase Set for case-insensitive header matching
|
|
const skipHeaderNamesSet = new Set(skipHeaderNames.map(name => name.toLowerCase()))
|
|
|
|
// Convert to lowercase Set for case-insensitive header exclusion from deduplication key
|
|
const excludeHeaderNamesSet = new Set(excludeHeaderNames.map(name => name.toLowerCase()))
|
|
|
|
/**
|
|
* Map of pending requests for deduplication
|
|
* @type {Map<string, DeduplicationHandler>}
|
|
*/
|
|
const pendingRequests = new Map()
|
|
|
|
return dispatch => {
|
|
return (opts, handler) => {
|
|
if (!opts.origin || methods.includes(opts.method) === false) {
|
|
return dispatch(opts, handler)
|
|
}
|
|
|
|
opts = {
|
|
...opts,
|
|
headers: normalizeHeaders(opts)
|
|
}
|
|
|
|
// Skip deduplication if request contains any of the specified headers
|
|
if (skipHeaderNamesSet.size > 0) {
|
|
for (const headerName of Object.keys(opts.headers)) {
|
|
if (skipHeaderNamesSet.has(headerName.toLowerCase())) {
|
|
return dispatch(opts, handler)
|
|
}
|
|
}
|
|
}
|
|
|
|
const cacheKey = makeCacheKey(opts)
|
|
const dedupeKey = makeDeduplicationKey(cacheKey, excludeHeaderNamesSet)
|
|
|
|
// Check if there's already a pending request for this key
|
|
const pendingHandler = pendingRequests.get(dedupeKey)
|
|
if (pendingHandler) {
|
|
// Add this handler to the waiting list when safe.
|
|
// If body streaming has already started, this request must be sent independently.
|
|
if (pendingHandler.addWaitingHandler(handler)) {
|
|
return true
|
|
}
|
|
|
|
return dispatch(opts, handler)
|
|
}
|
|
|
|
// Create a new deduplication handler
|
|
const deduplicationHandler = new DeduplicationHandler(
|
|
handler,
|
|
() => {
|
|
// Clean up when request completes
|
|
pendingRequests.delete(dedupeKey)
|
|
if (pendingRequestsChannel.hasSubscribers) {
|
|
pendingRequestsChannel.publish({ size: pendingRequests.size, key: dedupeKey, type: 'removed' })
|
|
}
|
|
},
|
|
maxBufferSize
|
|
)
|
|
|
|
// Register the pending request
|
|
pendingRequests.set(dedupeKey, deduplicationHandler)
|
|
if (pendingRequestsChannel.hasSubscribers) {
|
|
pendingRequestsChannel.publish({ size: pendingRequests.size, key: dedupeKey, type: 'added' })
|
|
}
|
|
|
|
return dispatch(opts, deduplicationHandler)
|
|
}
|
|
}
|
|
}
|