export interface StateIncrementer { (state: number): Promise; } export type NumberGroup = { start: number; remain: number; } const AllocatorGroupBucketSize = 100; const AllocatorPreserveThreshold = 50; export function* numberGenerator(numberGroups: NumberGroup[]): Generator { for (const numberGroup of numberGroups) { for (let i = 0; i < numberGroup.remain; i++) { yield numberGroup.start + i; } } return null; } export class UniqueNumberAllocator { private preservedNumbers: NumberGroup[]; private incrementer: StateIncrementer; private totalRemain: number; private promisedCnt: number; private promise: Promise; public constructor(incrementer: StateIncrementer, initialState?: NumberGroup) { this.incrementer = incrementer; if (!initialState) { this.preservedNumbers = []; this.totalRemain = 0; } else { if (initialState.remain < 1) { throw new Error('initialState.remain must be greater than 0'); } this.preservedNumbers = [initialState]; this.totalRemain = initialState.remain; } this.promisedCnt = 0; this.promise = Promise.resolve(); this.preserveNext(); } public static async generate(incrementer: StateIncrementer): Promise { const initialStateEnd = await incrementer(AllocatorGroupBucketSize); const initialState: NumberGroup = { start: initialStateEnd - AllocatorGroupBucketSize + 1, remain: AllocatorGroupBucketSize, } return new UniqueNumberAllocator(incrementer, initialState); } private preserveNext(): void { if (this.promisedCnt + this.totalRemain >= AllocatorGroupBucketSize) { return; } if (this.totalRemain >= AllocatorPreserveThreshold) { return; } const waiter = this.promise; this.promise = (async () => { this.promisedCnt += AllocatorGroupBucketSize; const nextP = this.incrementer(AllocatorGroupBucketSize); await waiter; const next = await nextP; if (this.preservedNumbers.length > 0) { const last = this.preservedNumbers[this.preservedNumbers.length - 1]; if (last.start + last.remain === next - AllocatorGroupBucketSize + 1) { // 마지막 그룹과 연속됨 last.remain += AllocatorGroupBucketSize; this.totalRemain += AllocatorGroupBucketSize; this.promisedCnt -= AllocatorGroupBucketSize; return; } } const nextGroup: NumberGroup = { start: next - AllocatorGroupBucketSize + 1, remain: AllocatorGroupBucketSize, } this.preservedNumbers.push(nextGroup); this.totalRemain += AllocatorGroupBucketSize; this.promisedCnt -= AllocatorGroupBucketSize; })(); } public async allocateOne(): Promise { if (this.totalRemain > 0) { const head = this.preservedNumbers[0]; if (head.remain > 1) { const next = head.start; head.start++; head.remain--; this.totalRemain--; this.preserveNext(); return next; } else { this.preservedNumbers.shift(); this.totalRemain--; this.preserveNext(); return head.start; } } const next = (await this.allocate(1)).next().value; if (next === null) { throw new Error('incrementer failed to allocate enough numbers'); } return next; } public async allocate(n: number): Promise> { n = Math.ceil(n); if (n < 1) { throw new Error('n must be greater than 0'); } if (n > this.totalRemain + this.promisedCnt) { const endP = this.incrementer(n); this.preserveNext(); const end = await endP; return numberGenerator([{ start: end - n + 1, remain: n, }]); } const result: NumberGroup[] = []; if (this.totalRemain > 0) { const head = this.preservedNumbers[0]; const headRemain = head.remain; if (n < headRemain) { result.push({ start: head.start, remain: n, }); head.start += n; head.remain -= n; this.totalRemain -= n; this.preserveNext(); return numberGenerator(result); } if (n === headRemain) { this.preservedNumbers.shift(); result.push(head); this.totalRemain -= n; this.preserveNext(); return numberGenerator(result); } } //assert n <= this.totalRemain + this.promisedRemain if (n > this.totalRemain) { await this.promise; if (n > this.totalRemain) { throw new Error('incrementer failed to allocate enough numbers'); } } this.totalRemain -= n; this.preserveNext(); let remain = n; let idx = 0; while (remain > 0) { const head = this.preservedNumbers[idx]; if (head.remain <= remain) { remain -= head.remain; result.push(head); idx++; continue; } result.push({ start: head.start, remain: remain, }); head.start += remain; head.remain -= remain; remain = 0; break; } this.preservedNumbers = this.preservedNumbers.slice(idx); return numberGenerator(result); } }