Skip to content

Commit 156aed0

Browse files
committed
feat: Added browser support
1 parent 6ccdb92 commit 156aed0

File tree

14 files changed

+1081
-126
lines changed

14 files changed

+1081
-126
lines changed

package-lock.json

Lines changed: 965 additions & 99 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
{
22
"name": "ai21",
3-
"version": "1.0.0-rc.4",
3+
"version": "1.0.0-rc.3",
44
"description": "AI21 TypeScript SDK",
5-
"main": "./dist/index.js",
5+
"main": "./dist/bundle.cjs.js",
66
"types": "./dist/index.d.ts",
7+
"module": "./dist/bundle.esm.js",
78
"type": "module",
89
"exports": {
910
".": {
10-
"require": "./dist/index.js",
11-
"import": "./dist/index.js",
11+
"require": "./dist/bundle.cjs.js",
12+
"import": "./dist/bundle.esm.js",
1213
"types": "./dist/index.d.ts"
1314
}
1415
},
1516
"scripts": {
16-
"build": "tsc",
17+
"build": "rollup -c",
1718
"test": "jest --coverage --no-cache --runInBand --config jest.config.ts",
1819
"unused-deps": "npx depcheck --json | jq '.dependencies == []'",
1920
"clean-build": "rm -rf ./dist",
@@ -45,6 +46,10 @@
4546
"devDependencies": {
4647
"@eslint/js": "^9.14.0",
4748
"@jest/types": "^29.6.3",
49+
"@rollup/plugin-commonjs": "^28.0.1",
50+
"@rollup/plugin-node-resolve": "^15.3.0",
51+
"@rollup/plugin-terser": "^0.4.4",
52+
"@rollup/plugin-typescript": "^12.1.1",
4853
"@semantic-release/exec": "^6.0.3",
4954
"@semantic-release/git": "^10.0.1",
5055
"@semantic-release/github": "^11.0.1",
@@ -61,6 +66,8 @@
6166
"jest-environment-jsdom": "^29.7.0",
6267
"madge": "^6.1.0",
6368
"prettier": "^3.3.3",
69+
"rollup": "^2.79.2",
70+
"rollup-plugin-terser": "^7.0.2",
6471
"semantic-release": "^24.2.0",
6572
"ts-jest": "^29.2.5",
6673
"ts-node": "^10.9.2",
@@ -70,5 +77,10 @@
7077
"typescript": "^4.9.5",
7178
"typescript-eslint": "^8.13.0",
7279
"uuid": "^11.0.3"
80+
},
81+
"browser": {
82+
"fs": false,
83+
"os": false,
84+
"path": false
7385
}
7486
}

rollup.config.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import resolve from '@rollup/plugin-node-resolve';
2+
import commonjs from '@rollup/plugin-commonjs';
3+
import typescript from '@rollup/plugin-typescript';
4+
import { terser } from 'rollup-plugin-terser';
5+
6+
export default {
7+
input: 'src/index.ts', // Entry point of your library
8+
output: [
9+
{
10+
file: 'dist/bundle.cjs.js',
11+
format: 'cjs', // CommonJS format for Node.js
12+
sourcemap: true,
13+
},
14+
{
15+
file: 'dist/bundle.esm.js',
16+
format: 'esm', // ES module format for modern browsers
17+
sourcemap: true,
18+
},
19+
],
20+
plugins: [
21+
resolve(), // Resolves node_modules
22+
commonjs(), // Converts CommonJS to ES6
23+
typescript(), // Compiles TypeScript
24+
terser(), // Minifies the bundle
25+
],
26+
external: ['node-fetch'], // Exclude node-fetch from the bundle
27+
};

src/APIClient.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { AI21Error } from './errors';
22
import { VERSION } from './version';
33

4-
import fetch from 'node-fetch';
5-
import { HeadersInit, RequestInit } from 'node-fetch';
64
import { RequestOptions, FinalRequestOptions, APIResponseProps, HTTPMethod, Headers } from './types/index.js';
75
import { AI21EnvConfig } from './EnvConfig';
86
import { handleAPIResponse } from './ResponseHandler';
7+
import { createFetchInstance } from 'envFetch';
8+
import { Fetch } from 'fetch';
99

1010
const validatePositiveInteger = (name: string, n: unknown): number => {
1111
if (typeof n !== 'number' || !Number.isInteger(n)) {
@@ -21,19 +21,23 @@ export abstract class APIClient {
2121
protected baseURL: string;
2222
protected maxRetries: number;
2323
protected timeout: number;
24+
protected fetch: Fetch;
2425

2526
constructor({
2627
baseURL,
2728
maxRetries = AI21EnvConfig.MAX_RETRIES,
2829
timeout = AI21EnvConfig.TIMEOUT_SECONDS,
30+
fetch = createFetchInstance(),
2931
}: {
3032
baseURL: string;
3133
maxRetries?: number | undefined;
3234
timeout: number | undefined;
35+
fetch?: Fetch;
3336
}) {
3437
this.baseURL = baseURL;
3538
this.maxRetries = validatePositiveInteger('maxRetries', maxRetries);
3639
this.timeout = validatePositiveInteger('timeout', timeout);
40+
this.fetch = fetch;
3741
}
3842
get<Req, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
3943
return this.makeRequest('get', path, opts);
@@ -93,11 +97,10 @@ export abstract class APIClient {
9397
...this.defaultHeaders(options),
9498
...options.headers,
9599
};
96-
97-
const response = await fetch(url, {
100+
const response = await this.fetch.call(url, {
98101
method: options.method,
99-
headers: headers as HeadersInit,
100-
signal: controller.signal as RequestInit['signal'],
102+
headers: headers as any,
103+
signal: controller.signal,
101104
body: options.body ? JSON.stringify(options.body) : undefined,
102105
});
103106

src/ResponseHandler.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { APIResponseProps } from './types';
22
import { AI21Error } from './errors';
33
import { Stream } from './Streaming';
4-
import { Response } from 'node-fetch';
54

65
type APIResponse<T> = {
76
data?: T;

src/Streaming/SSEDecoder.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { Response as NodeResponse } from 'node-fetch';
2-
import { Readable } from 'stream';
31
import { SSE_DATA_PREFIX } from './Consts';
42
import { StreamingDecodeError } from '../errors';
3+
import { getReadableStream } from 'envFetch';
54

65
export interface SSEDecoder {
76
decode(line: string): string | null;
8-
iterLines(response: NodeResponse): AsyncIterableIterator<string>;
7+
iterLines(response: Response): AsyncIterableIterator<string>;
98
}
109

1110
export class DefaultSSEDecoder implements SSEDecoder {
@@ -19,16 +18,15 @@ export class DefaultSSEDecoder implements SSEDecoder {
1918
throw new StreamingDecodeError(`Invalid SSE line: ${line}`);
2019
}
2120

22-
async *iterLines(response: NodeResponse): AsyncIterableIterator<string> {
21+
async *iterLines(response: Response): AsyncIterableIterator<string> {
2322
if (!response.body) {
2423
throw new Error('Response body is null');
2524
}
2625

27-
const webReadableStream = Readable.toWeb(response.body as Readable);
28-
const reader = webReadableStream.getReader();
29-
26+
const reader = await getReadableStream(response.body);
3027
let buffer = '';
3128

29+
3230
try {
3331
while (true) {
3432
const { done, value } = await reader.read();

src/Streaming/Stream.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Response as NodeResponse } from 'node-fetch';
21
import { DefaultSSEDecoder } from './SSEDecoder';
32
import { SSEDecoder } from './SSEDecoder';
43
import { SSE_DONE_MSG } from './Consts';
@@ -17,7 +16,7 @@ export class Stream<T> implements AsyncIterableIterator<T> {
1716
private iterator: AsyncIterableIterator<T>;
1817

1918
constructor(
20-
private response: NodeResponse,
19+
private response: Response,
2120
decoder?: SSEDecoder,
2221
) {
2322
this.decoder = decoder || new DefaultSSEDecoder();

src/envFetch.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { BrowserFetch, Fetch, NodeFetch } from 'fetch';
2+
3+
export const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined";
4+
export const isWebWorker =
5+
typeof self === "object" &&
6+
// @ts-ignore
7+
typeof self?.importScripts === "function" &&
8+
(self.constructor?.name === "DedicatedWorkerGlobalScope" ||
9+
self.constructor?.name === "ServiceWorkerGlobalScope" ||
10+
self.constructor?.name === "SharedWorkerGlobalScope");
11+
12+
13+
export const isNode = typeof process !== "undefined" && Boolean(process.version) && Boolean(process.versions?.node);
14+
15+
export const getReadableStream = async (responseBody: ReadableStream<Uint8Array>): Promise<ReadableStreamDefaultReader<Uint8Array>> => {
16+
if (isBrowser || isWebWorker) {
17+
return responseBody.getReader();
18+
} else {
19+
// @ts-ignore - ReadableStream.from() is available in Node.js but TypeScript doesn't recognize it
20+
return (await import("stream/web")).ReadableStream.from(responseBody).getReader();
21+
}
22+
};
23+
24+
25+
export function createFetchInstance(): Fetch {
26+
if (isBrowser || isWebWorker) {
27+
return new BrowserFetch();
28+
} else {
29+
return new NodeFetch();
30+
}
31+
}

src/fetch/BaseFetch.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export abstract class Fetch {
2+
abstract call(url: string, options?: RequestInit): Promise<Response>;
3+
}

src/fetch/BrowserFetch.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Fetch } from './BaseFetch';
2+
3+
export class BrowserFetch extends Fetch {
4+
call(url: string, options?: RequestInit): Promise<Response> {
5+
return fetch(url, options);
6+
}
7+
}

0 commit comments

Comments
 (0)