Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-core/src/packet-archive.ts
Snapshot at 34e3175
1import { HttpError } from "./http";
2import { publicPacket } from "./packets";
3import { openEntrants } from "./race-entrants";
4import type { Blocker, Packet, PacketArchiveAudit, Review } from "./workflow-types";
5
6/** FH-048 records; absent legacy resolution and pushing identity remain open/null. */
7type ArchiveBlocker = Pick<Blocker, "packet"> & Partial<Pick<Blocker, "resolved" | "pushingPacket">>;
8
9/** Called only after owner authorization, under the Supervisor serialization gate. */
10export async function archivePacket(
11 storage: DurableObjectStorage,
12 packetId: string,
13 action: PacketArchiveAudit["action"],
14): Promise<Packet> {
15 return storage.transaction(async txn => {
16 const stored = await txn.get<Packet>(`packet:${packetId}`);
17 if (!stored) throw new HttpError(404, "Packet not found.");
18 const packet = publicPacket(stored);
19 // Repeats preserve the timestamp and audit, even if later history is inconsistent.
20 if ((action === "archive") === (packet.archived !== null)) return packet;
21 if (action === "archive") {
22 if (await txn.get(`claim:${packetId}`) || (await openEntrants(txn, packetId)).length)
23 throw new HttpError(409, "Packet has a live claim.");
24 const reviews = await txn.list<Review>({ prefix: "review:" });
25 if ([...reviews.values()].some(review => review.packet === packetId &&
26 !review.promoted && !review.stale && !review.returned))
27 throw new HttpError(409, "Packet has an open review.");
28 const blockers = await txn.list<ArchiveBlocker>({ prefix: "blocker:" });
29 if ([...blockers.values()].some(blocker => blocker.resolved == null &&
30 (blocker.packet === packetId || blocker.pushingPacket === packetId)))
31 throw new HttpError(409, "Packet has an open blocker.");
32 }
33 // Keep the fixed timestamp key unique during rapid transitions in one millisecond.
34 const previous = [...(await txn.list<PacketArchiveAudit>({
35 prefix: `packetArchive:${packetId}:`, reverse: true, limit: 1,
36 })).values()][0];
37 const recordedAt = new Date(Math.max(Date.now(), previous ? Date.parse(previous.recordedAt) + 1 : 0)).toISOString();
38 packet.archived = action === "archive" ? { at: recordedAt, by: "owner" } : null;
39 const audit: PacketArchiveAudit = { packet: packetId, action, by: "owner", recordedAt };
40 await txn.put({ [`packet:${packetId}`]: packet, [`packetArchive:${packetId}:${recordedAt}`]: audit });
41 return packet;
42 });
43}
44