Skip to content
Closed
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
9 changes: 7 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,9 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
let startupPrewarmTriggered = false;
let lastCodexCliActiveSyncIndex: number | null = null;
let perProjectStorageWarningShown = false;
let liveAccountSync: LiveAccountSync | null = null;
let liveAccountSyncPath: string | null = null;
let liveAccountSync: LiveAccountSync | null = null;
let liveAccountSyncPath: string | null = null;
let liveAccountSyncConfigKey: string | null = null;
let refreshGuardian: RefreshGuardian | null = null;
let refreshGuardianConfigKey: string | null = null;
let refreshGuardianCleanupRegistered = false;
Expand Down Expand Up @@ -481,7 +482,10 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
authFallback,
currentSync: liveAccountSync,
currentPath: liveAccountSyncPath,
currentConfigKey: liveAccountSyncConfigKey,
getLiveAccountSync,
getLiveAccountSyncDebounceMs,
getLiveAccountSyncPollMs,
getStoragePath,
createSync: (oauthFallback) =>
new LiveAccountSync(
Expand All @@ -502,6 +506,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
});
liveAccountSync = next.liveAccountSync;
liveAccountSyncPath = next.liveAccountSyncPath;
liveAccountSyncConfigKey = next.liveAccountSyncConfigKey;
};
const ensureRefreshGuardian = (
pluginConfig: ReturnType<typeof loadPluginConfig>,
Expand Down
15 changes: 15 additions & 0 deletions lib/runtime/live-sync-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,17 @@ export async function ensureLiveAccountSyncEntry<
authFallback?: OAuthAuthDetails;
currentSync: TSync | null;
currentPath: string | null;
currentConfigKey?: string | null;
getLiveAccountSync: (
config: ReturnType<typeof import("../config.js").loadPluginConfig>,
) => boolean;
getStoragePath: () => string;
getLiveAccountSyncDebounceMs: (
config: ReturnType<typeof import("../config.js").loadPluginConfig>,
) => number;
getLiveAccountSyncPollMs: (
config: ReturnType<typeof import("../config.js").loadPluginConfig>,
) => number;
createSync: (authFallback?: OAuthAuthDetails) => TSync;
registerCleanup: (cleanup: () => void) => void;
logWarn: (message: string) => void;
Expand All @@ -25,6 +32,8 @@ export async function ensureLiveAccountSyncEntry<
targetPath: string;
currentSync: TSync | null;
currentPath: string | null;
currentConfigKey?: string | null;
configKey?: string | null;
authFallback?: OAuthAuthDetails;
createSync: (authFallback?: OAuthAuthDetails) => TSync;
registerCleanup: (cleanup: () => void) => void;
Expand All @@ -33,16 +42,22 @@ export async function ensureLiveAccountSyncEntry<
}) => Promise<{
liveAccountSync: TSync | null;
liveAccountSyncPath: string | null;
liveAccountSyncConfigKey: string | null;
}>;
}): Promise<{
liveAccountSync: TSync | null;
liveAccountSyncPath: string | null;
liveAccountSyncConfigKey: string | null;
}> {
const debounceMs = params.getLiveAccountSyncDebounceMs(params.pluginConfig);
const pollIntervalMs = params.getLiveAccountSyncPollMs(params.pluginConfig);
return params.ensureLiveAccountSyncState({
enabled: params.getLiveAccountSync(params.pluginConfig),
targetPath: params.getStoragePath(),
currentSync: params.currentSync,
currentPath: params.currentPath,
currentConfigKey: params.currentConfigKey,
configKey: `${debounceMs}:${pollIntervalMs}`,
authFallback: params.authFallback,
createSync: params.createSync,
registerCleanup: params.registerCleanup,
Expand Down
23 changes: 20 additions & 3 deletions lib/runtime/live-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export async function ensureRuntimeLiveAccountSync<
getStoragePath: () => string;
currentSync: TSync | null;
currentPath: string | null;
currentConfigKey?: string | null;
currentCleanupRegistered: boolean;
getCurrentSync: () => TSync | null;
createSync: (
Expand All @@ -29,6 +30,7 @@ export async function ensureRuntimeLiveAccountSync<
commitState: (state: {
sync: TSync | null;
path: string | null;
configKey: string | null;
cleanupRegistered: boolean;
}) => void;
registerCleanup: (cleanup: () => void) => void;
Expand All @@ -37,18 +39,24 @@ export async function ensureRuntimeLiveAccountSync<
}): Promise<{
sync: TSync | null;
path: string | null;
configKey: string | null;
cleanupRegistered: boolean;
}> {
const debounceMs = deps.getLiveAccountSyncDebounceMs(deps.pluginConfig);
const pollIntervalMs = deps.getLiveAccountSyncPollMs(deps.pluginConfig);
const nextConfigKey = `${debounceMs}:${pollIntervalMs}`;
if (!deps.getLiveAccountSync(deps.pluginConfig)) {
deps.currentSync?.stop();
deps.commitState({
sync: null,
path: null,
configKey: null,
cleanupRegistered: deps.currentCleanupRegistered,
});
return {
sync: null,
path: null,
configKey: null,
cleanupRegistered: deps.currentCleanupRegistered,
};
}
Expand All @@ -57,10 +65,18 @@ export async function ensureRuntimeLiveAccountSync<
let sync = deps.currentSync;
let cleanupRegistered = deps.currentCleanupRegistered;
let nextPath = deps.currentPath;
let configKey = deps.currentConfigKey ?? null;
if (sync && configKey !== null && configKey !== nextConfigKey) {
sync.stop();
sync = null;
nextPath = null;
configKey = null;
}
const commitState = (): void => {
deps.commitState({
sync,
path: nextPath,
configKey,
cleanupRegistered,
});
};
Expand All @@ -70,10 +86,11 @@ export async function ensureRuntimeLiveAccountSync<
await deps.reloadAccountManagerFromDisk(deps.authFallback);
},
{
debounceMs: deps.getLiveAccountSyncDebounceMs(deps.pluginConfig),
pollIntervalMs: deps.getLiveAccountSyncPollMs(deps.pluginConfig),
debounceMs,
pollIntervalMs,
},
);
configKey = nextConfigKey;
commitState();
if (!cleanupRegistered) {
deps.registerCleanup(() => {
Expand Down Expand Up @@ -106,5 +123,5 @@ export async function ensureRuntimeLiveAccountSync<
}
}

return { sync, path: nextPath, cleanupRegistered };
return { sync, path: nextPath, configKey, cleanupRegistered };
}
27 changes: 25 additions & 2 deletions lib/runtime/runtime-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export async function ensureLiveAccountSyncState<
targetPath: string;
currentSync: TSync | null;
currentPath: string | null;
currentConfigKey?: string | null;
configKey?: string | null;
authFallback?: OAuthAuthDetails;
createSync: (authFallback?: OAuthAuthDetails) => TSync;
registerCleanup: (cleanup: () => void) => void;
Expand All @@ -28,21 +30,42 @@ export async function ensureLiveAccountSyncState<
}): Promise<{
liveAccountSync: TSync | null;
liveAccountSyncPath: string | null;
liveAccountSyncConfigKey: string | null;
}> {
let liveAccountSync = params.currentSync;
let liveAccountSyncPath = params.currentPath;
let liveAccountSyncConfigKey = params.currentConfigKey ?? null;

if (!params.enabled) {
if (liveAccountSync) {
liveAccountSync.stop();
liveAccountSync = null;
liveAccountSyncPath = null;
liveAccountSyncConfigKey = null;
}
return { liveAccountSync, liveAccountSyncPath };
return {
liveAccountSync,
liveAccountSyncPath,
liveAccountSyncConfigKey,
};
}

const nextConfigKey = params.configKey ?? null;
if (
liveAccountSync &&
nextConfigKey !== null &&
liveAccountSyncConfigKey !== null &&
liveAccountSyncConfigKey !== nextConfigKey
) {
liveAccountSync.stop();
liveAccountSync = null;
liveAccountSyncPath = null;
liveAccountSyncConfigKey = null;
}

if (!liveAccountSync) {
liveAccountSync = params.createSync(params.authFallback);
liveAccountSyncConfigKey = nextConfigKey;
params.registerCleanup(() => {
liveAccountSync?.stop();
});
Expand Down Expand Up @@ -71,7 +94,7 @@ export async function ensureLiveAccountSyncState<
}
}

