diff --git a/src/core/store.ts b/src/core/store.ts index baa1cbf..f74dd1a 100644 --- a/src/core/store.ts +++ b/src/core/store.ts @@ -20,6 +20,7 @@ import { randomUUID } from 'node:crypto'; import { dirname, resolve } from 'node:path'; import type { AgentStore, LastMessageTarget } from './types.js'; import { getDataDir } from '../utils/paths.js'; +import { sleepSync } from '../utils/time.js'; import { createLogger } from '../logger.js'; const log = createLogger('Store'); @@ -28,7 +29,6 @@ const DEFAULT_STORE_PATH = 'lettabot-agent.json'; const LOCK_RETRY_MS = 25; const LOCK_TIMEOUT_MS = 5_000; const LOCK_STALE_MS = 30_000; -const SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4)); interface StoreV2 { version: 2; @@ -40,23 +40,6 @@ interface ParsedStore { wasV1: boolean; } -let warnedAboutBusyWait = false; - -function sleepSync(ms: number): void { - if (typeof Atomics.wait === 'function') { - Atomics.wait(SLEEP_BUFFER, 0, 0, ms); - return; - } - if (!warnedAboutBusyWait) { - log.warn('Atomics.wait unavailable, falling back to busy-wait for lock retries'); - warnedAboutBusyWait = true; - } - const end = Date.now() + ms; - while (Date.now() < end) { - // Busy-wait fallback -- should not be reached in standard Node.js (v8+) - } -} - export class Store { private readonly storePath: string; private readonly lockPath: string; @@ -197,7 +180,9 @@ export class Store { if (Date.now() - start >= LOCK_TIMEOUT_MS) { throw new Error(`Timed out waiting for store lock: ${this.lockPath}`); } - sleepSync(LOCK_RETRY_MS); + sleepSync(LOCK_RETRY_MS, () => { + log.warn('Atomics.wait unavailable, falling back to busy-wait for lock retries'); + }); } } } diff --git a/src/main.ts b/src/main.ts index ce3701b..2487a77 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,6 +25,7 @@ import { import { isLettaApiUrl } from './utils/server.js'; import { getDataDir, getWorkingDir, hasRailwayVolume } from './utils/paths.js'; import { parseCsvList, parseNonNegativeNumber } from './utils/parse.js'; +import { sleep } from './utils/time.js'; import { createLogger, setLogLevel } from './logger.js'; const log = createLogger('Config'); @@ -211,10 +212,6 @@ const DISCOVERY_LOCK_TIMEOUT_MS = 15_000; const DISCOVERY_LOCK_STALE_MS = 60_000; const DISCOVERY_LOCK_RETRY_MS = 100; -function sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); -} - function getDiscoveryLockPath(agentName: string): string { const safe = agentName .trim() diff --git a/src/utils/time.test.ts b/src/utils/time.test.ts new file mode 100644 index 0000000..d3eb1b8 --- /dev/null +++ b/src/utils/time.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; +import { sleep, sleepSync } from './time.js'; + +describe('sleep', () => { + it('waits asynchronously', async () => { + const start = Date.now(); + await sleep(10); + expect(Date.now() - start).toBeGreaterThanOrEqual(8); + }); +}); + +describe('sleepSync', () => { + it('does not throw for zero delay', () => { + expect(() => sleepSync(0)).not.toThrow(); + }); +}); diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000..10cc481 --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,25 @@ +/** + * Shared timing helpers used across startup and persistence paths. + */ + +const SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4)); +let warnedAboutBusyWait = false; + +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export function sleepSync(ms: number, onBusyWait?: () => void): void { + if (typeof Atomics.wait === 'function') { + Atomics.wait(SLEEP_BUFFER, 0, 0, ms); + return; + } + if (!warnedAboutBusyWait) { + onBusyWait?.(); + warnedAboutBusyWait = true; + } + const end = Date.now() + ms; + while (Date.now() < end) { + // Busy-wait fallback -- should not be reached in standard Node.js (v8+) + } +}