@harkenapp/sdk-react-native 0.0.1-alpha.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/README.md +67 -0
- package/app.plugin.cjs +135 -0
- package/app.plugin.js +1 -0
- package/dist/api/client.d.ts +67 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +163 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/errors.d.ts +46 -0
- package/dist/api/errors.d.ts.map +1 -0
- package/dist/api/errors.js +72 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +20 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/retry.d.ts +29 -0
- package/dist/api/retry.d.ts.map +1 -0
- package/dist/api/retry.js +74 -0
- package/dist/api/retry.js.map +1 -0
- package/dist/attachments/FeedbackSheet.d.ts +88 -0
- package/dist/attachments/FeedbackSheet.d.ts.map +1 -0
- package/dist/attachments/FeedbackSheet.js +250 -0
- package/dist/attachments/FeedbackSheet.js.map +1 -0
- package/dist/attachments/index.d.ts +20 -0
- package/dist/attachments/index.d.ts.map +1 -0
- package/dist/attachments/index.js +40 -0
- package/dist/attachments/index.js.map +1 -0
- package/dist/components/AttachmentGrid.d.ts +94 -0
- package/dist/components/AttachmentGrid.d.ts.map +1 -0
- package/dist/components/AttachmentGrid.js +132 -0
- package/dist/components/AttachmentGrid.js.map +1 -0
- package/dist/components/AttachmentPicker.d.ts +98 -0
- package/dist/components/AttachmentPicker.d.ts.map +1 -0
- package/dist/components/AttachmentPicker.js +297 -0
- package/dist/components/AttachmentPicker.js.map +1 -0
- package/dist/components/AttachmentPreview.d.ts +78 -0
- package/dist/components/AttachmentPreview.d.ts.map +1 -0
- package/dist/components/AttachmentPreview.js +133 -0
- package/dist/components/AttachmentPreview.js.map +1 -0
- package/dist/components/CategorySelector.d.ts +77 -0
- package/dist/components/CategorySelector.d.ts.map +1 -0
- package/dist/components/CategorySelector.js +117 -0
- package/dist/components/CategorySelector.js.map +1 -0
- package/dist/components/FeedbackForm.d.ts +50 -0
- package/dist/components/FeedbackForm.d.ts.map +1 -0
- package/dist/components/FeedbackForm.js +141 -0
- package/dist/components/FeedbackForm.js.map +1 -0
- package/dist/components/FeedbackSheet.d.ts +75 -0
- package/dist/components/FeedbackSheet.d.ts.map +1 -0
- package/dist/components/FeedbackSheet.js +215 -0
- package/dist/components/FeedbackSheet.js.map +1 -0
- package/dist/components/ThemedButton.d.ts +23 -0
- package/dist/components/ThemedButton.d.ts.map +1 -0
- package/dist/components/ThemedButton.js +77 -0
- package/dist/components/ThemedButton.js.map +1 -0
- package/dist/components/ThemedText.d.ts +16 -0
- package/dist/components/ThemedText.d.ts.map +1 -0
- package/dist/components/ThemedText.js +44 -0
- package/dist/components/ThemedText.js.map +1 -0
- package/dist/components/ThemedTextInput.d.ts +13 -0
- package/dist/components/ThemedTextInput.d.ts.map +1 -0
- package/dist/components/ThemedTextInput.js +76 -0
- package/dist/components/ThemedTextInput.js.map +1 -0
- package/dist/components/UploadStatusOverlay.d.ts +82 -0
- package/dist/components/UploadStatusOverlay.d.ts.map +1 -0
- package/dist/components/UploadStatusOverlay.js +319 -0
- package/dist/components/UploadStatusOverlay.js.map +1 -0
- package/dist/components/index.d.ts +19 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +28 -0
- package/dist/components/index.js.map +1 -0
- package/dist/context/HarkenContext.d.ts +62 -0
- package/dist/context/HarkenContext.d.ts.map +1 -0
- package/dist/context/HarkenContext.js +128 -0
- package/dist/context/HarkenContext.js.map +1 -0
- package/dist/context/index.d.ts +3 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +7 -0
- package/dist/context/index.js.map +1 -0
- package/dist/domain/index.d.ts +3 -0
- package/dist/domain/index.d.ts.map +1 -0
- package/dist/domain/index.js +7 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/domain/upload-queue.d.ts +116 -0
- package/dist/domain/upload-queue.d.ts.map +1 -0
- package/dist/domain/upload-queue.js +34 -0
- package/dist/domain/upload-queue.js.map +1 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +16 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/useAnonymousId.d.ts +28 -0
- package/dist/hooks/useAnonymousId.d.ts.map +1 -0
- package/dist/hooks/useAnonymousId.js +59 -0
- package/dist/hooks/useAnonymousId.js.map +1 -0
- package/dist/hooks/useAttachmentPicker.d.ts +84 -0
- package/dist/hooks/useAttachmentPicker.d.ts.map +1 -0
- package/dist/hooks/useAttachmentPicker.js +181 -0
- package/dist/hooks/useAttachmentPicker.js.map +1 -0
- package/dist/hooks/useAttachmentStatus.d.ts +51 -0
- package/dist/hooks/useAttachmentStatus.d.ts.map +1 -0
- package/dist/hooks/useAttachmentStatus.js +69 -0
- package/dist/hooks/useAttachmentStatus.js.map +1 -0
- package/dist/hooks/useAttachmentUpload.d.ts +101 -0
- package/dist/hooks/useAttachmentUpload.d.ts.map +1 -0
- package/dist/hooks/useAttachmentUpload.js +293 -0
- package/dist/hooks/useAttachmentUpload.js.map +1 -0
- package/dist/hooks/useFeedback.d.ts +55 -0
- package/dist/hooks/useFeedback.d.ts.map +1 -0
- package/dist/hooks/useFeedback.js +96 -0
- package/dist/hooks/useFeedback.js.map +1 -0
- package/dist/hooks/useHarkenContext.d.ts +25 -0
- package/dist/hooks/useHarkenContext.d.ts.map +1 -0
- package/dist/hooks/useHarkenContext.js +35 -0
- package/dist/hooks/useHarkenContext.js.map +1 -0
- package/dist/hooks/useHarkenTheme.d.ts +26 -0
- package/dist/hooks/useHarkenTheme.d.ts.map +1 -0
- package/dist/hooks/useHarkenTheme.js +36 -0
- package/dist/hooks/useHarkenTheme.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +9 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/uploadQueueService.d.ts +193 -0
- package/dist/services/uploadQueueService.d.ts.map +1 -0
- package/dist/services/uploadQueueService.js +623 -0
- package/dist/services/uploadQueueService.js.map +1 -0
- package/dist/services/uploadQueueStorage.d.ts +30 -0
- package/dist/services/uploadQueueStorage.d.ts.map +1 -0
- package/dist/services/uploadQueueStorage.js +77 -0
- package/dist/services/uploadQueueStorage.js.map +1 -0
- package/dist/storage/IdentityStore.d.ts +38 -0
- package/dist/storage/IdentityStore.d.ts.map +1 -0
- package/dist/storage/IdentityStore.js +83 -0
- package/dist/storage/IdentityStore.js.map +1 -0
- package/dist/storage/SecureStoreAdapter.d.ts +28 -0
- package/dist/storage/SecureStoreAdapter.d.ts.map +1 -0
- package/dist/storage/SecureStoreAdapter.js +52 -0
- package/dist/storage/SecureStoreAdapter.js.map +1 -0
- package/dist/storage/defaultStorage.d.ts +20 -0
- package/dist/storage/defaultStorage.d.ts.map +1 -0
- package/dist/storage/defaultStorage.js +131 -0
- package/dist/storage/defaultStorage.js.map +1 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +13 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/types.d.ts +32 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +11 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/theme/defaults.d.ts +43 -0
- package/dist/theme/defaults.d.ts.map +1 -0
- package/dist/theme/defaults.js +128 -0
- package/dist/theme/defaults.js.map +1 -0
- package/dist/theme/index.d.ts +3 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +14 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/types.d.ts +136 -0
- package/dist/theme/types.d.ts.map +1 -0
- package/dist/theme/types.js +3 -0
- package/dist/theme/types.js.map +1 -0
- package/dist/types/config.d.ts +100 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +3 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/openapi.d.ts +601 -0
- package/dist/types/openapi.d.ts.map +1 -0
- package/dist/types/openapi.js +7 -0
- package/dist/types/openapi.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/uuid.d.ts +10 -0
- package/dist/utils/uuid.d.ts.map +1 -0
- package/dist/utils/uuid.js +60 -0
- package/dist/utils/uuid.js.map +1 -0
- package/package.json +124 -0
- package/src/@types/expo-file-system-legacy.d.ts +13 -0
- package/src/api/client.ts +250 -0
- package/src/api/errors.ts +84 -0
- package/src/api/index.ts +15 -0
- package/src/api/retry.ts +99 -0
- package/src/attachments/FeedbackSheet.tsx +400 -0
- package/src/attachments/index.ts +70 -0
- package/src/components/AttachmentGrid.tsx +247 -0
- package/src/components/AttachmentPicker.tsx +391 -0
- package/src/components/AttachmentPreview.tsx +210 -0
- package/src/components/CategorySelector.tsx +174 -0
- package/src/components/FeedbackForm.tsx +216 -0
- package/src/components/FeedbackSheet.tsx +321 -0
- package/src/components/ThemedButton.tsx +127 -0
- package/src/components/ThemedText.tsx +65 -0
- package/src/components/ThemedTextInput.tsx +65 -0
- package/src/components/UploadStatusOverlay.tsx +440 -0
- package/src/components/index.ts +39 -0
- package/src/context/HarkenContext.tsx +129 -0
- package/src/context/index.ts +2 -0
- package/src/domain/index.ts +12 -0
- package/src/domain/upload-queue.ts +131 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/useAnonymousId.ts +68 -0
- package/src/hooks/useAttachmentPicker.ts +243 -0
- package/src/hooks/useAttachmentStatus.ts +86 -0
- package/src/hooks/useAttachmentUpload.ts +370 -0
- package/src/hooks/useFeedback.ts +139 -0
- package/src/hooks/useHarkenContext.ts +35 -0
- package/src/hooks/useHarkenTheme.ts +36 -0
- package/src/index.ts +168 -0
- package/src/services/index.ts +11 -0
- package/src/services/uploadQueueService.ts +727 -0
- package/src/services/uploadQueueStorage.ts +78 -0
- package/src/storage/IdentityStore.ts +89 -0
- package/src/storage/SecureStoreAdapter.ts +59 -0
- package/src/storage/defaultStorage.ts +109 -0
- package/src/storage/index.ts +5 -0
- package/src/storage/types.ts +34 -0
- package/src/theme/defaults.ts +151 -0
- package/src/theme/index.ts +23 -0
- package/src/theme/types.ts +157 -0
- package/src/types/config.ts +112 -0
- package/src/types/index.ts +10 -0
- package/src/types/openapi.ts +601 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/uuid.ts +77 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateUUID = generateUUID;
|
|
4
|
+
/**
|
|
5
|
+
* Generate a RFC4122 version 4 UUID.
|
|
6
|
+
*
|
|
7
|
+
* Uses crypto.getRandomValues when available (React Native),
|
|
8
|
+
* falls back to Math.random for older environments.
|
|
9
|
+
*
|
|
10
|
+
* @returns A random UUID string in the format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
11
|
+
*/
|
|
12
|
+
function generateUUID() {
|
|
13
|
+
// Use crypto.getRandomValues if available (React Native has this)
|
|
14
|
+
if (typeof crypto !== 'undefined' &&
|
|
15
|
+
typeof crypto.getRandomValues === 'function') {
|
|
16
|
+
return generateUUIDCrypto();
|
|
17
|
+
}
|
|
18
|
+
// Fallback to Math.random-based generation
|
|
19
|
+
return generateUUIDFallback();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generate UUID using crypto.getRandomValues
|
|
23
|
+
*/
|
|
24
|
+
function generateUUIDCrypto() {
|
|
25
|
+
const bytes = new Uint8Array(16);
|
|
26
|
+
crypto.getRandomValues(bytes);
|
|
27
|
+
// Set version (4) and variant (RFC4122)
|
|
28
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4
|
|
29
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant RFC4122
|
|
30
|
+
return formatUUID(bytes);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Generate UUID using Math.random (fallback)
|
|
34
|
+
*/
|
|
35
|
+
function generateUUIDFallback() {
|
|
36
|
+
const bytes = new Uint8Array(16);
|
|
37
|
+
for (let i = 0; i < 16; i++) {
|
|
38
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
39
|
+
}
|
|
40
|
+
// Set version (4) and variant (RFC4122)
|
|
41
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4
|
|
42
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant RFC4122
|
|
43
|
+
return formatUUID(bytes);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Format bytes as UUID string
|
|
47
|
+
*/
|
|
48
|
+
function formatUUID(bytes) {
|
|
49
|
+
const hex = Array.from(bytes)
|
|
50
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
51
|
+
.join('');
|
|
52
|
+
return [
|
|
53
|
+
hex.slice(0, 8),
|
|
54
|
+
hex.slice(8, 12),
|
|
55
|
+
hex.slice(12, 16),
|
|
56
|
+
hex.slice(16, 20),
|
|
57
|
+
hex.slice(20, 32),
|
|
58
|
+
].join('-');
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=uuid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uuid.js","sourceRoot":"","sources":["../../src/utils/uuid.ts"],"names":[],"mappings":";;AAkBA,oCAWC;AAnBD;;;;;;;GAOG;AACH,SAAgB,YAAY;IAC1B,kEAAkE;IAClE,IACE,OAAO,MAAM,KAAK,WAAW;QAC7B,OAAO,MAAM,CAAC,eAAe,KAAK,UAAU,EAC5C,CAAC;QACD,OAAO,kBAAkB,EAAE,CAAC;IAC9B,CAAC;IAED,2CAA2C;IAC3C,OAAO,oBAAoB,EAAE,CAAC;AAChC,CAAC;AAED;;GAEG;AACH,SAAS,kBAAkB;IACzB,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;IACjC,MAAO,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAE/B,wCAAwC;IACxC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,YAAY;IAClD,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,kBAAkB;IAExD,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED;;GAEG;AACH,SAAS,oBAAoB;IAC3B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;IACjC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;IAC7C,CAAC;IAED,wCAAwC;IACxC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,YAAY;IAClD,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,kBAAkB;IAExD,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED;;GAEG;AACH,SAAS,UAAU,CAAC,KAAiB;IACnC,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;SAC1B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;SAC3C,IAAI,CAAC,EAAE,CAAC,CAAC;IAEZ,OAAO;QACL,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;QACf,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;QAChB,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC;QACjB,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC;QACjB,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC;KAClB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@harkenapp/sdk-react-native",
|
|
3
|
+
"version": "0.0.1-alpha.1",
|
|
4
|
+
"description": "Harken React Native / Expo SDK for in-app feedback",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"react-native": "./src/index.ts",
|
|
9
|
+
"source": "./src/index.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"react-native": "./src/index.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./attachments": {
|
|
18
|
+
"types": "./dist/attachments/index.d.ts",
|
|
19
|
+
"react-native": "./src/attachments/index.ts",
|
|
20
|
+
"import": "./dist/attachments/index.js",
|
|
21
|
+
"default": "./dist/attachments/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./theme": {
|
|
24
|
+
"types": "./dist/theme/index.d.ts",
|
|
25
|
+
"react-native": "./src/theme/index.ts",
|
|
26
|
+
"import": "./dist/theme/index.js",
|
|
27
|
+
"default": "./dist/theme/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./app.plugin.js": "./app.plugin.js",
|
|
30
|
+
"./app.plugin": "./app.plugin.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src",
|
|
35
|
+
"app.plugin.js",
|
|
36
|
+
"app.plugin.cjs"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"generate": "openapi-typescript openapi.yaml -o src/types/openapi.ts",
|
|
40
|
+
"build": "pnpm run generate && tsc",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"clean": "rm -rf dist",
|
|
43
|
+
"prepublishOnly": "pnpm run clean && pnpm run build"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"react-native",
|
|
47
|
+
"expo",
|
|
48
|
+
"feedback",
|
|
49
|
+
"sdk",
|
|
50
|
+
"mobile",
|
|
51
|
+
"harken"
|
|
52
|
+
],
|
|
53
|
+
"license": "Apache-2.0",
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "https://github.com/harken-app/harken.git",
|
|
60
|
+
"directory": "packages/sdk-react-native"
|
|
61
|
+
},
|
|
62
|
+
"bugs": {
|
|
63
|
+
"url": "https://github.com/harken-app/harken/issues"
|
|
64
|
+
},
|
|
65
|
+
"homepage": "https://github.com/harken-app/harken#readme",
|
|
66
|
+
"sideEffects": false,
|
|
67
|
+
"expo": {
|
|
68
|
+
"configPlugin": "./app.plugin.cjs"
|
|
69
|
+
},
|
|
70
|
+
"peerDependencies": {
|
|
71
|
+
"react": ">=18.0.0",
|
|
72
|
+
"react-native": ">=0.72.0",
|
|
73
|
+
"react-native-safe-area-context": ">=4.0.0",
|
|
74
|
+
"expo": ">=54.0.0 <55.0.0",
|
|
75
|
+
"expo-secure-store": ">=13.0.0",
|
|
76
|
+
"expo-file-system": ">=19.0.0",
|
|
77
|
+
"expo-image-picker": ">=15.0.0",
|
|
78
|
+
"expo-document-picker": ">=12.0.0",
|
|
79
|
+
"@react-native-async-storage/async-storage": ">=1.21.0",
|
|
80
|
+
"@react-native-community/netinfo": ">=11.0.0"
|
|
81
|
+
},
|
|
82
|
+
"peerDependenciesMeta": {
|
|
83
|
+
"react-native": {
|
|
84
|
+
"optional": false
|
|
85
|
+
},
|
|
86
|
+
"expo": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"expo-secure-store": {
|
|
90
|
+
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"expo-file-system": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
95
|
+
"expo-image-picker": {
|
|
96
|
+
"optional": true
|
|
97
|
+
},
|
|
98
|
+
"expo-document-picker": {
|
|
99
|
+
"optional": true
|
|
100
|
+
},
|
|
101
|
+
"@react-native-async-storage/async-storage": {
|
|
102
|
+
"optional": true
|
|
103
|
+
},
|
|
104
|
+
"@react-native-community/netinfo": {
|
|
105
|
+
"optional": true
|
|
106
|
+
},
|
|
107
|
+
"react-native-safe-area-context": {
|
|
108
|
+
"optional": true
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"devDependencies": {
|
|
112
|
+
"openapi-typescript": "^7.4.4",
|
|
113
|
+
"@react-native-async-storage/async-storage": "^2.1.0",
|
|
114
|
+
"@react-native-community/netinfo": "^11.4.1",
|
|
115
|
+
"@types/react": "^19.1.0",
|
|
116
|
+
"expo-document-picker": "^14.0.0",
|
|
117
|
+
"expo-file-system": "^19.0.0",
|
|
118
|
+
"expo-image-picker": "^17.0.0",
|
|
119
|
+
"react": "^19.1.0",
|
|
120
|
+
"react-native": "^0.81.5",
|
|
121
|
+
"react-native-safe-area-context": "^5.0.0",
|
|
122
|
+
"typescript": "^5.9.0"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for expo-file-system/legacy subpath.
|
|
3
|
+
*
|
|
4
|
+
* In expo-file-system v19 (SDK 54), the legacy API containing
|
|
5
|
+
* createUploadTask() was moved to a separate subpath.
|
|
6
|
+
*
|
|
7
|
+
* This declaration enables TypeScript to resolve the import.
|
|
8
|
+
*/
|
|
9
|
+
declare module 'expo-file-system/legacy' {
|
|
10
|
+
// Re-export types from the build output
|
|
11
|
+
export * from 'expo-file-system/build/legacy/FileSystem';
|
|
12
|
+
export * from 'expo-file-system/build/legacy/FileSystem.types';
|
|
13
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { components } from '../types/index.js';
|
|
2
|
+
import { HarkenApiError, HarkenNetworkError } from './errors';
|
|
3
|
+
import { withRetry } from './retry';
|
|
4
|
+
import type { RetryConfig } from './retry';
|
|
5
|
+
|
|
6
|
+
// Re-export types from contracts for convenience
|
|
7
|
+
type FeedbackSubmission = components['schemas']['FeedbackSubmission'];
|
|
8
|
+
type FeedbackSubmissionResponse = components['schemas']['FeedbackSubmissionResponse'];
|
|
9
|
+
type ErrorResponse = components['schemas']['ErrorResponse'];
|
|
10
|
+
|
|
11
|
+
// Attachment types
|
|
12
|
+
type AttachmentPresignRequest = components['schemas']['AttachmentPresignRequest'];
|
|
13
|
+
type AttachmentPresignResponse = components['schemas']['AttachmentPresignResponse'];
|
|
14
|
+
type AttachmentConfirmRequest = components['schemas']['AttachmentConfirmRequest'];
|
|
15
|
+
type AttachmentStatusResponse = components['schemas']['AttachmentStatusResponse'];
|
|
16
|
+
|
|
17
|
+
const DEFAULT_API_BASE_URL = 'https://api.harken.app';
|
|
18
|
+
|
|
19
|
+
export interface HarkenClientConfig {
|
|
20
|
+
/** Publishable API key */
|
|
21
|
+
publishableKey: string;
|
|
22
|
+
/** Optional user token for verified identity */
|
|
23
|
+
userToken?: string;
|
|
24
|
+
/** Base URL for API (defaults to production) */
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
/** Retry configuration */
|
|
27
|
+
retry?: Partial<RetryConfig>;
|
|
28
|
+
/** Request timeout in ms (default: 30000) */
|
|
29
|
+
timeout?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Low-level API client for Harken.
|
|
34
|
+
*/
|
|
35
|
+
export class HarkenClient {
|
|
36
|
+
private readonly config: Required<
|
|
37
|
+
Pick<HarkenClientConfig, 'publishableKey' | 'baseUrl' | 'timeout'>
|
|
38
|
+
> &
|
|
39
|
+
Pick<HarkenClientConfig, 'userToken' | 'retry'>;
|
|
40
|
+
|
|
41
|
+
constructor(config: HarkenClientConfig) {
|
|
42
|
+
this.config = {
|
|
43
|
+
publishableKey: config.publishableKey,
|
|
44
|
+
userToken: config.userToken,
|
|
45
|
+
baseUrl: config.baseUrl ?? DEFAULT_API_BASE_URL,
|
|
46
|
+
retry: config.retry,
|
|
47
|
+
timeout: config.timeout ?? 30000,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Submit feedback to the API.
|
|
53
|
+
*/
|
|
54
|
+
async submitFeedback(
|
|
55
|
+
submission: FeedbackSubmission
|
|
56
|
+
): Promise<FeedbackSubmissionResponse> {
|
|
57
|
+
return withRetry(
|
|
58
|
+
() => this.request<FeedbackSubmissionResponse>('/v1/feedback', {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
body: JSON.stringify(submission),
|
|
61
|
+
}),
|
|
62
|
+
this.config.retry
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create a presigned URL for attachment upload.
|
|
68
|
+
*/
|
|
69
|
+
async createAttachmentUpload(
|
|
70
|
+
request: AttachmentPresignRequest
|
|
71
|
+
): Promise<AttachmentPresignResponse> {
|
|
72
|
+
return withRetry(
|
|
73
|
+
() =>
|
|
74
|
+
this.request<AttachmentPresignResponse>(
|
|
75
|
+
'/v1/feedback/attachments/presign',
|
|
76
|
+
{
|
|
77
|
+
method: 'POST',
|
|
78
|
+
body: JSON.stringify(request),
|
|
79
|
+
}
|
|
80
|
+
),
|
|
81
|
+
this.config.retry
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Confirm successful attachment upload.
|
|
87
|
+
*/
|
|
88
|
+
async confirmAttachment(
|
|
89
|
+
attachmentId: string,
|
|
90
|
+
request?: AttachmentConfirmRequest
|
|
91
|
+
): Promise<AttachmentStatusResponse> {
|
|
92
|
+
return withRetry(
|
|
93
|
+
() =>
|
|
94
|
+
this.request<AttachmentStatusResponse>(
|
|
95
|
+
`/v1/feedback/attachments/${attachmentId}/confirm`,
|
|
96
|
+
{
|
|
97
|
+
method: 'POST',
|
|
98
|
+
body: request ? JSON.stringify(request) : undefined,
|
|
99
|
+
}
|
|
100
|
+
),
|
|
101
|
+
this.config.retry
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Report attachment upload failure.
|
|
107
|
+
* This is a terminal state, so no retry is applied.
|
|
108
|
+
*/
|
|
109
|
+
async reportAttachmentFailure(
|
|
110
|
+
attachmentId: string,
|
|
111
|
+
error?: string
|
|
112
|
+
): Promise<AttachmentStatusResponse> {
|
|
113
|
+
return this.request<AttachmentStatusResponse>(
|
|
114
|
+
`/v1/feedback/attachments/${attachmentId}/fail`,
|
|
115
|
+
{
|
|
116
|
+
method: 'POST',
|
|
117
|
+
body: error ? JSON.stringify({ error }) : undefined,
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get attachment status and download URLs.
|
|
124
|
+
*/
|
|
125
|
+
async getAttachmentStatus(
|
|
126
|
+
attachmentId: string
|
|
127
|
+
): Promise<AttachmentStatusResponse> {
|
|
128
|
+
return this.request<AttachmentStatusResponse>(
|
|
129
|
+
`/v1/feedback/attachments/${attachmentId}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Make an authenticated request to the API.
|
|
135
|
+
*/
|
|
136
|
+
private async request<T>(
|
|
137
|
+
path: string,
|
|
138
|
+
options: RequestInit = {}
|
|
139
|
+
): Promise<T> {
|
|
140
|
+
const url = `${this.config.baseUrl}${path}`;
|
|
141
|
+
|
|
142
|
+
const headers: Record<string, string> = {
|
|
143
|
+
'Content-Type': 'application/json',
|
|
144
|
+
'X-Publishable-Key': this.config.publishableKey,
|
|
145
|
+
...(options.headers as Record<string, string>),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (this.config.userToken) {
|
|
149
|
+
headers['X-User-Token'] = this.config.userToken;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Create abort controller for timeout
|
|
153
|
+
const controller = new AbortController();
|
|
154
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const response = await fetch(url, {
|
|
158
|
+
...options,
|
|
159
|
+
headers,
|
|
160
|
+
signal: controller.signal,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
clearTimeout(timeoutId);
|
|
164
|
+
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
const errorBody = await this.parseErrorResponse(response);
|
|
167
|
+
// Only parse Retry-After for 429 responses
|
|
168
|
+
const retryAfter = response.status === 429
|
|
169
|
+
? this.parseRetryAfter(response)
|
|
170
|
+
: undefined;
|
|
171
|
+
throw new HarkenApiError(response.status, errorBody, { retryAfter });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return (await response.json()) as T;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
clearTimeout(timeoutId);
|
|
177
|
+
|
|
178
|
+
// Re-throw HarkenApiError as-is
|
|
179
|
+
if (error instanceof HarkenApiError) {
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Handle abort (timeout)
|
|
184
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
185
|
+
throw new HarkenNetworkError('Request timed out', error);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Handle other fetch errors (network issues)
|
|
189
|
+
if (error instanceof TypeError) {
|
|
190
|
+
throw new HarkenNetworkError('Network request failed', error);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Unknown error
|
|
194
|
+
throw new HarkenNetworkError(
|
|
195
|
+
error instanceof Error ? error.message : 'Unknown error',
|
|
196
|
+
error instanceof Error ? error : undefined
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Parse error response body, with fallback for non-JSON responses.
|
|
203
|
+
*/
|
|
204
|
+
private async parseErrorResponse(response: Response): Promise<ErrorResponse> {
|
|
205
|
+
try {
|
|
206
|
+
return (await response.json()) as ErrorResponse;
|
|
207
|
+
} catch {
|
|
208
|
+
// Fallback for non-JSON error responses
|
|
209
|
+
return {
|
|
210
|
+
error: {
|
|
211
|
+
code: `http_${response.status}`,
|
|
212
|
+
message: response.statusText || 'Request failed',
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Parse Retry-After header value in seconds.
|
|
220
|
+
* Supports both delta-seconds and HTTP-date formats.
|
|
221
|
+
*/
|
|
222
|
+
private parseRetryAfter(response: Response): number | undefined {
|
|
223
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
224
|
+
if (!retryAfter) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Try parsing as integer (delta-seconds)
|
|
229
|
+
const seconds = parseInt(retryAfter, 10);
|
|
230
|
+
if (!isNaN(seconds) && seconds >= 0) {
|
|
231
|
+
return seconds;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Try parsing as HTTP-date
|
|
235
|
+
const date = Date.parse(retryAfter);
|
|
236
|
+
if (!isNaN(date)) {
|
|
237
|
+
const delayMs = date - Date.now();
|
|
238
|
+
return delayMs > 0 ? Math.ceil(delayMs / 1000) : 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Create a configured Harken client.
|
|
247
|
+
*/
|
|
248
|
+
export function createHarkenClient(config: HarkenClientConfig): HarkenClient {
|
|
249
|
+
return new HarkenClient(config);
|
|
250
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { components } from '../types/index.js';
|
|
2
|
+
|
|
3
|
+
type ErrorResponse = components['schemas']['ErrorResponse'];
|
|
4
|
+
type ErrorDetail = components['schemas']['ErrorDetail'];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base error class for Harken API errors.
|
|
8
|
+
*/
|
|
9
|
+
export class HarkenError extends Error {
|
|
10
|
+
constructor(message: string) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'HarkenError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Error thrown when the API returns an error response.
|
|
18
|
+
*/
|
|
19
|
+
export class HarkenApiError extends HarkenError {
|
|
20
|
+
/** HTTP status code */
|
|
21
|
+
readonly status: number;
|
|
22
|
+
/** Machine-readable error code */
|
|
23
|
+
readonly code: string;
|
|
24
|
+
/** Validation error details (if present) */
|
|
25
|
+
readonly details?: ErrorDetail[];
|
|
26
|
+
/** Retry-After value in seconds (from 429 responses) */
|
|
27
|
+
readonly retryAfter?: number;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
status: number,
|
|
31
|
+
response: ErrorResponse,
|
|
32
|
+
options?: { retryAfter?: number }
|
|
33
|
+
) {
|
|
34
|
+
super(response.error.message);
|
|
35
|
+
this.name = 'HarkenApiError';
|
|
36
|
+
this.status = status;
|
|
37
|
+
this.code = response.error.code;
|
|
38
|
+
this.details = response.error.details;
|
|
39
|
+
this.retryAfter = options?.retryAfter;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** True if this is a validation error (400) */
|
|
43
|
+
get isValidationError(): boolean {
|
|
44
|
+
return this.status === 400;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True if this is an auth error (401) */
|
|
48
|
+
get isUnauthorized(): boolean {
|
|
49
|
+
return this.status === 401;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** True if this is a rate limit error (429) */
|
|
53
|
+
get isRateLimited(): boolean {
|
|
54
|
+
return this.status === 429;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** True if this is a server error (5xx) */
|
|
58
|
+
get isServerError(): boolean {
|
|
59
|
+
return this.status >= 500;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** True if this error is retryable */
|
|
63
|
+
get isRetryable(): boolean {
|
|
64
|
+
return this.isRateLimited || this.isServerError;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Error thrown when a network request fails.
|
|
70
|
+
*/
|
|
71
|
+
export class HarkenNetworkError extends HarkenError {
|
|
72
|
+
readonly cause?: Error;
|
|
73
|
+
|
|
74
|
+
constructor(message: string, cause?: Error) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = 'HarkenNetworkError';
|
|
77
|
+
this.cause = cause;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Network errors are always retryable */
|
|
81
|
+
get isRetryable(): boolean {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Client
|
|
2
|
+
export { HarkenClient, createHarkenClient } from './client';
|
|
3
|
+
export type { HarkenClientConfig } from './client';
|
|
4
|
+
|
|
5
|
+
// Errors
|
|
6
|
+
export {
|
|
7
|
+
HarkenError,
|
|
8
|
+
HarkenApiError,
|
|
9
|
+
HarkenNetworkError,
|
|
10
|
+
} from './errors';
|
|
11
|
+
|
|
12
|
+
// Retry utilities
|
|
13
|
+
export { withRetry, calculateRetryDelay, isRetryableError } from './retry';
|
|
14
|
+
export type { RetryConfig } from './retry';
|
|
15
|
+
export { DEFAULT_RETRY_CONFIG } from './retry';
|
package/src/api/retry.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { HarkenApiError, HarkenNetworkError } from './errors';
|
|
2
|
+
|
|
3
|
+
export interface RetryConfig {
|
|
4
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
5
|
+
maxRetries: number;
|
|
6
|
+
/** Base delay in ms for exponential backoff (default: 1000) */
|
|
7
|
+
baseDelay: number;
|
|
8
|
+
/** Maximum delay in ms (default: 30000) */
|
|
9
|
+
maxDelay: number;
|
|
10
|
+
/** Jitter factor 0-1 to randomize delays (default: 0.1) */
|
|
11
|
+
jitter: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_RETRY_CONFIG: RetryConfig = {
|
|
15
|
+
maxRetries: 3,
|
|
16
|
+
baseDelay: 1000,
|
|
17
|
+
maxDelay: 30000,
|
|
18
|
+
jitter: 0.1,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Calculate delay for a retry attempt with exponential backoff and jitter.
|
|
23
|
+
*/
|
|
24
|
+
export function calculateRetryDelay(
|
|
25
|
+
attempt: number,
|
|
26
|
+
config: RetryConfig,
|
|
27
|
+
retryAfter?: number
|
|
28
|
+
): number {
|
|
29
|
+
// If server specified Retry-After, respect it
|
|
30
|
+
if (retryAfter !== undefined && retryAfter > 0) {
|
|
31
|
+
return Math.min(retryAfter * 1000, config.maxDelay);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Exponential backoff: baseDelay * 2^attempt
|
|
35
|
+
const exponentialDelay = config.baseDelay * Math.pow(2, attempt);
|
|
36
|
+
const cappedDelay = Math.min(exponentialDelay, config.maxDelay);
|
|
37
|
+
|
|
38
|
+
// Add jitter
|
|
39
|
+
const jitterRange = cappedDelay * config.jitter;
|
|
40
|
+
const jitter = (Math.random() - 0.5) * 2 * jitterRange;
|
|
41
|
+
|
|
42
|
+
return Math.max(0, cappedDelay + jitter);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if an error is retryable.
|
|
47
|
+
*/
|
|
48
|
+
export function isRetryableError(
|
|
49
|
+
error: unknown
|
|
50
|
+
): error is HarkenApiError | HarkenNetworkError {
|
|
51
|
+
if (error instanceof HarkenApiError) {
|
|
52
|
+
return error.isRetryable;
|
|
53
|
+
}
|
|
54
|
+
if (error instanceof HarkenNetworkError) {
|
|
55
|
+
return error.isRetryable;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Sleep for a given number of milliseconds.
|
|
62
|
+
*/
|
|
63
|
+
export function sleep(ms: number): Promise<void> {
|
|
64
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Execute a function with retry logic.
|
|
69
|
+
*/
|
|
70
|
+
export async function withRetry<T>(
|
|
71
|
+
fn: () => Promise<T>,
|
|
72
|
+
config: Partial<RetryConfig> = {}
|
|
73
|
+
): Promise<T> {
|
|
74
|
+
const fullConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
75
|
+
let lastError: unknown;
|
|
76
|
+
|
|
77
|
+
for (let attempt = 0; attempt <= fullConfig.maxRetries; attempt++) {
|
|
78
|
+
try {
|
|
79
|
+
return await fn();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
lastError = error;
|
|
82
|
+
|
|
83
|
+
// Don't retry if this is the last attempt or error isn't retryable
|
|
84
|
+
if (attempt >= fullConfig.maxRetries || !isRetryableError(error)) {
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Calculate delay, respecting Retry-After header if present
|
|
89
|
+
const retryAfter =
|
|
90
|
+
error instanceof HarkenApiError ? error.retryAfter : undefined;
|
|
91
|
+
|
|
92
|
+
const delay = calculateRetryDelay(attempt, fullConfig, retryAfter);
|
|
93
|
+
await sleep(delay);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Should never reach here, but TypeScript needs this
|
|
98
|
+
throw lastError;
|
|
99
|
+
}
|