pdfdancer-client-typescript 1.0.14 → 1.0.16
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/.eslintrc.js +26 -18
- package/.github/workflows/ci.yml +51 -2
- package/.github/workflows/daily-tests.yml +54 -0
- package/README.md +44 -1
- package/dist/__tests__/e2e/test-helpers.d.ts.map +1 -1
- package/dist/__tests__/e2e/test-helpers.js +9 -2
- package/dist/__tests__/e2e/test-helpers.js.map +1 -1
- package/dist/fingerprint.d.ts.map +1 -1
- package/dist/fingerprint.js +16 -5
- package/dist/fingerprint.js.map +1 -1
- package/dist/pdfdancer_v1.d.ts +75 -2
- package/dist/pdfdancer_v1.d.ts.map +1 -1
- package/dist/pdfdancer_v1.js +287 -15
- package/dist/pdfdancer_v1.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/e2e/acroform.test.ts +58 -0
- package/src/__tests__/e2e/form_x_object.test.ts +29 -0
- package/src/__tests__/e2e/image.test.ts +34 -0
- package/src/__tests__/e2e/line-showcase.test.ts +0 -5
- package/src/__tests__/e2e/line.test.ts +62 -8
- package/src/__tests__/e2e/paragraph-showcase.test.ts +0 -8
- package/src/__tests__/e2e/paragraph.test.ts +63 -9
- package/src/__tests__/e2e/path.test.ts +29 -0
- package/src/__tests__/e2e/test-helpers.ts +8 -2
- package/src/__tests__/e2e/token_from_env.test.ts +0 -1
- package/src/__tests__/retry-mechanism.test.ts +642 -0
- package/src/fingerprint.ts +20 -7
- package/src/pdfdancer_v1.ts +456 -50
package/src/pdfdancer_v1.ts
CHANGED
|
@@ -61,6 +61,213 @@ const DEBUG =
|
|
|
61
61
|
(process.env.PDFDANCER_CLIENT_DEBUG ?? '') === '1' ||
|
|
62
62
|
(process.env.PDFDANCER_CLIENT_DEBUG ?? '').toLowerCase() === 'yes';
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Configuration for retry mechanism on REST API calls.
|
|
66
|
+
*/
|
|
67
|
+
export interface RetryConfig {
|
|
68
|
+
/**
|
|
69
|
+
* Maximum number of retry attempts (default: 3)
|
|
70
|
+
*/
|
|
71
|
+
maxRetries?: number;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Initial delay in milliseconds before first retry (default: 1000)
|
|
75
|
+
* Subsequent delays use exponential backoff
|
|
76
|
+
*/
|
|
77
|
+
initialDelay?: number;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Maximum delay in milliseconds between retries (default: 10000)
|
|
81
|
+
*/
|
|
82
|
+
maxDelay?: number;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* HTTP status codes that should trigger a retry (default: [429, 500, 502, 503, 504])
|
|
86
|
+
*/
|
|
87
|
+
retryableStatusCodes?: number[];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether to retry on network errors (connection failures, timeouts) (default: true)
|
|
91
|
+
*/
|
|
92
|
+
retryOnNetworkError?: boolean;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Exponential backoff multiplier (default: 2)
|
|
96
|
+
*/
|
|
97
|
+
backoffMultiplier?: number;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether to add random jitter to retry delays to prevent thundering herd (default: true)
|
|
101
|
+
*/
|
|
102
|
+
useJitter?: boolean;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Whether to respect Retry-After headers from server responses (default: true)
|
|
106
|
+
* When enabled, the client will use the server-specified delay instead of exponential backoff
|
|
107
|
+
* for responses that include a Retry-After header (typically 429 or 503 responses)
|
|
108
|
+
*/
|
|
109
|
+
respectRetryAfter?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Default retry configuration
|
|
114
|
+
*/
|
|
115
|
+
const DEFAULT_RETRY_CONFIG: Required<RetryConfig> = {
|
|
116
|
+
maxRetries: 3,
|
|
117
|
+
initialDelay: 1000,
|
|
118
|
+
maxDelay: 10000,
|
|
119
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
120
|
+
retryOnNetworkError: true,
|
|
121
|
+
backoffMultiplier: 2,
|
|
122
|
+
useJitter: true,
|
|
123
|
+
respectRetryAfter: true
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Static helper function for retry logic with exponential backoff.
|
|
128
|
+
* Used by static methods that don't have access to instance retry config.
|
|
129
|
+
*/
|
|
130
|
+
async function fetchWithRetry(
|
|
131
|
+
url: string,
|
|
132
|
+
// eslint-disable-next-line no-undef
|
|
133
|
+
options: RequestInit,
|
|
134
|
+
retryConfig: Required<RetryConfig>,
|
|
135
|
+
context: string = 'request'
|
|
136
|
+
): Promise<Response> {
|
|
137
|
+
let lastError: Error | null = null;
|
|
138
|
+
let lastResponse: Response | null = null;
|
|
139
|
+
|
|
140
|
+
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetch(url, options);
|
|
143
|
+
|
|
144
|
+
// Check if we should retry based on status code
|
|
145
|
+
if (!response.ok && retryConfig.retryableStatusCodes.includes(response.status)) {
|
|
146
|
+
lastResponse = response;
|
|
147
|
+
|
|
148
|
+
// If this is not the last attempt, wait and retry
|
|
149
|
+
if (attempt < retryConfig.maxRetries) {
|
|
150
|
+
let delay: number;
|
|
151
|
+
let delaySource = 'exponential backoff';
|
|
152
|
+
|
|
153
|
+
// Check for Retry-After header if configured
|
|
154
|
+
if (retryConfig.respectRetryAfter) {
|
|
155
|
+
const retryAfterDelay = parseRetryAfter(response);
|
|
156
|
+
if (retryAfterDelay !== null) {
|
|
157
|
+
// Use Retry-After header value, but cap at maxDelay
|
|
158
|
+
delay = Math.min(retryAfterDelay, retryConfig.maxDelay);
|
|
159
|
+
delaySource = 'Retry-After header';
|
|
160
|
+
} else {
|
|
161
|
+
// Fall back to exponential backoff
|
|
162
|
+
delay = calculateRetryDelay(attempt, retryConfig);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
// Use exponential backoff
|
|
166
|
+
delay = calculateRetryDelay(attempt, retryConfig);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (DEBUG) {
|
|
170
|
+
console.log(`${Date.now() / 1000}|Retry attempt ${attempt + 1}/${retryConfig.maxRetries} for ${context} after ${delay}ms (status: ${response.status}, source: ${delaySource})`);
|
|
171
|
+
}
|
|
172
|
+
await sleep(delay);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Request succeeded or non-retryable error
|
|
178
|
+
return response;
|
|
179
|
+
|
|
180
|
+
} catch (error) {
|
|
181
|
+
lastError = error as Error;
|
|
182
|
+
|
|
183
|
+
// Check if this is a network error and we should retry
|
|
184
|
+
if (retryConfig.retryOnNetworkError && attempt < retryConfig.maxRetries) {
|
|
185
|
+
const delay = calculateRetryDelay(attempt, retryConfig);
|
|
186
|
+
if (DEBUG) {
|
|
187
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
188
|
+
console.log(`${Date.now() / 1000}|Retry attempt ${attempt + 1}/${retryConfig.maxRetries} for ${context} after ${delay}ms (error: ${errorMessage})`);
|
|
189
|
+
}
|
|
190
|
+
await sleep(delay);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Non-retryable error or last attempt
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// If we exhausted all retries due to retryable status codes, return the last response
|
|
200
|
+
if (lastResponse) {
|
|
201
|
+
return lastResponse;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// If we exhausted all retries due to network errors, throw the last error
|
|
205
|
+
if (lastError) {
|
|
206
|
+
throw lastError;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// This should never happen, but just in case
|
|
210
|
+
throw new Error('Unexpected retry exhaustion');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Parses the Retry-After header from a response.
|
|
215
|
+
* Supports both delay-seconds (integer) and HTTP-date formats.
|
|
216
|
+
* Returns the delay in milliseconds, or null if the header is invalid or missing.
|
|
217
|
+
*/
|
|
218
|
+
function parseRetryAfter(response: Response): number | null {
|
|
219
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
220
|
+
if (!retryAfter) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Try parsing as delay-seconds (integer)
|
|
225
|
+
const delaySeconds = parseInt(retryAfter, 10);
|
|
226
|
+
if (!isNaN(delaySeconds) && delaySeconds >= 0) {
|
|
227
|
+
return delaySeconds * 1000; // Convert to milliseconds
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Try parsing as HTTP-date
|
|
231
|
+
try {
|
|
232
|
+
const retryDate = new Date(retryAfter);
|
|
233
|
+
if (!isNaN(retryDate.getTime())) {
|
|
234
|
+
const now = Date.now();
|
|
235
|
+
const delay = retryDate.getTime() - now;
|
|
236
|
+
// Only return positive delays
|
|
237
|
+
return delay > 0 ? delay : 0;
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// Invalid date format
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Calculates the delay for the next retry attempt using exponential backoff.
|
|
248
|
+
*/
|
|
249
|
+
function calculateRetryDelay(attemptNumber: number, retryConfig: Required<RetryConfig>): number {
|
|
250
|
+
// Calculate base delay: initialDelay * (backoffMultiplier ^ attemptNumber)
|
|
251
|
+
let delay = retryConfig.initialDelay * Math.pow(retryConfig.backoffMultiplier, attemptNumber);
|
|
252
|
+
|
|
253
|
+
// Cap at maxDelay
|
|
254
|
+
delay = Math.min(delay, retryConfig.maxDelay);
|
|
255
|
+
|
|
256
|
+
// Add jitter if enabled (randomize between 50% and 100% of calculated delay)
|
|
257
|
+
if (retryConfig.useJitter) {
|
|
258
|
+
delay = delay * (0.5 + Math.random() * 0.5);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return Math.floor(delay);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Sleep for the specified number of milliseconds.
|
|
266
|
+
*/
|
|
267
|
+
function sleep(ms: number): Promise<void> {
|
|
268
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
269
|
+
}
|
|
270
|
+
|
|
64
271
|
/**
|
|
65
272
|
* Generate a timestamp string in the format expected by the API.
|
|
66
273
|
* Format: YYYY-MM-DDTHH:MM:SS.ffffffZ (with microseconds)
|
|
@@ -326,6 +533,93 @@ class PageClient {
|
|
|
326
533
|
async getSnapshot(types?: ObjectType[]): Promise<PageSnapshot> {
|
|
327
534
|
return this._client.getPageSnapshot(this._pageIndex, types);
|
|
328
535
|
}
|
|
536
|
+
|
|
537
|
+
// Singular convenience methods - return the first element or null
|
|
538
|
+
|
|
539
|
+
async selectPath() {
|
|
540
|
+
const paths = await this.selectPaths();
|
|
541
|
+
return paths.length > 0 ? paths[0] : null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async selectPathAt(x: number, y: number, tolerance: number = 0) {
|
|
545
|
+
const paths = await this.selectPathsAt(x, y, tolerance);
|
|
546
|
+
return paths.length > 0 ? paths[0] : null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async selectImage() {
|
|
550
|
+
const images = await this.selectImages();
|
|
551
|
+
return images.length > 0 ? images[0] : null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async selectImageAt(x: number, y: number, tolerance: number = 0) {
|
|
555
|
+
const images = await this.selectImagesAt(x, y, tolerance);
|
|
556
|
+
return images.length > 0 ? images[0] : null;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async selectForm() {
|
|
560
|
+
const forms = await this.selectForms();
|
|
561
|
+
return forms.length > 0 ? forms[0] : null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async selectFormAt(x: number, y: number, tolerance: number = 0) {
|
|
565
|
+
const forms = await this.selectFormsAt(x, y, tolerance);
|
|
566
|
+
return forms.length > 0 ? forms[0] : null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async selectFormField() {
|
|
570
|
+
const fields = await this.selectFormFields();
|
|
571
|
+
return fields.length > 0 ? fields[0] : null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async selectFormFieldAt(x: number, y: number, tolerance: number = 0) {
|
|
575
|
+
const fields = await this.selectFormFieldsAt(x, y, tolerance);
|
|
576
|
+
return fields.length > 0 ? fields[0] : null;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async selectFormFieldByName(fieldName: string) {
|
|
580
|
+
const fields = await this.selectFormFieldsByName(fieldName);
|
|
581
|
+
return fields.length > 0 ? fields[0] : null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async selectParagraph() {
|
|
585
|
+
const paragraphs = await this.selectParagraphs();
|
|
586
|
+
return paragraphs.length > 0 ? paragraphs[0] : null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async selectParagraphStartingWith(text: string) {
|
|
590
|
+
const paragraphs = await this.selectParagraphsStartingWith(text);
|
|
591
|
+
return paragraphs.length > 0 ? paragraphs[0] : null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async selectParagraphMatching(pattern: string) {
|
|
595
|
+
const paragraphs = await this.selectParagraphsMatching(pattern);
|
|
596
|
+
return paragraphs.length > 0 ? paragraphs[0] : null;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async selectParagraphAt(x: number, y: number, tolerance: number = DEFAULT_TOLERANCE) {
|
|
600
|
+
const paragraphs = await this.selectParagraphsAt(x, y, tolerance);
|
|
601
|
+
return paragraphs.length > 0 ? paragraphs[0] : null;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async selectTextLine() {
|
|
605
|
+
const lines = await this.selectTextLines();
|
|
606
|
+
return lines.length > 0 ? lines[0] : null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async selectTextLineStartingWith(text: string) {
|
|
610
|
+
const lines = await this.selectTextLinesStartingWith(text);
|
|
611
|
+
return lines.length > 0 ? lines[0] : null;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async selectTextLineMatching(pattern: string) {
|
|
615
|
+
const lines = await this.selectTextLinesMatching(pattern);
|
|
616
|
+
return lines.length > 0 ? lines[0] : null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async selectTextLineAt(x: number, y: number, tolerance: number = DEFAULT_TOLERANCE) {
|
|
620
|
+
const lines = await this.selectTextLinesAt(x, y, tolerance);
|
|
621
|
+
return lines.length > 0 ? lines[0] : null;
|
|
622
|
+
}
|
|
329
623
|
}
|
|
330
624
|
|
|
331
625
|
// noinspection ExceptionCaughtLocallyJS,JSUnusedLocalSymbols
|
|
@@ -344,6 +638,7 @@ export class PDFDancer {
|
|
|
344
638
|
private _sessionId!: string;
|
|
345
639
|
private _userId?: string;
|
|
346
640
|
private _fingerprintCache?: string;
|
|
641
|
+
private _retryConfig: Required<RetryConfig>;
|
|
347
642
|
|
|
348
643
|
// Snapshot caches for optimizing find operations
|
|
349
644
|
private _documentSnapshotCache: DocumentSnapshot | null = null;
|
|
@@ -357,9 +652,10 @@ export class PDFDancer {
|
|
|
357
652
|
*/
|
|
358
653
|
private constructor(
|
|
359
654
|
token: string,
|
|
360
|
-
pdfData: Uint8Array | File | ArrayBuffer,
|
|
655
|
+
pdfData: Uint8Array | File | ArrayBuffer | string,
|
|
361
656
|
baseUrl: string | null = null,
|
|
362
|
-
readTimeout: number = 60000
|
|
657
|
+
readTimeout: number = 60000,
|
|
658
|
+
retryConfig?: RetryConfig
|
|
363
659
|
) {
|
|
364
660
|
|
|
365
661
|
if (!token || !token.trim()) {
|
|
@@ -384,6 +680,12 @@ export class PDFDancer {
|
|
|
384
680
|
this._baseUrl = resolvedBaseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
385
681
|
this._readTimeout = readTimeout;
|
|
386
682
|
|
|
683
|
+
// Merge retry config with defaults
|
|
684
|
+
this._retryConfig = {
|
|
685
|
+
...DEFAULT_RETRY_CONFIG,
|
|
686
|
+
...retryConfig
|
|
687
|
+
};
|
|
688
|
+
|
|
387
689
|
// Process PDF data with validation
|
|
388
690
|
this._pdfBytes = this._processPdfData(pdfData);
|
|
389
691
|
|
|
@@ -402,7 +704,13 @@ export class PDFDancer {
|
|
|
402
704
|
return this;
|
|
403
705
|
}
|
|
404
706
|
|
|
405
|
-
static async open(
|
|
707
|
+
static async open(
|
|
708
|
+
pdfData: Uint8Array,
|
|
709
|
+
token?: string,
|
|
710
|
+
baseUrl?: string,
|
|
711
|
+
timeout?: number,
|
|
712
|
+
retryConfig?: RetryConfig
|
|
713
|
+
): Promise<PDFDancer> {
|
|
406
714
|
const resolvedBaseUrl =
|
|
407
715
|
baseUrl ??
|
|
408
716
|
process.env.PDFDANCER_BASE_URL ??
|
|
@@ -414,7 +722,7 @@ export class PDFDancer {
|
|
|
414
722
|
resolvedToken = await PDFDancer._obtainAnonymousToken(resolvedBaseUrl, resolvedTimeout);
|
|
415
723
|
}
|
|
416
724
|
|
|
417
|
-
const client = new PDFDancer(resolvedToken, pdfData, resolvedBaseUrl, resolvedTimeout);
|
|
725
|
+
const client = new PDFDancer(resolvedToken, pdfData, resolvedBaseUrl, resolvedTimeout, retryConfig);
|
|
418
726
|
return await client.init();
|
|
419
727
|
}
|
|
420
728
|
|
|
@@ -428,6 +736,7 @@ export class PDFDancer {
|
|
|
428
736
|
* @param token Authentication token (optional, can use PDFDANCER_TOKEN env var)
|
|
429
737
|
* @param baseUrl Base URL for the PDFDancer API (optional)
|
|
430
738
|
* @param timeout Request timeout in milliseconds (default: 60000)
|
|
739
|
+
* @param retryConfig Retry configuration (optional, uses defaults if not specified)
|
|
431
740
|
*/
|
|
432
741
|
static async new(
|
|
433
742
|
options?: {
|
|
@@ -437,7 +746,8 @@ export class PDFDancer {
|
|
|
437
746
|
},
|
|
438
747
|
token?: string,
|
|
439
748
|
baseUrl?: string,
|
|
440
|
-
timeout?: number
|
|
749
|
+
timeout?: number,
|
|
750
|
+
retryConfig?: RetryConfig
|
|
441
751
|
): Promise<PDFDancer> {
|
|
442
752
|
const resolvedBaseUrl =
|
|
443
753
|
baseUrl ??
|
|
@@ -471,18 +781,23 @@ export class PDFDancer {
|
|
|
471
781
|
// Generate fingerprint for this request
|
|
472
782
|
const fingerprint = await generateFingerprint();
|
|
473
783
|
|
|
474
|
-
// Make request to create endpoint
|
|
475
|
-
const response = await
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
'
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
784
|
+
// Make request to create endpoint with retry logic
|
|
785
|
+
const response = await fetchWithRetry(
|
|
786
|
+
url,
|
|
787
|
+
{
|
|
788
|
+
method: 'POST',
|
|
789
|
+
headers: {
|
|
790
|
+
'Authorization': `Bearer ${resolvedToken}`,
|
|
791
|
+
'Content-Type': 'application/json',
|
|
792
|
+
'X-Generated-At': generateTimestamp(),
|
|
793
|
+
'X-Fingerprint': fingerprint
|
|
794
|
+
},
|
|
795
|
+
body: JSON.stringify(createRequest.toDict()),
|
|
796
|
+
signal: resolvedTimeout > 0 ? AbortSignal.timeout(resolvedTimeout) : undefined
|
|
482
797
|
},
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
798
|
+
DEFAULT_RETRY_CONFIG,
|
|
799
|
+
'POST /session/new'
|
|
800
|
+
);
|
|
486
801
|
|
|
487
802
|
logGeneratedAtHeader(response, 'POST', '/session/new');
|
|
488
803
|
|
|
@@ -503,6 +818,11 @@ export class PDFDancer {
|
|
|
503
818
|
client._readTimeout = resolvedTimeout;
|
|
504
819
|
client._pdfBytes = new Uint8Array();
|
|
505
820
|
client._sessionId = sessionId;
|
|
821
|
+
// Initialize retry config
|
|
822
|
+
client._retryConfig = {
|
|
823
|
+
...DEFAULT_RETRY_CONFIG,
|
|
824
|
+
...retryConfig
|
|
825
|
+
};
|
|
506
826
|
// Initialize caches
|
|
507
827
|
client._documentSnapshotCache = null;
|
|
508
828
|
client._pageSnapshotCache = new Map();
|
|
@@ -523,15 +843,20 @@ export class PDFDancer {
|
|
|
523
843
|
|
|
524
844
|
try {
|
|
525
845
|
const fingerprint = await generateFingerprint();
|
|
526
|
-
const response = await
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
846
|
+
const response = await fetchWithRetry(
|
|
847
|
+
url,
|
|
848
|
+
{
|
|
849
|
+
method: 'POST',
|
|
850
|
+
headers: {
|
|
851
|
+
'Content-Type': 'application/json',
|
|
852
|
+
'X-Fingerprint': fingerprint,
|
|
853
|
+
'X-Generated-At': generateTimestamp()
|
|
854
|
+
},
|
|
855
|
+
signal: timeout > 0 ? AbortSignal.timeout(timeout) : undefined
|
|
532
856
|
},
|
|
533
|
-
|
|
534
|
-
|
|
857
|
+
DEFAULT_RETRY_CONFIG,
|
|
858
|
+
'POST /keys/anon'
|
|
859
|
+
);
|
|
535
860
|
|
|
536
861
|
if (!response.ok) {
|
|
537
862
|
const errorText = await response.text().catch(() => '');
|
|
@@ -561,7 +886,7 @@ export class PDFDancer {
|
|
|
561
886
|
/**
|
|
562
887
|
* Process PDF data from various input types with strict validation.
|
|
563
888
|
*/
|
|
564
|
-
private _processPdfData(pdfData: Uint8Array | File | ArrayBuffer): Uint8Array {
|
|
889
|
+
private _processPdfData(pdfData: Uint8Array | File | ArrayBuffer | string): Uint8Array {
|
|
565
890
|
if (!pdfData) {
|
|
566
891
|
throw new ValidationException("PDF data cannot be null");
|
|
567
892
|
}
|
|
@@ -581,6 +906,16 @@ export class PDFDancer {
|
|
|
581
906
|
} else if (pdfData instanceof File) {
|
|
582
907
|
// Note: File reading will be handled asynchronously in the session creation
|
|
583
908
|
return new Uint8Array(); // Placeholder, will be replaced in _createSession
|
|
909
|
+
} else if (typeof pdfData === 'string') {
|
|
910
|
+
// Handle string as filepath
|
|
911
|
+
if (!fs.existsSync(pdfData)) {
|
|
912
|
+
throw new ValidationException(`PDF file not found: ${pdfData}`);
|
|
913
|
+
}
|
|
914
|
+
const fileData = new Uint8Array(fs.readFileSync(pdfData));
|
|
915
|
+
if (fileData.length === 0) {
|
|
916
|
+
throw new ValidationException("PDF file is empty");
|
|
917
|
+
}
|
|
918
|
+
return fileData;
|
|
584
919
|
} else {
|
|
585
920
|
throw new ValidationException(`Unsupported PDF data type: ${typeof pdfData}`);
|
|
586
921
|
}
|
|
@@ -662,16 +997,20 @@ export class PDFDancer {
|
|
|
662
997
|
|
|
663
998
|
const fingerprint = await this._getFingerprint();
|
|
664
999
|
|
|
665
|
-
const response = await
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
'
|
|
669
|
-
|
|
670
|
-
|
|
1000
|
+
const response = await this._fetchWithRetry(
|
|
1001
|
+
this._buildUrl('/session/create'),
|
|
1002
|
+
{
|
|
1003
|
+
method: 'POST',
|
|
1004
|
+
headers: {
|
|
1005
|
+
'Authorization': `Bearer ${this._token}`,
|
|
1006
|
+
'X-Generated-At': generateTimestamp(),
|
|
1007
|
+
'X-Fingerprint': fingerprint
|
|
1008
|
+
},
|
|
1009
|
+
body: formData,
|
|
1010
|
+
signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
|
|
671
1011
|
},
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
});
|
|
1012
|
+
'POST /session/create'
|
|
1013
|
+
);
|
|
675
1014
|
|
|
676
1015
|
logGeneratedAtHeader(response, 'POST', '/session/create');
|
|
677
1016
|
|
|
@@ -716,6 +1055,19 @@ export class PDFDancer {
|
|
|
716
1055
|
return this._fingerprintCache;
|
|
717
1056
|
}
|
|
718
1057
|
|
|
1058
|
+
/**
|
|
1059
|
+
* Executes a fetch request with retry logic based on the configured retry policy.
|
|
1060
|
+
* Implements exponential backoff with optional jitter.
|
|
1061
|
+
*/
|
|
1062
|
+
private async _fetchWithRetry(
|
|
1063
|
+
url: string,
|
|
1064
|
+
// eslint-disable-next-line no-undef
|
|
1065
|
+
options: RequestInit,
|
|
1066
|
+
context: string = 'request'
|
|
1067
|
+
): Promise<Response> {
|
|
1068
|
+
return fetchWithRetry(url, options, this._retryConfig, context);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
719
1071
|
/**
|
|
720
1072
|
* Make HTTP request with session headers and error handling.
|
|
721
1073
|
*/
|
|
@@ -743,12 +1095,16 @@ export class PDFDancer {
|
|
|
743
1095
|
};
|
|
744
1096
|
|
|
745
1097
|
try {
|
|
746
|
-
const response = await
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1098
|
+
const response = await this._fetchWithRetry(
|
|
1099
|
+
url.toString(),
|
|
1100
|
+
{
|
|
1101
|
+
method,
|
|
1102
|
+
headers,
|
|
1103
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
1104
|
+
signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
|
|
1105
|
+
},
|
|
1106
|
+
`${method} ${path}`
|
|
1107
|
+
);
|
|
752
1108
|
|
|
753
1109
|
logGeneratedAtHeader(response, method, path);
|
|
754
1110
|
|
|
@@ -1460,17 +1816,21 @@ export class PDFDancer {
|
|
|
1460
1816
|
|
|
1461
1817
|
const fingerprint = await this._getFingerprint();
|
|
1462
1818
|
|
|
1463
|
-
const response = await
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
'
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1819
|
+
const response = await this._fetchWithRetry(
|
|
1820
|
+
this._buildUrl('/font/register'),
|
|
1821
|
+
{
|
|
1822
|
+
method: 'POST',
|
|
1823
|
+
headers: {
|
|
1824
|
+
'Authorization': `Bearer ${this._token}`,
|
|
1825
|
+
'X-Session-Id': this._sessionId,
|
|
1826
|
+
'X-Generated-At': generateTimestamp(),
|
|
1827
|
+
'X-Fingerprint': fingerprint
|
|
1828
|
+
},
|
|
1829
|
+
body: formData,
|
|
1830
|
+
signal: AbortSignal.timeout(60000)
|
|
1470
1831
|
},
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
});
|
|
1832
|
+
'POST /font/register'
|
|
1833
|
+
);
|
|
1474
1834
|
|
|
1475
1835
|
logGeneratedAtHeader(response, 'POST', '/font/register');
|
|
1476
1836
|
|
|
@@ -1874,4 +2234,50 @@ export class PDFDancer {
|
|
|
1874
2234
|
async selectLines() {
|
|
1875
2235
|
return this.selectTextLines();
|
|
1876
2236
|
}
|
|
2237
|
+
|
|
2238
|
+
// Singular convenience methods - return the first element or null
|
|
2239
|
+
|
|
2240
|
+
async selectImage() {
|
|
2241
|
+
const images = await this.selectImages();
|
|
2242
|
+
return images.length > 0 ? images[0] : null;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
async selectPath() {
|
|
2246
|
+
const paths = await this.selectPaths();
|
|
2247
|
+
return paths.length > 0 ? paths[0] : null;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
async selectForm() {
|
|
2251
|
+
const forms = await this.selectForms();
|
|
2252
|
+
return forms.length > 0 ? forms[0] : null;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
async selectFormField() {
|
|
2256
|
+
const fields = await this.selectFormFields();
|
|
2257
|
+
return fields.length > 0 ? fields[0] : null;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
async selectFieldByName(fieldName: string) {
|
|
2261
|
+
const fields = await this.selectFieldsByName(fieldName);
|
|
2262
|
+
return fields.length > 0 ? fields[0] : null;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
async selectParagraph() {
|
|
2266
|
+
const paragraphs = await this.selectParagraphs();
|
|
2267
|
+
return paragraphs.length > 0 ? paragraphs[0] : null;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
async selectParagraphMatching(pattern: string) {
|
|
2271
|
+
const paragraphs = await this.selectParagraphsMatching(pattern);
|
|
2272
|
+
return paragraphs.length > 0 ? paragraphs[0] : null;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
async selectTextLine() {
|
|
2276
|
+
const lines = await this.selectTextLines();
|
|
2277
|
+
return lines.length > 0 ? lines[0] : null;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
async selectLine() {
|
|
2281
|
+
return this.selectTextLine();
|
|
2282
|
+
}
|
|
1877
2283
|
}
|