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 a couple of headers automatically: Content-Type based on the body type (JSON, text, or binary — omitted for FormData so the browser can set the multipart boundary), and an Accept header (application/json, text/plain, */*) when one isn't already provided. Both can be overridden by supplying your own header of the same name.

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 groupKey for 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.