Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 77 additions & 13 deletions apps/daemon/src/collab/active-workspace-selection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fs from 'node:fs';
import { randomUUID } from 'node:crypto';
import path from 'node:path';

interface ActiveWorkspaceSelectionFile {
Expand All @@ -10,6 +11,11 @@ export interface ActiveWorkspaceSelectionStore {
snapshot(): { workspaceId: string | null; generation: number };
set(workspaceId: string): Promise<void>;
clear(): Promise<void>;
clearIf(workspaceId: string): Promise<boolean>;
replaceIf(
expectedWorkspaceId: string | null,
workspaceId: string,
): Promise<string | null>;
subscribe(listener: (workspaceId: string | null) => void): () => void;
}

Expand Down Expand Up @@ -53,6 +59,7 @@ export function createActiveWorkspaceSelectionStore(
const filePath = path.join(dataDir, 'workspace-selection.json');
let cached: string | null | undefined;
let generation = 0;
let mutationTail = Promise.resolve();
const listeners = new Set<(workspaceId: string | null) => void>();

const read = (): string | null => {
Expand All @@ -79,6 +86,37 @@ export function createActiveWorkspaceSelectionStore(
}
};

const enqueueMutation = <T>(mutation: () => Promise<T>): Promise<T> => {
const result = mutationTail.then(mutation);
mutationTail = result.then(
() => undefined,
() => undefined,
);
return result;
};

const persist = async (workspaceId: string) => {
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
try {
await fs.promises.writeFile(
tempPath,
JSON.stringify({ workspaceId }, null, 2),
'utf8',
);
await fs.promises.rename(tempPath, filePath);
} catch (error) {
await fs.promises.rm(tempPath, { force: true }).catch(() => undefined);
throw error;
}
};

const commit = (workspaceId: string) => {
cached = workspaceId;
generation += 1;
notify(workspaceId);
};

return {
get: read,
snapshot() {
Expand All @@ -87,21 +125,47 @@ export function createActiveWorkspaceSelectionStore(
async set(workspaceId: string) {
const next = workspaceId.trim();
if (!next) throw new Error('workspaceId is required');
cached = next;
generation += 1;
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
await fs.promises.writeFile(
filePath,
JSON.stringify({ workspaceId: next }, null, 2),
'utf8',
);
notify(next);
await enqueueMutation(async () => {
await persist(next);
commit(next);
});
},
async clear() {
cached = null;
generation += 1;
await fs.promises.rm(filePath, { force: true });
notify(null);
await enqueueMutation(async () => {
await fs.promises.rm(filePath, { force: true });
cached = null;
generation += 1;
notify(null);
});
},
async clearIf(workspaceId: string) {
const expected = workspaceId.trim();
if (!expected) return false;
return enqueueMutation(async () => {
if (read() !== expected) return false;
await fs.promises.rm(filePath, { force: true });
cached = null;
generation += 1;
notify(null);
return true;
});
},
async replaceIf(expectedWorkspaceId: string | null, workspaceId: string) {
const expected = expectedWorkspaceId?.trim() || null;
const next = workspaceId.trim();
if (!next) throw new Error('workspaceId is required');
await enqueueMutation(async () => {
if (read() !== expected) return;
await persist(next);
commit(next);
});

// A user switch can queue while the conditional write is in flight.
// Drain mutations that were already queued when this write settled, then
// report the selection that actually won instead of the temporary value.
const queuedThroughCommit = mutationTail;
await queuedThroughCommit;
return read();
},
subscribe(listener) {
listeners.add(listener);
Expand Down
Loading
Loading