@infograb/notion-cli 5.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1386 -0
- package/bin/dev +17 -0
- package/bin/dev.cmd +3 -0
- package/bin/run +14 -0
- package/bin/run.cmd +3 -0
- package/dist/base-command.d.ts +73 -0
- package/dist/base-command.js +179 -0
- package/dist/base-flags.d.ts +14 -0
- package/dist/base-flags.js +59 -0
- package/dist/cache.d.ts +84 -0
- package/dist/cache.js +351 -0
- package/dist/commands/batch/retrieve.d.ts +43 -0
- package/dist/commands/batch/retrieve.js +265 -0
- package/dist/commands/block/append.d.ts +42 -0
- package/dist/commands/block/append.js +219 -0
- package/dist/commands/block/delete.d.ts +30 -0
- package/dist/commands/block/delete.js +94 -0
- package/dist/commands/block/retrieve/children.d.ts +31 -0
- package/dist/commands/block/retrieve/children.js +174 -0
- package/dist/commands/block/retrieve.d.ts +30 -0
- package/dist/commands/block/retrieve.js +98 -0
- package/dist/commands/block/update.d.ts +45 -0
- package/dist/commands/block/update.js +241 -0
- package/dist/commands/cache/info.d.ts +19 -0
- package/dist/commands/cache/info.js +145 -0
- package/dist/commands/config/set-token.d.ts +30 -0
- package/dist/commands/config/set-token.js +201 -0
- package/dist/commands/db/create.d.ts +31 -0
- package/dist/commands/db/create.js +124 -0
- package/dist/commands/db/query.d.ts +41 -0
- package/dist/commands/db/query.js +355 -0
- package/dist/commands/db/retrieve.d.ts +33 -0
- package/dist/commands/db/retrieve.js +134 -0
- package/dist/commands/db/schema.d.ts +32 -0
- package/dist/commands/db/schema.js +308 -0
- package/dist/commands/db/update.d.ts +31 -0
- package/dist/commands/db/update.js +117 -0
- package/dist/commands/doctor.d.ts +50 -0
- package/dist/commands/doctor.js +420 -0
- package/dist/commands/init.d.ts +57 -0
- package/dist/commands/init.js +471 -0
- package/dist/commands/list.d.ts +29 -0
- package/dist/commands/list.js +184 -0
- package/dist/commands/page/create.d.ts +33 -0
- package/dist/commands/page/create.js +240 -0
- package/dist/commands/page/retrieve/property_item.d.ts +24 -0
- package/dist/commands/page/retrieve/property_item.js +72 -0
- package/dist/commands/page/retrieve.d.ts +36 -0
- package/dist/commands/page/retrieve.js +244 -0
- package/dist/commands/page/update.d.ts +34 -0
- package/dist/commands/page/update.js +184 -0
- package/dist/commands/search.d.ts +40 -0
- package/dist/commands/search.js +348 -0
- package/dist/commands/sync.d.ts +24 -0
- package/dist/commands/sync.js +183 -0
- package/dist/commands/user/list.d.ts +27 -0
- package/dist/commands/user/list.js +99 -0
- package/dist/commands/user/retrieve/bot.d.ts +28 -0
- package/dist/commands/user/retrieve/bot.js +96 -0
- package/dist/commands/user/retrieve.d.ts +30 -0
- package/dist/commands/user/retrieve.js +103 -0
- package/dist/commands/whoami.d.ts +19 -0
- package/dist/commands/whoami.js +175 -0
- package/dist/deduplication.d.ts +41 -0
- package/dist/deduplication.js +71 -0
- package/dist/envelope.d.ts +169 -0
- package/dist/envelope.js +257 -0
- package/dist/errors/enhanced-errors.d.ts +168 -0
- package/dist/errors/enhanced-errors.js +570 -0
- package/dist/errors/index.d.ts +18 -0
- package/dist/errors/index.js +33 -0
- package/dist/examples/cache-retry-examples.d.ts +64 -0
- package/dist/examples/cache-retry-examples.js +375 -0
- package/dist/helper.d.ts +102 -0
- package/dist/helper.js +885 -0
- package/dist/http-agent.d.ts +38 -0
- package/dist/http-agent.js +60 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/interface.d.ts +4 -0
- package/dist/interface.js +2 -0
- package/dist/notion.d.ts +144 -0
- package/dist/notion.js +547 -0
- package/dist/retry.d.ts +72 -0
- package/dist/retry.js +381 -0
- package/dist/utils/disk-cache.d.ts +80 -0
- package/dist/utils/disk-cache.js +291 -0
- package/dist/utils/markdown-to-blocks.d.ts +19 -0
- package/dist/utils/markdown-to-blocks.js +259 -0
- package/dist/utils/notion-resolver.d.ts +48 -0
- package/dist/utils/notion-resolver.js +262 -0
- package/dist/utils/notion-url-parser.d.ts +46 -0
- package/dist/utils/notion-url-parser.js +111 -0
- package/dist/utils/property-expander.d.ts +45 -0
- package/dist/utils/property-expander.js +323 -0
- package/dist/utils/schema-examples.d.ts +40 -0
- package/dist/utils/schema-examples.js +359 -0
- package/dist/utils/schema-extractor.d.ts +65 -0
- package/dist/utils/schema-extractor.js +235 -0
- package/dist/utils/table-formatter.d.ts +36 -0
- package/dist/utils/table-formatter.js +122 -0
- package/dist/utils/terminal-banner.d.ts +24 -0
- package/dist/utils/terminal-banner.js +34 -0
- package/dist/utils/token-validator.d.ts +55 -0
- package/dist/utils/token-validator.js +85 -0
- package/dist/utils/update-notifier.d.ts +26 -0
- package/dist/utils/update-notifier.js +54 -0
- package/dist/utils/workspace-cache.d.ts +58 -0
- package/dist/utils/workspace-cache.js +185 -0
- package/oclif.manifest.json +4497 -0
- package/package.json +115 -0
- package/scripts/banner.js +38 -0
- package/scripts/postinstall.js +56 -0
package/dist/retry.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Enhanced retry logic with exponential backoff and jitter
|
|
4
|
+
* Handles rate limiting, network errors, and transient failures
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.CircuitBreaker = void 0;
|
|
8
|
+
exports.isRetryableError = isRetryableError;
|
|
9
|
+
exports.calculateDelay = calculateDelay;
|
|
10
|
+
exports.fetchWithRetry = fetchWithRetry;
|
|
11
|
+
exports.batchWithRetry = batchWithRetry;
|
|
12
|
+
/**
|
|
13
|
+
* Default retry configuration
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_CONFIG = {
|
|
16
|
+
maxRetries: parseInt(process.env.NOTION_CLI_MAX_RETRIES || '3', 10),
|
|
17
|
+
baseDelay: parseInt(process.env.NOTION_CLI_BASE_DELAY || '1000', 10), // 1 second
|
|
18
|
+
maxDelay: parseInt(process.env.NOTION_CLI_MAX_DELAY || '30000', 10), // 30 seconds
|
|
19
|
+
exponentialBase: parseFloat(process.env.NOTION_CLI_EXP_BASE || '2'),
|
|
20
|
+
jitterFactor: parseFloat(process.env.NOTION_CLI_JITTER_FACTOR || '0.1'),
|
|
21
|
+
// HTTP status codes that should trigger a retry
|
|
22
|
+
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
23
|
+
// Notion API error codes that are retryable
|
|
24
|
+
retryableErrorCodes: [
|
|
25
|
+
'rate_limited',
|
|
26
|
+
'service_unavailable',
|
|
27
|
+
'internal_server_error',
|
|
28
|
+
'conflict_error',
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Check if verbose logging is enabled
|
|
33
|
+
*/
|
|
34
|
+
function isVerboseEnabled() {
|
|
35
|
+
return process.env.DEBUG === 'true' ||
|
|
36
|
+
process.env.NOTION_CLI_DEBUG === 'true' ||
|
|
37
|
+
process.env.NOTION_CLI_VERBOSE === 'true';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Log structured retry event to stderr
|
|
41
|
+
* Never pollutes stdout - safe for JSON output
|
|
42
|
+
*/
|
|
43
|
+
function logRetryEvent(event) {
|
|
44
|
+
// Only log if verbose mode is enabled
|
|
45
|
+
if (!isVerboseEnabled()) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Always write to stderr, never stdout
|
|
49
|
+
console.error(JSON.stringify(event));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Extract error reason from error object
|
|
53
|
+
*/
|
|
54
|
+
function getErrorReason(error) {
|
|
55
|
+
if (error.code === 'rate_limited' || error.status === 429)
|
|
56
|
+
return 'RATE_LIMITED';
|
|
57
|
+
if (error.status === 503)
|
|
58
|
+
return 'SERVICE_UNAVAILABLE';
|
|
59
|
+
if (error.status === 502)
|
|
60
|
+
return 'BAD_GATEWAY';
|
|
61
|
+
if (error.status === 504)
|
|
62
|
+
return 'GATEWAY_TIMEOUT';
|
|
63
|
+
if (error.status === 500)
|
|
64
|
+
return 'INTERNAL_SERVER_ERROR';
|
|
65
|
+
if (error.status === 408)
|
|
66
|
+
return 'REQUEST_TIMEOUT';
|
|
67
|
+
if (error.code === 'ECONNRESET')
|
|
68
|
+
return 'CONNECTION_RESET';
|
|
69
|
+
if (error.code === 'ETIMEDOUT')
|
|
70
|
+
return 'TIMEOUT';
|
|
71
|
+
if (error.code === 'ENOTFOUND')
|
|
72
|
+
return 'DNS_ERROR';
|
|
73
|
+
if (error.code === 'EAI_AGAIN')
|
|
74
|
+
return 'DNS_LOOKUP_FAILED';
|
|
75
|
+
if (error.code === 'service_unavailable')
|
|
76
|
+
return 'SERVICE_UNAVAILABLE';
|
|
77
|
+
if (error.code === 'internal_server_error')
|
|
78
|
+
return 'INTERNAL_SERVER_ERROR';
|
|
79
|
+
if (error.code === 'conflict_error')
|
|
80
|
+
return 'CONFLICT';
|
|
81
|
+
return 'UNKNOWN';
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Extract URL/endpoint from error object
|
|
85
|
+
*/
|
|
86
|
+
function extractUrl(error, context) {
|
|
87
|
+
var _a, _b;
|
|
88
|
+
if (error.url)
|
|
89
|
+
return error.url;
|
|
90
|
+
if ((_a = error.request) === null || _a === void 0 ? void 0 : _a.url)
|
|
91
|
+
return error.request.url;
|
|
92
|
+
if ((_b = error.config) === null || _b === void 0 ? void 0 : _b.url)
|
|
93
|
+
return error.config.url;
|
|
94
|
+
return context;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Categorize errors into retryable and non-retryable
|
|
98
|
+
*/
|
|
99
|
+
function isRetryableError(error, config = DEFAULT_CONFIG) {
|
|
100
|
+
// Network errors (no response)
|
|
101
|
+
if (!error.status && (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' ||
|
|
102
|
+
error.code === 'ENOTFOUND' || error.code === 'EAI_AGAIN')) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
// HTTP status codes
|
|
106
|
+
if (error.status && config.retryableStatusCodes.includes(error.status)) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
// Notion API error codes
|
|
110
|
+
if (error.code && config.retryableErrorCodes.includes(error.code)) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
// Don't retry client errors (400-499, except 408 and 429)
|
|
114
|
+
if (error.status >= 400 && error.status < 500 && error.status !== 408 && error.status !== 429) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Calculate delay with exponential backoff and jitter
|
|
121
|
+
*/
|
|
122
|
+
function calculateDelay(attempt, config = DEFAULT_CONFIG, retryAfterHeader) {
|
|
123
|
+
// If we have a Retry-After header from rate limiting, use it
|
|
124
|
+
if (retryAfterHeader) {
|
|
125
|
+
const retryAfter = parseInt(retryAfterHeader, 10);
|
|
126
|
+
if (!isNaN(retryAfter)) {
|
|
127
|
+
return Math.min(retryAfter * 1000, config.maxDelay);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Calculate exponential backoff: baseDelay * (exponentialBase ^ attempt)
|
|
131
|
+
const exponentialDelay = config.baseDelay * Math.pow(config.exponentialBase, attempt - 1);
|
|
132
|
+
// Cap at maxDelay
|
|
133
|
+
const cappedDelay = Math.min(exponentialDelay, config.maxDelay);
|
|
134
|
+
// Add jitter: random value between -jitterFactor and +jitterFactor
|
|
135
|
+
const jitter = cappedDelay * config.jitterFactor * (Math.random() * 2 - 1);
|
|
136
|
+
const finalDelay = Math.max(0, cappedDelay + jitter);
|
|
137
|
+
return Math.round(finalDelay);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Sleep for specified milliseconds
|
|
141
|
+
*/
|
|
142
|
+
function sleep(ms) {
|
|
143
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Enhanced retry wrapper with exponential backoff and jitter
|
|
147
|
+
*/
|
|
148
|
+
async function fetchWithRetry(fn, options = {}) {
|
|
149
|
+
var _a, _b;
|
|
150
|
+
const config = { ...DEFAULT_CONFIG, ...options.config };
|
|
151
|
+
const { onRetry, context } = options;
|
|
152
|
+
let lastError;
|
|
153
|
+
let totalDelay = 0;
|
|
154
|
+
for (let attempt = 1; attempt <= config.maxRetries + 1; attempt++) {
|
|
155
|
+
try {
|
|
156
|
+
// Log attempt start (if verbose and not first attempt)
|
|
157
|
+
if (attempt > 1 && isVerboseEnabled()) {
|
|
158
|
+
logRetryEvent({
|
|
159
|
+
level: 'info',
|
|
160
|
+
event: 'retry_attempt',
|
|
161
|
+
attempt,
|
|
162
|
+
max_retries: config.maxRetries,
|
|
163
|
+
context,
|
|
164
|
+
timestamp: new Date().toISOString(),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return await fn();
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
lastError = error;
|
|
171
|
+
// Check if we should retry
|
|
172
|
+
const shouldRetry = attempt <= config.maxRetries && isRetryableError(error, config);
|
|
173
|
+
if (!shouldRetry) {
|
|
174
|
+
// Log non-retryable error if verbose
|
|
175
|
+
if (isVerboseEnabled() && attempt > 1) {
|
|
176
|
+
logRetryEvent({
|
|
177
|
+
level: 'error',
|
|
178
|
+
event: 'retry_exhausted',
|
|
179
|
+
attempt,
|
|
180
|
+
max_retries: config.maxRetries,
|
|
181
|
+
reason: getErrorReason(error),
|
|
182
|
+
context,
|
|
183
|
+
status_code: error.status,
|
|
184
|
+
error_code: error.code,
|
|
185
|
+
timestamp: new Date().toISOString(),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
// Calculate delay
|
|
191
|
+
const retryAfter = ((_a = error.headers) === null || _a === void 0 ? void 0 : _a['retry-after']) || ((_b = error.headers) === null || _b === void 0 ? void 0 : _b['Retry-After']);
|
|
192
|
+
const delay = calculateDelay(attempt, config, retryAfter);
|
|
193
|
+
totalDelay += delay;
|
|
194
|
+
// Log rate limit event specifically
|
|
195
|
+
if (error.status === 429 || error.code === 'rate_limited') {
|
|
196
|
+
logRetryEvent({
|
|
197
|
+
level: 'warn',
|
|
198
|
+
event: 'rate_limited',
|
|
199
|
+
attempt,
|
|
200
|
+
max_retries: config.maxRetries,
|
|
201
|
+
reason: 'RATE_LIMITED',
|
|
202
|
+
retry_after_ms: delay,
|
|
203
|
+
url: extractUrl(error, context),
|
|
204
|
+
context,
|
|
205
|
+
status_code: error.status,
|
|
206
|
+
timestamp: new Date().toISOString(),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// Log general retry event
|
|
211
|
+
logRetryEvent({
|
|
212
|
+
level: 'warn',
|
|
213
|
+
event: 'retry',
|
|
214
|
+
attempt,
|
|
215
|
+
max_retries: config.maxRetries,
|
|
216
|
+
reason: getErrorReason(error),
|
|
217
|
+
retry_after_ms: delay,
|
|
218
|
+
url: extractUrl(error, context),
|
|
219
|
+
context,
|
|
220
|
+
status_code: error.status,
|
|
221
|
+
error_code: error.code,
|
|
222
|
+
timestamp: new Date().toISOString(),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// Create retry context
|
|
226
|
+
const retryContext = {
|
|
227
|
+
attempt,
|
|
228
|
+
maxRetries: config.maxRetries,
|
|
229
|
+
lastError: error,
|
|
230
|
+
totalDelay,
|
|
231
|
+
};
|
|
232
|
+
// Call retry callback if provided (for custom logging/monitoring)
|
|
233
|
+
if (onRetry) {
|
|
234
|
+
onRetry(retryContext);
|
|
235
|
+
}
|
|
236
|
+
// Wait before retrying
|
|
237
|
+
await sleep(delay);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Should never reach here, but TypeScript needs it
|
|
241
|
+
throw lastError;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Batch retry wrapper for multiple operations
|
|
245
|
+
* Executes operations with retry logic and collects results
|
|
246
|
+
*/
|
|
247
|
+
async function batchWithRetry(operations, options = {}) {
|
|
248
|
+
const { concurrency = 5 } = options;
|
|
249
|
+
const results = [];
|
|
250
|
+
// Process operations in batches
|
|
251
|
+
for (let i = 0; i < operations.length; i += concurrency) {
|
|
252
|
+
const batch = operations.slice(i, i + concurrency);
|
|
253
|
+
const batchPromises = batch.map(async (op, index) => {
|
|
254
|
+
try {
|
|
255
|
+
const data = await fetchWithRetry(op, {
|
|
256
|
+
...options,
|
|
257
|
+
context: `Operation ${i + index + 1}/${operations.length}`,
|
|
258
|
+
});
|
|
259
|
+
return { success: true, data };
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
return { success: false, error };
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
const batchResults = await Promise.all(batchPromises);
|
|
266
|
+
results.push(...batchResults);
|
|
267
|
+
}
|
|
268
|
+
return results;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Retry wrapper with circuit breaker pattern
|
|
272
|
+
* Prevents cascading failures by stopping retries after too many failures
|
|
273
|
+
*/
|
|
274
|
+
class CircuitBreaker {
|
|
275
|
+
constructor(failureThreshold = 5, successThreshold = 2, timeout = 60000 // 1 minute
|
|
276
|
+
) {
|
|
277
|
+
this.failureThreshold = failureThreshold;
|
|
278
|
+
this.successThreshold = successThreshold;
|
|
279
|
+
this.timeout = timeout;
|
|
280
|
+
this.failures = 0;
|
|
281
|
+
this.successes = 0;
|
|
282
|
+
this.state = 'closed';
|
|
283
|
+
this.nextAttempt = 0;
|
|
284
|
+
}
|
|
285
|
+
async execute(fn, retryOptions) {
|
|
286
|
+
if (this.state === 'open') {
|
|
287
|
+
if (Date.now() < this.nextAttempt) {
|
|
288
|
+
// Log circuit breaker open event
|
|
289
|
+
if (isVerboseEnabled()) {
|
|
290
|
+
logRetryEvent({
|
|
291
|
+
level: 'error',
|
|
292
|
+
event: 'retry_exhausted',
|
|
293
|
+
attempt: 0,
|
|
294
|
+
max_retries: 0,
|
|
295
|
+
reason: 'CIRCUIT_OPEN',
|
|
296
|
+
context: 'Circuit breaker is open',
|
|
297
|
+
timestamp: new Date().toISOString(),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
throw new Error('Circuit breaker is open. Too many failures.');
|
|
301
|
+
}
|
|
302
|
+
this.state = 'half-open';
|
|
303
|
+
// Log circuit breaker half-open event
|
|
304
|
+
if (isVerboseEnabled()) {
|
|
305
|
+
logRetryEvent({
|
|
306
|
+
level: 'info',
|
|
307
|
+
event: 'retry_attempt',
|
|
308
|
+
attempt: 1,
|
|
309
|
+
max_retries: this.successThreshold,
|
|
310
|
+
context: 'Circuit breaker entering half-open state',
|
|
311
|
+
timestamp: new Date().toISOString(),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
const result = await fetchWithRetry(fn, retryOptions);
|
|
317
|
+
this.onSuccess();
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
this.onFailure();
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
onSuccess() {
|
|
326
|
+
this.failures = 0;
|
|
327
|
+
if (this.state === 'half-open') {
|
|
328
|
+
this.successes++;
|
|
329
|
+
if (this.successes >= this.successThreshold) {
|
|
330
|
+
this.state = 'closed';
|
|
331
|
+
this.successes = 0;
|
|
332
|
+
// Log circuit breaker closed event
|
|
333
|
+
if (isVerboseEnabled()) {
|
|
334
|
+
logRetryEvent({
|
|
335
|
+
level: 'info',
|
|
336
|
+
event: 'retry_attempt',
|
|
337
|
+
attempt: this.successThreshold,
|
|
338
|
+
max_retries: this.successThreshold,
|
|
339
|
+
context: 'Circuit breaker closed - service recovered',
|
|
340
|
+
timestamp: new Date().toISOString(),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
onFailure() {
|
|
347
|
+
this.failures++;
|
|
348
|
+
this.successes = 0;
|
|
349
|
+
if (this.failures >= this.failureThreshold) {
|
|
350
|
+
this.state = 'open';
|
|
351
|
+
this.nextAttempt = Date.now() + this.timeout;
|
|
352
|
+
// Log circuit breaker open event
|
|
353
|
+
if (isVerboseEnabled()) {
|
|
354
|
+
logRetryEvent({
|
|
355
|
+
level: 'error',
|
|
356
|
+
event: 'retry_exhausted',
|
|
357
|
+
attempt: this.failures,
|
|
358
|
+
max_retries: this.failureThreshold,
|
|
359
|
+
reason: 'CIRCUIT_OPENED',
|
|
360
|
+
retry_after_ms: this.timeout,
|
|
361
|
+
context: `Circuit breaker opened after ${this.failures} failures`,
|
|
362
|
+
timestamp: new Date().toISOString(),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
getState() {
|
|
368
|
+
return {
|
|
369
|
+
state: this.state,
|
|
370
|
+
failures: this.failures,
|
|
371
|
+
successes: this.successes,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
reset() {
|
|
375
|
+
this.state = 'closed';
|
|
376
|
+
this.failures = 0;
|
|
377
|
+
this.successes = 0;
|
|
378
|
+
this.nextAttempt = 0;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
exports.CircuitBreaker = CircuitBreaker;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disk Cache Manager
|
|
3
|
+
*
|
|
4
|
+
* Provides persistent caching to disk, maintaining cache across CLI invocations.
|
|
5
|
+
* Cache entries are stored in ~/.notion-cli/cache/ directory.
|
|
6
|
+
*/
|
|
7
|
+
export interface DiskCacheEntry<T = any> {
|
|
8
|
+
key: string;
|
|
9
|
+
data: T;
|
|
10
|
+
expiresAt: number;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
size: number;
|
|
13
|
+
}
|
|
14
|
+
export interface DiskCacheStats {
|
|
15
|
+
totalEntries: number;
|
|
16
|
+
totalSize: number;
|
|
17
|
+
oldestEntry: number | null;
|
|
18
|
+
newestEntry: number | null;
|
|
19
|
+
}
|
|
20
|
+
export declare class DiskCacheManager {
|
|
21
|
+
private cacheDir;
|
|
22
|
+
private maxSize;
|
|
23
|
+
private syncInterval;
|
|
24
|
+
private dirtyKeys;
|
|
25
|
+
private syncTimer;
|
|
26
|
+
private initialized;
|
|
27
|
+
constructor(options?: {
|
|
28
|
+
cacheDir?: string;
|
|
29
|
+
maxSize?: number;
|
|
30
|
+
syncInterval?: number;
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Initialize disk cache (create directory, start sync timer)
|
|
34
|
+
*/
|
|
35
|
+
initialize(): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Get a cache entry from disk
|
|
38
|
+
*/
|
|
39
|
+
get<T>(key: string): Promise<DiskCacheEntry<T> | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Set a cache entry to disk
|
|
42
|
+
*/
|
|
43
|
+
set<T>(key: string, data: T, ttl: number): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Invalidate (delete) a cache entry
|
|
46
|
+
*/
|
|
47
|
+
invalidate(key: string): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Clear all cache entries
|
|
50
|
+
*/
|
|
51
|
+
clear(): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Sync dirty entries to disk
|
|
54
|
+
*/
|
|
55
|
+
sync(): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Shutdown (flush and cleanup)
|
|
58
|
+
*/
|
|
59
|
+
shutdown(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Get cache statistics
|
|
62
|
+
*/
|
|
63
|
+
getStats(): Promise<DiskCacheStats>;
|
|
64
|
+
/**
|
|
65
|
+
* Enforce maximum cache size by removing oldest entries
|
|
66
|
+
*/
|
|
67
|
+
private enforceMaxSize;
|
|
68
|
+
/**
|
|
69
|
+
* Ensure cache directory exists
|
|
70
|
+
*/
|
|
71
|
+
private ensureCacheDir;
|
|
72
|
+
/**
|
|
73
|
+
* Get file path for a cache key
|
|
74
|
+
*/
|
|
75
|
+
private getFilePath;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Global singleton instance
|
|
79
|
+
*/
|
|
80
|
+
export declare const diskCacheManager: DiskCacheManager;
|