Resilience — retry, chunking, helpers
Pushing data to the ShopiMind API means dealing with load spikes, transient outages and batches of several thousand entities. The @shopimind/sdk-js SDK ships three mechanisms to absorb all of this without you having to write a single retry loop: an automatic retry, chunking of large arrays, and an SpmHelpers toolbox to aggregate and format results.
Everything lives in the envelope
The SDK never throws on an HTTP or network error: everything is encoded in the { ok, statusCode, data, error } envelope. Resilience is therefore read from res.ok, res.error.retryable and res.error.attempts, never from a try/catch. → See the response envelope
Automatic retry
Retry is enabled by default. With no configuration, the SDK retries a failing request 3 times when it fails on one of these conditions:
- HTTP
408,429,500,502,503,504; - network codes:
ECONNRESET,ETIMEDOUT,ECONNREFUSED,ENETUNREACH,ENOTFOUND,EAI_AGAIN.
Between two attempts, the delay follows an exponential backoff with jitter: min(backoffBaseMs * 2^(attempt-1) + jitter, backoffCapMs), where jitter is a random value from 0 to 500 ms. With the default values (backoffBaseMs: 1000, backoffCapMs: 10000), this yields approximately 1s → 2s → 4s, capped at 10s, with a small random spread so that your requests don't all retry at the same time.
Configuring retry
Pass a retry object to SpmClient.getClient. All of its fields are optional; the ones you omit keep their default value.
import { SpmClient } from '@shopimind/sdk-js';
// Customize
const client = SpmClient.getClient('v1', process.env.SHOPIMIND_API_KEY!, {
retry: {
maxRetries: 5,
backoffBaseMs: 2000,
backoffCapMs: 30000,
retryableStatus: [429, 502, 503, 504],
},
});| Field | Default | Role |
|---|---|---|
maxRetries | 3 | Number of retries after the initial failure |
backoffBaseMs | 1000 | Base of the exponential backoff (ms) |
backoffCapMs | 10000 | Cap on the delay between two attempts (ms) |
retryableStatus | [408, 429, 500, 502, 503, 504] | HTTP statuses that trigger a retry |
retryableCodes | ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'ENETUNREACH', 'ENOTFOUND', 'EAI_AGAIN'] | Retryable network codes |
retryablePatterns | ['timeout', 'timed out', 'connection refused', …] | Retryable error-message patterns |
To fully disable retry (for example if you manage your own queue with its own retry policy), pass retry: false:
import { SpmClient } from '@shopimind/sdk-js';
// Disable
const client = SpmClient.getClient('v1', process.env.SHOPIMIND_API_KEY!, { retry: false });Reading the result after attempts are exhausted
When all attempts fail, the envelope returns ok: false. The error object carries retryable (was the failure of a retryable nature?) and attempts (the number of attempts actually performed):
const res = await SpmOrders.bulkSave(client, orders);
if (!res.ok && res.error.retryable) {
// the failure is transient in nature: requeue / retry later
console.warn(`Failed after ${res.error.attempts} attempt(s): ${res.error.message}`);
}TIP
error.attempts reflects the number of actual attempts: 1 means no retry happened (non-retryable failure, or retry disabled), 4 means the initial call plus the 3 default retries.
Chunking of large batches
The bulkSave / bulkUpdate methods accept an options object { chunk?: boolean; chunkSize?: number }. Chunking is disabled by default: without chunk: true, the entire array goes out in a single request. To enable it, pass { chunk: true } — the SDK then splits the array into batches, sends them sequentially (one await per batch) and aggregates the results.
import { SpmCustomers } from '@shopimind/sdk-js';
const hugeArray = Array.from({ length: 500 }, (_, i) => ({ /* customer data */ }));
const res = await SpmCustomers.bulkSave(client, hugeArray, { chunk: true });
console.log(res.data.sent_count); // total aggregated across all batches
console.log(res.data.chunks); // per-batch detail (here 10 × 50)Batch size per resource
Without chunkSize, each resource uses its own CHUNK_SIZE constant, aligned with the limit the API accepts per request for that entity. Resources without a dedicated CHUNK_SIZE fall back on the generic value 50.
| Resource | CHUNK_SIZE |
|---|---|
SpmCustomDataRecords | 20 |
SpmCustomers, SpmCustomersAddresses, SpmProducts, SpmProductsImages, SpmProductsVariations, SpmOrders, SpmVouchers | 50 |
SpmCustomersGroups, SpmProductsCategories, SpmProductsManufacturers, SpmOrdersCarriers, SpmOrdersStatuses | 100 |
You can override the batch size on a per-resource basis via chunkSize:
await SpmProducts.bulkSave(client, items, { chunk: true, chunkSize: 25 }); // override
console.log(SpmCustomers.CHUNK_SIZE); // 50Chunking changes the shape of data
With { chunk: true }, the envelope's data property is no longer the API body but an aggregated summary of type SpmChunkedData. Adapt how you read it accordingly.
Shape of a chunked response (SpmChunkedData)
When chunking is active, res.data aggregates all batches:
interface SpmChunkedData {
sent_count: number; // total sent successfully
rejected_count: number; // total rejected by the API
failed_count: number; // total items from failed batches (HTTP/network)
rejected_items: unknown[]; // rejected items, consolidated
chunks: SpmChunkSummary[]; // one summary per batch
}
interface SpmChunkSummary {
statusCode: number;
ok: boolean;
sent_count: number;
rejected_count: number;
}The overall envelope is ok: true only if all batches succeeded. If at least one batch fails, the envelope is ok: false with a consolidated error:
code: 'PARTIAL_FAILURE';messageof the form« <n> chunk(s) failed out of <m> »;retryable: trueonly if all failed batches were retryable;attempts= the maximum number of attempts across the batches;details= the list of per-batch errors.
const res = await SpmCustomers.bulkSave(client, hugeArray, { chunk: true });
if (!res.ok) {
console.error(res.error.code); // 'PARTIAL_FAILURE'
console.error(res.error.message); // e.g. « 2 chunk(s) failed out of 10 »
}
console.log(res.data.failed_count); // number of items in the failed batchesShared helpers — SpmHelpers
SpmHelpers bundles six utility functions to split, aggregate, count, format and unwrap without duplicating the SDK's logic. They are particularly useful if you orchestrate batch sending yourself, or to produce structured logs.
import { SpmHelpers } from '@shopimind/sdk-js';
// Split an array into fixed-size sub-arrays
SpmHelpers.chunk(array, 50);
// → [[...50], [...50], ...]
// Aggregate several envelopes into one (same logic as the internal chunking)
SpmHelpers.mergeResponses([envelope1, envelope2, /* ... */]);
// → an envelope whose data is an SpmChunkedData:
// { sent_count, rejected_count, failed_count, rejected_items, chunks }
// Extract the counters, whether you pass an envelope or a raw body
SpmHelpers.extractCounts(envelope);
// → { sent: number, rejected: number, failed: number }
// Test whether an error / envelope is retryable in nature
SpmHelpers.isRetryable(error);
// → false on a successful envelope; otherwise honors error.retryable (429 / 5xx / network)
// Format an error, safe to serialize for logs
SpmHelpers.formatError(envelope);
// → { message, code, statusCode, retryable, attempts, details? } (or null on success)
// Opt-in exception mode: unwraps the double-nesting (res.data.data) and throws if !ok
SpmHelpers.unwrapOrThrow(envelope);
// → the unwrapped business payload; throws SpmApiError if the envelope failed| Function | Signature | Returns |
|---|---|---|
chunk | chunk<T>(array: T[], size: number) | T[][] |
mergeResponses | mergeResponses(envelopes: SpmEnvelope[]) | SpmEnvelope<SpmChunkedData> |
extractCounts | extractCounts(envelopeOrData) | { sent, rejected, failed } |
isRetryable | isRetryable(errOrEnvelope, options?) | boolean (false on success) |
formatError | formatError(errOrEnvelope) | SpmFormattedError | null (null on success) |
unwrapOrThrow | unwrapOrThrow<T>(envelope) | unwrapped business payload (throws SpmApiError if !ok) |
Opt-in exception mode — unwrapOrThrow and SpmApiError
By default the SDK never throws: everything goes through the envelope. If you prefer a try/catch style, SpmHelpers.unwrapOrThrow is the opt-in entry point for that mode. It unwraps the double-nesting (the business payload of reads lives at res.data.data) and returns the useful data directly; if the envelope failed (ok: false), it throws an SpmApiError.
import { SpmHelpers, SpmApiError } from '@shopimind/sdk-js';
try {
const customer = SpmHelpers.unwrapOrThrow(await SpmCustomers.get(client, '42'));
// `customer` is already the unwrapped payload (equivalent to res.data.data)
} catch (e) {
if (e instanceof SpmApiError) {
console.error(e.statusCode, e.code); // the original envelope is on e.envelope
}
}SpmApiError is the only exception thrown by unwrapOrThrow. It carries statusCode, code and envelope (the full original envelope), so you can switch from exception mode to envelope mode without losing any information.
Robust synchronization pattern
Combine bulkSave(client, items, { chunk: true }) (chunking + aggregation) with the built-in retry and SpmHelpers.formatError for actionable logs: you get a synchronization that is resilient to load spikes and transient outages, without writing a single retry loop.
import { SpmCustomers, SpmHelpers } from '@shopimind/sdk-js';
const res = await SpmCustomers.bulkSave(client, customers, { chunk: true });
if (!res.ok) {
const err = SpmHelpers.formatError(res);
logger.error('Customer sync failed', err); // { message, code, statusCode, retryable, attempts }
}
const { sent, rejected, failed } = SpmHelpers.extractCounts(res);
logger.info(`Customers: ${sent} sent, ${rejected} rejected, ${failed} failed`);Going further
- → First calls with the SDK — the envelope, the double nesting
res.data.data. - → Resource reference — methods, endpoints and
CHUNK_SIZEof each resource. - → TypeScript types —
SpmEnvelope,SpmChunkedData,SpmRetryOptions,SpmFormattedError.