import { remoteCell } from './rpc.js';
import { decode as decodeValue } from './wire.js';
export function liveUrl(keys, cursor) {
const query = new URLSearchParams();
query.set('keys', keys.join(','));
if (cursor !== null && cursor !== undefined) query.set('since', String(cursor));
return `/_zd/live?${query.toString()}`;
}
export function pollUrl(keys, cursor) {
const query = new URLSearchParams();
query.set('keys', keys.join(','));
if (cursor !== null && cursor !== undefined) query.set('since', String(cursor));
return `/_zd/poll?${query.toString()}`;
}
const cells = new Map();
export function durable(name, key, inputs) {
const [read, apply, refetch] = remoteCell(name, inputs);
let existing = cells.get(key);
if (!existing) {
existing = [];
cells.set(key, existing);
}
existing.push({ apply, refetch });
return read;
}
export function watchedKeys() {
const keys = [];
cells.forEach((_bound, key) => keys.push(key));
return keys.sort();
}
export function applyUpdate(key, value) {
const bound = cells.get(key);
if (!bound) return false;
for (const cell of bound) cell.apply(value);
return true;
}
export function resyncAll() {
cells.forEach((bound) => {
for (const cell of bound) cell.refetch();
});
}
export function receive(event, cursor) {
const seq = typeof event.seq === 'number' ? event.seq : undefined;
const seen = seq !== undefined && typeof cursor === 'number' && seq <= cursor;
if (event.event === 'resync') {
resyncAll();
return seen ? cursor : (seq ?? cursor);
}
if (event.event === 'update') {
if (seen) return cursor;
applyUpdate(event.key, event.value);
return seq ?? cursor;
}
return seen ? cursor : (seq ?? cursor);
}
export function streamTransport(keys, cursor, onEvent) {
const source = new EventSource(liveUrl(keys, cursor));
const handle = (name) => (message) => {
onEvent(decodeFrame(name, message.data, message.lastEventId));
};
for (const name of ['update', 'resync', 'ready']) {
source.addEventListener(name, handle(name));
}
return () => source.close();
}
export function pollTransport(keys, cursor, onEvent, options) {
const wait = (options && options.interval) || 1000;
const fetchImpl =
(options && options.fetch) || (typeof fetch === 'function' ? fetch : null);
if (!fetchImpl) {
return () => {};
}
const sleep = (options && options.sleep) || ((ms) => new Promise((r) => setTimeout(r, ms)));
let live = true;
let at = cursor;
(async () => {
while (live) {
try {
const response = await fetchImpl(pollUrl(keys, at));
const events = await response.json();
for (const event of events) {
if (!live) return;
at = onEvent(event);
}
} catch (error) {
}
if (!live) return;
await sleep(wait);
}
})();
return () => {
live = false;
};
}
export function canStream() {
return typeof EventSource === 'function';
}
export function subscribe(options) {
const settings = options || {};
const keys = settings.keys || watchedKeys();
if (keys.length === 0) return () => {};
let cursor = settings.since === undefined ? null : settings.since;
const transport = settings.transport || (canStream() ? streamTransport : pollTransport);
const onEvent = (event) => {
cursor = receive(event, cursor);
return cursor;
};
return transport(keys, cursor, onEvent, settings);
}
export function decodeFrame(name, data, lastEventId) {
let payload = {};
try {
payload = JSON.parse(data);
} catch (error) {
payload = {};
}
const seq =
typeof payload.seq === 'number'
? payload.seq
: lastEventId === undefined || lastEventId === null || lastEventId === ''
? undefined
: Number(lastEventId);
let value = null;
try {
if (payload.value !== undefined) value = decodeValue(payload.value);
} catch (error) {
return { event: 'resync', seq: Number.isFinite(seq) ? seq : undefined, key: undefined, value: null };
}
return {
event: name,
seq: Number.isFinite(seq) ? seq : undefined,
key: payload.key,
value,
};
}