WakaSync Standalone
WakaSync is an HTTP client built on the Fetch API that solves the problems plain fetch() leaves to you: race conditions from stale responses, retrying failed requests, cancelling in-flight calls, and parsing response bodies. It wraps all of this in a promise-based API with no dependencies.
Getting Started
Include the script and start making requests with the global wakaSync instance:
<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/plugins/wakasync.min.js"></script>
<script>
const users = await wakaSync.get('/api/users');
</script>
All standard HTTP methods are available. Methods that send a body (POST, PUT, PATCH) accept data as the second argument — plain objects are automatically serialized to JSON:
// GET
const users = await wakaSync.get('/api/users');
// POST with data (auto-serialized to JSON)
const newUser = await wakaSync.post('/api/users', {
name: 'Alice',
email: 'alice@example.com'
});
// PUT, PATCH, DELETE, HEAD
await wakaSync.put('/api/users/123', { name: 'Alice Smith' });
await wakaSync.patch('/api/users/123', { lastLogin: new Date().toISOString() });
await wakaSync.delete('/api/users/123');
await wakaSync.head('/api/users/123');
For anything beyond the defaults, create a configured instance. This is the recommended approach — it keeps your configuration scoped and reusable:
const http = wakaSync.create({
timeout: 10000,
retries: 2,
headers: {
'Authorization': 'Bearer my-token',
'X-API-Version': '2'
}
});
const user = await http.get('/api/user');
Instances can be derived from existing ones. Headers are deep-merged, so the child inherits the parent's headers while adding its own:
const adminApi = http.create({
headers: { 'X-Admin': 'true' }
});
// adminApi sends Authorization, X-API-Version, AND X-Admin
Headers
Headers can be set at two levels: instance defaults and per-request. Per-request headers are merged with defaults, not replaced:
const http = wakaSync.create({
headers: {
'Authorization': 'Bearer my-token'
}
});
// Sends both Authorization and X-Request-Id
const data = await http.get('/api/data', {
headers: { 'X-Request-Id': 'abc-123' }
});
// Override Authorization for just this request
const admin = await http.get('/api/admin', {
headers: { 'Authorization': 'Bearer admin-token' }
});
For headers that change between requests — like tokens that expire — use a request interceptor instead. Inside interceptors, config.headers is a Headers instance, so use .set():
http.addRequestInterceptor(function(config) {
config.headers.set('Authorization', 'Bearer ' + getToken());
return config;
});
WakaSync also sets several headers automatically: Content-Type based on the body type (JSON, text, or binary — omitted for FormData so the browser can set the multipart boundary), an Accept header, and the tracking headers X-WakaSync-Request and X-WakaSync-Version. All can be overridden.
Cancellation
Stale responses are a common source of bugs — a slow first request arriving after a fast second one. WakaSync solves this with request groups. Assign a groupKey, and any new request in that group automatically cancels the previous one:
async function search(query) {
try {
const results = await http.get('/api/search?q=' + encodeURIComponent(query), {
groupKey: 'search'
});
renderResults(results);
} catch (err) {
if (!http.isCancellationError(err)) {
console.error('Search failed:', err);
}
}
}
You can also cancel groups manually, cancel everything at once, or check what's in flight:
http.cancelGroup('search');
http.cancelAll();
const count = http.getActiveRequestCount();
For an even simpler pattern, latestOnly uses the request URL as the group key automatically:
http.get('/api/status', { latestOnly: true });
http.get('/api/status', { latestOnly: true }); // first is cancelled
If you need external control, pass your own AbortController. WakaSync combines it with its internal controller so both work independently:
const controller = new AbortController();
http.get('/api/data', { abortController: controller });
controller.abort();
Retries
WakaSync can retry failed requests automatically. By default it retries on network errors, 5xx server errors, and 429 (rate limiting) — but not 4xx client errors, since those indicate the request itself is wrong:
const http = wakaSync.create({
retries: 3,
retryDelay: 1000,
retryBackoff: 'exponential'
});
const data = await http.get('/api/unstable-endpoint');
The backoff strategy controls delay between attempts: 'exponential' (default, with jitter to prevent thundering herd), 'linear', or 'fixed'. All strategies are capped by retryBackoffMax (default 30s). On 429 responses, WakaSync respects the Retry-After header if present.
Use shouldRetry to override the default retry logic when you need to be more selective:
const data = await http.get('/api/data', {
retries: 3,
shouldRetry: function(error, attempt, maxAttempts) {
return error.response && error.response.status === 503;
}
});
Interceptors
Interceptors hook into the request/response pipeline globally. Both addRequestInterceptor() and addResponseInterceptor() accept sync or async functions and return an unsubscribe function:
// Async request interceptor: refresh expired tokens
http.addRequestInterceptor(async function(config) {
if (isTokenExpired()) {
await refreshToken();
config.headers.set('Authorization', 'Bearer ' + getToken());
}
return config;
});
// Response interceptor: log request timing
http.addResponseInterceptor(function(data, config, timing) {
console.log(config.method + ' ' + config.url + ' — ' + timing.duration + 'ms');
return data;
});
// Unsubscribe when no longer needed
const unsubscribe = http.addResponseInterceptor(function(data) {
return Object.assign({}, data, { _receivedAt: Date.now() });
});
unsubscribe();
Interceptors should return the modified config or data. If nothing is returned (undefined), the original value is preserved. Any other return value — including falsy ones like 0, "", false, or null — replaces it.
By default, derived instances created with .create() do not inherit interceptors. Pass copyInterceptors: true to carry them over:
const adminApi = http.create(
{ headers: { 'X-Admin': 'true' } },
{ copyInterceptors: true }
);
Error Handling
WakaSync errors always include an error.code property for programmatic handling. HTTP errors also attach the original Response object as error.response:
try {
const data = await http.get('/api/data');
} catch (err) {
if (http.isCancellationError(err)) {
return; // expected — don't show error UI
}
if (err.response) {
// HTTP error: err.code is 'HTTP_404', 'HTTP_500', etc.
console.error('HTTP ' + err.response.status + ':', err.message);
} else {
// Network error, parse error, or invalid config
console.error('Error:', err.message, err.code);
}
}
Response Types
WakaSync auto-detects the response format from the Content-Type header. Override with responseType when you need explicit control:
const blob = await http.get('/api/download', { responseType: 'blob' });
const raw = await http.get('/api/stream', { responseType: 'response' });
const html = await http.get('/api/page', { responseType: 'text' });
Status Validation
By default, any non-ok status (outside 200–299) throws an error. Override validateStatus to treat specific error statuses as successful:
const data = await http.get('/api/optional', {
validateStatus: function(response) {
return response.ok || response.status === 404;
}
});
File Uploads
Pass FormData as the body. WakaSync omits Content-Type so the browser can set the correct multipart boundary:
const formData = new FormData();
formData.append('file', file);
const result = await http.post('/api/upload', formData);
Fetch Passthrough
Options not specific to WakaSync — credentials, mode, cache, redirect, referrer, referrerPolicy, integrity, keepalive, and priority — are passed directly to the underlying fetch() call:
const data = await http.get('https://api.example.com/data', {
credentials: 'include',
mode: 'cors',
cache: 'no-store'
});
Reference
Configuration Options
All options can be set as instance defaults via wakaSync.create() and overridden per-request.
| Option | Default | Description |
|---|---|---|
timeout |
30000 | Request timeout in milliseconds. Set to 0 to disable. |
headers |
{} | Default headers, deep-merged with per-request headers. |
responseType |
'auto' | 'json', 'text', 'blob', 'response', or 'auto' |
validateStatus |
response.ok | Function(response) → true if the status is considered successful. |
retries |
0 | Number of retry attempts. |
retryDelay |
1000 | Base delay between retries in milliseconds. |
retryBackoff |
'exponential' | 'exponential' (with jitter), 'linear', or 'fixed' |
retryBackoffMax |
30000 | Maximum backoff delay in milliseconds. |
groupKey |
— | Group identifier for cancellation via cancelGroup(). |
latestOnly |
false | Auto-cancel previous requests to the same URL. |
ignoreAbort |
false | Resolve with undefined instead of throwing on cancellation. |
abortController |
— | External AbortController, combined with WakaSync's internal controller. |
onSuccess |
— | Callback fired before the promise resolves. |
onError |
— | Callback fired before the promise rejects. |
shouldRetry |
— | Custom function(error, attempt, maxAttempts) to control retry logic. |
urlNormalizer |
— | Custom function(url, opts) returning a group key for latestOnly. |
baseUrl |
— | Base URL used for URL normalization in latestOnly grouping. |
Error Codes
| Code | Meaning |
|---|---|
INVALID_URL |
Malformed or empty URL. |
HTTP_{status} |
HTTP error, e.g. HTTP_404 or HTTP_500. |
PARSE_ERROR |
Failed to parse the response body. |
CANCEL_TIMEOUT |
Request exceeded the configured timeout. |
CANCEL_CANCELLED |
Request was manually cancelled. |
CANCEL_SUPERSEDED |
Request was replaced by a newer one in the same group. |
INTERCEPTOR_ERROR |
An interceptor threw an error. |
INVALID_CALLBACK |
A callback option was not a function. |
Best Practices
- Use
groupKeyfor searches to prevent race conditions and stale results. - Always check
isCancellationError()before treating a caught error as a failure — cancellations are expected. - Call
cancelAll()on teardown to avoid responses arriving after cleanup. - Use
create()for isolation — separate instances for different APIs keep configuration and interceptors scoped. - Set appropriate timeouts — the 30s default suits most APIs, but long-running endpoints may need adjustment.