@lobehub/chat 1.112.4 → 1.113.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/.vscode/settings.json +1 -1
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +14 -0
- package/package.json +2 -2
- package/packages/const/src/image.ts +9 -1
- package/packages/model-runtime/src/bfl/createImage.test.ts +846 -0
- package/packages/model-runtime/src/bfl/createImage.ts +279 -0
- package/packages/model-runtime/src/bfl/index.test.ts +269 -0
- package/packages/model-runtime/src/bfl/index.ts +49 -0
- package/packages/model-runtime/src/bfl/types.ts +113 -0
- package/packages/model-runtime/src/index.ts +1 -0
- package/packages/model-runtime/src/qwen/createImage.ts +37 -82
- package/packages/model-runtime/src/runtimeMap.ts +2 -0
- package/packages/model-runtime/src/utils/asyncifyPolling.test.ts +491 -0
- package/packages/model-runtime/src/utils/asyncifyPolling.ts +175 -0
- package/src/app/[variants]/(main)/chat/(workspace)/features/TelemetryNotification.tsx +1 -4
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +1 -1
- package/src/app/[variants]/(main)/settings/about/index.tsx +1 -4
- package/src/config/aiModels/bfl.ts +145 -0
- package/src/config/aiModels/index.ts +3 -0
- package/src/config/llm.ts +6 -1
- package/src/config/modelProviders/bfl.ts +21 -0
- package/src/config/modelProviders/index.ts +3 -0
- package/src/server/routers/lambda/market/index.ts +0 -3
- package/src/services/discover.ts +6 -0
- package/src/services/mcp.ts +0 -2
- package/src/store/image/slices/generationConfig/hooks.ts +6 -10
@@ -3,6 +3,7 @@ export { LobeAzureAI } from './azureai';
|
|
3
3
|
export { LobeAzureOpenAI } from './azureOpenai';
|
4
4
|
export * from './BaseAI';
|
5
5
|
export { LobeBedrockAI } from './bedrock';
|
6
|
+
export { LobeBflAI } from './bfl';
|
6
7
|
export { LobeDeepSeekAI } from './deepseek';
|
7
8
|
export * from './error';
|
8
9
|
export { LobeGoogleAI } from './google';
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import createDebug from 'debug';
|
2
2
|
|
3
3
|
import { CreateImagePayload, CreateImageResponse } from '../types/image';
|
4
|
+
import { type TaskResult, asyncifyPolling } from '../utils/asyncifyPolling';
|
4
5
|
import { AgentRuntimeError } from '../utils/createError';
|
5
6
|
import { CreateImageOptions } from '../utils/openaiCompatibleFactory';
|
6
7
|
|
@@ -139,93 +140,47 @@ export async function createQwenImage(
|
|
139
140
|
// 1. Create image generation task
|
140
141
|
const taskId = await createImageTask(payload, apiKey);
|
141
142
|
|
142
|
-
// 2. Poll task status until completion
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
log(
|
163
|
-
'Failed to query task status (attempt %d/%d, consecutive failures: %d/%d): %O',
|
164
|
-
retries + 1,
|
165
|
-
maxRetries,
|
166
|
-
consecutiveFailures,
|
167
|
-
maxConsecutiveFailures,
|
168
|
-
error,
|
169
|
-
);
|
170
|
-
|
171
|
-
// If we've failed too many times in a row, give up
|
172
|
-
if (consecutiveFailures >= maxConsecutiveFailures) {
|
173
|
-
throw new Error(
|
174
|
-
`Failed to query task status after ${consecutiveFailures} consecutive attempts: ${error}`,
|
175
|
-
);
|
143
|
+
// 2. Poll task status until completion using asyncifyPolling
|
144
|
+
const result = await asyncifyPolling<QwenImageTaskResponse, CreateImageResponse>({
|
145
|
+
checkStatus: (taskStatus: QwenImageTaskResponse): TaskResult<CreateImageResponse> => {
|
146
|
+
log('Task %s status: %s', taskId, taskStatus.output.task_status);
|
147
|
+
|
148
|
+
if (taskStatus.output.task_status === 'SUCCEEDED') {
|
149
|
+
if (!taskStatus.output.results || taskStatus.output.results.length === 0) {
|
150
|
+
return {
|
151
|
+
error: new Error('Task succeeded but no images generated'),
|
152
|
+
status: 'failed',
|
153
|
+
};
|
154
|
+
}
|
155
|
+
|
156
|
+
const imageUrl = taskStatus.output.results[0].url;
|
157
|
+
log('Image generated successfully: %s', imageUrl);
|
158
|
+
|
159
|
+
return {
|
160
|
+
data: { imageUrl },
|
161
|
+
status: 'success',
|
162
|
+
};
|
176
163
|
}
|
177
164
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
setTimeout(resolve, currentRetryInterval);
|
185
|
-
});
|
186
|
-
retries++;
|
187
|
-
continue; // Skip the rest of the loop and retry
|
188
|
-
}
|
189
|
-
|
190
|
-
// At this point, taskStatus should not be null since we just got it successfully
|
191
|
-
log(
|
192
|
-
'Task %s status: %s (attempt %d/%d)',
|
193
|
-
taskId,
|
194
|
-
taskStatus!.output.task_status,
|
195
|
-
retries + 1,
|
196
|
-
maxRetries,
|
197
|
-
);
|
198
|
-
|
199
|
-
if (taskStatus!.output.task_status === 'SUCCEEDED') {
|
200
|
-
if (!taskStatus!.output.results || taskStatus!.output.results.length === 0) {
|
201
|
-
throw new Error('Task succeeded but no images generated');
|
165
|
+
if (taskStatus.output.task_status === 'FAILED') {
|
166
|
+
const errorMessage = taskStatus.output.error_message || 'Image generation task failed';
|
167
|
+
return {
|
168
|
+
error: new Error(`Qwen image generation failed: ${errorMessage}`),
|
169
|
+
status: 'failed',
|
170
|
+
};
|
202
171
|
}
|
203
172
|
|
204
|
-
//
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
// Calculate dynamic retry interval with exponential backoff
|
214
|
-
const currentRetryInterval = Math.min(
|
215
|
-
initialRetryInterval * Math.pow(backoffMultiplier, retries),
|
216
|
-
maxRetryInterval,
|
217
|
-
);
|
218
|
-
|
219
|
-
log('Waiting %dms before next retry', currentRetryInterval);
|
220
|
-
|
221
|
-
// Wait before retrying
|
222
|
-
await new Promise((resolve) => {
|
223
|
-
setTimeout(resolve, currentRetryInterval);
|
224
|
-
});
|
225
|
-
retries++;
|
226
|
-
}
|
173
|
+
// Continue polling for pending/running status or other unknown statuses
|
174
|
+
return { status: 'pending' };
|
175
|
+
},
|
176
|
+
logger: {
|
177
|
+
debug: (message: any, ...args: any[]) => log(message, ...args),
|
178
|
+
error: (message: any, ...args: any[]) => log(message, ...args),
|
179
|
+
},
|
180
|
+
pollingQuery: () => queryTaskStatus(taskId, apiKey),
|
181
|
+
});
|
227
182
|
|
228
|
-
|
183
|
+
return result;
|
229
184
|
} catch (error) {
|
230
185
|
log('Error in createQwenImage: %O', error);
|
231
186
|
|
@@ -7,6 +7,7 @@ import { LobeAzureOpenAI } from './azureOpenai';
|
|
7
7
|
import { LobeAzureAI } from './azureai';
|
8
8
|
import { LobeBaichuanAI } from './baichuan';
|
9
9
|
import { LobeBedrockAI } from './bedrock';
|
10
|
+
import { LobeBflAI } from './bfl';
|
10
11
|
import { LobeCloudflareAI } from './cloudflare';
|
11
12
|
import { LobeCohereAI } from './cohere';
|
12
13
|
import { LobeDeepSeekAI } from './deepseek';
|
@@ -65,6 +66,7 @@ export const providerRuntimeMap = {
|
|
65
66
|
azureai: LobeAzureAI,
|
66
67
|
baichuan: LobeBaichuanAI,
|
67
68
|
bedrock: LobeBedrockAI,
|
69
|
+
bfl: LobeBflAI,
|
68
70
|
cloudflare: LobeCloudflareAI,
|
69
71
|
cohere: LobeCohereAI,
|
70
72
|
deepseek: LobeDeepSeekAI,
|
@@ -0,0 +1,491 @@
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import { type TaskResult, asyncifyPolling } from './asyncifyPolling';
|
4
|
+
|
5
|
+
describe('asyncifyPolling', () => {
|
6
|
+
describe('basic functionality', () => {
|
7
|
+
it('should return data when task succeeds immediately', async () => {
|
8
|
+
const mockTask = vi.fn().mockResolvedValue({ status: 'completed', data: 'result' });
|
9
|
+
const mockCheckStatus = vi.fn().mockReturnValue({
|
10
|
+
status: 'success',
|
11
|
+
data: 'result',
|
12
|
+
} as TaskResult<string>);
|
13
|
+
|
14
|
+
const result = await asyncifyPolling({
|
15
|
+
pollingQuery: mockTask,
|
16
|
+
checkStatus: mockCheckStatus,
|
17
|
+
});
|
18
|
+
|
19
|
+
expect(result).toBe('result');
|
20
|
+
expect(mockTask).toHaveBeenCalledTimes(1);
|
21
|
+
expect(mockCheckStatus).toHaveBeenCalledTimes(1);
|
22
|
+
});
|
23
|
+
|
24
|
+
it('should poll multiple times until success', async () => {
|
25
|
+
const mockTask = vi
|
26
|
+
.fn()
|
27
|
+
.mockResolvedValueOnce({ status: 'pending' })
|
28
|
+
.mockResolvedValueOnce({ status: 'pending' })
|
29
|
+
.mockResolvedValueOnce({ status: 'completed', data: 'final-result' });
|
30
|
+
|
31
|
+
const mockCheckStatus = vi
|
32
|
+
.fn()
|
33
|
+
.mockReturnValueOnce({ status: 'pending' })
|
34
|
+
.mockReturnValueOnce({ status: 'pending' })
|
35
|
+
.mockReturnValueOnce({ status: 'success', data: 'final-result' });
|
36
|
+
|
37
|
+
const result = await asyncifyPolling({
|
38
|
+
pollingQuery: mockTask,
|
39
|
+
checkStatus: mockCheckStatus,
|
40
|
+
initialInterval: 10, // fast test
|
41
|
+
});
|
42
|
+
|
43
|
+
expect(result).toBe('final-result');
|
44
|
+
expect(mockTask).toHaveBeenCalledTimes(3);
|
45
|
+
expect(mockCheckStatus).toHaveBeenCalledTimes(3);
|
46
|
+
});
|
47
|
+
|
48
|
+
it('should throw error when task fails', async () => {
|
49
|
+
const mockTask = vi.fn().mockResolvedValue({ status: 'failed', error: 'Task failed' });
|
50
|
+
const mockCheckStatus = vi.fn().mockReturnValue({
|
51
|
+
status: 'failed',
|
52
|
+
error: new Error('Task failed'),
|
53
|
+
});
|
54
|
+
|
55
|
+
await expect(
|
56
|
+
asyncifyPolling({
|
57
|
+
pollingQuery: mockTask,
|
58
|
+
checkStatus: mockCheckStatus,
|
59
|
+
}),
|
60
|
+
).rejects.toThrow('Task failed');
|
61
|
+
|
62
|
+
expect(mockTask).toHaveBeenCalledTimes(1);
|
63
|
+
});
|
64
|
+
|
65
|
+
it('should handle pending status correctly', async () => {
|
66
|
+
const mockTask = vi
|
67
|
+
.fn()
|
68
|
+
.mockResolvedValueOnce({ status: 'processing' })
|
69
|
+
.mockResolvedValueOnce({ status: 'done' });
|
70
|
+
|
71
|
+
const mockCheckStatus = vi
|
72
|
+
.fn()
|
73
|
+
.mockReturnValueOnce({ status: 'pending' })
|
74
|
+
.mockReturnValueOnce({ status: 'success', data: 'completed' });
|
75
|
+
|
76
|
+
const result = await asyncifyPolling({
|
77
|
+
pollingQuery: mockTask,
|
78
|
+
checkStatus: mockCheckStatus,
|
79
|
+
initialInterval: 10,
|
80
|
+
});
|
81
|
+
|
82
|
+
expect(result).toBe('completed');
|
83
|
+
expect(mockTask).toHaveBeenCalledTimes(2);
|
84
|
+
});
|
85
|
+
});
|
86
|
+
|
87
|
+
describe('retry mechanism', () => {
|
88
|
+
it('should retry with exponential backoff', async () => {
|
89
|
+
const startTime = Date.now();
|
90
|
+
const mockTask = vi
|
91
|
+
.fn()
|
92
|
+
.mockResolvedValueOnce({ status: 'pending' })
|
93
|
+
.mockResolvedValueOnce({ status: 'pending' })
|
94
|
+
.mockResolvedValueOnce({ status: 'success' });
|
95
|
+
|
96
|
+
const mockCheckStatus = vi
|
97
|
+
.fn()
|
98
|
+
.mockReturnValueOnce({ status: 'pending' })
|
99
|
+
.mockReturnValueOnce({ status: 'pending' })
|
100
|
+
.mockReturnValueOnce({ status: 'success', data: 'done' });
|
101
|
+
|
102
|
+
await asyncifyPolling({
|
103
|
+
pollingQuery: mockTask,
|
104
|
+
checkStatus: mockCheckStatus,
|
105
|
+
initialInterval: 50,
|
106
|
+
backoffMultiplier: 2,
|
107
|
+
maxInterval: 200,
|
108
|
+
});
|
109
|
+
|
110
|
+
const elapsed = Date.now() - startTime;
|
111
|
+
// Should wait at least 50ms + 100ms = 150ms
|
112
|
+
expect(elapsed).toBeGreaterThan(140);
|
113
|
+
});
|
114
|
+
|
115
|
+
it('should respect maxInterval limit', async () => {
|
116
|
+
const intervals: number[] = [];
|
117
|
+
const originalSetTimeout = global.setTimeout;
|
118
|
+
|
119
|
+
global.setTimeout = vi.fn((callback, delay) => {
|
120
|
+
intervals.push(delay as number);
|
121
|
+
return originalSetTimeout(callback, 1); // fast execution
|
122
|
+
}) as any;
|
123
|
+
|
124
|
+
const mockTask = vi
|
125
|
+
.fn()
|
126
|
+
.mockResolvedValueOnce({ status: 'pending' })
|
127
|
+
.mockResolvedValueOnce({ status: 'pending' })
|
128
|
+
.mockResolvedValueOnce({ status: 'pending' })
|
129
|
+
.mockResolvedValueOnce({ status: 'success' });
|
130
|
+
|
131
|
+
const mockCheckStatus = vi
|
132
|
+
.fn()
|
133
|
+
.mockReturnValueOnce({ status: 'pending' })
|
134
|
+
.mockReturnValueOnce({ status: 'pending' })
|
135
|
+
.mockReturnValueOnce({ status: 'pending' })
|
136
|
+
.mockReturnValueOnce({ status: 'success', data: 'done' });
|
137
|
+
|
138
|
+
await asyncifyPolling({
|
139
|
+
pollingQuery: mockTask,
|
140
|
+
checkStatus: mockCheckStatus,
|
141
|
+
initialInterval: 100,
|
142
|
+
backoffMultiplier: 3,
|
143
|
+
maxInterval: 200,
|
144
|
+
});
|
145
|
+
|
146
|
+
// Intervals should be: 100, 200 (capped), 200 (capped)
|
147
|
+
expect(intervals).toEqual([100, 200, 200]);
|
148
|
+
|
149
|
+
global.setTimeout = originalSetTimeout;
|
150
|
+
});
|
151
|
+
|
152
|
+
it('should stop after maxRetries', async () => {
|
153
|
+
const mockTask = vi.fn().mockResolvedValue({ status: 'pending' });
|
154
|
+
const mockCheckStatus = vi.fn().mockReturnValue({ status: 'pending' });
|
155
|
+
|
156
|
+
await expect(
|
157
|
+
asyncifyPolling({
|
158
|
+
pollingQuery: mockTask,
|
159
|
+
checkStatus: mockCheckStatus,
|
160
|
+
maxRetries: 3,
|
161
|
+
initialInterval: 1,
|
162
|
+
}),
|
163
|
+
).rejects.toThrow(/timeout after 3 attempts/);
|
164
|
+
|
165
|
+
expect(mockTask).toHaveBeenCalledTimes(3);
|
166
|
+
});
|
167
|
+
});
|
168
|
+
|
169
|
+
describe('error handling', () => {
|
170
|
+
it('should handle consecutive failures', async () => {
|
171
|
+
const mockTask = vi
|
172
|
+
.fn()
|
173
|
+
.mockRejectedValueOnce(new Error('Network error 1'))
|
174
|
+
.mockRejectedValueOnce(new Error('Network error 2'))
|
175
|
+
.mockResolvedValueOnce({ status: 'success' });
|
176
|
+
|
177
|
+
const mockCheckStatus = vi.fn().mockReturnValue({ status: 'success', data: 'recovered' });
|
178
|
+
|
179
|
+
const result = await asyncifyPolling({
|
180
|
+
pollingQuery: mockTask,
|
181
|
+
checkStatus: mockCheckStatus,
|
182
|
+
maxConsecutiveFailures: 3,
|
183
|
+
initialInterval: 1,
|
184
|
+
});
|
185
|
+
|
186
|
+
expect(result).toBe('recovered');
|
187
|
+
expect(mockTask).toHaveBeenCalledTimes(3);
|
188
|
+
});
|
189
|
+
|
190
|
+
it('should throw after maxConsecutiveFailures', async () => {
|
191
|
+
const mockTask = vi
|
192
|
+
.fn()
|
193
|
+
.mockRejectedValueOnce(new Error('Network error 1'))
|
194
|
+
.mockRejectedValueOnce(new Error('Network error 2'))
|
195
|
+
.mockRejectedValueOnce(new Error('Network error 3'));
|
196
|
+
|
197
|
+
const mockCheckStatus = vi.fn();
|
198
|
+
|
199
|
+
await expect(
|
200
|
+
asyncifyPolling({
|
201
|
+
pollingQuery: mockTask,
|
202
|
+
checkStatus: mockCheckStatus,
|
203
|
+
maxConsecutiveFailures: 2, // 允许最多2次连续失败
|
204
|
+
initialInterval: 1,
|
205
|
+
}),
|
206
|
+
).rejects.toThrow(/consecutive attempts/);
|
207
|
+
|
208
|
+
expect(mockTask).toHaveBeenCalledTimes(2); // 第1次失败,第2次失败,然后抛出错误
|
209
|
+
expect(mockCheckStatus).not.toHaveBeenCalled();
|
210
|
+
});
|
211
|
+
|
212
|
+
it('should reset consecutive failures on success', async () => {
|
213
|
+
const mockTask = vi
|
214
|
+
.fn()
|
215
|
+
.mockRejectedValueOnce(new Error('Network error 1')) // Failure 1 (consecutiveFailures=1)
|
216
|
+
.mockResolvedValueOnce({ status: 'pending' }) // Success 1 (reset to 0)
|
217
|
+
.mockRejectedValueOnce(new Error('Network error 2')) // Failure 2 (consecutiveFailures=1)
|
218
|
+
.mockRejectedValueOnce(new Error('Network error 3')) // Failure 3 (consecutiveFailures=2)
|
219
|
+
.mockResolvedValueOnce({ status: 'success' }); // Success 2 (return result)
|
220
|
+
|
221
|
+
const mockCheckStatus = vi
|
222
|
+
.fn()
|
223
|
+
.mockReturnValueOnce({ status: 'pending' }) // For success 1
|
224
|
+
.mockReturnValueOnce({ status: 'success', data: 'final' }); // For success 2
|
225
|
+
|
226
|
+
const result = await asyncifyPolling({
|
227
|
+
pollingQuery: mockTask,
|
228
|
+
checkStatus: mockCheckStatus,
|
229
|
+
maxConsecutiveFailures: 3, // Allow up to 3 consecutive failures (since there are 2 consecutive failures)
|
230
|
+
initialInterval: 1,
|
231
|
+
});
|
232
|
+
|
233
|
+
expect(result).toBe('final');
|
234
|
+
expect(mockTask).toHaveBeenCalledTimes(5); // Total 5 calls
|
235
|
+
});
|
236
|
+
});
|
237
|
+
|
238
|
+
describe('configuration', () => {
|
239
|
+
it('should use custom intervals and multipliers', async () => {
|
240
|
+
const intervals: number[] = [];
|
241
|
+
const originalSetTimeout = global.setTimeout;
|
242
|
+
|
243
|
+
global.setTimeout = vi.fn((callback, delay) => {
|
244
|
+
intervals.push(delay as number);
|
245
|
+
return originalSetTimeout(callback, 1);
|
246
|
+
}) as any;
|
247
|
+
|
248
|
+
const mockTask = vi
|
249
|
+
.fn()
|
250
|
+
.mockResolvedValueOnce({ status: 'pending' })
|
251
|
+
.mockResolvedValueOnce({ status: 'success' });
|
252
|
+
|
253
|
+
const mockCheckStatus = vi
|
254
|
+
.fn()
|
255
|
+
.mockReturnValueOnce({ status: 'pending' })
|
256
|
+
.mockReturnValueOnce({ status: 'success', data: 'done' });
|
257
|
+
|
258
|
+
await asyncifyPolling({
|
259
|
+
pollingQuery: mockTask,
|
260
|
+
checkStatus: mockCheckStatus,
|
261
|
+
initialInterval: 200,
|
262
|
+
backoffMultiplier: 1.2,
|
263
|
+
});
|
264
|
+
|
265
|
+
expect(intervals[0]).toBe(200);
|
266
|
+
|
267
|
+
global.setTimeout = originalSetTimeout;
|
268
|
+
});
|
269
|
+
|
270
|
+
it('should accept custom logger function', async () => {
|
271
|
+
const mockLogger = {
|
272
|
+
debug: vi.fn(),
|
273
|
+
error: vi.fn(),
|
274
|
+
};
|
275
|
+
|
276
|
+
const mockTask = vi
|
277
|
+
.fn()
|
278
|
+
.mockRejectedValueOnce(new Error('Test error'))
|
279
|
+
.mockResolvedValueOnce({ status: 'success' });
|
280
|
+
|
281
|
+
const mockCheckStatus = vi.fn().mockReturnValue({ status: 'success', data: 'done' });
|
282
|
+
|
283
|
+
await asyncifyPolling({
|
284
|
+
pollingQuery: mockTask,
|
285
|
+
checkStatus: mockCheckStatus,
|
286
|
+
logger: mockLogger,
|
287
|
+
maxConsecutiveFailures: 3,
|
288
|
+
initialInterval: 1,
|
289
|
+
});
|
290
|
+
|
291
|
+
expect(mockLogger.debug).toHaveBeenCalled();
|
292
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
293
|
+
});
|
294
|
+
});
|
295
|
+
|
296
|
+
describe('edge cases', () => {
|
297
|
+
it('should handle immediate failure', async () => {
|
298
|
+
const mockTask = vi.fn().mockResolvedValue({ error: 'immediate failure' });
|
299
|
+
const mockCheckStatus = vi.fn().mockReturnValue({
|
300
|
+
status: 'failed',
|
301
|
+
error: new Error('immediate failure'),
|
302
|
+
});
|
303
|
+
|
304
|
+
await expect(
|
305
|
+
asyncifyPolling({
|
306
|
+
pollingQuery: mockTask,
|
307
|
+
checkStatus: mockCheckStatus,
|
308
|
+
}),
|
309
|
+
).rejects.toThrow('immediate failure');
|
310
|
+
|
311
|
+
expect(mockTask).toHaveBeenCalledTimes(1);
|
312
|
+
});
|
313
|
+
|
314
|
+
it('should handle task throwing exceptions', async () => {
|
315
|
+
const mockTask = vi.fn().mockRejectedValue(new Error('Task exception'));
|
316
|
+
const mockCheckStatus = vi.fn();
|
317
|
+
|
318
|
+
await expect(
|
319
|
+
asyncifyPolling({
|
320
|
+
pollingQuery: mockTask,
|
321
|
+
checkStatus: mockCheckStatus,
|
322
|
+
maxConsecutiveFailures: 1,
|
323
|
+
initialInterval: 1,
|
324
|
+
}),
|
325
|
+
).rejects.toThrow(/consecutive attempts/);
|
326
|
+
});
|
327
|
+
|
328
|
+
it('should timeout correctly with maxRetries = 1', async () => {
|
329
|
+
const mockTask = vi.fn().mockResolvedValue({ status: 'pending' });
|
330
|
+
const mockCheckStatus = vi.fn().mockReturnValue({ status: 'pending' });
|
331
|
+
|
332
|
+
await expect(
|
333
|
+
asyncifyPolling({
|
334
|
+
pollingQuery: mockTask,
|
335
|
+
checkStatus: mockCheckStatus,
|
336
|
+
maxRetries: 1,
|
337
|
+
initialInterval: 1,
|
338
|
+
}),
|
339
|
+
).rejects.toThrow(/timeout after 1 attempts/);
|
340
|
+
|
341
|
+
expect(mockTask).toHaveBeenCalledTimes(1);
|
342
|
+
});
|
343
|
+
});
|
344
|
+
|
345
|
+
describe('custom error handling', () => {
|
346
|
+
it('should allow continuing polling via onPollingError', async () => {
|
347
|
+
const mockTask = vi
|
348
|
+
.fn()
|
349
|
+
.mockRejectedValueOnce(new Error('Network error'))
|
350
|
+
.mockRejectedValueOnce(new Error('Another error'))
|
351
|
+
.mockResolvedValueOnce({ status: 'success' });
|
352
|
+
|
353
|
+
const mockCheckStatus = vi.fn().mockReturnValue({ status: 'success', data: 'final-result' });
|
354
|
+
|
355
|
+
const onPollingError = vi.fn().mockReturnValue({
|
356
|
+
isContinuePolling: true,
|
357
|
+
});
|
358
|
+
|
359
|
+
const result = await asyncifyPolling({
|
360
|
+
pollingQuery: mockTask,
|
361
|
+
checkStatus: mockCheckStatus,
|
362
|
+
onPollingError,
|
363
|
+
initialInterval: 1,
|
364
|
+
});
|
365
|
+
|
366
|
+
expect(result).toBe('final-result');
|
367
|
+
expect(mockTask).toHaveBeenCalledTimes(3);
|
368
|
+
expect(onPollingError).toHaveBeenCalledTimes(2);
|
369
|
+
|
370
|
+
// Check that error context was passed correctly
|
371
|
+
expect(onPollingError).toHaveBeenCalledWith({
|
372
|
+
error: expect.any(Error),
|
373
|
+
retries: expect.any(Number),
|
374
|
+
consecutiveFailures: expect.any(Number),
|
375
|
+
});
|
376
|
+
});
|
377
|
+
|
378
|
+
it('should stop polling when onPollingError returns false', async () => {
|
379
|
+
const mockTask = vi.fn().mockRejectedValue(new Error('Fatal error'));
|
380
|
+
const mockCheckStatus = vi.fn();
|
381
|
+
|
382
|
+
const onPollingError = vi.fn().mockReturnValue({
|
383
|
+
isContinuePolling: false,
|
384
|
+
});
|
385
|
+
|
386
|
+
await expect(
|
387
|
+
asyncifyPolling({
|
388
|
+
pollingQuery: mockTask,
|
389
|
+
checkStatus: mockCheckStatus,
|
390
|
+
onPollingError,
|
391
|
+
initialInterval: 1,
|
392
|
+
}),
|
393
|
+
).rejects.toThrow('Fatal error');
|
394
|
+
|
395
|
+
expect(mockTask).toHaveBeenCalledTimes(1);
|
396
|
+
expect(onPollingError).toHaveBeenCalledTimes(1);
|
397
|
+
expect(mockCheckStatus).not.toHaveBeenCalled();
|
398
|
+
});
|
399
|
+
|
400
|
+
it('should throw custom error when provided by onPollingError', async () => {
|
401
|
+
const mockTask = vi.fn().mockRejectedValue(new Error('Original error'));
|
402
|
+
const mockCheckStatus = vi.fn();
|
403
|
+
|
404
|
+
const customError = new Error('Custom error message');
|
405
|
+
const onPollingError = vi.fn().mockReturnValue({
|
406
|
+
isContinuePolling: false,
|
407
|
+
error: customError,
|
408
|
+
});
|
409
|
+
|
410
|
+
await expect(
|
411
|
+
asyncifyPolling({
|
412
|
+
pollingQuery: mockTask,
|
413
|
+
checkStatus: mockCheckStatus,
|
414
|
+
onPollingError,
|
415
|
+
initialInterval: 1,
|
416
|
+
}),
|
417
|
+
).rejects.toThrow('Custom error message');
|
418
|
+
|
419
|
+
expect(onPollingError).toHaveBeenCalledWith({
|
420
|
+
error: expect.objectContaining({ message: 'Original error' }),
|
421
|
+
retries: 0,
|
422
|
+
consecutiveFailures: 1,
|
423
|
+
});
|
424
|
+
});
|
425
|
+
|
426
|
+
it('should provide correct context information to onPollingError', async () => {
|
427
|
+
const mockTask = vi
|
428
|
+
.fn()
|
429
|
+
.mockRejectedValueOnce(new Error('Error 1'))
|
430
|
+
.mockRejectedValueOnce(new Error('Error 2'))
|
431
|
+
.mockRejectedValueOnce(new Error('Error 3'));
|
432
|
+
|
433
|
+
const mockCheckStatus = vi.fn();
|
434
|
+
|
435
|
+
const onPollingError = vi
|
436
|
+
.fn()
|
437
|
+
.mockReturnValueOnce({ isContinuePolling: true })
|
438
|
+
.mockReturnValueOnce({ isContinuePolling: true })
|
439
|
+
.mockReturnValueOnce({ isContinuePolling: false });
|
440
|
+
|
441
|
+
await expect(
|
442
|
+
asyncifyPolling({
|
443
|
+
pollingQuery: mockTask,
|
444
|
+
checkStatus: mockCheckStatus,
|
445
|
+
onPollingError,
|
446
|
+
initialInterval: 1,
|
447
|
+
}),
|
448
|
+
).rejects.toThrow('Error 3');
|
449
|
+
|
450
|
+
// Verify context progression
|
451
|
+
expect(onPollingError).toHaveBeenNthCalledWith(1, {
|
452
|
+
error: expect.objectContaining({ message: 'Error 1' }),
|
453
|
+
retries: 0,
|
454
|
+
consecutiveFailures: 1,
|
455
|
+
});
|
456
|
+
|
457
|
+
expect(onPollingError).toHaveBeenNthCalledWith(2, {
|
458
|
+
error: expect.objectContaining({ message: 'Error 2' }),
|
459
|
+
retries: 1,
|
460
|
+
consecutiveFailures: 2,
|
461
|
+
});
|
462
|
+
|
463
|
+
expect(onPollingError).toHaveBeenNthCalledWith(3, {
|
464
|
+
error: expect.objectContaining({ message: 'Error 3' }),
|
465
|
+
retries: 2,
|
466
|
+
consecutiveFailures: 3,
|
467
|
+
});
|
468
|
+
});
|
469
|
+
|
470
|
+
it('should fall back to default behavior when onPollingError is not provided', async () => {
|
471
|
+
const mockTask = vi
|
472
|
+
.fn()
|
473
|
+
.mockRejectedValueOnce(new Error('Error 1'))
|
474
|
+
.mockRejectedValueOnce(new Error('Error 2'))
|
475
|
+
.mockRejectedValueOnce(new Error('Error 3'));
|
476
|
+
|
477
|
+
const mockCheckStatus = vi.fn();
|
478
|
+
|
479
|
+
await expect(
|
480
|
+
asyncifyPolling({
|
481
|
+
pollingQuery: mockTask,
|
482
|
+
checkStatus: mockCheckStatus,
|
483
|
+
maxConsecutiveFailures: 2,
|
484
|
+
initialInterval: 1,
|
485
|
+
}),
|
486
|
+
).rejects.toThrow(/consecutive attempts/);
|
487
|
+
|
488
|
+
expect(mockTask).toHaveBeenCalledTimes(2);
|
489
|
+
});
|
490
|
+
});
|
491
|
+
});
|