Skip to content

Commit 65682b9

Browse files
rissrice2105-agentjsdavid278-cyber
andauthored
fix(core): serialize config store mutations (#719)
* fix(core): serialize config store mutations * fix(core): serialize public config writes --------- Co-authored-by: jsdavid278-cyber <jsdavid278-cyber@users.noreply.github.com>
1 parent 813fa89 commit 65682b9

2 files changed

Lines changed: 59 additions & 10 deletions

File tree

packages/core/src/config-store.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { mkdir, rm, writeFile } from 'node:fs/promises';
22
import { tmpdir } from 'node:os';
33
import path from 'node:path';
44
import { afterEach, describe, expect, it } from 'vitest';
5-
import { configPath, readConfig } from './config-store.js';
5+
import { configPath, deleteAdapterConfig, readConfig, setAdapterConfig, writeConfig } from './config-store.js';
66

77
const ORIGINAL_XDG_CONFIG_HOME = process.env.XDG_CONFIG_HOME;
88
let tempDir: string | undefined;
@@ -32,4 +32,34 @@ describe('readConfig', () => {
3232
adapters: {},
3333
});
3434
});
35+
36+
it('preserves concurrent adapter configuration mutations', async () => {
37+
tempDir = path.join(tmpdir(), `sh1pt-config-${Date.now()}`);
38+
process.env.XDG_CONFIG_HOME = tempDir;
39+
40+
await Promise.all([
41+
writeConfig({
42+
version: 1,
43+
adapters: { base: { enabled: true } },
44+
}),
45+
setAdapterConfig('target-a', { region: 'us-east' }),
46+
setAdapterConfig('target-b', { region: 'eu-west' }),
47+
setAdapterConfig('target-c', { region: 'ap-south' }),
48+
]);
49+
50+
await Promise.all([
51+
deleteAdapterConfig('target-b'),
52+
setAdapterConfig('target-d', { region: 'us-west' }),
53+
]);
54+
55+
await expect(readConfig()).resolves.toEqual({
56+
version: 1,
57+
adapters: {
58+
base: { enabled: true },
59+
'target-a': { region: 'us-east' },
60+
'target-c': { region: 'ap-south' },
61+
'target-d': { region: 'us-west' },
62+
},
63+
});
64+
});
3565
});

packages/core/src/config-store.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ function isRecord(value: unknown): value is Record<string, unknown> {
2222
return !!value && typeof value === 'object' && !Array.isArray(value);
2323
}
2424

25+
let configMutationLock: Promise<void> = Promise.resolve();
26+
27+
function withConfigMutationLock<T>(mutation: () => Promise<T>): Promise<T> {
28+
const next = configMutationLock.then(mutation);
29+
configMutationLock = next.then(
30+
() => {},
31+
() => {},
32+
);
33+
return next;
34+
}
35+
2536
export async function readConfig(): Promise<Sh1ptConfig> {
2637
try {
2738
const raw = await fs.readFile(configPath(), 'utf8');
@@ -37,28 +48,36 @@ export async function readConfig(): Promise<Sh1ptConfig> {
3748
}
3849
}
3950

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

58+
export function writeConfig(cfg: Sh1ptConfig): Promise<void> {
59+
return withConfigMutationLock(() => writeConfigFile(cfg));
60+
}
61+
4762
export async function getAdapterConfig<T = unknown>(adapterId: string): Promise<T | undefined> {
4863
const cfg = await readConfig();
4964
const entry = cfg.adapters[adapterId];
5065
return entry === undefined ? undefined : (entry as T);
5166
}
5267

5368
export async function setAdapterConfig(adapterId: string, adapterConfig: unknown): Promise<void> {
54-
const cfg = await readConfig();
55-
cfg.adapters[adapterId] = adapterConfig;
56-
await writeConfig(cfg);
69+
return withConfigMutationLock(async () => {
70+
const cfg = await readConfig();
71+
cfg.adapters[adapterId] = adapterConfig;
72+
await writeConfigFile(cfg);
73+
});
5774
}
5875

5976
export async function deleteAdapterConfig(adapterId: string): Promise<void> {
60-
const cfg = await readConfig();
61-
if (!(adapterId in cfg.adapters)) return;
62-
delete cfg.adapters[adapterId];
63-
await writeConfig(cfg);
77+
return withConfigMutationLock(async () => {
78+
const cfg = await readConfig();
79+
if (!(adapterId in cfg.adapters)) return;
80+
delete cfg.adapters[adapterId];
81+
await writeConfigFile(cfg);
82+
});
6483
}

0 commit comments

Comments
 (0)