pdfdancer-client-typescript 1.0.13 → 1.0.15
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 +50 -6
- package/dist/__tests__/e2e/test-helpers.d.ts.map +1 -1
- package/dist/__tests__/e2e/test-helpers.js +17 -5
- 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/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts +18 -15
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +21 -18
- package/dist/models.js.map +1 -1
- package/dist/pdfdancer_v1.d.ts +71 -3
- package/dist/pdfdancer_v1.d.ts.map +1 -1
- package/dist/pdfdancer_v1.js +301 -35
- package/dist/pdfdancer_v1.js.map +1 -1
- package/docs/openapi.yml +637 -73
- package/jest.config.js +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-showcase.test.ts +6 -6
- package/src/__tests__/e2e/image.test.ts +39 -5
- package/src/__tests__/e2e/line-showcase.test.ts +6 -11
- package/src/__tests__/e2e/line.test.ts +63 -7
- package/src/__tests__/e2e/page-showcase.test.ts +12 -12
- package/src/__tests__/e2e/page.test.ts +3 -3
- package/src/__tests__/e2e/paragraph-showcase.test.ts +0 -8
- package/src/__tests__/e2e/paragraph.test.ts +64 -8
- package/src/__tests__/e2e/path.test.ts +33 -4
- package/src/__tests__/e2e/snapshot-showcase.test.ts +10 -10
- package/src/__tests__/e2e/snapshot.test.ts +18 -18
- package/src/__tests__/e2e/test-helpers.ts +16 -5
- package/src/__tests__/e2e/token_from_env.test.ts +0 -15
- package/src/__tests__/retry-mechanism.test.ts +420 -0
- package/src/fingerprint.ts +20 -7
- package/src/index.ts +3 -1
- package/src/models.ts +21 -17
- package/src/pdfdancer_v1.ts +467 -71
package/src/pdfdancer_v1.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
DocumentSnapshot,
|
|
24
24
|
FindRequest,
|
|
25
25
|
Font,
|
|
26
|
-
|
|
26
|
+
DocumentFontInfo,
|
|
27
27
|
FontType,
|
|
28
28
|
FormFieldRef,
|
|
29
29
|
Image,
|
|
@@ -61,6 +61,154 @@ 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
|
+
/**
|
|
106
|
+
* Default retry configuration
|
|
107
|
+
*/
|
|
108
|
+
const DEFAULT_RETRY_CONFIG: Required<RetryConfig> = {
|
|
109
|
+
maxRetries: 3,
|
|
110
|
+
initialDelay: 1000,
|
|
111
|
+
maxDelay: 10000,
|
|
112
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
113
|
+
retryOnNetworkError: true,
|
|
114
|
+
backoffMultiplier: 2,
|
|
115
|
+
useJitter: true
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Static helper function for retry logic with exponential backoff.
|
|
120
|
+
* Used by static methods that don't have access to instance retry config.
|
|
121
|
+
*/
|
|
122
|
+
async function fetchWithRetry(
|
|
123
|
+
url: string,
|
|
124
|
+
// eslint-disable-next-line no-undef
|
|
125
|
+
options: RequestInit,
|
|
126
|
+
retryConfig: Required<RetryConfig>,
|
|
127
|
+
context: string = 'request'
|
|
128
|
+
): Promise<Response> {
|
|
129
|
+
let lastError: Error | null = null;
|
|
130
|
+
let lastResponse: Response | null = null;
|
|
131
|
+
|
|
132
|
+
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
|
|
133
|
+
try {
|
|
134
|
+
const response = await fetch(url, options);
|
|
135
|
+
|
|
136
|
+
// Check if we should retry based on status code
|
|
137
|
+
if (!response.ok && retryConfig.retryableStatusCodes.includes(response.status)) {
|
|
138
|
+
lastResponse = response;
|
|
139
|
+
|
|
140
|
+
// If this is not the last attempt, wait and retry
|
|
141
|
+
if (attempt < retryConfig.maxRetries) {
|
|
142
|
+
const delay = calculateRetryDelay(attempt, retryConfig);
|
|
143
|
+
if (DEBUG) {
|
|
144
|
+
console.log(`${Date.now() / 1000}|Retry attempt ${attempt + 1}/${retryConfig.maxRetries} for ${context} after ${delay}ms (status: ${response.status})`);
|
|
145
|
+
}
|
|
146
|
+
await sleep(delay);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Request succeeded or non-retryable error
|
|
152
|
+
return response;
|
|
153
|
+
|
|
154
|
+
} catch (error) {
|
|
155
|
+
lastError = error as Error;
|
|
156
|
+
|
|
157
|
+
// Check if this is a network error and we should retry
|
|
158
|
+
if (retryConfig.retryOnNetworkError && attempt < retryConfig.maxRetries) {
|
|
159
|
+
const delay = calculateRetryDelay(attempt, retryConfig);
|
|
160
|
+
if (DEBUG) {
|
|
161
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
162
|
+
console.log(`${Date.now() / 1000}|Retry attempt ${attempt + 1}/${retryConfig.maxRetries} for ${context} after ${delay}ms (error: ${errorMessage})`);
|
|
163
|
+
}
|
|
164
|
+
await sleep(delay);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Non-retryable error or last attempt
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// If we exhausted all retries due to retryable status codes, return the last response
|
|
174
|
+
if (lastResponse) {
|
|
175
|
+
return lastResponse;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// If we exhausted all retries due to network errors, throw the last error
|
|
179
|
+
if (lastError) {
|
|
180
|
+
throw lastError;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// This should never happen, but just in case
|
|
184
|
+
throw new Error('Unexpected retry exhaustion');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Calculates the delay for the next retry attempt using exponential backoff.
|
|
189
|
+
*/
|
|
190
|
+
function calculateRetryDelay(attemptNumber: number, retryConfig: Required<RetryConfig>): number {
|
|
191
|
+
// Calculate base delay: initialDelay * (backoffMultiplier ^ attemptNumber)
|
|
192
|
+
let delay = retryConfig.initialDelay * Math.pow(retryConfig.backoffMultiplier, attemptNumber);
|
|
193
|
+
|
|
194
|
+
// Cap at maxDelay
|
|
195
|
+
delay = Math.min(delay, retryConfig.maxDelay);
|
|
196
|
+
|
|
197
|
+
// Add jitter if enabled (randomize between 50% and 100% of calculated delay)
|
|
198
|
+
if (retryConfig.useJitter) {
|
|
199
|
+
delay = delay * (0.5 + Math.random() * 0.5);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return Math.floor(delay);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Sleep for the specified number of milliseconds.
|
|
207
|
+
*/
|
|
208
|
+
function sleep(ms: number): Promise<void> {
|
|
209
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
210
|
+
}
|
|
211
|
+
|
|
64
212
|
/**
|
|
65
213
|
* Generate a timestamp string in the format expected by the API.
|
|
66
214
|
* Format: YYYY-MM-DDTHH:MM:SS.ffffffZ (with microseconds)
|
|
@@ -326,6 +474,93 @@ class PageClient {
|
|
|
326
474
|
async getSnapshot(types?: ObjectType[]): Promise<PageSnapshot> {
|
|
327
475
|
return this._client.getPageSnapshot(this._pageIndex, types);
|
|
328
476
|
}
|
|
477
|
+
|
|
478
|
+
// Singular convenience methods - return the first element or null
|
|
479
|
+
|
|
480
|
+
async selectPath() {
|
|
481
|
+
const paths = await this.selectPaths();
|
|
482
|
+
return paths.length > 0 ? paths[0] : null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async selectPathAt(x: number, y: number, tolerance: number = 0) {
|
|
486
|
+
const paths = await this.selectPathsAt(x, y, tolerance);
|
|
487
|
+
return paths.length > 0 ? paths[0] : null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async selectImage() {
|
|
491
|
+
const images = await this.selectImages();
|
|
492
|
+
return images.length > 0 ? images[0] : null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async selectImageAt(x: number, y: number, tolerance: number = 0) {
|
|
496
|
+
const images = await this.selectImagesAt(x, y, tolerance);
|
|
497
|
+
return images.length > 0 ? images[0] : null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async selectForm() {
|
|
501
|
+
const forms = await this.selectForms();
|
|
502
|
+
return forms.length > 0 ? forms[0] : null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async selectFormAt(x: number, y: number, tolerance: number = 0) {
|
|
506
|
+
const forms = await this.selectFormsAt(x, y, tolerance);
|
|
507
|
+
return forms.length > 0 ? forms[0] : null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async selectFormField() {
|
|
511
|
+
const fields = await this.selectFormFields();
|
|
512
|
+
return fields.length > 0 ? fields[0] : null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async selectFormFieldAt(x: number, y: number, tolerance: number = 0) {
|
|
516
|
+
const fields = await this.selectFormFieldsAt(x, y, tolerance);
|
|
517
|
+
return fields.length > 0 ? fields[0] : null;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async selectFormFieldByName(fieldName: string) {
|
|
521
|
+
const fields = await this.selectFormFieldsByName(fieldName);
|
|
522
|
+
return fields.length > 0 ? fields[0] : null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async selectParagraph() {
|
|
526
|
+
const paragraphs = await this.selectParagraphs();
|
|
527
|
+
return paragraphs.length > 0 ? paragraphs[0] : null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async selectParagraphStartingWith(text: string) {
|
|
531
|
+
const paragraphs = await this.selectParagraphsStartingWith(text);
|
|
532
|
+
return paragraphs.length > 0 ? paragraphs[0] : null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async selectParagraphMatching(pattern: string) {
|
|
536
|
+
const paragraphs = await this.selectParagraphsMatching(pattern);
|
|
537
|
+
return paragraphs.length > 0 ? paragraphs[0] : null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async selectParagraphAt(x: number, y: number, tolerance: number = DEFAULT_TOLERANCE) {
|
|
541
|
+
const paragraphs = await this.selectParagraphsAt(x, y, tolerance);
|
|
542
|
+
return paragraphs.length > 0 ? paragraphs[0] : null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async selectTextLine() {
|
|
546
|
+
const lines = await this.selectTextLines();
|
|
547
|
+
return lines.length > 0 ? lines[0] : null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async selectTextLineStartingWith(text: string) {
|
|
551
|
+
const lines = await this.selectTextLinesStartingWith(text);
|
|
552
|
+
return lines.length > 0 ? lines[0] : null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async selectTextLineMatching(pattern: string) {
|
|
556
|
+
const lines = await this.selectTextLinesMatching(pattern);
|
|
557
|
+
return lines.length > 0 ? lines[0] : null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async selectTextLineAt(x: number, y: number, tolerance: number = DEFAULT_TOLERANCE) {
|
|
561
|
+
const lines = await this.selectTextLinesAt(x, y, tolerance);
|
|
562
|
+
return lines.length > 0 ? lines[0] : null;
|
|
563
|
+
}
|
|
329
564
|
}
|
|
330
565
|
|
|
331
566
|
// noinspection ExceptionCaughtLocallyJS,JSUnusedLocalSymbols
|
|
@@ -344,6 +579,7 @@ export class PDFDancer {
|
|
|
344
579
|
private _sessionId!: string;
|
|
345
580
|
private _userId?: string;
|
|
346
581
|
private _fingerprintCache?: string;
|
|
582
|
+
private _retryConfig: Required<RetryConfig>;
|
|
347
583
|
|
|
348
584
|
// Snapshot caches for optimizing find operations
|
|
349
585
|
private _documentSnapshotCache: DocumentSnapshot | null = null;
|
|
@@ -357,9 +593,10 @@ export class PDFDancer {
|
|
|
357
593
|
*/
|
|
358
594
|
private constructor(
|
|
359
595
|
token: string,
|
|
360
|
-
pdfData: Uint8Array | File | ArrayBuffer,
|
|
596
|
+
pdfData: Uint8Array | File | ArrayBuffer | string,
|
|
361
597
|
baseUrl: string | null = null,
|
|
362
|
-
readTimeout: number =
|
|
598
|
+
readTimeout: number = 60000,
|
|
599
|
+
retryConfig?: RetryConfig
|
|
363
600
|
) {
|
|
364
601
|
|
|
365
602
|
if (!token || !token.trim()) {
|
|
@@ -384,6 +621,12 @@ export class PDFDancer {
|
|
|
384
621
|
this._baseUrl = resolvedBaseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
385
622
|
this._readTimeout = readTimeout;
|
|
386
623
|
|
|
624
|
+
// Merge retry config with defaults
|
|
625
|
+
this._retryConfig = {
|
|
626
|
+
...DEFAULT_RETRY_CONFIG,
|
|
627
|
+
...retryConfig
|
|
628
|
+
};
|
|
629
|
+
|
|
387
630
|
// Process PDF data with validation
|
|
388
631
|
this._pdfBytes = this._processPdfData(pdfData);
|
|
389
632
|
|
|
@@ -402,19 +645,25 @@ export class PDFDancer {
|
|
|
402
645
|
return this;
|
|
403
646
|
}
|
|
404
647
|
|
|
405
|
-
static async open(
|
|
406
|
-
|
|
648
|
+
static async open(
|
|
649
|
+
pdfData: Uint8Array,
|
|
650
|
+
token?: string,
|
|
651
|
+
baseUrl?: string,
|
|
652
|
+
timeout?: number,
|
|
653
|
+
retryConfig?: RetryConfig
|
|
654
|
+
): Promise<PDFDancer> {
|
|
407
655
|
const resolvedBaseUrl =
|
|
408
656
|
baseUrl ??
|
|
409
657
|
process.env.PDFDANCER_BASE_URL ??
|
|
410
658
|
"https://api.pdfdancer.com";
|
|
411
|
-
const resolvedTimeout = timeout ??
|
|
659
|
+
const resolvedTimeout = timeout ?? 60000;
|
|
412
660
|
|
|
661
|
+
let resolvedToken = token?.trim() ?? process.env.PDFDANCER_TOKEN?.trim() ?? null;
|
|
413
662
|
if (!resolvedToken) {
|
|
414
|
-
|
|
663
|
+
resolvedToken = await PDFDancer._obtainAnonymousToken(resolvedBaseUrl, resolvedTimeout);
|
|
415
664
|
}
|
|
416
665
|
|
|
417
|
-
const client = new PDFDancer(resolvedToken, pdfData, resolvedBaseUrl, resolvedTimeout);
|
|
666
|
+
const client = new PDFDancer(resolvedToken, pdfData, resolvedBaseUrl, resolvedTimeout, retryConfig);
|
|
418
667
|
return await client.init();
|
|
419
668
|
}
|
|
420
669
|
|
|
@@ -427,7 +676,8 @@ export class PDFDancer {
|
|
|
427
676
|
* @param options.initialPageCount Number of initial pages (default: 1)
|
|
428
677
|
* @param token Authentication token (optional, can use PDFDANCER_TOKEN env var)
|
|
429
678
|
* @param baseUrl Base URL for the PDFDancer API (optional)
|
|
430
|
-
* @param timeout Request timeout in milliseconds (default:
|
|
679
|
+
* @param timeout Request timeout in milliseconds (default: 60000)
|
|
680
|
+
* @param retryConfig Retry configuration (optional, uses defaults if not specified)
|
|
431
681
|
*/
|
|
432
682
|
static async new(
|
|
433
683
|
options?: {
|
|
@@ -437,17 +687,18 @@ export class PDFDancer {
|
|
|
437
687
|
},
|
|
438
688
|
token?: string,
|
|
439
689
|
baseUrl?: string,
|
|
440
|
-
timeout?: number
|
|
690
|
+
timeout?: number,
|
|
691
|
+
retryConfig?: RetryConfig
|
|
441
692
|
): Promise<PDFDancer> {
|
|
442
|
-
const resolvedToken = token ?? process.env.PDFDANCER_TOKEN;
|
|
443
693
|
const resolvedBaseUrl =
|
|
444
694
|
baseUrl ??
|
|
445
695
|
process.env.PDFDANCER_BASE_URL ??
|
|
446
696
|
"https://api.pdfdancer.com";
|
|
447
|
-
const resolvedTimeout = timeout ??
|
|
697
|
+
const resolvedTimeout = timeout ?? 60000;
|
|
448
698
|
|
|
699
|
+
let resolvedToken = token?.trim() ?? process.env.PDFDANCER_TOKEN?.trim() ?? null;
|
|
449
700
|
if (!resolvedToken) {
|
|
450
|
-
|
|
701
|
+
resolvedToken = await PDFDancer._obtainAnonymousToken(resolvedBaseUrl, resolvedTimeout);
|
|
451
702
|
}
|
|
452
703
|
|
|
453
704
|
let createRequest: CreatePdfRequest;
|
|
@@ -471,18 +722,23 @@ export class PDFDancer {
|
|
|
471
722
|
// Generate fingerprint for this request
|
|
472
723
|
const fingerprint = await generateFingerprint();
|
|
473
724
|
|
|
474
|
-
// Make request to create endpoint
|
|
475
|
-
const response = await
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
'
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
725
|
+
// Make request to create endpoint with retry logic
|
|
726
|
+
const response = await fetchWithRetry(
|
|
727
|
+
url,
|
|
728
|
+
{
|
|
729
|
+
method: 'POST',
|
|
730
|
+
headers: {
|
|
731
|
+
'Authorization': `Bearer ${resolvedToken}`,
|
|
732
|
+
'Content-Type': 'application/json',
|
|
733
|
+
'X-Generated-At': generateTimestamp(),
|
|
734
|
+
'X-Fingerprint': fingerprint
|
|
735
|
+
},
|
|
736
|
+
body: JSON.stringify(createRequest.toDict()),
|
|
737
|
+
signal: resolvedTimeout > 0 ? AbortSignal.timeout(resolvedTimeout) : undefined
|
|
482
738
|
},
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
739
|
+
DEFAULT_RETRY_CONFIG,
|
|
740
|
+
'POST /session/new'
|
|
741
|
+
);
|
|
486
742
|
|
|
487
743
|
logGeneratedAtHeader(response, 'POST', '/session/new');
|
|
488
744
|
|
|
@@ -503,6 +759,11 @@ export class PDFDancer {
|
|
|
503
759
|
client._readTimeout = resolvedTimeout;
|
|
504
760
|
client._pdfBytes = new Uint8Array();
|
|
505
761
|
client._sessionId = sessionId;
|
|
762
|
+
// Initialize retry config
|
|
763
|
+
client._retryConfig = {
|
|
764
|
+
...DEFAULT_RETRY_CONFIG,
|
|
765
|
+
...retryConfig
|
|
766
|
+
};
|
|
506
767
|
// Initialize caches
|
|
507
768
|
client._documentSnapshotCache = null;
|
|
508
769
|
client._pageSnapshotCache = new Map();
|
|
@@ -517,10 +778,56 @@ export class PDFDancer {
|
|
|
517
778
|
}
|
|
518
779
|
}
|
|
519
780
|
|
|
781
|
+
private static async _obtainAnonymousToken(baseUrl: string, timeout: number = 60000): Promise<string> {
|
|
782
|
+
const normalizedBaseUrl = (baseUrl || "https://api.pdfdancer.com").replace(/\/+$/, '');
|
|
783
|
+
const url = `${normalizedBaseUrl}/keys/anon`;
|
|
784
|
+
|
|
785
|
+
try {
|
|
786
|
+
const fingerprint = await generateFingerprint();
|
|
787
|
+
const response = await fetchWithRetry(
|
|
788
|
+
url,
|
|
789
|
+
{
|
|
790
|
+
method: 'POST',
|
|
791
|
+
headers: {
|
|
792
|
+
'Content-Type': 'application/json',
|
|
793
|
+
'X-Fingerprint': fingerprint,
|
|
794
|
+
'X-Generated-At': generateTimestamp()
|
|
795
|
+
},
|
|
796
|
+
signal: timeout > 0 ? AbortSignal.timeout(timeout) : undefined
|
|
797
|
+
},
|
|
798
|
+
DEFAULT_RETRY_CONFIG,
|
|
799
|
+
'POST /keys/anon'
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
if (!response.ok) {
|
|
803
|
+
const errorText = await response.text().catch(() => '');
|
|
804
|
+
throw new HttpClientException(
|
|
805
|
+
`Failed to obtain anonymous token: ${errorText || `HTTP ${response.status}`}`,
|
|
806
|
+
response
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const tokenPayload: any = await response.json().catch(() => null);
|
|
811
|
+
const tokenValue = typeof tokenPayload?.token === 'string' ? tokenPayload.token.trim() : '';
|
|
812
|
+
|
|
813
|
+
if (!tokenValue) {
|
|
814
|
+
throw new HttpClientException("Invalid anonymous token response format", response);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return tokenValue;
|
|
818
|
+
} catch (error) {
|
|
819
|
+
if (error instanceof HttpClientException) {
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
823
|
+
throw new HttpClientException(`Failed to obtain anonymous token: ${errorMessage}`, undefined, error as Error);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
520
827
|
/**
|
|
521
828
|
* Process PDF data from various input types with strict validation.
|
|
522
829
|
*/
|
|
523
|
-
private _processPdfData(pdfData: Uint8Array | File | ArrayBuffer): Uint8Array {
|
|
830
|
+
private _processPdfData(pdfData: Uint8Array | File | ArrayBuffer | string): Uint8Array {
|
|
524
831
|
if (!pdfData) {
|
|
525
832
|
throw new ValidationException("PDF data cannot be null");
|
|
526
833
|
}
|
|
@@ -540,6 +847,16 @@ export class PDFDancer {
|
|
|
540
847
|
} else if (pdfData instanceof File) {
|
|
541
848
|
// Note: File reading will be handled asynchronously in the session creation
|
|
542
849
|
return new Uint8Array(); // Placeholder, will be replaced in _createSession
|
|
850
|
+
} else if (typeof pdfData === 'string') {
|
|
851
|
+
// Handle string as filepath
|
|
852
|
+
if (!fs.existsSync(pdfData)) {
|
|
853
|
+
throw new ValidationException(`PDF file not found: ${pdfData}`);
|
|
854
|
+
}
|
|
855
|
+
const fileData = new Uint8Array(fs.readFileSync(pdfData));
|
|
856
|
+
if (fileData.length === 0) {
|
|
857
|
+
throw new ValidationException("PDF file is empty");
|
|
858
|
+
}
|
|
859
|
+
return fileData;
|
|
543
860
|
} else {
|
|
544
861
|
throw new ValidationException(`Unsupported PDF data type: ${typeof pdfData}`);
|
|
545
862
|
}
|
|
@@ -621,16 +938,20 @@ export class PDFDancer {
|
|
|
621
938
|
|
|
622
939
|
const fingerprint = await this._getFingerprint();
|
|
623
940
|
|
|
624
|
-
const response = await
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
'
|
|
628
|
-
|
|
629
|
-
|
|
941
|
+
const response = await this._fetchWithRetry(
|
|
942
|
+
this._buildUrl('/session/create'),
|
|
943
|
+
{
|
|
944
|
+
method: 'POST',
|
|
945
|
+
headers: {
|
|
946
|
+
'Authorization': `Bearer ${this._token}`,
|
|
947
|
+
'X-Generated-At': generateTimestamp(),
|
|
948
|
+
'X-Fingerprint': fingerprint
|
|
949
|
+
},
|
|
950
|
+
body: formData,
|
|
951
|
+
signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
|
|
630
952
|
},
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
});
|
|
953
|
+
'POST /session/create'
|
|
954
|
+
);
|
|
634
955
|
|
|
635
956
|
logGeneratedAtHeader(response, 'POST', '/session/create');
|
|
636
957
|
|
|
@@ -675,6 +996,19 @@ export class PDFDancer {
|
|
|
675
996
|
return this._fingerprintCache;
|
|
676
997
|
}
|
|
677
998
|
|
|
999
|
+
/**
|
|
1000
|
+
* Executes a fetch request with retry logic based on the configured retry policy.
|
|
1001
|
+
* Implements exponential backoff with optional jitter.
|
|
1002
|
+
*/
|
|
1003
|
+
private async _fetchWithRetry(
|
|
1004
|
+
url: string,
|
|
1005
|
+
// eslint-disable-next-line no-undef
|
|
1006
|
+
options: RequestInit,
|
|
1007
|
+
context: string = 'request'
|
|
1008
|
+
): Promise<Response> {
|
|
1009
|
+
return fetchWithRetry(url, options, this._retryConfig, context);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
678
1012
|
/**
|
|
679
1013
|
* Make HTTP request with session headers and error handling.
|
|
680
1014
|
*/
|
|
@@ -702,12 +1036,16 @@ export class PDFDancer {
|
|
|
702
1036
|
};
|
|
703
1037
|
|
|
704
1038
|
try {
|
|
705
|
-
const response = await
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1039
|
+
const response = await this._fetchWithRetry(
|
|
1040
|
+
url.toString(),
|
|
1041
|
+
{
|
|
1042
|
+
method,
|
|
1043
|
+
headers,
|
|
1044
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
1045
|
+
signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
|
|
1046
|
+
},
|
|
1047
|
+
`${method} ${path}`
|
|
1048
|
+
);
|
|
711
1049
|
|
|
712
1050
|
logGeneratedAtHeader(response, method, path);
|
|
713
1051
|
|
|
@@ -1419,17 +1757,21 @@ export class PDFDancer {
|
|
|
1419
1757
|
|
|
1420
1758
|
const fingerprint = await this._getFingerprint();
|
|
1421
1759
|
|
|
1422
|
-
const response = await
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
'
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1760
|
+
const response = await this._fetchWithRetry(
|
|
1761
|
+
this._buildUrl('/font/register'),
|
|
1762
|
+
{
|
|
1763
|
+
method: 'POST',
|
|
1764
|
+
headers: {
|
|
1765
|
+
'Authorization': `Bearer ${this._token}`,
|
|
1766
|
+
'X-Session-Id': this._sessionId,
|
|
1767
|
+
'X-Generated-At': generateTimestamp(),
|
|
1768
|
+
'X-Fingerprint': fingerprint
|
|
1769
|
+
},
|
|
1770
|
+
body: formData,
|
|
1771
|
+
signal: AbortSignal.timeout(60000)
|
|
1429
1772
|
},
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
});
|
|
1773
|
+
'POST /font/register'
|
|
1774
|
+
);
|
|
1433
1775
|
|
|
1434
1776
|
logGeneratedAtHeader(response, 'POST', '/font/register');
|
|
1435
1777
|
|
|
@@ -1534,25 +1876,30 @@ export class PDFDancer {
|
|
|
1534
1876
|
let status: TextStatus | undefined;
|
|
1535
1877
|
const statusData = objData.status;
|
|
1536
1878
|
if (statusData && typeof statusData === 'object') {
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
// Create empty font recommendation if not provided
|
|
1548
|
-
fontRec = new FontRecommendation('', FontType.SYSTEM, 0.0);
|
|
1879
|
+
const fontInfoSource = statusData.fontInfoDto ?? statusData.fontRecommendation;
|
|
1880
|
+
let fontInfo: DocumentFontInfo | undefined;
|
|
1881
|
+
if (fontInfoSource && typeof fontInfoSource === 'object') {
|
|
1882
|
+
const documentFontName = typeof fontInfoSource.documentFontName === 'string'
|
|
1883
|
+
? fontInfoSource.documentFontName
|
|
1884
|
+
: (typeof fontInfoSource.fontName === 'string' ? fontInfoSource.fontName : '');
|
|
1885
|
+
const systemFontName = typeof fontInfoSource.systemFontName === 'string'
|
|
1886
|
+
? fontInfoSource.systemFontName
|
|
1887
|
+
: (typeof fontInfoSource.fontName === 'string' ? fontInfoSource.fontName : '');
|
|
1888
|
+
fontInfo = new DocumentFontInfo(documentFontName, systemFontName);
|
|
1549
1889
|
}
|
|
1550
1890
|
|
|
1891
|
+
const modified = statusData.modified !== undefined ? Boolean(statusData.modified) : false;
|
|
1892
|
+
const encodable = statusData.encodable !== undefined ? Boolean(statusData.encodable) : true;
|
|
1893
|
+
const fontTypeValue = typeof statusData.fontType === 'string'
|
|
1894
|
+
&& (Object.values(FontType) as string[]).includes(statusData.fontType)
|
|
1895
|
+
? statusData.fontType as FontType
|
|
1896
|
+
: FontType.SYSTEM;
|
|
1897
|
+
|
|
1551
1898
|
status = new TextStatus(
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1899
|
+
modified,
|
|
1900
|
+
encodable,
|
|
1901
|
+
fontTypeValue,
|
|
1902
|
+
fontInfo
|
|
1556
1903
|
);
|
|
1557
1904
|
}
|
|
1558
1905
|
|
|
@@ -1704,14 +2051,17 @@ export class PDFDancer {
|
|
|
1704
2051
|
const pageCount = typeof data.pageCount === 'number' ? data.pageCount : 0;
|
|
1705
2052
|
|
|
1706
2053
|
// Parse fonts
|
|
1707
|
-
const fonts:
|
|
2054
|
+
const fonts: DocumentFontInfo[] = [];
|
|
1708
2055
|
if (Array.isArray(data.fonts)) {
|
|
1709
2056
|
for (const fontData of data.fonts) {
|
|
1710
2057
|
if (fontData && typeof fontData === 'object') {
|
|
1711
|
-
const
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
2058
|
+
const documentFontName = typeof fontData.documentFontName === 'string'
|
|
2059
|
+
? fontData.documentFontName
|
|
2060
|
+
: (typeof fontData.fontName === 'string' ? fontData.fontName : '');
|
|
2061
|
+
const systemFontName = typeof fontData.systemFontName === 'string'
|
|
2062
|
+
? fontData.systemFontName
|
|
2063
|
+
: (typeof fontData.fontName === 'string' ? fontData.fontName : '');
|
|
2064
|
+
fonts.push(new DocumentFontInfo(documentFontName, systemFontName));
|
|
1715
2065
|
}
|
|
1716
2066
|
}
|
|
1717
2067
|
}
|
|
@@ -1825,4 +2175,50 @@ export class PDFDancer {
|
|
|
1825
2175
|
async selectLines() {
|
|
1826
2176
|
return this.selectTextLines();
|
|
1827
2177
|
}
|
|
2178
|
+
|
|
2179
|
+
// Singular convenience methods - return the first element or null
|
|
2180
|
+
|
|
2181
|
+
async selectImage() {
|
|
2182
|
+
const images = await this.selectImages();
|
|
2183
|
+
return images.length > 0 ? images[0] : null;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
async selectPath() {
|
|
2187
|
+
const paths = await this.selectPaths();
|
|
2188
|
+
return paths.length > 0 ? paths[0] : null;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
async selectForm() {
|
|
2192
|
+
const forms = await this.selectForms();
|
|
2193
|
+
return forms.length > 0 ? forms[0] : null;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
async selectFormField() {
|
|
2197
|
+
const fields = await this.selectFormFields();
|
|
2198
|
+
return fields.length > 0 ? fields[0] : null;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
async selectFieldByName(fieldName: string) {
|
|
2202
|
+
const fields = await this.selectFieldsByName(fieldName);
|
|
2203
|
+
return fields.length > 0 ? fields[0] : null;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
async selectParagraph() {
|
|
2207
|
+
const paragraphs = await this.selectParagraphs();
|
|
2208
|
+
return paragraphs.length > 0 ? paragraphs[0] : null;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
async selectParagraphMatching(pattern: string) {
|
|
2212
|
+
const paragraphs = await this.selectParagraphsMatching(pattern);
|
|
2213
|
+
return paragraphs.length > 0 ? paragraphs[0] : null;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
async selectTextLine() {
|
|
2217
|
+
const lines = await this.selectTextLines();
|
|
2218
|
+
return lines.length > 0 ? lines[0] : null;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
async selectLine() {
|
|
2222
|
+
return this.selectTextLine();
|
|
2223
|
+
}
|
|
1828
2224
|
}
|