Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions src/connectors/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,134 @@ export async function resolveUrl(url: string): Promise<ResolutionResult | null>
return connector.resolve(url)
}

/**
* Resolve a URL using all matching connectors and merge results.
*
* Returns a merged result with contributions from all connectors that can handle the URL.
* Results are merged with priority: higher quality results take precedence for core fields.
*/
export async function resolveAll(url: string): Promise<ResolutionResult | null> {
const connectors = registry.findAllForUrl(url)

if (connectors.length === 0) {
// No connectors found — return a minimal fallback
return createFallbackResult(url)
}

if (connectors.length === 1) {
// Single connector — use it directly
return connectors[0]!.resolve(url)
}

// Multiple connectors — resolve with all and merge results
// Note: Promise.all runs all connectors simultaneously for parallelism.
// For URLs matching many connectors, this could be expensive.
const results = await Promise.all(
connectors.map(connector => connector.resolve(url).catch(err => {
console.warn(`Connector "${connector.id}" failed:`, err)
return null
}))
)

// Filter out failed resolutions
const validResults = results.filter(Boolean) as ResolutionResult[]

// Guard against empty results array - all connectors failed
if (validResults.length === 0) {
return createFallbackResult(url)
}

return mergeResults(validResults, url)
}

/**
* Merge multiple resolution results into a single result.
*
* Priority system:
* - Quality: highest quality result's core fields take precedence
* - Representations: all unique representations are included
* - Annotations: all unique annotations are included
* - SuggestedTools: union of all tools, deduplicated
* - SuggestedActions: union of all actions, deduplicated
* - Warnings: all warnings are included
* - Source: attributed to all contributing connectors
*/
function mergeResults(results: ResolutionResult[], sourceUrl: string): ResolutionResult {
// Sort by quality: high > medium > low
const qualityOrder = { high: 3, medium: 2, low: 1 }
const sorted = [...results].sort((a, b) => {
const aQuality = qualityOrder[a.quality || "low"] || 0
const bQuality = qualityOrder[b.quality || "low"] || 0
return bQuality - aQuality
})

// Primary result (highest quality) provides core fields
const primary = sorted[0]!

// Merge representations (deduplicate by @id)
const seenIds = new Set<string>()
const mergedRepresentations: ResolutionResult["representations"] = []
for (const result of sorted) {
for (const rep of result.representations || []) {
const repId = rep["@id"] || rep["@type"]
if (!seenIds.has(repId)) {
seenIds.add(repId)
mergedRepresentations.push(rep)
}
}
}

// Merge annotations (deduplicate by @id, with generated fallback)
// Note: oa:hasTarget is not unique - two annotations can target the same resource.
// We use @id only, with a generated fallback to avoid false deduplication.
const seenAnnotationIds = new Set<string>()
const mergedAnnotations: ResolutionResult["annotations"] = []
for (const result of sorted) {
for (const ann of result.annotations || []) {
const annId = ann["@id"] || `ann-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
if (!seenAnnotationIds.has(annId)) {
seenAnnotationIds.add(annId)
mergedAnnotations.push(ann)
}
}
}

// Merge suggested tools (union, preserve order)
const mergedTools = Array.from(new Set(
sorted.flatMap(r => r.suggestedTools || [])
))

// Merge suggested actions (union, preserve order)
const mergedActions = Array.from(new Set(
sorted.flatMap(r => r.suggestedActions || [])
))

// Merge warnings (deduplicated - same warning from multiple connectors appears once)
const mergedWarnings = Array.from(new Set(
sorted.flatMap(r => r.warnings || [])
))

// Connector attribution
const connectorIds = sorted.map(r => r.connector).filter(Boolean)
const connector = connectorIds.length > 1
? `merged(${Array.from(new Set(connectorIds)).join(",")})`
: connectorIds[0] || "unknown"

return {
sourceUrl,
suggestedThingId: primary.suggestedThingId,
suggestedLabel: primary.suggestedLabel,
suggestedType: primary.suggestedType,
representations: mergedRepresentations,
annotations: mergedAnnotations,
suggestedTools: mergedTools,
suggestedActions: mergedActions,
warnings: mergedWarnings,
connector,
quality: primary.quality,
}
}

/**
* Resolve a URL using a specific connector by ID.
*/
Expand Down
Loading