@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 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** within one client call (`dispatchWithRetry`).
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.4.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,