import { signal, effect } from './signal.js';
const LOADING = { tag: 'Loading', fields: [] };
function ready(value) {
return { tag: 'Ready', fields: [value] };
}
const CODES = Object.freeze({
UNREACHABLE: 'Unreachable',
TIMEOUT: 'Timeout',
REJECTED: 'Rejected',
});
const HEADERS = Object.freeze({ accept: 'text/plain, application/json' });
const DEADLINE_MS = 30000;
export function requestUrl(destination, pairs) {
if (pairs.length === 0) return destination;
const query = pairs
.map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`)
.join('&');
return `${destination}?${query}`;
}
class RequestFailure extends Error {
constructor(code, message) {
super(message);
this.name = 'RequestFailure';
this.zdCode = code;
}
}
function codeOf(error) {
if (error instanceof RequestFailure) return error.zdCode;
const name = error && error.name;
if (name === 'AbortError' || name === 'TimeoutError') return CODES.TIMEOUT;
return CODES.UNREACHABLE;
}
function failed(destination, error) {
const message =
error instanceof RequestFailure
? error.message
: `${destination} could not be reached (${(error && error.name) || 'error'})`;
return {
tag: 'Failed',
fields: [{ message, code: { tag: codeOf(error), fields: [] } }],
};
}
function startDeadline() {
const has = typeof AbortController === 'function' && typeof setTimeout === 'function';
if (!has) return { signal: undefined, cancel: () => {}, expired: () => false };
const controller = new AbortController();
let fired = false;
const timer = setTimeout(() => {
fired = true;
controller.abort();
}, DEADLINE_MS);
return {
signal: controller.signal,
cancel: () => clearTimeout(timer),
expired: () => fired,
};
}
let transport = defaultTransport;
export function setRequestTransport(next) {
transport = next || defaultTransport;
}
async function defaultTransport(url) {
const deadline = startDeadline();
let response;
try {
response = await fetch(url, {
method: 'GET',
headers: HEADERS,
body: undefined,
credentials: 'omit',
signal: deadline.signal,
});
} catch (error) {
throw deadline.expired()
? new RequestFailure(CODES.TIMEOUT, `${url} did not answer within ${DEADLINE_MS}ms`)
: new RequestFailure(CODES.UNREACHABLE, `${url} could not be reached`);
} finally {
deadline.cancel();
}
if (!response.ok) {
throw new RequestFailure(CODES.REJECTED, `${url} answered with ${response.status}`);
}
return response.text();
}
export function request(destination, pairs) {
const [read, write] = signal(LOADING);
let generation = 0;
effect(() => {
const resolved = pairs.map(([name, get]) => [name, String(get())]);
const url = requestUrl(destination, resolved);
const mine = ++generation;
write(LOADING);
Promise.resolve()
.then(() => transport(url))
.then(
(text) => {
if (mine === generation) write(ready(text));
},
(error) => {
if (mine === generation) write(failed(destination, error));
},
);
});
return read;
}