@ai-sdk/provider-utils 4.0.4 → 4.0.6
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/CHANGELOG.md +14 -0
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
- package/src/__snapshots__/schema.test.ts.snap +346 -0
- package/src/add-additional-properties-to-json-schema.test.ts +289 -0
- package/src/add-additional-properties-to-json-schema.ts +53 -0
- package/src/combine-headers.ts +11 -0
- package/src/convert-async-iterator-to-readable-stream.test.ts +78 -0
- package/src/convert-async-iterator-to-readable-stream.ts +47 -0
- package/src/convert-image-model-file-to-data-uri.test.ts +85 -0
- package/src/convert-image-model-file-to-data-uri.ts +19 -0
- package/src/convert-to-form-data.test.ts +167 -0
- package/src/convert-to-form-data.ts +61 -0
- package/src/create-tool-name-mapping.test.ts +163 -0
- package/src/create-tool-name-mapping.ts +66 -0
- package/src/delay.test.ts +212 -0
- package/src/delay.ts +47 -0
- package/src/delayed-promise.test.ts +132 -0
- package/src/delayed-promise.ts +61 -0
- package/src/download-blob.test.ts +145 -0
- package/src/download-blob.ts +31 -0
- package/src/download-error.ts +39 -0
- package/src/extract-response-headers.ts +9 -0
- package/src/fetch-function.ts +4 -0
- package/src/generate-id.test.ts +31 -0
- package/src/generate-id.ts +57 -0
- package/src/get-error-message.ts +15 -0
- package/src/get-from-api.test.ts +199 -0
- package/src/get-from-api.ts +97 -0
- package/src/get-runtime-environment-user-agent.test.ts +47 -0
- package/src/get-runtime-environment-user-agent.ts +24 -0
- package/src/handle-fetch-error.ts +39 -0
- package/src/index.ts +67 -0
- package/src/inject-json-instruction.test.ts +404 -0
- package/src/inject-json-instruction.ts +63 -0
- package/src/is-abort-error.ts +8 -0
- package/src/is-async-iterable.ts +3 -0
- package/src/is-non-nullable.ts +12 -0
- package/src/is-url-supported.test.ts +282 -0
- package/src/is-url-supported.ts +40 -0
- package/src/load-api-key.ts +45 -0
- package/src/load-optional-setting.ts +30 -0
- package/src/load-setting.ts +62 -0
- package/src/maybe-promise-like.ts +3 -0
- package/src/media-type-to-extension.test.ts +26 -0
- package/src/media-type-to-extension.ts +22 -0
- package/src/normalize-headers.test.ts +64 -0
- package/src/normalize-headers.ts +38 -0
- package/src/parse-json-event-stream.ts +33 -0
- package/src/parse-json.test.ts +191 -0
- package/src/parse-json.ts +122 -0
- package/src/parse-provider-options.ts +32 -0
- package/src/post-to-api.ts +166 -0
- package/src/provider-tool-factory.ts +125 -0
- package/src/remove-undefined-entries.test.ts +57 -0
- package/src/remove-undefined-entries.ts +12 -0
- package/src/resolve.test.ts +125 -0
- package/src/resolve.ts +17 -0
- package/src/response-handler.test.ts +89 -0
- package/src/response-handler.ts +187 -0
- package/src/schema.test-d.ts +11 -0
- package/src/schema.test.ts +502 -0
- package/src/schema.ts +267 -0
- package/src/secure-json-parse.test.ts +59 -0
- package/src/secure-json-parse.ts +92 -0
- package/src/test/convert-array-to-async-iterable.ts +9 -0
- package/src/test/convert-array-to-readable-stream.ts +15 -0
- package/src/test/convert-async-iterable-to-array.ts +9 -0
- package/src/test/convert-readable-stream-to-array.ts +14 -0
- package/src/test/convert-response-stream-to-array.ts +9 -0
- package/src/test/index.ts +7 -0
- package/src/test/is-node-version.ts +4 -0
- package/src/test/mock-id.ts +8 -0
- package/src/to-json-schema/zod3-to-json-schema/LICENSE +16 -0
- package/src/to-json-schema/zod3-to-json-schema/README.md +24 -0
- package/src/to-json-schema/zod3-to-json-schema/get-relative-path.ts +7 -0
- package/src/to-json-schema/zod3-to-json-schema/index.ts +1 -0
- package/src/to-json-schema/zod3-to-json-schema/options.ts +98 -0
- package/src/to-json-schema/zod3-to-json-schema/parse-def.test.ts +224 -0
- package/src/to-json-schema/zod3-to-json-schema/parse-def.ts +109 -0
- package/src/to-json-schema/zod3-to-json-schema/parse-types.ts +57 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/any.ts +5 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/array.test.ts +98 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/array.ts +38 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/bigint.test.ts +51 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/bigint.ts +44 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/boolean.ts +7 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/branded.test.ts +16 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/branded.ts +7 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/catch.test.ts +15 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/catch.ts +7 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/date.test.ts +97 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/date.ts +64 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/default.test.ts +54 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/default.ts +14 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/effects.test.ts +41 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/effects.ts +14 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/enum.ts +13 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/intersection.test.ts +92 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/intersection.ts +52 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/literal.ts +29 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/map.test.ts +48 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/map.ts +47 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/native-enum.test.ts +102 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/native-enum.ts +31 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/never.ts +9 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/null.ts +9 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/nullable.test.ts +67 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/nullable.ts +42 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/number.test.ts +65 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/number.ts +44 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/object.test.ts +149 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/object.ts +88 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/optional.test.ts +147 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/optional.ts +23 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/pipe.test.ts +35 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/pipeline.ts +29 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/promise.test.ts +15 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/promise.ts +11 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/readonly.test.ts +20 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/readonly.ts +7 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/record.test.ts +108 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/record.ts +71 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/set.test.ts +20 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/set.ts +35 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/string.test.ts +438 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/string.ts +426 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/tuple.test.ts +33 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/tuple.ts +61 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/undefined.ts +11 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/union.test.ts +226 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/union.ts +144 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/unknown.ts +7 -0
- package/src/to-json-schema/zod3-to-json-schema/refs.test.ts +919 -0
- package/src/to-json-schema/zod3-to-json-schema/refs.ts +39 -0
- package/src/to-json-schema/zod3-to-json-schema/select-parser.ts +115 -0
- package/src/to-json-schema/zod3-to-json-schema/zod3-to-json-schema.test.ts +862 -0
- package/src/to-json-schema/zod3-to-json-schema/zod3-to-json-schema.ts +93 -0
- package/src/types/assistant-model-message.ts +39 -0
- package/src/types/content-part.ts +379 -0
- package/src/types/data-content.ts +4 -0
- package/src/types/execute-tool.ts +27 -0
- package/src/types/index.ts +40 -0
- package/src/types/model-message.ts +14 -0
- package/src/types/provider-options.ts +9 -0
- package/src/types/system-model-message.ts +20 -0
- package/src/types/tool-approval-request.ts +16 -0
- package/src/types/tool-approval-response.ts +27 -0
- package/src/types/tool-call.ts +31 -0
- package/src/types/tool-model-message.ts +23 -0
- package/src/types/tool-result.ts +35 -0
- package/src/types/tool.test-d.ts +193 -0
- package/src/types/tool.ts +324 -0
- package/src/types/user-model-message.ts +22 -0
- package/src/uint8-utils.ts +26 -0
- package/src/validate-types.test.ts +105 -0
- package/src/validate-types.ts +81 -0
- package/src/version.ts +6 -0
- package/src/with-user-agent-suffix.test.ts +84 -0
- package/src/with-user-agent-suffix.ts +27 -0
- package/src/without-trailing-slash.ts +3 -0
- package/LICENSE +0 -13
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LanguageModelV3FunctionTool,
|
|
3
|
+
LanguageModelV3ProviderTool,
|
|
4
|
+
} from '@ai-sdk/provider';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Interface for mapping between custom tool names and provider tool names.
|
|
8
|
+
*/
|
|
9
|
+
export interface ToolNameMapping {
|
|
10
|
+
/**
|
|
11
|
+
* Maps a custom tool name (used by the client) to the provider's tool name.
|
|
12
|
+
* If the custom tool name does not have a mapping, returns the input name.
|
|
13
|
+
*
|
|
14
|
+
* @param customToolName - The custom name of the tool defined by the client.
|
|
15
|
+
* @returns The corresponding provider tool name, or the input name if not mapped.
|
|
16
|
+
*/
|
|
17
|
+
toProviderToolName: (customToolName: string) => string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Maps a provider tool name to the custom tool name used by the client.
|
|
21
|
+
* If the provider tool name does not have a mapping, returns the input name.
|
|
22
|
+
*
|
|
23
|
+
* @param providerToolName - The name of the tool as understood by the provider.
|
|
24
|
+
* @returns The corresponding custom tool name, or the input name if not mapped.
|
|
25
|
+
*/
|
|
26
|
+
toCustomToolName: (providerToolName: string) => string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param tools - Tools that were passed to the language model.
|
|
31
|
+
* @param providerToolNames - Maps the provider tool ids to the provider tool names.
|
|
32
|
+
*/
|
|
33
|
+
export function createToolNameMapping({
|
|
34
|
+
tools = [],
|
|
35
|
+
providerToolNames,
|
|
36
|
+
}: {
|
|
37
|
+
/**
|
|
38
|
+
* Tools that were passed to the language model.
|
|
39
|
+
*/
|
|
40
|
+
tools:
|
|
41
|
+
| Array<LanguageModelV3FunctionTool | LanguageModelV3ProviderTool>
|
|
42
|
+
| undefined;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Maps the provider tool ids to the provider tool names.
|
|
46
|
+
*/
|
|
47
|
+
providerToolNames: Record<`${string}.${string}`, string>;
|
|
48
|
+
}): ToolNameMapping {
|
|
49
|
+
const customToolNameToProviderToolName: Record<string, string> = {};
|
|
50
|
+
const providerToolNameToCustomToolName: Record<string, string> = {};
|
|
51
|
+
|
|
52
|
+
for (const tool of tools) {
|
|
53
|
+
if (tool.type === 'provider' && tool.id in providerToolNames) {
|
|
54
|
+
const providerToolName = providerToolNames[tool.id];
|
|
55
|
+
customToolNameToProviderToolName[tool.name] = providerToolName;
|
|
56
|
+
providerToolNameToCustomToolName[providerToolName] = tool.name;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
toProviderToolName: (customToolName: string) =>
|
|
62
|
+
customToolNameToProviderToolName[customToolName] ?? customToolName,
|
|
63
|
+
toCustomToolName: (providerToolName: string) =>
|
|
64
|
+
providerToolNameToCustomToolName[providerToolName] ?? providerToolName,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { delay } from './delay';
|
|
2
|
+
import { describe, beforeEach, afterEach, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('delay', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.useFakeTimers();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('basic delay functionality', () => {
|
|
14
|
+
it('should resolve after the specified delay', async () => {
|
|
15
|
+
const delayPromise = delay(1000);
|
|
16
|
+
|
|
17
|
+
// Promise should not be resolved immediately
|
|
18
|
+
let resolved = false;
|
|
19
|
+
delayPromise.then(() => {
|
|
20
|
+
resolved = true;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(resolved).toBe(false);
|
|
24
|
+
|
|
25
|
+
// Advance timers by less than the delay
|
|
26
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
27
|
+
expect(resolved).toBe(false);
|
|
28
|
+
|
|
29
|
+
// Advance timers to complete the delay
|
|
30
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
31
|
+
expect(resolved).toBe(true);
|
|
32
|
+
|
|
33
|
+
// Verify the promise resolves
|
|
34
|
+
await expect(delayPromise).resolves.toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should resolve immediately when delayInMs is null', async () => {
|
|
38
|
+
const delayPromise = delay(null);
|
|
39
|
+
await expect(delayPromise).resolves.toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should resolve immediately when delayInMs is undefined', async () => {
|
|
43
|
+
const delayPromise = delay(undefined);
|
|
44
|
+
await expect(delayPromise).resolves.toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should resolve immediately when delayInMs is 0', async () => {
|
|
48
|
+
const delayPromise = delay(0);
|
|
49
|
+
|
|
50
|
+
// Even with 0 delay, setTimeout is used, so we need to advance timers
|
|
51
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
52
|
+
await expect(delayPromise).resolves.toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('abort signal functionality', () => {
|
|
57
|
+
it('should reject immediately if signal is already aborted', async () => {
|
|
58
|
+
const controller = new AbortController();
|
|
59
|
+
controller.abort();
|
|
60
|
+
|
|
61
|
+
const delayPromise = delay(1000, { abortSignal: controller.signal });
|
|
62
|
+
|
|
63
|
+
await expect(delayPromise).rejects.toThrow('Delay was aborted');
|
|
64
|
+
expect(vi.getTimerCount()).toBe(0); // No timer should be set
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should reject when signal is aborted during delay', async () => {
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
const delayPromise = delay(1000, { abortSignal: controller.signal });
|
|
70
|
+
|
|
71
|
+
// Advance time partially
|
|
72
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
73
|
+
|
|
74
|
+
// Abort the signal
|
|
75
|
+
controller.abort();
|
|
76
|
+
|
|
77
|
+
await expect(delayPromise).rejects.toThrow('Delay was aborted');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should clean up timeout when aborted', async () => {
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const delayPromise = delay(1000, { abortSignal: controller.signal });
|
|
83
|
+
|
|
84
|
+
expect(vi.getTimerCount()).toBe(1);
|
|
85
|
+
|
|
86
|
+
controller.abort();
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await delayPromise;
|
|
90
|
+
} catch {
|
|
91
|
+
// Expected to throw
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should clean up event listener when delay completes normally', async () => {
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const addEventListenerSpy = vi.spyOn(
|
|
100
|
+
controller.signal,
|
|
101
|
+
'addEventListener',
|
|
102
|
+
);
|
|
103
|
+
const removeEventListenerSpy = vi.spyOn(
|
|
104
|
+
controller.signal,
|
|
105
|
+
'removeEventListener',
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const delayPromise = delay(1000, { abortSignal: controller.signal });
|
|
109
|
+
|
|
110
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
|
111
|
+
'abort',
|
|
112
|
+
expect.any(Function),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
116
|
+
await delayPromise;
|
|
117
|
+
|
|
118
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith(
|
|
119
|
+
'abort',
|
|
120
|
+
expect.any(Function),
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should work without signal option', async () => {
|
|
125
|
+
const delayPromise = delay(1000);
|
|
126
|
+
|
|
127
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
128
|
+
await expect(delayPromise).resolves.toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('error handling', () => {
|
|
133
|
+
it('should create proper DOMException for abort', async () => {
|
|
134
|
+
const controller = new AbortController();
|
|
135
|
+
controller.abort();
|
|
136
|
+
|
|
137
|
+
const delayPromise = delay(1000, { abortSignal: controller.signal });
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
await delayPromise;
|
|
141
|
+
expect.fail('Should have thrown');
|
|
142
|
+
} catch (error) {
|
|
143
|
+
expect(error).toBeInstanceOf(DOMException);
|
|
144
|
+
expect((error as DOMException).message).toBe('Delay was aborted');
|
|
145
|
+
expect((error as DOMException).name).toBe('AbortError');
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('edge cases', () => {
|
|
151
|
+
it('should handle very large delays', async () => {
|
|
152
|
+
const delayPromise = delay(Number.MAX_SAFE_INTEGER);
|
|
153
|
+
|
|
154
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
155
|
+
let resolved = false;
|
|
156
|
+
delayPromise.then(() => {
|
|
157
|
+
resolved = true;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(resolved).toBe(false);
|
|
161
|
+
|
|
162
|
+
// Fast forward to complete
|
|
163
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
164
|
+
await expect(delayPromise).resolves.toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should handle negative delays (treated as 0)', async () => {
|
|
168
|
+
const delayPromise = delay(-100);
|
|
169
|
+
|
|
170
|
+
vi.advanceTimersByTime(0);
|
|
171
|
+
await expect(delayPromise).resolves.toBeUndefined();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should handle multiple delays simultaneously', async () => {
|
|
175
|
+
const delay1 = delay(100);
|
|
176
|
+
const delay2 = delay(200);
|
|
177
|
+
const delay3 = delay(300);
|
|
178
|
+
|
|
179
|
+
let resolved1 = false;
|
|
180
|
+
let resolved2 = false;
|
|
181
|
+
let resolved3 = false;
|
|
182
|
+
|
|
183
|
+
delay1.then(() => {
|
|
184
|
+
resolved1 = true;
|
|
185
|
+
});
|
|
186
|
+
delay2.then(() => {
|
|
187
|
+
resolved2 = true;
|
|
188
|
+
});
|
|
189
|
+
delay3.then(() => {
|
|
190
|
+
resolved3 = true;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// After 100ms, only first should resolve
|
|
194
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
195
|
+
expect(resolved1).toBe(true);
|
|
196
|
+
expect(resolved2).toBe(false);
|
|
197
|
+
expect(resolved3).toBe(false);
|
|
198
|
+
|
|
199
|
+
// After 200ms, first two should resolve
|
|
200
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
201
|
+
expect(resolved1).toBe(true);
|
|
202
|
+
expect(resolved2).toBe(true);
|
|
203
|
+
expect(resolved3).toBe(false);
|
|
204
|
+
|
|
205
|
+
// After 300ms, all should resolve
|
|
206
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
207
|
+
expect(resolved1).toBe(true);
|
|
208
|
+
expect(resolved2).toBe(true);
|
|
209
|
+
expect(resolved3).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
package/src/delay.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a Promise that resolves after a specified delay
|
|
3
|
+
* @param delayInMs - The delay duration in milliseconds. If null or undefined, resolves immediately.
|
|
4
|
+
* @param signal - Optional AbortSignal to cancel the delay
|
|
5
|
+
* @returns A Promise that resolves after the specified delay
|
|
6
|
+
* @throws {DOMException} When the signal is aborted
|
|
7
|
+
*/
|
|
8
|
+
export async function delay(
|
|
9
|
+
delayInMs?: number | null,
|
|
10
|
+
options?: {
|
|
11
|
+
abortSignal?: AbortSignal;
|
|
12
|
+
},
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
if (delayInMs == null) {
|
|
15
|
+
return Promise.resolve();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const signal = options?.abortSignal;
|
|
19
|
+
|
|
20
|
+
return new Promise<void>((resolve, reject) => {
|
|
21
|
+
if (signal?.aborted) {
|
|
22
|
+
reject(createAbortError());
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const timeoutId = setTimeout(() => {
|
|
27
|
+
cleanup();
|
|
28
|
+
resolve();
|
|
29
|
+
}, delayInMs);
|
|
30
|
+
|
|
31
|
+
const cleanup = () => {
|
|
32
|
+
clearTimeout(timeoutId);
|
|
33
|
+
signal?.removeEventListener('abort', onAbort);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const onAbort = () => {
|
|
37
|
+
cleanup();
|
|
38
|
+
reject(createAbortError());
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
signal?.addEventListener('abort', onAbort);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createAbortError(): DOMException {
|
|
46
|
+
return new DOMException('Delay was aborted', 'AbortError');
|
|
47
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { DelayedPromise } from './delayed-promise';
|
|
2
|
+
import { delay } from '@ai-sdk/provider-utils';
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
|
|
5
|
+
describe('DelayedPromise', () => {
|
|
6
|
+
it('should resolve when accessed after resolution', async () => {
|
|
7
|
+
const dp = new DelayedPromise<string>();
|
|
8
|
+
dp.resolve('success');
|
|
9
|
+
expect(await dp.promise).toBe('success');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should reject when accessed after rejection', async () => {
|
|
13
|
+
const dp = new DelayedPromise<string>();
|
|
14
|
+
const error = new Error('failure');
|
|
15
|
+
dp.reject(error);
|
|
16
|
+
await expect(dp.promise).rejects.toThrow('failure');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should resolve when accessed before resolution', async () => {
|
|
20
|
+
const dp = new DelayedPromise<string>();
|
|
21
|
+
const promise = dp.promise;
|
|
22
|
+
dp.resolve('success');
|
|
23
|
+
expect(await promise).toBe('success');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should reject when accessed before rejection', async () => {
|
|
27
|
+
const dp = new DelayedPromise<string>();
|
|
28
|
+
const promise = dp.promise;
|
|
29
|
+
const error = new Error('failure');
|
|
30
|
+
dp.reject(error);
|
|
31
|
+
await expect(promise).rejects.toThrow('failure');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should maintain the resolved state after multiple accesses', async () => {
|
|
35
|
+
const dp = new DelayedPromise<string>();
|
|
36
|
+
dp.resolve('success');
|
|
37
|
+
expect(await dp.promise).toBe('success');
|
|
38
|
+
expect(await dp.promise).toBe('success');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should maintain the rejected state after multiple accesses', async () => {
|
|
42
|
+
const dp = new DelayedPromise<string>();
|
|
43
|
+
const error = new Error('failure');
|
|
44
|
+
dp.reject(error);
|
|
45
|
+
await expect(dp.promise).rejects.toThrow('failure');
|
|
46
|
+
await expect(dp.promise).rejects.toThrow('failure');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should block until resolved when accessed before resolution', async () => {
|
|
50
|
+
const dp = new DelayedPromise<string>();
|
|
51
|
+
let resolved = false;
|
|
52
|
+
|
|
53
|
+
// Access the promise before resolving
|
|
54
|
+
const promise = dp.promise.then(value => {
|
|
55
|
+
resolved = true;
|
|
56
|
+
return value;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Promise should not be resolved yet
|
|
60
|
+
expect(resolved).toBe(false);
|
|
61
|
+
|
|
62
|
+
// Wait a bit to ensure it's truly blocking
|
|
63
|
+
await delay(10);
|
|
64
|
+
expect(resolved).toBe(false);
|
|
65
|
+
|
|
66
|
+
// Now resolve it
|
|
67
|
+
dp.resolve('delayed-success');
|
|
68
|
+
|
|
69
|
+
// Should now resolve
|
|
70
|
+
const result = await promise;
|
|
71
|
+
expect(result).toBe('delayed-success');
|
|
72
|
+
expect(resolved).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should block until rejected when accessed before rejection', async () => {
|
|
76
|
+
const dp = new DelayedPromise<string>();
|
|
77
|
+
let rejected = false;
|
|
78
|
+
|
|
79
|
+
// Access the promise before rejecting
|
|
80
|
+
const promise = dp.promise.catch(error => {
|
|
81
|
+
rejected = true;
|
|
82
|
+
throw error;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Promise should not be rejected yet
|
|
86
|
+
expect(rejected).toBe(false);
|
|
87
|
+
|
|
88
|
+
// Wait a bit to ensure it's truly blocking
|
|
89
|
+
await delay(10);
|
|
90
|
+
expect(rejected).toBe(false);
|
|
91
|
+
|
|
92
|
+
// Now reject it
|
|
93
|
+
const error = new Error('delayed-failure');
|
|
94
|
+
dp.reject(error);
|
|
95
|
+
|
|
96
|
+
// Should now reject
|
|
97
|
+
await expect(promise).rejects.toThrow('delayed-failure');
|
|
98
|
+
expect(rejected).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should resolve all pending promises when resolved after access', async () => {
|
|
102
|
+
const dp = new DelayedPromise<string>();
|
|
103
|
+
const results: string[] = [];
|
|
104
|
+
|
|
105
|
+
// Access the promise multiple times before resolution
|
|
106
|
+
const promise1 = dp.promise.then(value => {
|
|
107
|
+
results.push(`first: ${value}`);
|
|
108
|
+
return value;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const promise2 = dp.promise.then(value => {
|
|
112
|
+
results.push(`second: ${value}`);
|
|
113
|
+
return value;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Neither should be resolved yet
|
|
117
|
+
expect(results).toHaveLength(0);
|
|
118
|
+
|
|
119
|
+
// Wait to ensure they're blocking
|
|
120
|
+
await delay(10);
|
|
121
|
+
expect(results).toHaveLength(0);
|
|
122
|
+
|
|
123
|
+
// Resolve the promise
|
|
124
|
+
dp.resolve('success');
|
|
125
|
+
|
|
126
|
+
// Both should resolve
|
|
127
|
+
await Promise.all([promise1, promise2]);
|
|
128
|
+
expect(results).toHaveLength(2);
|
|
129
|
+
expect(results).toContain('first: success');
|
|
130
|
+
expect(results).toContain('second: success');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delayed promise. It is only constructed once the value is accessed.
|
|
3
|
+
* This is useful to avoid unhandled promise rejections when the promise is created
|
|
4
|
+
* but not accessed.
|
|
5
|
+
*/
|
|
6
|
+
export class DelayedPromise<T> {
|
|
7
|
+
private status:
|
|
8
|
+
| { type: 'pending' }
|
|
9
|
+
| { type: 'resolved'; value: T }
|
|
10
|
+
| { type: 'rejected'; error: unknown } = { type: 'pending' };
|
|
11
|
+
private _promise: Promise<T> | undefined;
|
|
12
|
+
private _resolve: undefined | ((value: T) => void) = undefined;
|
|
13
|
+
private _reject: undefined | ((error: unknown) => void) = undefined;
|
|
14
|
+
|
|
15
|
+
get promise(): Promise<T> {
|
|
16
|
+
if (this._promise) {
|
|
17
|
+
return this._promise;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
this._promise = new Promise<T>((resolve, reject) => {
|
|
21
|
+
if (this.status.type === 'resolved') {
|
|
22
|
+
resolve(this.status.value);
|
|
23
|
+
} else if (this.status.type === 'rejected') {
|
|
24
|
+
reject(this.status.error);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this._resolve = resolve;
|
|
28
|
+
this._reject = reject;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return this._promise;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
resolve(value: T): void {
|
|
35
|
+
this.status = { type: 'resolved', value };
|
|
36
|
+
|
|
37
|
+
if (this._promise) {
|
|
38
|
+
this._resolve?.(value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
reject(error: unknown): void {
|
|
43
|
+
this.status = { type: 'rejected', error };
|
|
44
|
+
|
|
45
|
+
if (this._promise) {
|
|
46
|
+
this._reject?.(error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
isResolved(): boolean {
|
|
51
|
+
return this.status.type === 'resolved';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
isRejected(): boolean {
|
|
55
|
+
return this.status.type === 'rejected';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
isPending(): boolean {
|
|
59
|
+
return this.status.type === 'pending';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { downloadBlob } from './download-blob';
|
|
3
|
+
import { DownloadError } from './download-error';
|
|
4
|
+
|
|
5
|
+
describe('downloadBlob()', () => {
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.resetAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
globalThis.fetch = originalFetch;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should download a blob successfully', async () => {
|
|
17
|
+
const mockBlob = new Blob(['test content'], { type: 'image/png' });
|
|
18
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
19
|
+
ok: true,
|
|
20
|
+
blob: () => Promise.resolve(mockBlob),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const result = await downloadBlob('https://example.com/image.png');
|
|
24
|
+
|
|
25
|
+
expect(result).toBe(mockBlob);
|
|
26
|
+
expect(fetch).toHaveBeenCalledWith('https://example.com/image.png');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should throw DownloadError on non-ok response', async () => {
|
|
30
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
31
|
+
ok: false,
|
|
32
|
+
status: 404,
|
|
33
|
+
statusText: 'Not Found',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await expect(
|
|
37
|
+
downloadBlob('https://example.com/not-found.png'),
|
|
38
|
+
).rejects.toThrow(DownloadError);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await downloadBlob('https://example.com/not-found.png');
|
|
42
|
+
} catch (error) {
|
|
43
|
+
expect(DownloadError.isInstance(error)).toBe(true);
|
|
44
|
+
if (DownloadError.isInstance(error)) {
|
|
45
|
+
expect(error.url).toBe('https://example.com/not-found.png');
|
|
46
|
+
expect(error.statusCode).toBe(404);
|
|
47
|
+
expect(error.statusText).toBe('Not Found');
|
|
48
|
+
expect(error.message).toBe(
|
|
49
|
+
'Failed to download https://example.com/not-found.png: 404 Not Found',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should throw DownloadError on network error', async () => {
|
|
56
|
+
const networkError = new Error('Network error');
|
|
57
|
+
globalThis.fetch = vi.fn().mockRejectedValue(networkError);
|
|
58
|
+
|
|
59
|
+
await expect(
|
|
60
|
+
downloadBlob('https://example.com/network-error.png'),
|
|
61
|
+
).rejects.toThrow(DownloadError);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await downloadBlob('https://example.com/network-error.png');
|
|
65
|
+
} catch (error) {
|
|
66
|
+
expect(DownloadError.isInstance(error)).toBe(true);
|
|
67
|
+
if (DownloadError.isInstance(error)) {
|
|
68
|
+
expect(error.url).toBe('https://example.com/network-error.png');
|
|
69
|
+
expect(error.cause).toBe(networkError);
|
|
70
|
+
expect(error.message).toContain('Network error');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should re-throw DownloadError without wrapping', async () => {
|
|
76
|
+
const originalError = new DownloadError({
|
|
77
|
+
url: 'https://example.com/original.png',
|
|
78
|
+
statusCode: 500,
|
|
79
|
+
statusText: 'Internal Server Error',
|
|
80
|
+
});
|
|
81
|
+
globalThis.fetch = vi.fn().mockRejectedValue(originalError);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await downloadBlob('https://example.com/test.png');
|
|
85
|
+
} catch (error) {
|
|
86
|
+
expect(error).toBe(originalError);
|
|
87
|
+
expect(DownloadError.isInstance(error)).toBe(true);
|
|
88
|
+
if (DownloadError.isInstance(error)) {
|
|
89
|
+
expect(error.url).toBe('https://example.com/original.png');
|
|
90
|
+
expect(error.statusCode).toBe(500);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('DownloadError', () => {
|
|
97
|
+
it('should create error with status code and text', () => {
|
|
98
|
+
const error = new DownloadError({
|
|
99
|
+
url: 'https://example.com/test.png',
|
|
100
|
+
statusCode: 403,
|
|
101
|
+
statusText: 'Forbidden',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(error.name).toBe('AI_DownloadError');
|
|
105
|
+
expect(error.url).toBe('https://example.com/test.png');
|
|
106
|
+
expect(error.statusCode).toBe(403);
|
|
107
|
+
expect(error.statusText).toBe('Forbidden');
|
|
108
|
+
expect(error.message).toBe(
|
|
109
|
+
'Failed to download https://example.com/test.png: 403 Forbidden',
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should create error with cause', () => {
|
|
114
|
+
const cause = new Error('Connection refused');
|
|
115
|
+
const error = new DownloadError({
|
|
116
|
+
url: 'https://example.com/test.png',
|
|
117
|
+
cause,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(error.url).toBe('https://example.com/test.png');
|
|
121
|
+
expect(error.cause).toBe(cause);
|
|
122
|
+
expect(error.message).toContain('Connection refused');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should create error with custom message', () => {
|
|
126
|
+
const error = new DownloadError({
|
|
127
|
+
url: 'https://example.com/test.png',
|
|
128
|
+
message: 'Custom error message',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(error.message).toBe('Custom error message');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should identify DownloadError instances correctly', () => {
|
|
135
|
+
const downloadError = new DownloadError({
|
|
136
|
+
url: 'https://example.com/test.png',
|
|
137
|
+
});
|
|
138
|
+
const regularError = new Error('Not a download error');
|
|
139
|
+
|
|
140
|
+
expect(DownloadError.isInstance(downloadError)).toBe(true);
|
|
141
|
+
expect(DownloadError.isInstance(regularError)).toBe(false);
|
|
142
|
+
expect(DownloadError.isInstance(null)).toBe(false);
|
|
143
|
+
expect(DownloadError.isInstance(undefined)).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { DownloadError } from './download-error';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Download a file from a URL and return it as a Blob.
|
|
5
|
+
*
|
|
6
|
+
* @param url - The URL to download from.
|
|
7
|
+
* @returns A Promise that resolves to the downloaded Blob.
|
|
8
|
+
*
|
|
9
|
+
* @throws DownloadError if the download fails.
|
|
10
|
+
*/
|
|
11
|
+
export async function downloadBlob(url: string): Promise<Blob> {
|
|
12
|
+
try {
|
|
13
|
+
const response = await fetch(url);
|
|
14
|
+
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
throw new DownloadError({
|
|
17
|
+
url,
|
|
18
|
+
statusCode: response.status,
|
|
19
|
+
statusText: response.statusText,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return await response.blob();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (DownloadError.isInstance(error)) {
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
throw new DownloadError({ url, cause: error });
|
|
30
|
+
}
|
|
31
|
+
}
|