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.
Files changed (45) hide show
  1. package/.eslintrc.js +26 -18
  2. package/.github/workflows/ci.yml +51 -2
  3. package/.github/workflows/daily-tests.yml +54 -0
  4. package/README.md +50 -6
  5. package/dist/__tests__/e2e/test-helpers.d.ts.map +1 -1
  6. package/dist/__tests__/e2e/test-helpers.js +17 -5
  7. package/dist/__tests__/e2e/test-helpers.js.map +1 -1
  8. package/dist/fingerprint.d.ts.map +1 -1
  9. package/dist/fingerprint.js +16 -5
  10. package/dist/fingerprint.js.map +1 -1
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +4 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/models.d.ts +18 -15
  16. package/dist/models.d.ts.map +1 -1
  17. package/dist/models.js +21 -18
  18. package/dist/models.js.map +1 -1
  19. package/dist/pdfdancer_v1.d.ts +71 -3
  20. package/dist/pdfdancer_v1.d.ts.map +1 -1
  21. package/dist/pdfdancer_v1.js +301 -35
  22. package/dist/pdfdancer_v1.js.map +1 -1
  23. package/docs/openapi.yml +637 -73
  24. package/jest.config.js +1 -1
  25. package/package.json +2 -2
  26. package/src/__tests__/e2e/acroform.test.ts +58 -0
  27. package/src/__tests__/e2e/form_x_object.test.ts +29 -0
  28. package/src/__tests__/e2e/image-showcase.test.ts +6 -6
  29. package/src/__tests__/e2e/image.test.ts +39 -5
  30. package/src/__tests__/e2e/line-showcase.test.ts +6 -11
  31. package/src/__tests__/e2e/line.test.ts +63 -7
  32. package/src/__tests__/e2e/page-showcase.test.ts +12 -12
  33. package/src/__tests__/e2e/page.test.ts +3 -3
  34. package/src/__tests__/e2e/paragraph-showcase.test.ts +0 -8
  35. package/src/__tests__/e2e/paragraph.test.ts +64 -8
  36. package/src/__tests__/e2e/path.test.ts +33 -4
  37. package/src/__tests__/e2e/snapshot-showcase.test.ts +10 -10
  38. package/src/__tests__/e2e/snapshot.test.ts +18 -18
  39. package/src/__tests__/e2e/test-helpers.ts +16 -5
  40. package/src/__tests__/e2e/token_from_env.test.ts +0 -15
  41. package/src/__tests__/retry-mechanism.test.ts +420 -0
  42. package/src/fingerprint.ts +20 -7
  43. package/src/index.ts +3 -1
  44. package/src/models.ts +21 -17
  45. package/src/pdfdancer_v1.ts +467 -71
@@ -23,7 +23,7 @@ import {
23
23
  DocumentSnapshot,
24
24
  FindRequest,
25
25
  Font,
26
- FontRecommendation,
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 = 30000
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(pdfData: Uint8Array, token?: string, baseUrl?: string, timeout?: number): Promise<PDFDancer> {
406
- const resolvedToken = token ?? process.env.PDFDANCER_TOKEN;
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 ?? 30000;
659
+ const resolvedTimeout = timeout ?? 60000;
412
660
 
661
+ let resolvedToken = token?.trim() ?? process.env.PDFDANCER_TOKEN?.trim() ?? null;
413
662
  if (!resolvedToken) {
414
- throw new ValidationException("Missing PDFDancer API token. Pass a token via the `token` argument or set the PDFDANCER_TOKEN environment variable.");
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: 30000)
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 ?? 30000;
697
+ const resolvedTimeout = timeout ?? 60000;
448
698
 
699
+ let resolvedToken = token?.trim() ?? process.env.PDFDANCER_TOKEN?.trim() ?? null;
449
700
  if (!resolvedToken) {
450
- throw new ValidationException("Missing PDFDancer token (pass it explicitly or set PDFDANCER_TOKEN in environment).");
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 fetch(url, {
476
- method: 'POST',
477
- headers: {
478
- 'Authorization': `Bearer ${resolvedToken}`,
479
- 'Content-Type': 'application/json',
480
- 'X-Generated-At': generateTimestamp(),
481
- 'X-Fingerprint': fingerprint
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
- body: JSON.stringify(createRequest.toDict()),
484
- signal: resolvedTimeout > 0 ? AbortSignal.timeout(resolvedTimeout) : undefined
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 fetch(this._buildUrl('/session/create'), {
625
- method: 'POST',
626
- headers: {
627
- 'Authorization': `Bearer ${this._token}`,
628
- 'X-Generated-At': generateTimestamp(),
629
- 'X-Fingerprint': fingerprint
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
- body: formData,
632
- signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
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 fetch(url.toString(), {
706
- method,
707
- headers,
708
- body: data ? JSON.stringify(data) : undefined,
709
- signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
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 fetch(this._buildUrl('/font/register'), {
1423
- method: 'POST',
1424
- headers: {
1425
- 'Authorization': `Bearer ${this._token}`,
1426
- 'X-Session-Id': this._sessionId,
1427
- 'X-Generated-At': generateTimestamp(),
1428
- 'X-Fingerprint': fingerprint
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
- body: formData,
1431
- signal: AbortSignal.timeout(30000)
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
- // Parse font recommendation
1538
- const fontRecData = statusData.fontRecommendation;
1539
- let fontRec: FontRecommendation;
1540
- if (fontRecData && typeof fontRecData === 'object') {
1541
- fontRec = new FontRecommendation(
1542
- fontRecData.fontName || '',
1543
- (fontRecData.fontType as FontType) || FontType.SYSTEM,
1544
- fontRecData.similarityScore || 0.0
1545
- );
1546
- } else {
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
- statusData.modified || false,
1553
- statusData.encodable !== undefined ? statusData.encodable : true,
1554
- (statusData.fontType as FontType) || FontType.SYSTEM,
1555
- fontRec
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: FontRecommendation[] = [];
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 fontName = fontData.fontName || '';
1712
- const fontType = fontData.fontType as FontType || FontType.SYSTEM;
1713
- const similarityScore = typeof fontData.similarityScore === 'number' ? fontData.similarityScore : 0;
1714
- fonts.push(new FontRecommendation(fontName, fontType, similarityScore));
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
  }