-
Notifications
You must be signed in to change notification settings - Fork 34
Expand file tree
/
Copy pathsettings-write-queue.ts
More file actions
108 lines (100 loc) · 2.99 KB
/
Copy pathsettings-write-queue.ts
File metadata and controls
108 lines (100 loc) · 2.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
const SETTINGS_WRITE_MAX_ATTEMPTS = 4;
const SETTINGS_WRITE_BASE_DELAY_MS = 50;
const SETTINGS_WRITE_MAX_DELAY_MS = 30_000;
const RETRYABLE_SETTINGS_WRITE_CODES = new Set([
"EBUSY",
"EPERM",
"EAGAIN",
"ENOTEMPTY",
"EACCES",
]);
const settingsWriteQueues = new Map<string, Promise<void>>();
function readErrorNumber(value: unknown): number | undefined {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim().length > 0) {
const parsed = Number.parseInt(value, 10);
if (Number.isFinite(parsed)) return parsed;
}
return undefined;
}
function getErrorStatusCode(error: unknown): number | undefined {
if (!error || typeof error !== "object") return undefined;
const record = error as Record<string, unknown>;
return readErrorNumber(record.status) ?? readErrorNumber(record.statusCode);
}
function getRetryAfterMs(error: unknown): number | undefined {
if (!error || typeof error !== "object") return undefined;
const record = error as Record<string, unknown>;
return (
readErrorNumber(record.retryAfterMs) ??
readErrorNumber(record.retry_after_ms) ??
readErrorNumber(record.retryAfter) ??
readErrorNumber(record.retry_after)
);
}
function isRetryableSettingsWriteError(error: unknown): boolean {
const statusCode = getErrorStatusCode(error);
if (statusCode === 429) return true;
const code = (error as NodeJS.ErrnoException | undefined)?.code;
return typeof code === "string" && RETRYABLE_SETTINGS_WRITE_CODES.has(code);
}
function resolveRetryDelayMs(error: unknown, attempt: number): number {
const retryAfterMs = getRetryAfterMs(error);
if (
typeof retryAfterMs === "number" &&
Number.isFinite(retryAfterMs) &&
retryAfterMs > 0
) {
return Math.max(
10,
Math.min(SETTINGS_WRITE_MAX_DELAY_MS, Math.round(retryAfterMs)),
);
}
return Math.min(
SETTINGS_WRITE_MAX_DELAY_MS,
SETTINGS_WRITE_BASE_DELAY_MS * 2 ** attempt,
);
}
async function enqueueSettingsWrite<T>(
pathKey: string,
task: () => Promise<T>,
): Promise<T> {
const previous = settingsWriteQueues.get(pathKey) ?? Promise.resolve();
const queued = previous.catch(() => {}).then(task);
const queueTail = queued.then(
() => undefined,
() => undefined,
);
settingsWriteQueues.set(pathKey, queueTail);
try {
return await queued;
} finally {
if (settingsWriteQueues.get(pathKey) === queueTail) {
settingsWriteQueues.delete(pathKey);
}
}
}
export async function withQueuedRetry<T>(
pathKey: string,
task: () => Promise<T>,
deps: { sleep: (ms: number) => Promise<void> },
): Promise<T> {
return enqueueSettingsWrite(pathKey, async () => {
let lastError: unknown;
for (let attempt = 0; attempt < SETTINGS_WRITE_MAX_ATTEMPTS; attempt += 1) {
try {
return await task();
} catch (error) {
lastError = error;
if (!isRetryableSettingsWriteError(error)) {
throw error;
}
if (attempt >= SETTINGS_WRITE_MAX_ATTEMPTS - 1) {
break;
}
await deps.sleep(resolveRetryDelayMs(error, attempt));
}
}
throw lastError;
});
}