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