Standalone HTTP Client

WakaSync is an HTTP client built on the Fetch API. It adds request grouping, cancellation, automatic retries, and intelligent response parsing — all through a clean promise-based API.

Getting Started

Include the script and start making requests with the global wakaSync instance:

<script src="wakasync.js"></script>

<script>
    const users = await wakaSync.get('/api/users');
</script>

All the 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');

You can derive new instances from existing ones. Headers are deep-merged, so the child inherits headers from the parent 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, so you can add a one-off header without losing your defaults:

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, binary, or omitted for FormData so the browser can set the multipart boundary), an Accept header, and tracking headers X-WakaSync-Request and X-WakaSync-Version. All of these can be overridden through your own headers.

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. Repeated calls to the same endpoint keep only the most recent:

http.get('/api/status', { latestOnly: true });
http.get('/api/status', { latestOnly: true }); // first is cancelled

If you need external control over cancellation, 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(); // cancel manually

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.

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 let you 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 with timing metadata
http.addResponseInterceptor(function(data, config, timing) {
    console.log(config.method + ' ' + config.url + ' — ' + timing.duration + 'ms');
    return data;
});

// Remove an interceptor later
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, and HTTP errors 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);
    }
}

The isCancellationError() helper catches all cancellation types in one check — timeouts, manual cancellation, and superseded requests. Use it to distinguish expected cancellations from real errors.

Other Features

Response Types

WakaSync auto-detects the response format from the Content-Type header. Override with responseType when you need specific handling:

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 change this:

const data = await http.get('/api/optional', {
    validateStatus: function(response) {
        return response.ok || response.status === 404;
    }
});

File Uploads

Pass FormData as the body — WakaSync skips setting 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

Options like credentials, mode, cache, redirect, referrer, referrerPolicy, integrity, keepalive, and priority are passed directly through 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 valid
retries 0 Number of retry attempts
retryDelay 1000 Base delay between retries in ms
retryBackoff 'exponential' 'exponential' (with jitter), 'linear', or 'fixed'
retryBackoffMax 30000 Cap on backoff delay in ms
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 internal controller)
onSuccess Callback fired before the promise resolves
onError Callback fired before the promise rejects
shouldRetry Custom function(error, attempt, maxAttempts) for retry logic
urlNormalizer Custom function(url, opts) returning a group key for latestOnly
baseUrl Base URL for URL normalization in latestOnly grouping

Error Codes

Code Meaning
INVALID_URL Malformed or empty URL
HTTP_{status} HTTP error (e.g. HTTP_404, HTTP_500)
PARSE_ERROR Failed to parse response body
CANCEL_TIMEOUT Request timed out
CANCEL_CANCELLED Manually cancelled
CANCEL_SUPERSEDED Replaced by a newer request 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 catch errors and use isCancellationError() to distinguish expected cancellations from real failures.
  • 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 reasonable timeouts — the 30s default is fine for most APIs, but adjust for your use case.