diff --git a/src/database.ts b/src/database.ts index 460b85f..0731f2f 100644 --- a/src/database.ts +++ b/src/database.ts @@ -14,9 +14,10 @@ import * as searchMetrics from './search-metrics.js'; import { normalizeStatusValue } from './status-stage-rules.js'; const UNIQUE_TIME_LENGTH = 9; -const UNIQUE_RANDOM_BYTES = 4; -const UNIQUE_RANDOM_LENGTH = 7; -const UNIQUE_ID_LENGTH = UNIQUE_TIME_LENGTH + UNIQUE_RANDOM_LENGTH; +const UNIQUE_SEQUENCE_LENGTH = 2; +const UNIQUE_RANDOM_BYTES = 3; +const UNIQUE_RANDOM_LENGTH = 5; +const UNIQUE_ID_LENGTH = UNIQUE_TIME_LENGTH + UNIQUE_SEQUENCE_LENGTH + UNIQUE_RANDOM_LENGTH; const MAX_ID_GENERATION_ATTEMPTS = 10; export class WorklogDatabase { @@ -27,6 +28,8 @@ export class WorklogDatabase { private autoSync: boolean; private syncProvider?: () => Promise; private lockPath: string; + private _lastIdTime: number = 0; + private _idSequence: number = 0; constructor( prefix: string = 'WI', @@ -677,18 +680,28 @@ export class WorklogDatabase { } /** - * Generate a globally unique, human-readable identifier + * Generate a globally unique, human-readable identifier. + * Uses a sequence counter to ensure deterministic ordering when multiple + * IDs are generated within the same millisecond. */ private generateUniqueId(): string { - const timeRaw = Date.now().toString(36).toUpperCase(); + const now = Date.now(); + if (now !== this._lastIdTime) { + this._lastIdTime = now; + this._idSequence = 0; + } else { + this._idSequence++; + } + const timeRaw = now.toString(36).toUpperCase(); if (timeRaw.length > UNIQUE_TIME_LENGTH) { throw new Error('Timestamp overflow while generating unique ID'); } const timePart = timeRaw.padStart(UNIQUE_TIME_LENGTH, '0'); const randomBytesValue = randomBytes(UNIQUE_RANDOM_BYTES); - const randomNumber = randomBytesValue.readUInt32BE(0); + const randomNumber = randomBytesValue.readUIntBE(0, UNIQUE_RANDOM_BYTES); const randomPart = randomNumber.toString(36).toUpperCase().padStart(UNIQUE_RANDOM_LENGTH, '0'); - const id = `${timePart}${randomPart}`; + const sequencePart = this._idSequence.toString(36).toUpperCase().padStart(2, '0'); + const id = `${timePart}${sequencePart}${randomPart}`; if (id.length !== UNIQUE_ID_LENGTH) { throw new Error('Generated unique ID has unexpected length'); } diff --git a/tests/database.test.ts b/tests/database.test.ts index 0bee316..9406e70 100644 --- a/tests/database.test.ts +++ b/tests/database.test.ts @@ -18,6 +18,9 @@ describe('WorklogDatabase', () => { tempDir = createTempDir(); dbPath = createTempDbPath(tempDir); jsonlPath = createTempJsonlPath(tempDir); + if (fs.existsSync(jsonlPath)) { + fs.unlinkSync(jsonlPath); + } db = new WorklogDatabase('TEST', dbPath, jsonlPath, true, true); }); diff --git a/tests/next-regression.test.ts b/tests/next-regression.test.ts index a38c48f..61214b1 100644 --- a/tests/next-regression.test.ts +++ b/tests/next-regression.test.ts @@ -35,6 +35,9 @@ describe('wl next regression tests (WL-0MM2FKKOW1H0C0G4)', () => { tempDir = createTempDir(); dbPath = createTempDbPath(tempDir); jsonlPath = createTempJsonlPath(tempDir); + if (fs.existsSync(jsonlPath)) { + fs.unlinkSync(jsonlPath); + } db = new WorklogDatabase('TEST', dbPath, jsonlPath, true, true); });