@vahidkaargar/customized-api-client 0.4.0 → 0.5.0
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 +38 -2
- package/dist/index.cjs +70 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +65 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -347,6 +347,8 @@ Synthetic codes when the body is missing or invalid: `EMPTY_ERROR_BODY`, `INVALI
|
|
|
347
347
|
| `hasErrorCode` / `isApiClientErrorWithCode` | matching `errors[].code` |
|
|
348
348
|
| `isPreconditionFailedError` | 412 |
|
|
349
349
|
| `isConflictError` | 409 |
|
|
350
|
+
| `isIdempotencyKeyReusedError` | **409** + `IDEMPOTENCY_KEY_REUSED` |
|
|
351
|
+
| `isIdempotencyInProgressError` | **409** + `IDEMPOTENCY_REQUEST_IN_PROGRESS` |
|
|
350
352
|
| `isPayloadTooLargeError` | 413 |
|
|
351
353
|
| `isRetryablePerPolicy` | Would retry per client policy (UI hints); pass `{ retryMutationsOnServerError: true }` to match clients that opt into mutation **5xx** retries |
|
|
352
354
|
|
|
@@ -354,12 +356,46 @@ Synthetic codes when the body is missing or invalid: `EMPTY_ERROR_BODY`, `INVALI
|
|
|
354
356
|
|
|
355
357
|
## Idempotency
|
|
356
358
|
|
|
359
|
+
### Transport (this client)
|
|
360
|
+
|
|
357
361
|
- **POST, PATCH, PUT, DELETE** send **`Idempotency-Key`** (ULID per request by default).
|
|
358
|
-
- Retries reuse the **same key and body**
|
|
359
|
-
- For **separate** `post`/`patch`/… invocations, pass the same `idempotencyKey` yourself if they represent the same user intent.
|
|
362
|
+
- Retries **inside one** `client.post()` / `patch()` / … reuse the **same key and body** (`dispatchWithRetry`).
|
|
360
363
|
- Server replay → header `Idempotent-Replayed: true` → `headers.idempotentReplayed` + optional `onIdempotencyReplay`.
|
|
361
364
|
- **GET / HEAD** never send idempotency keys.
|
|
362
365
|
|
|
366
|
+
### Intent (your app)
|
|
367
|
+
|
|
368
|
+
Separate user actions (Save, Retry button, confirm dialog) are **separate client calls**. Reuse the same key only when retrying the **same intent** after abort/network/`IDEMPOTENCY_REQUEST_IN_PROGRESS`; rotate after validation fixes or conflicting payload.
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
import {
|
|
372
|
+
createApiClient,
|
|
373
|
+
createIdempotencyIntent,
|
|
374
|
+
idempotencyRotationForRetry,
|
|
375
|
+
} from '@vahidkaargar/customized-api-client';
|
|
376
|
+
|
|
377
|
+
const client = createApiClient({ baseURL: '…' });
|
|
378
|
+
const intent = createIdempotencyIntent();
|
|
379
|
+
|
|
380
|
+
async function save(rotation: 'reuse' | 'rotate' = 'rotate') {
|
|
381
|
+
await client.patch('/widgets/42', payload, {
|
|
382
|
+
idempotencyKey: intent.keyFor(rotation),
|
|
383
|
+
});
|
|
384
|
+
intent.complete();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// UI Retry after network → save('reuse')
|
|
388
|
+
// Next Save after 422 → save('rotate') or save(idempotencyRotationForRetry(lastError))
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
| Helper | Role |
|
|
392
|
+
|--------|------|
|
|
393
|
+
| `createIdempotencyIntent()` | `begin` / `keyFor('reuse'\|'rotate')` / `complete` / `abandon` |
|
|
394
|
+
| `idempotencyRotationForRetry(error)` | Suggested `'reuse'` vs `'rotate'` from an error |
|
|
395
|
+
| `createMutationIdempotency` | Deprecated alias of `createIdempotencyIntent` |
|
|
396
|
+
|
|
397
|
+
**412 If-Match** is not idempotency rotation — refresh `If-Match` version first, then retry with `'reuse'` if the payload is unchanged.
|
|
398
|
+
|
|
363
399
|
---
|
|
364
400
|
|
|
365
401
|
## Optimistic concurrency
|
package/dist/index.cjs
CHANGED
|
@@ -44,6 +44,8 @@ __export(index_exports, {
|
|
|
44
44
|
buildOffsetPageParams: () => buildOffsetPageParams,
|
|
45
45
|
createApiClient: () => createApiClient,
|
|
46
46
|
createHealthCheck: () => createHealthCheck,
|
|
47
|
+
createIdempotencyIntent: () => createIdempotencyIntent,
|
|
48
|
+
createMutationIdempotency: () => createMutationIdempotency,
|
|
47
49
|
defaultIdempotencyKey: () => defaultIdempotencyKey,
|
|
48
50
|
dispatchWithRetry: () => dispatchWithRetry,
|
|
49
51
|
etagFromResponseHeaders: () => etagFromResponseHeaders,
|
|
@@ -53,13 +55,16 @@ __export(index_exports, {
|
|
|
53
55
|
getNextPageUrl: () => getNextPageUrl,
|
|
54
56
|
groupValidationErrorsByPointer: () => groupValidationErrorsByPointer,
|
|
55
57
|
hasErrorCode: () => hasErrorCode,
|
|
58
|
+
idempotencyRotationForRetry: () => idempotencyRotationForRetry,
|
|
56
59
|
indexIncluded: () => indexIncluded,
|
|
57
60
|
isApiClientError: () => isApiClientError,
|
|
58
61
|
isApiClientErrorWithCode: () => isApiClientErrorWithCode,
|
|
59
62
|
isAuthenticationError: () => isAuthenticationError,
|
|
60
63
|
isConflictError: () => isConflictError,
|
|
61
64
|
isForbiddenError: () => isForbiddenError,
|
|
65
|
+
isIdempotencyInProgressError: () => isIdempotencyInProgressError,
|
|
62
66
|
isIdempotencyKeyRequiredError: () => isIdempotencyKeyRequiredError,
|
|
67
|
+
isIdempotencyKeyReusedError: () => isIdempotencyKeyReusedError,
|
|
63
68
|
isIfMatchRequiredError: () => isIfMatchRequiredError,
|
|
64
69
|
isMfaVerificationRequiredError: () => isMfaVerificationRequiredError,
|
|
65
70
|
isMutationMethod: () => isMutationMethod,
|
|
@@ -99,7 +104,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
99
104
|
// package.json
|
|
100
105
|
var package_default = {
|
|
101
106
|
name: "@vahidkaargar/customized-api-client",
|
|
102
|
-
version: "0.
|
|
107
|
+
version: "0.5.0",
|
|
103
108
|
description: "TypeScript Axios client for JSON:API v1.1 with idempotency, retries, and normalized results",
|
|
104
109
|
type: "module",
|
|
105
110
|
engines: {
|
|
@@ -1041,6 +1046,12 @@ function isValidationError(e) {
|
|
|
1041
1046
|
function isConflictError(e) {
|
|
1042
1047
|
return isApiErr(e, 409);
|
|
1043
1048
|
}
|
|
1049
|
+
function isIdempotencyKeyReusedError(e) {
|
|
1050
|
+
return isApiErrWithCode(e, 409, "IDEMPOTENCY_KEY_REUSED");
|
|
1051
|
+
}
|
|
1052
|
+
function isIdempotencyInProgressError(e) {
|
|
1053
|
+
return isApiErrWithCode(e, 409, "IDEMPOTENCY_REQUEST_IN_PROGRESS");
|
|
1054
|
+
}
|
|
1044
1055
|
function isPayloadTooLargeError(e) {
|
|
1045
1056
|
return isApiErr(e, 413);
|
|
1046
1057
|
}
|
|
@@ -1061,6 +1072,59 @@ function isApiErrWithCode(e, status, code) {
|
|
|
1061
1072
|
return isApiErr(e, status) && hasErrorCode(e, code);
|
|
1062
1073
|
}
|
|
1063
1074
|
|
|
1075
|
+
// src/idempotency/intent.ts
|
|
1076
|
+
function createIdempotencyIntent(options) {
|
|
1077
|
+
const generateKey = options?.generateKey ?? defaultIdempotencyKey;
|
|
1078
|
+
let activeKey = null;
|
|
1079
|
+
return {
|
|
1080
|
+
get activeKey() {
|
|
1081
|
+
return activeKey;
|
|
1082
|
+
},
|
|
1083
|
+
hasActiveIntent() {
|
|
1084
|
+
return activeKey !== null;
|
|
1085
|
+
},
|
|
1086
|
+
begin() {
|
|
1087
|
+
activeKey = generateKey();
|
|
1088
|
+
return activeKey;
|
|
1089
|
+
},
|
|
1090
|
+
keyFor(rotation) {
|
|
1091
|
+
if (rotation === "rotate") {
|
|
1092
|
+
activeKey = generateKey();
|
|
1093
|
+
return activeKey;
|
|
1094
|
+
}
|
|
1095
|
+
if (activeKey === null) {
|
|
1096
|
+
activeKey = generateKey();
|
|
1097
|
+
return activeKey;
|
|
1098
|
+
}
|
|
1099
|
+
return activeKey;
|
|
1100
|
+
},
|
|
1101
|
+
complete() {
|
|
1102
|
+
activeKey = null;
|
|
1103
|
+
},
|
|
1104
|
+
abandon() {
|
|
1105
|
+
activeKey = null;
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
var createMutationIdempotency = createIdempotencyIntent;
|
|
1110
|
+
|
|
1111
|
+
// src/idempotency/rotation.ts
|
|
1112
|
+
function idempotencyRotationForRetry(error) {
|
|
1113
|
+
if (!(error instanceof ApiClientError)) {
|
|
1114
|
+
return "reuse";
|
|
1115
|
+
}
|
|
1116
|
+
if (isIdempotencyInProgressError(error)) {
|
|
1117
|
+
return "reuse";
|
|
1118
|
+
}
|
|
1119
|
+
if (isIdempotencyKeyReusedError(error)) {
|
|
1120
|
+
return "rotate";
|
|
1121
|
+
}
|
|
1122
|
+
if (isValidationError(error)) {
|
|
1123
|
+
return "rotate";
|
|
1124
|
+
}
|
|
1125
|
+
return "reuse";
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1064
1128
|
// src/parse/pagination.ts
|
|
1065
1129
|
function parsePaginationKind(meta, links) {
|
|
1066
1130
|
const m = meta ?? {};
|
|
@@ -1265,6 +1329,8 @@ var PACKAGE_VERSION = package_default.version;
|
|
|
1265
1329
|
buildOffsetPageParams,
|
|
1266
1330
|
createApiClient,
|
|
1267
1331
|
createHealthCheck,
|
|
1332
|
+
createIdempotencyIntent,
|
|
1333
|
+
createMutationIdempotency,
|
|
1268
1334
|
defaultIdempotencyKey,
|
|
1269
1335
|
dispatchWithRetry,
|
|
1270
1336
|
etagFromResponseHeaders,
|
|
@@ -1274,13 +1340,16 @@ var PACKAGE_VERSION = package_default.version;
|
|
|
1274
1340
|
getNextPageUrl,
|
|
1275
1341
|
groupValidationErrorsByPointer,
|
|
1276
1342
|
hasErrorCode,
|
|
1343
|
+
idempotencyRotationForRetry,
|
|
1277
1344
|
indexIncluded,
|
|
1278
1345
|
isApiClientError,
|
|
1279
1346
|
isApiClientErrorWithCode,
|
|
1280
1347
|
isAuthenticationError,
|
|
1281
1348
|
isConflictError,
|
|
1282
1349
|
isForbiddenError,
|
|
1350
|
+
isIdempotencyInProgressError,
|
|
1283
1351
|
isIdempotencyKeyRequiredError,
|
|
1352
|
+
isIdempotencyKeyReusedError,
|
|
1284
1353
|
isIfMatchRequiredError,
|
|
1285
1354
|
isMfaVerificationRequiredError,
|
|
1286
1355
|
isMutationMethod,
|