1import type { EnvironmentId } from "@t3tools/contracts";
2import * as Effect from "effect/Effect";
3import * as PartitionedSemaphore from "effect/PartitionedSemaphore";
4import { Atom, type AtomRegistry } from "effect/reactivity";
5
6import { safeErrorLogAttributes } from "../errors/safeLog.ts";
7import * as Persistence from "../platform/persistence.ts";
8
9export interface VcsRefsInvalidationTarget {
10 readonly environmentId: EnvironmentId;
11}
12
13export interface CachedVcsRefsInvalidationTarget extends VcsRefsInvalidationTarget {
14 readonly cwd: string;
15}
16
17export interface VcsRefsCacheState {
18 readonly revision: number;
19 readonly persistedCacheReadable: boolean;
20}
21
22const stateByEnvironment = Atom.family((environmentId: EnvironmentId) =>
23 Atom.make<VcsRefsCacheState>({
24 revision: 0,
25 persistedCacheReadable: true,
26 }).pipe(Atom.keepAlive, Atom.withLabel(`environment-data:vcs:list-refs-state:${environmentId}`)),
27);
28const persistenceLock = PartitionedSemaphore.makeUnsafe<EnvironmentId>({ permits: 1 });
29
30export function vcsRefsCacheStateAtom(target: VcsRefsInvalidationTarget) {
31 return stateByEnvironment(target.environmentId);
32}
33
34export function invalidateVcsRefs(
35 registry: AtomRegistry.AtomRegistry,
36 target: VcsRefsInvalidationTarget,
37 persistedCacheReadable?: boolean,
38): void {
39 registry.update(vcsRefsCacheStateAtom(target), (state) => ({
40 revision: state.revision + 1,
41 persistedCacheReadable: persistedCacheReadable ?? state.persistedCacheReadable,
42 }));
43}
44
45export function withVcsRefsPersistenceLock<A, E, R>(
46 environmentId: EnvironmentId,
47 effect: Effect.Effect<A, E, R>,
48): Effect.Effect<A, E, R> {
49 return persistenceLock.withPermit(environmentId)(effect);
50}
51
52/**
53 * Clears persisted snapshots and then restarts live ref streams while holding
54 * the same lock used by generation-checked refresh writes. Clearing first
55 * prevents the restarted stream from rehydrating the invalidated snapshot. A
56 * refresh that wins the lock is cleared afterward; one that loses observes the
57 * new revision and cannot repersist its pre-mutation result.
58 */
59export const invalidateCachedVcsRefs = Effect.fn("VcsRefsState.invalidateCached")(function* (
60 registry: AtomRegistry.AtomRegistry,
61 target: CachedVcsRefsInvalidationTarget,
62) {
63 const cache = yield* Persistence.EnvironmentCacheStore;
64 yield* withVcsRefsPersistenceLock(
65 target.environmentId,
66 Effect.gen(function* () {
67 const persistedCacheReadable = yield* cache.clearVcsRefs(target.environmentId).pipe(
68 Effect.as(true),
69 Effect.catch((error) =>
70 Effect.logWarning("Could not remove invalidated cached Git refs.").pipe(
71 Effect.annotateLogs({
72 environmentId: target.environmentId,
73 cwd: target.cwd,
74 ...safeErrorLogAttributes(error),
75 }),
76 Effect.as(false),
77 ),
78 ),
79 );
80 invalidateVcsRefs(registry, target, persistedCacheReadable);
81 }),
82 );
83});