Skip to content

Commit 5f1fe13

Browse files
committed
Remove saved environments atomically
1 parent b7c2e20 commit 5f1fe13

16 files changed

Lines changed: 136 additions & 11 deletions

apps/desktop/src/ipc/DesktopIpcHandlers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getClientSettings, setClientSettings } from "./methods/clientSettings.t
55
import {
66
getSavedEnvironmentRegistry,
77
getSavedEnvironmentSecret,
8+
removeSavedEnvironment,
89
removeSavedEnvironmentSecret,
910
setSavedEnvironmentRegistry,
1011
setSavedEnvironmentSecret,
@@ -52,6 +53,7 @@ export const installDesktopIpcHandlers = Effect.gen(function* () {
5253
yield* ipc.handle(setClientSettings);
5354
yield* ipc.handle(getSavedEnvironmentRegistry);
5455
yield* ipc.handle(setSavedEnvironmentRegistry);
56+
yield* ipc.handle(removeSavedEnvironment);
5557
yield* ipc.handle(getSavedEnvironmentSecret);
5658
yield* ipc.handle(setSavedEnvironmentSecret);
5759
yield* ipc.handle(removeSavedEnvironmentSecret);

apps/desktop/src/ipc/channels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const GET_CLIENT_SETTINGS_CHANNEL = "desktop:get-client-settings";
1616
export const SET_CLIENT_SETTINGS_CHANNEL = "desktop:set-client-settings";
1717
export const GET_SAVED_ENVIRONMENT_REGISTRY_CHANNEL = "desktop:get-saved-environment-registry";
1818
export const SET_SAVED_ENVIRONMENT_REGISTRY_CHANNEL = "desktop:set-saved-environment-registry";
19+
export const REMOVE_SAVED_ENVIRONMENT_CHANNEL = "desktop:remove-saved-environment";
1920
export const GET_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:get-saved-environment-secret";
2021
export const SET_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:set-saved-environment-secret";
2122
export const REMOVE_SAVED_ENVIRONMENT_SECRET_CHANNEL = "desktop:remove-saved-environment-secret";

apps/desktop/src/ipc/methods/savedEnvironments.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ export const setSavedEnvironmentRegistry = makeIpcMethod({
3939
}),
4040
});
4141

42+
export const removeSavedEnvironment = makeIpcMethod({
43+
channel: IpcChannels.REMOVE_SAVED_ENVIRONMENT_CHANNEL,
44+
payload: EnvironmentId,
45+
result: Schema.Void,
46+
handler: Effect.fn("desktop.ipc.savedEnvironments.removeEnvironment")(function* (environmentId) {
47+
const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments;
48+
yield* savedEnvironments.removeEnvironment(environmentId);
49+
}),
50+
});
51+
4252
export const getSavedEnvironmentSecret = makeIpcMethod({
4353
channel: IpcChannels.GET_SAVED_ENVIRONMENT_SECRET_CHANNEL,
4454
payload: EnvironmentId,

apps/desktop/src/preload.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ contextBridge.exposeInMainWorld("desktopBridge", {
4141
ipcRenderer.invoke(IpcChannels.GET_SAVED_ENVIRONMENT_REGISTRY_CHANNEL),
4242
setSavedEnvironmentRegistry: (records) =>
4343
ipcRenderer.invoke(IpcChannels.SET_SAVED_ENVIRONMENT_REGISTRY_CHANNEL, records),
44+
removeSavedEnvironment: (environmentId) =>
45+
ipcRenderer.invoke(IpcChannels.REMOVE_SAVED_ENVIRONMENT_CHANNEL, environmentId),
4446
getSavedEnvironmentSecret: (environmentId) =>
4547
ipcRenderer.invoke(IpcChannels.GET_SAVED_ENVIRONMENT_SECRET_CHANNEL, environmentId),
4648
setSavedEnvironmentSecret: (environmentId, secret) =>

apps/desktop/src/settings/DesktopSavedEnvironments.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,26 @@ describe("DesktopSavedEnvironments", () => {
293293
),
294294
);
295295

296+
it.effect("removes saved environment metadata and its embedded secret atomically", () =>
297+
withSavedEnvironments(
298+
Effect.gen(function* () {
299+
const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments;
300+
yield* savedEnvironments.setRegistry([savedRegistryRecord]);
301+
yield* savedEnvironments.setSecret({
302+
environmentId: savedRegistryRecord.environmentId,
303+
secret: "bearer-token",
304+
});
305+
306+
yield* savedEnvironments.removeEnvironment(savedRegistryRecord.environmentId);
307+
308+
assert.deepEqual(yield* savedEnvironments.getRegistry, []);
309+
assert.isTrue(
310+
Option.isNone(yield* savedEnvironments.getSecret(savedRegistryRecord.environmentId)),
311+
);
312+
}),
313+
),
314+
);
315+
296316
it.effect("treats empty saved environment documents as empty", () =>
297317
withSavedEnvironments(
298318
Effect.gen(function* () {

apps/desktop/src/settings/DesktopSavedEnvironments.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ export interface DesktopSavedEnvironmentsShape {
122122
readonly setRegistry: (
123123
records: readonly PersistedSavedEnvironmentRecord[],
124124
) => Effect.Effect<void, DesktopSavedEnvironmentsWriteError>;
125+
readonly removeEnvironment: (
126+
environmentId: string,
127+
) => Effect.Effect<void, DesktopSavedEnvironmentsWriteError>;
125128
readonly getSecret: (
126129
environmentId: string,
127130
) => Effect.Effect<Option.Option<string>, DesktopSavedEnvironmentsGetSecretError>;
@@ -303,6 +306,23 @@ export const layer = Layer.effect(
303306
);
304307
yield* writeDocument(preserveExistingSecrets(currentDocument, records));
305308
}),
309+
removeEnvironment: Effect.fn("desktop.savedEnvironments.removeEnvironment")(
310+
function* (environmentId) {
311+
yield* Effect.annotateCurrentSpan({ environmentId });
312+
const document = yield* readRegistryDocument(
313+
fileSystem,
314+
environment.savedEnvironmentRegistryPath,
315+
);
316+
if (!document.records.some((record) => record.environmentId === environmentId)) {
317+
return;
318+
}
319+
320+
yield* writeDocument({
321+
version: document.version,
322+
records: document.records.filter((record) => record.environmentId !== environmentId),
323+
});
324+
},
325+
),
306326
getSecret: Effect.fn("desktop.savedEnvironments.getSecret")(function* (environmentId) {
307327
yield* Effect.annotateCurrentSpan({ environmentId });
308328
const document = yield* readRegistryDocument(
@@ -403,6 +423,18 @@ export const layerTest = (input?: {
403423
return DesktopSavedEnvironments.of({
404424
getRegistry: Ref.get(recordsRef),
405425
setRegistry: (records) => Ref.set(recordsRef, records),
426+
removeEnvironment: (environmentId) =>
427+
Ref.update(recordsRef, (records) =>
428+
records.filter((record) => record.environmentId !== environmentId),
429+
).pipe(
430+
Effect.andThen(
431+
Ref.update(secretsRef, (secrets) => {
432+
const nextSecrets = new Map(secrets);
433+
nextSecrets.delete(environmentId);
434+
return nextSecrets;
435+
}),
436+
),
437+
),
406438
getSecret: (environmentId) =>
407439
Ref.get(secretsRef).pipe(
408440
Effect.map((secrets) => Option.fromNullishOr(secrets.get(environmentId))),

apps/web/src/clientPersistenceStorage.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,14 @@ export function writeBrowserSavedEnvironmentRegistry(
152152
);
153153
}
154154

155+
export function removeBrowserSavedEnvironment(environmentId: EnvironmentIdValue): void {
156+
const document = readBrowserSavedEnvironmentRegistryDocument();
157+
writeBrowserSavedEnvironmentRegistryDocument({
158+
version: document.version ?? 1,
159+
records: (document.records ?? []).filter((record) => record.environmentId !== environmentId),
160+
});
161+
}
162+
155163
export function readBrowserSavedEnvironmentSecret(
156164
environmentId: EnvironmentIdValue,
157165
): string | null {

apps/web/src/components/settings/SettingsPanels.browser.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ const createDesktopBridgeStub = (overrides?: {
352352
setClientSettings: vi.fn().mockResolvedValue(undefined),
353353
getSavedEnvironmentRegistry: vi.fn().mockResolvedValue([]),
354354
setSavedEnvironmentRegistry: vi.fn().mockResolvedValue(undefined),
355+
removeSavedEnvironment: vi.fn().mockResolvedValue(undefined),
355356
getSavedEnvironmentSecret: vi.fn().mockResolvedValue(null),
356357
setSavedEnvironmentSecret: vi.fn().mockResolvedValue(true),
357358
removeSavedEnvironmentSecret: vi.fn().mockResolvedValue(undefined),

apps/web/src/environments/runtime/catalog.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ describe("environment runtime catalog stores", () => {
2222
setClientSettings: async () => undefined,
2323
getSavedEnvironmentRegistry: async () => [],
2424
setSavedEnvironmentRegistry: async () => undefined,
25+
removeSavedEnvironment: async () => undefined,
2526
getSavedEnvironmentSecret: async () => null,
2627
setSavedEnvironmentSecret: async () => true,
2728
removeSavedEnvironmentSecret: async () => undefined,
@@ -109,6 +110,7 @@ describe("environment runtime catalog stores", () => {
109110
resolveRegistryRead = () => resolve([]);
110111
}),
111112
setSavedEnvironmentRegistry: async () => undefined,
113+
removeSavedEnvironment: async () => undefined,
112114
getSavedEnvironmentSecret: async () => null,
113115
setSavedEnvironmentSecret: async () => true,
114116
removeSavedEnvironmentSecret: async () => undefined,

apps/web/src/environments/runtime/catalog.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@ export async function persistSavedEnvironmentRecord(record: SavedEnvironmentReco
244244
);
245245
}
246246

247+
export async function removePersistedSavedEnvironment(environmentId: EnvironmentId): Promise<void> {
248+
await ensureLocalApi().persistence.removeSavedEnvironment(environmentId);
249+
}
250+
247251
export async function readSavedEnvironmentBearerToken(
248252
environmentId: EnvironmentId,
249253
): Promise<string | null> {

0 commit comments

Comments
 (0)