Skip to content
Open
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
32 changes: 31 additions & 1 deletion packages/core/src/config-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mkdir, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { configPath, readConfig } from './config-store.js';
import { configPath, deleteAdapterConfig, readConfig, setAdapterConfig, writeConfig } from './config-store.js';

const ORIGINAL_XDG_CONFIG_HOME = process.env.XDG_CONFIG_HOME;
let tempDir: string | undefined;
Expand Down Expand Up @@ -32,4 +32,34 @@ describe('readConfig', () => {
adapters: {},
});
});

it('preserves concurrent adapter configuration mutations', async () => {
tempDir = path.join(tmpdir(), `sh1pt-config-${Date.now()}`);
process.env.XDG_CONFIG_HOME = tempDir;

await Promise.all([
writeConfig({
version: 1,
adapters: { base: { enabled: true } },
}),
setAdapterConfig('target-a', { region: 'us-east' }),
setAdapterConfig('target-b', { region: 'eu-west' }),
setAdapterConfig('target-c', { region: 'ap-south' }),
]);

await Promise.all([
deleteAdapterConfig('target-b'),
setAdapterConfig('target-d', { region: 'us-west' }),
]);

await expect(readConfig()).resolves.toEqual({
version: 1,
adapters: {
base: { enabled: true },
'target-a': { region: 'us-east' },
'target-c': { region: 'ap-south' },
'target-d': { region: 'us-west' },
},
});
});
});
37 changes: 28 additions & 9 deletions packages/core/src/config-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}

let configMutationLock: Promise<void> = Promise.resolve();

function withConfigMutationLock<T>(mutation: () => Promise<T>): Promise<T> {
const next = configMutationLock.then(mutation);
configMutationLock = next.then(
() => {},
() => {},
);
return next;
}

export async function readConfig(): Promise<Sh1ptConfig> {
try {
const raw = await fs.readFile(configPath(), 'utf8');
Expand All @@ -37,28 +48,36 @@ export async function readConfig(): Promise<Sh1ptConfig> {
}
}

export async function writeConfig(cfg: Sh1ptConfig): Promise<void> {
async function writeConfigFile(cfg: Sh1ptConfig): Promise<void> {
await fs.mkdir(configDir(), { recursive: true, mode: 0o700 });
const tmp = `${configPath()}.tmp`;
const tmp = `${configPath()}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
await fs.writeFile(tmp, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
await fs.rename(tmp, configPath());
}

export function writeConfig(cfg: Sh1ptConfig): Promise<void> {
return withConfigMutationLock(() => writeConfigFile(cfg));
}

export async function getAdapterConfig<T = unknown>(adapterId: string): Promise<T | undefined> {
const cfg = await readConfig();
const entry = cfg.adapters[adapterId];
return entry === undefined ? undefined : (entry as T);
}

export async function setAdapterConfig(adapterId: string, adapterConfig: unknown): Promise<void> {
const cfg = await readConfig();
cfg.adapters[adapterId] = adapterConfig;
await writeConfig(cfg);
return withConfigMutationLock(async () => {
const cfg = await readConfig();
cfg.adapters[adapterId] = adapterConfig;
await writeConfigFile(cfg);
});
}

export async function deleteAdapterConfig(adapterId: string): Promise<void> {
const cfg = await readConfig();
if (!(adapterId in cfg.adapters)) return;
delete cfg.adapters[adapterId];
await writeConfig(cfg);
return withConfigMutationLock(async () => {
const cfg = await readConfig();
if (!(adapterId in cfg.adapters)) return;
delete cfg.adapters[adapterId];
await writeConfigFile(cfg);
});
}
Loading