101 lines
5.2 KiB
TypeScript
101 lines
5.2 KiB
TypeScript
import type { SubshapeRef, TopoRefValue } from './types'
|
|
import { matchSubshapes, type SubshapeMatch, type SubshapeSignature } from './topologyNaming'
|
|
|
|
export type PersistedTopoRef = TopoRefValue
|
|
|
|
export type TopoRefResolution = {
|
|
status: 'resolved' | 'ambiguous' | 'deleted'
|
|
ref?: SubshapeRef
|
|
candidates?: SubshapeRef[]
|
|
}
|
|
|
|
export type TopologyMigration = {
|
|
records: PersistedTopoRef[]
|
|
matches: SubshapeMatch[]
|
|
counts: Record<NonNullable<SubshapeRef['status']>, number>
|
|
}
|
|
|
|
const transientIndexKeys = new Set(['faceIndex', 'edgeIndex', 'vertexIndex', 'subshapeIndex'])
|
|
const statuses = new Set<NonNullable<SubshapeRef['status']>>(['stable', 'ambiguous', 'new', 'deleted'])
|
|
const kinds = new Set<SubshapeRef['kind']>(['face', 'edge', 'vertex'])
|
|
|
|
const finiteNonNegativeInteger = (value: unknown) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0
|
|
|
|
export const createPersistedTopoRef = (objectId: string, ref: SubshapeRef, generation: number): PersistedTopoRef => {
|
|
if (!objectId.trim()) throw new RangeError('TopoRef objectId is required.')
|
|
if (!finiteNonNegativeInteger(generation)) throw new RangeError('TopoRef generation must be a non-negative integer.')
|
|
if (!ref.persistentId.trim()) throw new RangeError('TopoRef persistentId is required.')
|
|
if (!finiteNonNegativeInteger(ref.topologyVersion)) throw new RangeError('TopoRef topologyVersion must be a non-negative integer.')
|
|
return {
|
|
schemaVersion: 1,
|
|
objectId,
|
|
kind: ref.kind,
|
|
persistentId: ref.persistentId,
|
|
topologyVersion: ref.topologyVersion,
|
|
generation,
|
|
status: ref.status ?? 'stable',
|
|
signature: ref.signature,
|
|
candidates: ref.candidates ? [...new Set(ref.candidates)] : undefined,
|
|
}
|
|
}
|
|
|
|
export const serializeTopoRef = (record: PersistedTopoRef) => JSON.stringify(createPersistedTopoRef(record.objectId, {
|
|
shapeId: record.objectId,
|
|
kind: record.kind,
|
|
persistentId: record.persistentId,
|
|
topologyVersion: record.topologyVersion,
|
|
status: record.status,
|
|
signature: record.signature,
|
|
candidates: record.candidates,
|
|
}, record.generation))
|
|
|
|
export const parseTopoRef = (json: string): PersistedTopoRef => {
|
|
let value: unknown
|
|
try { value = JSON.parse(json) } catch { throw new SyntaxError('TopoRef JSON is invalid.') }
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('TopoRef must be a JSON object.')
|
|
const source = value as Record<string, unknown>
|
|
for (const key of transientIndexKeys) if (key in source) throw new RangeError(`TopoRef must not persist transient ${key}.`)
|
|
if (source.schemaVersion !== 1) throw new RangeError('TopoRef schemaVersion is not supported.')
|
|
if (typeof source.objectId !== 'string' || typeof source.persistentId !== 'string') throw new TypeError('TopoRef objectId and persistentId must be strings.')
|
|
if (!kinds.has(source.kind as SubshapeRef['kind'])) throw new RangeError('TopoRef kind is invalid.')
|
|
if (!statuses.has(source.status as NonNullable<SubshapeRef['status']>)) throw new RangeError('TopoRef status is invalid.')
|
|
if (!finiteNonNegativeInteger(source.topologyVersion) || !finiteNonNegativeInteger(source.generation)) throw new RangeError('TopoRef version fields must be non-negative integers.')
|
|
if (source.signature !== undefined && typeof source.signature !== 'string') throw new TypeError('TopoRef signature must be a string.')
|
|
if (source.candidates !== undefined && (!Array.isArray(source.candidates) || !source.candidates.every((candidate) => typeof candidate === 'string'))) throw new TypeError('TopoRef candidates must be strings.')
|
|
return createPersistedTopoRef(source.objectId, {
|
|
shapeId: source.objectId,
|
|
kind: source.kind as SubshapeRef['kind'],
|
|
persistentId: source.persistentId,
|
|
topologyVersion: source.topologyVersion as number,
|
|
status: source.status as NonNullable<SubshapeRef['status']>,
|
|
signature: source.signature as string | undefined,
|
|
candidates: source.candidates as string[] | undefined,
|
|
}, source.generation as number)
|
|
}
|
|
|
|
export const resolveTopoRef = (record: PersistedTopoRef, current: SubshapeRef[]): TopoRefResolution => {
|
|
const sameKind = current.filter((ref) => ref.kind === record.kind)
|
|
const byId = sameKind.filter((ref) => ref.persistentId === record.persistentId)
|
|
if (byId.length === 1 && byId[0].status !== 'ambiguous') return { status: 'resolved', ref: byId[0] }
|
|
const bySignature = record.signature ? sameKind.filter((ref) => ref.signature === record.signature) : []
|
|
const candidates = byId.length > 0 ? byId : bySignature
|
|
if (candidates.length === 1 && candidates[0].status !== 'ambiguous') return { status: 'resolved', ref: candidates[0] }
|
|
if (candidates.length > 0) return { status: 'ambiguous', candidates }
|
|
return { status: 'deleted' }
|
|
}
|
|
|
|
export const migrateTopoRefs = (
|
|
objectId: string,
|
|
previous: Array<{ ref: SubshapeRef; signature: SubshapeSignature }>,
|
|
current: Array<{ ref: SubshapeRef; signature: SubshapeSignature }>,
|
|
generation: number,
|
|
): TopologyMigration => {
|
|
const matches = matchSubshapes(previous, current)
|
|
const counts: TopologyMigration['counts'] = { stable: 0, ambiguous: 0, new: 0, deleted: 0 }
|
|
const records = matches.map((match) => {
|
|
counts[match.status] += 1
|
|
return createPersistedTopoRef(objectId, match.current, generation)
|
|
})
|
|
return { records, matches, counts }
|
|
}
|