Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-core/src/packet-archive.ts

Snapshot at 34e3175

Containing directory

2,463 bytes · typescriptView raw
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