return { liveAccountSync, liveAccountSyncPath };
return { liveAccountSync, liveAccountSyncPath, liveAccountSyncConfigKey };
}

export function ensureRefreshGuardianState<
Expand Down
4 changes: 4 additions & 0 deletions test/live-sync-entry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ describe("live sync entry", () => {
} as never,
currentSync: null,
currentPath: null,
currentConfigKey: null,
getLiveAccountSync: () => true,
getLiveAccountSyncDebounceMs: () => 25,
getLiveAccountSyncPollMs: () => 250,
getStoragePath: () => "/tmp/accounts.json",
createSync: vi.fn(() => ({ stop: vi.fn(), syncToPath: vi.fn() })),
registerCleanup: vi.fn(),
Expand All @@ -30,6 +33,7 @@ describe("live sync entry", () => {
expect.objectContaining({
enabled: true,
targetPath: "/tmp/accounts.json",
configKey: "25:250",
pluginName: "plugin",
}),
);
Expand Down
46 changes: 46 additions & 0 deletions test/runtime-live-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe("runtime live sync", () => {
let committedState = {
sync: overrides.currentSync ?? null,
path: overrides.currentPath ?? null,
configKey: null as string | null,
cleanupRegistered: overrides.currentCleanupRegistered ?? false,
};
let cleanupCallback: (() => void) | null = null;
Expand Down Expand Up @@ -92,12 +93,14 @@ describe("runtime live sync", () => {
await expect(ensureRuntimeLiveAccountSync(deps)).resolves.toEqual({
sync: null,
path: null,
configKey: null,
cleanupRegistered: true,
});
expect(currentSync.stop).toHaveBeenCalledTimes(1);
expect(deps.commitState).toHaveBeenCalledWith({
sync: null,
path: null,
configKey: null,
cleanupRegistered: true,
});
});
Expand All @@ -111,6 +114,7 @@ describe("runtime live sync", () => {
expect(createSync).toHaveBeenCalledTimes(1);
expect(registerCleanup).toHaveBeenCalledTimes(1);
expect(first.path).toBe("C:\\repo\\accounts.json");
expect(first.configKey).toBe("25:250");
expect(first.cleanupRegistered).toBe(true);
expect(first.sync?.syncToPath).toHaveBeenCalledWith(
"C:\\repo\\accounts.json",
Expand All @@ -120,6 +124,7 @@ describe("runtime live sync", () => {
...deps,
currentSync: first.sync,
currentPath: first.path,
currentConfigKey: first.configKey,
currentCleanupRegistered: first.cleanupRegistered,
});

Expand Down Expand Up @@ -151,6 +156,7 @@ describe("runtime live sync", () => {
await expect(pending).resolves.toMatchObject({
sync: currentSync,
path: "C:\\repo\\new.json",
configKey: null,
cleanupRegistered: true,
});
expect(currentSync.syncToPath).toHaveBeenCalledTimes(3);
Expand All @@ -176,6 +182,7 @@ describe("runtime live sync", () => {
await expect(pending).resolves.toMatchObject({
sync: currentSync,
path: "C:\\repo\\old.json",
configKey: null,
cleanupRegistered: true,
});
expect(currentSync.syncToPath).toHaveBeenCalledTimes(3);
Expand Down Expand Up @@ -213,6 +220,7 @@ describe("runtime live sync", () => {
const committed = getCommittedState();
expect(committed.sync).toBe(createdSync);
expect(committed.path).toBeNull();
expect(committed.configKey).toBe("25:250");
expect(committed.cleanupRegistered).toBe(true);

const cleanup = getCleanupCallback();
Expand All @@ -238,12 +246,14 @@ describe("runtime live sync", () => {

const committed = getCommittedState();
expect(committed.sync).toBe(createdSync);
expect(committed.configKey).toBe("25:250");
expect(committed.cleanupRegistered).toBe(true);

const second = ensureRuntimeLiveAccountSync({
...deps,
currentSync: committed.sync,
currentPath: committed.path,
currentConfigKey: committed.configKey,
currentCleanupRegistered: committed.cleanupRegistered,
});
await vi.runAllTicks();
Expand All @@ -253,11 +263,13 @@ describe("runtime live sync", () => {
await expect(pending).resolves.toMatchObject({
sync: createdSync,
path: "C:\\repo\\accounts.json",
configKey: "25:250",
cleanupRegistered: true,
});
await expect(second).resolves.toMatchObject({
sync: createdSync,
path: "C:\\repo\\accounts.json",
configKey: "25:250",
cleanupRegistered: true,
});
});
Expand All @@ -273,6 +285,7 @@ describe("runtime live sync", () => {
...deps,
currentSync: first.sync,
currentPath: first.path,
currentConfigKey: first.configKey,
currentCleanupRegistered: first.cleanupRegistered,
getLiveAccountSync: vi.fn().mockReturnValue(false),
});
Expand All @@ -282,6 +295,7 @@ describe("runtime live sync", () => {
...deps,
currentSync: disabled.sync,
currentPath: disabled.path,
currentConfigKey: disabled.configKey,
currentCleanupRegistered: disabled.cleanupRegistered,
});
setLiveSync(reenabled.sync);
Expand All @@ -293,4 +307,36 @@ describe("runtime live sync", () => {
expect((reenabled.sync as { stop: ReturnType<typeof vi.fn> }).stop).toHaveBeenCalledTimes(1);
expect((first.sync as { stop: ReturnType<typeof vi.fn> }).stop).toHaveBeenCalledTimes(1);
});

it("recreates live sync when debounce/poll settings change", async () => {
const firstSync = {
stop: vi.fn(),
syncToPath: vi.fn().mockResolvedValue(undefined),
};
const secondSync = {
stop: vi.fn(),
syncToPath: vi.fn().mockResolvedValue(undefined),
};
const { deps, createSync } = createDeps({
currentSync: firstSync,
currentPath: "C:\\repo\\accounts.json",
currentCleanupRegistered: true,
});
createSync.mockReturnValue(secondSync);
deps.getLiveAccountSyncDebounceMs = vi.fn().mockReturnValue(50);
deps.getLiveAccountSyncPollMs = vi.fn().mockReturnValue(500);

const result = await ensureRuntimeLiveAccountSync({
...deps,
currentSync: firstSync,
currentPath: "C:\\repo\\accounts.json",
currentConfigKey: "25:250",
});

expect(firstSync.stop).toHaveBeenCalledTimes(1);
expect(createSync).toHaveBeenCalledTimes(1);
expect(result.sync).toBe(secondSync);
expect(result.configKey).toBe("50:500");
expect(secondSync.syncToPath).toHaveBeenCalledWith("C:\\repo\\accounts.json");
});
});
Loading