useprint-js 0.0.1
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/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
- package/README.md +15 -0
- package/index.ts +444 -0
- package/package.json +19 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
|
3
|
+
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Default to using Bun instead of Node.js.
|
|
8
|
+
|
|
9
|
+
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
10
|
+
- Use `bun test` instead of `jest` or `vitest`
|
|
11
|
+
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
12
|
+
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
13
|
+
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
14
|
+
- Bun automatically loads .env, so don't use dotenv.
|
|
15
|
+
|
|
16
|
+
## APIs
|
|
17
|
+
|
|
18
|
+
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
|
19
|
+
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
|
20
|
+
- `Bun.redis` for Redis. Don't use `ioredis`.
|
|
21
|
+
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
|
22
|
+
- `WebSocket` is built-in. Don't use `ws`.
|
|
23
|
+
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
|
24
|
+
- Bun.$`ls` instead of execa.
|
|
25
|
+
|
|
26
|
+
## Testing
|
|
27
|
+
|
|
28
|
+
Use `bun test` to run tests.
|
|
29
|
+
|
|
30
|
+
```ts#index.test.ts
|
|
31
|
+
import { test, expect } from "bun:test";
|
|
32
|
+
|
|
33
|
+
test("hello world", () => {
|
|
34
|
+
expect(1).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Frontend
|
|
39
|
+
|
|
40
|
+
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
|
41
|
+
|
|
42
|
+
Server:
|
|
43
|
+
|
|
44
|
+
```ts#index.ts
|
|
45
|
+
import index from "./index.html"
|
|
46
|
+
|
|
47
|
+
Bun.serve({
|
|
48
|
+
routes: {
|
|
49
|
+
"/": index,
|
|
50
|
+
"/api/users/:id": {
|
|
51
|
+
GET: (req) => {
|
|
52
|
+
return new Response(JSON.stringify({ id: req.params.id }));
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
// optional websocket support
|
|
57
|
+
websocket: {
|
|
58
|
+
open: (ws) => {
|
|
59
|
+
ws.send("Hello, world!");
|
|
60
|
+
},
|
|
61
|
+
message: (ws, message) => {
|
|
62
|
+
ws.send(message);
|
|
63
|
+
},
|
|
64
|
+
close: (ws) => {
|
|
65
|
+
// handle close
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
development: {
|
|
69
|
+
hmr: true,
|
|
70
|
+
console: true,
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
|
76
|
+
|
|
77
|
+
```html#index.html
|
|
78
|
+
<html>
|
|
79
|
+
<body>
|
|
80
|
+
<h1>Hello, world!</h1>
|
|
81
|
+
<script type="module" src="./frontend.tsx"></script>
|
|
82
|
+
</body>
|
|
83
|
+
</html>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
With the following `frontend.tsx`:
|
|
87
|
+
|
|
88
|
+
```tsx#frontend.tsx
|
|
89
|
+
import React from "react";
|
|
90
|
+
|
|
91
|
+
// import .css files directly and it works
|
|
92
|
+
import './index.css';
|
|
93
|
+
|
|
94
|
+
import { createRoot } from "react-dom/client";
|
|
95
|
+
|
|
96
|
+
const root = createRoot(document.body);
|
|
97
|
+
|
|
98
|
+
export default function Frontend() {
|
|
99
|
+
return <h1>Hello, world!</h1>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
root.render(<Frontend />);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then, run index.ts
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
bun --hot ./index.ts
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
|
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @useprint/client
|
|
2
|
+
|
|
3
|
+
To install dependencies:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
To run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun run index.ts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This project was created using `bun init` in bun v1.3.2. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
package/index.ts
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { render } from "@skrift/render";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error codes that can be thrown by the UsePrint client
|
|
6
|
+
*/
|
|
7
|
+
export type UsePrintErrorCode =
|
|
8
|
+
| "API_KEY_MISSING"
|
|
9
|
+
| "NETWORK_ERROR"
|
|
10
|
+
| "TIMEOUT"
|
|
11
|
+
| "RATE_LIMIT"
|
|
12
|
+
| "NOT_FOUND"
|
|
13
|
+
| "VALIDATION_ERROR"
|
|
14
|
+
| "AUTHENTICATION_ERROR"
|
|
15
|
+
| "SERVER_ERROR"
|
|
16
|
+
| "UNKNOWN_ERROR";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Single error class for all UsePrint API errors
|
|
20
|
+
*/
|
|
21
|
+
export class UsePrintError extends Error {
|
|
22
|
+
code: UsePrintErrorCode;
|
|
23
|
+
statusCode?: number;
|
|
24
|
+
retryable: boolean;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
code: UsePrintErrorCode,
|
|
28
|
+
message: string,
|
|
29
|
+
options?: { statusCode?: number; retryable?: boolean },
|
|
30
|
+
) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "UsePrintError";
|
|
33
|
+
this.code = code;
|
|
34
|
+
this.statusCode = options?.statusCode;
|
|
35
|
+
this.retryable = options?.retryable ?? false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type PdfOptions = {
|
|
40
|
+
margin?: {
|
|
41
|
+
top: number;
|
|
42
|
+
right: number;
|
|
43
|
+
bottom: number;
|
|
44
|
+
left: number;
|
|
45
|
+
};
|
|
46
|
+
format?: "A4" | "A3" | "A2" | "A1" | "A0";
|
|
47
|
+
quality?: 100 | 90 | 80 | 70 | 60 | 50 | 40 | 30 | 20 | 10;
|
|
48
|
+
scale?: number;
|
|
49
|
+
dpi?: number;
|
|
50
|
+
orientation?: "portrait" | "landscape";
|
|
51
|
+
background?: string;
|
|
52
|
+
font?: string;
|
|
53
|
+
fontSize?: number;
|
|
54
|
+
timeout?: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type RetryOptions = {
|
|
58
|
+
maxRetries?: number;
|
|
59
|
+
initialDelay?: number;
|
|
60
|
+
maxDelay?: number;
|
|
61
|
+
retryableStatusCodes?: number[];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type CreatePdfParams = {
|
|
65
|
+
pdfOptions?: PdfOptions;
|
|
66
|
+
retryOptions?: RetryOptions;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type CreatePdfResult = {
|
|
70
|
+
url: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type UploadUrlResult = {
|
|
74
|
+
url: string;
|
|
75
|
+
id: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type QueuePdfResult =
|
|
79
|
+
| {
|
|
80
|
+
id: string;
|
|
81
|
+
status: "pending" | "processing" | "failed";
|
|
82
|
+
createdAt: Date;
|
|
83
|
+
updatedAt: Date;
|
|
84
|
+
}
|
|
85
|
+
| {
|
|
86
|
+
id: string;
|
|
87
|
+
status: "completed";
|
|
88
|
+
url: string;
|
|
89
|
+
createdAt: Date;
|
|
90
|
+
updatedAt: Date;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const DEFAULT_RETRY_OPTIONS: Required<RetryOptions> = {
|
|
94
|
+
maxRetries: 3,
|
|
95
|
+
initialDelay: 1000, // 1 second minimum since PDF creation takes at least 1s
|
|
96
|
+
maxDelay: 8000,
|
|
97
|
+
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Creates a UsePrint API client
|
|
102
|
+
* @param apiKey - Your UsePrint API key
|
|
103
|
+
* @param retryOptions - Default retry options for all requests (can be overridden per request)
|
|
104
|
+
*/
|
|
105
|
+
export const usePrint = ({
|
|
106
|
+
apiKey,
|
|
107
|
+
retryOptions,
|
|
108
|
+
}: {
|
|
109
|
+
apiKey?: string;
|
|
110
|
+
retryOptions?: RetryOptions;
|
|
111
|
+
}) => {
|
|
112
|
+
const BASE_URL = "https://api.useprint.dev";
|
|
113
|
+
const headers = new Headers();
|
|
114
|
+
const key = apiKey ?? process.env.USEPRINT_API_KEY;
|
|
115
|
+
if (!key) {
|
|
116
|
+
throw new UsePrintError(
|
|
117
|
+
"API_KEY_MISSING",
|
|
118
|
+
"You have to provide an API key, either via the apiKey parameter or the USEPRINT_API_KEY environment variable.",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
headers.set("Authorization", `x-api-key ${key}`);
|
|
122
|
+
headers.set("Content-Type", "application/json");
|
|
123
|
+
|
|
124
|
+
const defaultRetryOptions: Required<RetryOptions> = {
|
|
125
|
+
...DEFAULT_RETRY_OPTIONS,
|
|
126
|
+
...retryOptions,
|
|
127
|
+
retryableStatusCodes:
|
|
128
|
+
retryOptions?.retryableStatusCodes ??
|
|
129
|
+
DEFAULT_RETRY_OPTIONS.retryableStatusCodes,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Creates an error from a fetch response
|
|
134
|
+
*/
|
|
135
|
+
const createErrorFromResponse = async (
|
|
136
|
+
res: Response,
|
|
137
|
+
): Promise<UsePrintError> => {
|
|
138
|
+
let errorMessage = `Request failed with status ${res.status}`;
|
|
139
|
+
try {
|
|
140
|
+
const errorBody = (await res.json()) as
|
|
141
|
+
| { message?: string; error?: string }
|
|
142
|
+
| unknown;
|
|
143
|
+
if (
|
|
144
|
+
typeof errorBody === "object" &&
|
|
145
|
+
errorBody !== null &&
|
|
146
|
+
("message" in errorBody || "error" in errorBody)
|
|
147
|
+
) {
|
|
148
|
+
errorMessage =
|
|
149
|
+
("message" in errorBody && typeof errorBody.message === "string"
|
|
150
|
+
? errorBody.message
|
|
151
|
+
: null) ||
|
|
152
|
+
("error" in errorBody && typeof errorBody.error === "string"
|
|
153
|
+
? errorBody.error
|
|
154
|
+
: null) ||
|
|
155
|
+
errorMessage;
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// If response body is not JSON, use default message
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const statusCode = res.status;
|
|
162
|
+
let code: UsePrintErrorCode;
|
|
163
|
+
let retryable = false;
|
|
164
|
+
|
|
165
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
166
|
+
code = "AUTHENTICATION_ERROR";
|
|
167
|
+
} else if (statusCode === 404) {
|
|
168
|
+
code = "NOT_FOUND";
|
|
169
|
+
} else if (statusCode === 400) {
|
|
170
|
+
code = "VALIDATION_ERROR";
|
|
171
|
+
} else if (statusCode === 429) {
|
|
172
|
+
code = "RATE_LIMIT";
|
|
173
|
+
retryable = true;
|
|
174
|
+
} else if (statusCode >= 500) {
|
|
175
|
+
code = "SERVER_ERROR";
|
|
176
|
+
retryable = true;
|
|
177
|
+
} else if (statusCode === 408) {
|
|
178
|
+
code = "TIMEOUT";
|
|
179
|
+
retryable = true;
|
|
180
|
+
} else {
|
|
181
|
+
code = "UNKNOWN_ERROR";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return new UsePrintError(code, errorMessage, {
|
|
185
|
+
statusCode,
|
|
186
|
+
retryable,
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Retries a function with exponential backoff
|
|
192
|
+
*/
|
|
193
|
+
const retryWithBackoff = async <T>(
|
|
194
|
+
fn: () => Promise<T>,
|
|
195
|
+
options: Required<RetryOptions>,
|
|
196
|
+
): Promise<T> => {
|
|
197
|
+
let lastError: Error | UsePrintError | undefined;
|
|
198
|
+
let delay = options.initialDelay;
|
|
199
|
+
|
|
200
|
+
for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
|
|
201
|
+
try {
|
|
202
|
+
return await fn();
|
|
203
|
+
} catch (error) {
|
|
204
|
+
lastError = error as Error | UsePrintError;
|
|
205
|
+
|
|
206
|
+
// Don't retry if it's the last attempt
|
|
207
|
+
if (attempt === options.maxRetries) {
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Don't retry if error is not retryable
|
|
212
|
+
if (lastError instanceof UsePrintError && !lastError.retryable) {
|
|
213
|
+
throw lastError;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check if it's a network error (not a UsePrintError)
|
|
217
|
+
if (!(lastError instanceof UsePrintError)) {
|
|
218
|
+
// Network errors are always retryable
|
|
219
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
220
|
+
delay = Math.min(delay * 2, options.maxDelay);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check if status code is retryable
|
|
225
|
+
if (
|
|
226
|
+
lastError.statusCode &&
|
|
227
|
+
options.retryableStatusCodes.includes(lastError.statusCode)
|
|
228
|
+
) {
|
|
229
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
230
|
+
delay = Math.min(delay * 2, options.maxDelay);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Error is not retryable
|
|
235
|
+
throw lastError;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// If we get here, all retries failed
|
|
240
|
+
if (!lastError) {
|
|
241
|
+
throw new UsePrintError(
|
|
242
|
+
"UNKNOWN_ERROR",
|
|
243
|
+
"Request failed for unknown reason",
|
|
244
|
+
{ retryable: false },
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (lastError instanceof UsePrintError) {
|
|
249
|
+
throw lastError;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Network error after all retries
|
|
253
|
+
throw new UsePrintError(
|
|
254
|
+
"NETWORK_ERROR",
|
|
255
|
+
`Network request failed after ${options.maxRetries + 1} attempts: ${lastError.message}`,
|
|
256
|
+
{ retryable: true },
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Makes a fetch request with error handling and optional retry
|
|
262
|
+
*/
|
|
263
|
+
const fetchWithErrorHandling = async (
|
|
264
|
+
url: string,
|
|
265
|
+
options: RequestInit & { retryOptions?: RetryOptions } = {},
|
|
266
|
+
): Promise<Response> => {
|
|
267
|
+
const { retryOptions: requestRetryOptions, ...fetchOptions } = options;
|
|
268
|
+
const effectiveRetryOptions: Required<RetryOptions> = {
|
|
269
|
+
...defaultRetryOptions,
|
|
270
|
+
...requestRetryOptions,
|
|
271
|
+
retryableStatusCodes:
|
|
272
|
+
requestRetryOptions?.retryableStatusCodes ??
|
|
273
|
+
defaultRetryOptions.retryableStatusCodes,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
return retryWithBackoff(async () => {
|
|
277
|
+
try {
|
|
278
|
+
const res = await fetch(url, fetchOptions);
|
|
279
|
+
|
|
280
|
+
if (!res.ok) {
|
|
281
|
+
const error = await createErrorFromResponse(res);
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return res;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
// Convert abort errors to timeout errors
|
|
288
|
+
if (
|
|
289
|
+
error instanceof Error &&
|
|
290
|
+
(error.name === "AbortError" ||
|
|
291
|
+
(error instanceof DOMException && error.name === "AbortError"))
|
|
292
|
+
) {
|
|
293
|
+
throw new UsePrintError("TIMEOUT", "Request timed out", {
|
|
294
|
+
retryable: true,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}, effectiveRetryOptions);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const getUploadUrl = async (retryOptions?: RetryOptions) => {
|
|
303
|
+
const res = await fetchWithErrorHandling(`${BASE_URL}/pdf/queue`, {
|
|
304
|
+
method: "POST",
|
|
305
|
+
headers,
|
|
306
|
+
retryOptions,
|
|
307
|
+
});
|
|
308
|
+
return (await res.json()) as UploadUrlResult;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const getPdfStatus = async ({
|
|
312
|
+
id,
|
|
313
|
+
retryOptions,
|
|
314
|
+
}: {
|
|
315
|
+
id: string;
|
|
316
|
+
retryOptions?: RetryOptions;
|
|
317
|
+
}) => {
|
|
318
|
+
const res = await fetchWithErrorHandling(`${BASE_URL}/pdf/status/${id}`, {
|
|
319
|
+
headers,
|
|
320
|
+
retryOptions,
|
|
321
|
+
});
|
|
322
|
+
return (await res.json()) as QueuePdfResult;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
print: async (
|
|
327
|
+
codeOrHtml: React.ReactNode | string,
|
|
328
|
+
{ pdfOptions, retryOptions }: CreatePdfParams,
|
|
329
|
+
) => {
|
|
330
|
+
const isHtml = typeof codeOrHtml === "string";
|
|
331
|
+
const html = isHtml ? codeOrHtml : await render(codeOrHtml);
|
|
332
|
+
const effectiveRetryOptions: Required<RetryOptions> = {
|
|
333
|
+
...defaultRetryOptions,
|
|
334
|
+
...retryOptions,
|
|
335
|
+
retryableStatusCodes:
|
|
336
|
+
retryOptions?.retryableStatusCodes ??
|
|
337
|
+
defaultRetryOptions.retryableStatusCodes,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const timeout = pdfOptions?.timeout ?? 30000; // Default 30 seconds
|
|
341
|
+
const sizeInBytes = new TextEncoder().encode(html).length;
|
|
342
|
+
const directUpload = sizeInBytes < 5 * 1024 * 1024;
|
|
343
|
+
let result: CreatePdfResult;
|
|
344
|
+
|
|
345
|
+
if (directUpload) {
|
|
346
|
+
const abortController = new AbortController();
|
|
347
|
+
const timeoutId = setTimeout(() => abortController.abort(), timeout);
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const res = await fetchWithErrorHandling(`${BASE_URL}/pdf/create`, {
|
|
351
|
+
method: "POST",
|
|
352
|
+
headers,
|
|
353
|
+
body: JSON.stringify({
|
|
354
|
+
html,
|
|
355
|
+
...pdfOptions,
|
|
356
|
+
}),
|
|
357
|
+
signal: abortController.signal,
|
|
358
|
+
retryOptions,
|
|
359
|
+
});
|
|
360
|
+
clearTimeout(timeoutId);
|
|
361
|
+
result = (await res.json()) as CreatePdfResult;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
clearTimeout(timeoutId);
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
try {
|
|
368
|
+
const uploadUrl = await getUploadUrl(retryOptions);
|
|
369
|
+
let queue = await getPdfStatus({
|
|
370
|
+
id: uploadUrl.id,
|
|
371
|
+
retryOptions,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const pollingMaxDelay = effectiveRetryOptions.maxDelay;
|
|
375
|
+
let pollingDelay = effectiveRetryOptions.initialDelay;
|
|
376
|
+
|
|
377
|
+
while (queue.status !== "completed") {
|
|
378
|
+
if (queue.status === "failed") {
|
|
379
|
+
throw new UsePrintError(
|
|
380
|
+
"SERVER_ERROR",
|
|
381
|
+
"PDF generation failed in queue",
|
|
382
|
+
{ retryable: false },
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
await new Promise((resolve) => setTimeout(resolve, pollingDelay));
|
|
387
|
+
pollingDelay = Math.min(pollingDelay * 2, pollingMaxDelay);
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
queue = await getPdfStatus({
|
|
391
|
+
id: queue.id,
|
|
392
|
+
retryOptions,
|
|
393
|
+
});
|
|
394
|
+
} catch (error) {
|
|
395
|
+
// If status check fails, retry with backoff
|
|
396
|
+
if (error instanceof UsePrintError && error.retryable) {
|
|
397
|
+
await new Promise((resolve) =>
|
|
398
|
+
setTimeout(resolve, pollingDelay),
|
|
399
|
+
);
|
|
400
|
+
pollingDelay = Math.min(pollingDelay * 2, pollingMaxDelay);
|
|
401
|
+
queue = await getPdfStatus({
|
|
402
|
+
id: queue.id,
|
|
403
|
+
retryOptions,
|
|
404
|
+
});
|
|
405
|
+
} else {
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
result = queue as UploadUrlResult;
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (error instanceof UsePrintError) {
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
throw new UsePrintError(
|
|
417
|
+
"UNKNOWN_ERROR",
|
|
418
|
+
`Failed to create PDF: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
419
|
+
{ retryable: false },
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Fetch the final PDF
|
|
425
|
+
const abortController = new AbortController();
|
|
426
|
+
const timeoutId = setTimeout(() => abortController.abort(), timeout);
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
const res = await fetchWithErrorHandling(result.url, {
|
|
430
|
+
headers,
|
|
431
|
+
signal: abortController.signal,
|
|
432
|
+
retryOptions,
|
|
433
|
+
});
|
|
434
|
+
clearTimeout(timeoutId);
|
|
435
|
+
return await res.arrayBuffer();
|
|
436
|
+
} catch (error) {
|
|
437
|
+
clearTimeout(timeoutId);
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
getUploadUrl,
|
|
442
|
+
getPdfStatus,
|
|
443
|
+
};
|
|
444
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "useprint-js",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"module": "index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@types/bun": "latest",
|
|
8
|
+
"@useprint/api": "workspace:*"
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"typescript": "^5"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@useprint/render": "^0.1.0"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|