import { davConditionalFailure } from "@/worker/dav/conditional"; import type { ParsedIfHeader } from "@/worker/dav/if-header"; import { parseCollectionObjectPath } from "@/worker/dav/paths"; export interface CollectionConditionalInput { subjectId: string; nowMs: number; ifMatch: string | null; ifNoneMatch: string | null; ifHeader: ParsedIfHeader; } export interface CollectionPreconditionSpec { basePath: string; collectionHref: (collectionName: string) => string; ensureCollection: (db: TDb, subjectId: string, nowMs: number, collectionName: string) => TCollection | null; objectEtag: (db: TDb, collectionId: string, objectName: string) => string | null; latestChangeSeq: (db: TDb) => number; syncToken: (seq: number) => string; preconditionFailed: (message: string) => TError; } export function collectionWritePrecondition( db: TDb, input: CollectionConditionalInput, targetHref: string, targetEtag: string | null, spec: CollectionPreconditionSpec, ): TError | null { const stateHref = collectionStateHrefForTarget(targetHref, spec.basePath, spec.collectionHref); const failure = davConditionalFailure({ ifMatch: input.ifMatch, ifNoneMatch: input.ifNoneMatch, ifHeader: input.ifHeader, targetHref, targetEtag, getEtag: (href) => collectionEtagForHref(db, input, href, spec), getStateToken: (href) => (href === stateHref ? collectionStateTokenForHref(db, input, href, spec) : null), }); return failure ? spec.preconditionFailed(failure) : null; } function collectionStateHrefForTarget( targetHref: string, basePath: string, collectionHref: (collectionName: string) => string, ): string | null { const path = parseCollectionObjectPath(targetHref, basePath); if (!path || path.kind === "home") return null; return collectionHref(path.collectionName); } function collectionEtagForHref( db: TDb, input: CollectionConditionalInput, href: string, spec: CollectionPreconditionSpec, ): string | null { const path = parseCollectionObjectPath(href, spec.basePath); if (!path || path.kind !== "object") return null; const collection = spec.ensureCollection(db, input.subjectId, input.nowMs, path.collectionName); if (!collection) return null; return spec.objectEtag(db, collection.id, path.objectName); } function collectionStateTokenForHref( db: TDb, input: CollectionConditionalInput, href: string, spec: CollectionPreconditionSpec, ): string | null { const path = parseCollectionObjectPath(href, spec.basePath); if (!path || path.kind !== "collection") return null; const collection = spec.ensureCollection(db, input.subjectId, input.nowMs, path.collectionName); return collection ? spec.syncToken(spec.latestChangeSeq(db)) : null; }