@electric-sql/client
Version:
Postgres everywhere - your data, in sync, wherever you need it.
3,693 lines • 137 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __typeError = (msg) => {
throw TypeError(msg);
};
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __objRest = (source, exclude) => {
var target = {};
for (var prop in source)
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
target[prop] = source[prop];
if (source != null && __getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(source)) {
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
target[prop] = source[prop];
}
return target;
};
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
var __privateWrapper = (obj, member, setter, getter) => ({
set _(value) {
__privateSet(obj, member, value, setter);
},
get _() {
return __privateGet(obj, member, getter);
}
});
// src/error.ts
var FetchError = class _FetchError extends Error {
constructor(status, text, json, headers, url, message) {
super(
message || `HTTP Error ${status} at ${url}: ${text != null ? text : JSON.stringify(json)}`
);
this.url = url;
this.name = `FetchError`;
this.status = status;
this.text = text;
this.json = json;
this.headers = headers;
}
static async fromResponse(response, url) {
const status = response.status;
const headers = Object.fromEntries([...response.headers.entries()]);
let text = void 0;
let json = void 0;
const contentType = response.headers.get(`content-type`);
if (!response.bodyUsed) {
if (contentType && contentType.includes(`application/json`)) {
json = await response.json();
} else {
text = await response.text();
}
}
return new _FetchError(status, text, json, headers, url);
}
};
var FetchBackoffAbortError = class extends Error {
constructor() {
super(`Fetch with backoff aborted`);
this.name = `FetchBackoffAbortError`;
}
};
var InvalidShapeOptionsError = class extends Error {
constructor(message) {
super(message);
this.name = `InvalidShapeOptionsError`;
}
};
var MissingShapeUrlError = class extends Error {
constructor() {
super(`Invalid shape options: missing required url parameter`);
this.name = `MissingShapeUrlError`;
}
};
var InvalidSignalError = class extends Error {
constructor() {
super(`Invalid signal option. It must be an instance of AbortSignal.`);
this.name = `InvalidSignalError`;
}
};
var MissingShapeHandleError = class extends Error {
constructor() {
super(
`shapeHandle is required if this isn't an initial fetch (i.e. offset > -1)`
);
this.name = `MissingShapeHandleError`;
}
};
var ReservedParamError = class extends Error {
constructor(reservedParams) {
super(
`Cannot use reserved Electric parameter names in custom params: ${reservedParams.join(`, `)}`
);
this.name = `ReservedParamError`;
}
};
var ParserNullValueError = class extends Error {
constructor(columnName) {
super(`Column "${columnName != null ? columnName : `unknown`}" does not allow NULL values`);
this.name = `ParserNullValueError`;
}
};
var MissingHeadersError = class extends Error {
constructor(url, missingHeaders) {
let msg = `The response for the shape request to ${url} didn't include the following required headers:
`;
missingHeaders.forEach((h) => {
msg += `- ${h}
`;
});
msg += `
This is often due to a proxy not setting CORS correctly so that all Electric headers can be read by the client.`;
msg += `
For more information visit the troubleshooting guide: /docs/guides/troubleshooting/missing-headers`;
super(msg);
}
};
var StaleCacheError = class extends Error {
constructor(message) {
super(message);
this.name = `StaleCacheError`;
}
};
// src/parser.ts
var parseNumber = (value) => Number(value);
var parseBool = (value) => value === `true` || value === `t`;
var parseBigInt = (value) => BigInt(value);
var parseJson = (value) => JSON.parse(value);
var identityParser = (v) => v;
var defaultParser = {
int2: parseNumber,
int4: parseNumber,
int8: parseBigInt,
bool: parseBool,
float4: parseNumber,
float8: parseNumber,
json: parseJson,
jsonb: parseJson
};
function pgArrayParser(value, parser) {
let i = 0;
let char = null;
let str = ``;
let quoted = false;
let last = 0;
let p = void 0;
function extractValue(x, start, end) {
let val = x.slice(start, end);
val = val === `NULL` ? null : val;
return parser ? parser(val) : val;
}
function loop(x) {
const xs = [];
for (; i < x.length; i++) {
char = x[i];
if (quoted) {
if (char === `\\`) {
str += x[++i];
} else if (char === `"`) {
xs.push(parser ? parser(str) : str);
str = ``;
quoted = x[i + 1] === `"`;
last = i + 2;
} else {
str += char;
}
} else if (char === `"`) {
quoted = true;
} else if (char === `{`) {
last = ++i;
xs.push(loop(x));
} else if (char === `}`) {
quoted = false;
last < i && xs.push(extractValue(x, last, i));
last = i + 1;
break;
} else if (char === `,` && p !== `}` && p !== `"`) {
xs.push(extractValue(x, last, i));
last = i + 1;
}
p = char;
}
last < i && xs.push(xs.push(extractValue(x, last, i + 1)));
return xs;
}
return loop(value)[0];
}
var MessageParser = class {
constructor(parser, transformer) {
this.parser = __spreadValues(__spreadValues({}, defaultParser), parser);
this.transformer = transformer;
}
parse(messages, schema) {
return JSON.parse(messages, (key, value) => {
if ((key === `value` || key === `old_value`) && typeof value === `object` && value !== null) {
return this.transformMessageValue(value, schema);
}
return value;
});
}
/**
* Parse an array of ChangeMessages from a snapshot response.
* Applies type parsing and transformations to the value and old_value properties.
*/
parseSnapshotData(messages, schema) {
return messages.map((message) => {
const msg = message;
if (msg.value && typeof msg.value === `object` && msg.value !== null) {
msg.value = this.transformMessageValue(msg.value, schema);
}
if (msg.old_value && typeof msg.old_value === `object` && msg.old_value !== null) {
msg.old_value = this.transformMessageValue(msg.old_value, schema);
}
return msg;
});
}
/**
* Transform a message value or old_value object by parsing its columns.
*/
transformMessageValue(value, schema) {
const row = value;
Object.keys(row).forEach((key) => {
row[key] = this.parseRow(key, row[key], schema);
});
return this.transformer ? this.transformer(row) : row;
}
// Parses the message values using the provided parser based on the schema information
parseRow(key, value, schema) {
var _b;
const columnInfo = schema[key];
if (!columnInfo) {
return value;
}
const _a = columnInfo, { type: typ, dims: dimensions } = _a, additionalInfo = __objRest(_a, ["type", "dims"]);
const typeParser = (_b = this.parser[typ]) != null ? _b : identityParser;
const parser = makeNullableParser(typeParser, columnInfo, key);
if (dimensions && dimensions > 0) {
const nullablePgArrayParser = makeNullableParser(
(value2, _) => pgArrayParser(value2, parser),
columnInfo,
key
);
return nullablePgArrayParser(value);
}
return parser(value, additionalInfo);
}
};
function makeNullableParser(parser, columnInfo, columnName) {
var _a;
const isNullable = !((_a = columnInfo.not_null) != null ? _a : false);
return (value) => {
if (value === null) {
if (!isNullable) {
throw new ParserNullValueError(columnName != null ? columnName : `unknown`);
}
return null;
}
return parser(value, columnInfo);
};
}
// src/column-mapper.ts
function quoteIdentifier(identifier) {
const escaped = identifier.replace(/"/g, `""`);
return `"${escaped}"`;
}
function snakeToCamel(str) {
var _a, _b, _c, _d;
const leadingUnderscores = (_b = (_a = str.match(/^_+/)) == null ? void 0 : _a[0]) != null ? _b : ``;
const withoutLeading = str.slice(leadingUnderscores.length);
const trailingUnderscores = (_d = (_c = withoutLeading.match(/_+$/)) == null ? void 0 : _c[0]) != null ? _d : ``;
const core = trailingUnderscores ? withoutLeading.slice(
0,
withoutLeading.length - trailingUnderscores.length
) : withoutLeading;
const normalized = core.toLowerCase();
const camelCased = normalized.replace(/_+([a-z])/g, (match, letter) => {
const extraUnderscores = `_`.repeat(match.length - 2);
return extraUnderscores + letter.toUpperCase();
});
return leadingUnderscores + camelCased + trailingUnderscores;
}
function camelToSnake(str) {
return str.replace(/([a-z_])([A-Z])/g, `$1_$2`).replace(/([A-Z]+)([A-Z][a-z])/g, `$1_$2`).toLowerCase();
}
function createColumnMapper(mapping) {
const reverseMapping = {};
for (const [dbName, appName] of Object.entries(mapping)) {
reverseMapping[appName] = dbName;
}
return {
decode: (dbColumnName) => {
var _a;
return (_a = mapping[dbColumnName]) != null ? _a : dbColumnName;
},
encode: (appColumnName) => {
var _a;
return (_a = reverseMapping[appColumnName]) != null ? _a : appColumnName;
}
};
}
function encodeWhereClause(whereClause, encode) {
if (!whereClause || !encode) return whereClause != null ? whereClause : ``;
const sqlKeywords = /* @__PURE__ */ new Set([
`SELECT`,
`FROM`,
`WHERE`,
`AND`,
`OR`,
`NOT`,
`IN`,
`IS`,
`NULL`,
`NULLS`,
`FIRST`,
`LAST`,
`TRUE`,
`FALSE`,
`LIKE`,
`ILIKE`,
`BETWEEN`,
`ASC`,
`DESC`,
`LIMIT`,
`OFFSET`,
`ORDER`,
`BY`,
`GROUP`,
`HAVING`,
`DISTINCT`,
`AS`,
`ON`,
`JOIN`,
`LEFT`,
`RIGHT`,
`INNER`,
`OUTER`,
`CROSS`,
`CASE`,
`WHEN`,
`THEN`,
`ELSE`,
`END`,
`CAST`,
`LOWER`,
`UPPER`,
`COALESCE`,
`NULLIF`
]);
const quotedRanges = [];
let pos = 0;
while (pos < whereClause.length) {
const ch = whereClause[pos];
if (ch === `'` || ch === `"`) {
const start = pos;
const quoteChar = ch;
pos++;
while (pos < whereClause.length) {
if (whereClause[pos] === quoteChar) {
if (whereClause[pos + 1] === quoteChar) {
pos += 2;
} else {
pos++;
break;
}
} else {
pos++;
}
}
quotedRanges.push({ start, end: pos });
} else {
pos++;
}
}
const isInQuotedString = (pos2) => {
return quotedRanges.some((range) => pos2 >= range.start && pos2 < range.end);
};
const identifierPattern = new RegExp("(?<![a-zA-Z0-9_])([a-zA-Z_][a-zA-Z0-9_]*)(?![a-zA-Z0-9_])", "g");
return whereClause.replace(identifierPattern, (match, _p1, offset) => {
if (isInQuotedString(offset)) {
return match;
}
if (sqlKeywords.has(match.toUpperCase())) {
return match;
}
if (match.startsWith(`$`)) {
return match;
}
const encoded = encode(match);
return encoded;
});
}
function snakeCamelMapper(schema) {
if (schema) {
const mapping = {};
for (const dbColumn of Object.keys(schema)) {
mapping[dbColumn] = snakeToCamel(dbColumn);
}
return createColumnMapper(mapping);
}
return {
decode: (dbColumnName) => {
return snakeToCamel(dbColumnName);
},
encode: (appColumnName) => {
return camelToSnake(appColumnName);
}
};
}
// src/helpers.ts
function isChangeMessage(message) {
return message != null && `key` in message;
}
function isControlMessage(message) {
return message != null && `headers` in message && `control` in message.headers;
}
function isUpToDateMessage(message) {
return isControlMessage(message) && message.headers.control === `up-to-date`;
}
function getOffset(message) {
if (message.headers.control != `up-to-date`) return;
const lsn = message.headers.global_last_seen_lsn;
return lsn ? `${lsn}_0` : void 0;
}
function bigintReplacer(_key, value) {
return typeof value === `bigint` ? value.toString() : value;
}
function bigintSafeStringify(value) {
return JSON.stringify(value, bigintReplacer);
}
function canonicalBigintSafeStringify(value) {
return JSON.stringify(canonicalize(value));
}
function canonicalize(value) {
if (typeof value === `bigint`) return value.toString();
if (value === null || typeof value !== `object`) return value;
if (Array.isArray(value)) return value.map(canonicalize);
const sorted = {};
for (const k of Object.keys(value).sort()) {
sorted[k] = canonicalize(value[k]);
}
return sorted;
}
function isVisibleInSnapshot(txid, snapshot) {
const xid = BigInt(txid);
const xmin = BigInt(snapshot.xmin);
const xmax = BigInt(snapshot.xmax);
const xip = snapshot.xip_list.map(BigInt);
return xid < xmin || xid < xmax && !xip.includes(xid);
}
// src/constants.ts
var LIVE_CACHE_BUSTER_HEADER = `electric-cursor`;
var SHAPE_HANDLE_HEADER = `electric-handle`;
var CHUNK_LAST_OFFSET_HEADER = `electric-offset`;
var SHAPE_SCHEMA_HEADER = `electric-schema`;
var CHUNK_UP_TO_DATE_HEADER = `electric-up-to-date`;
var SNAPSHOT_HEADER = `electric-snapshot`;
var COLUMNS_QUERY_PARAM = `columns`;
var LIVE_CACHE_BUSTER_QUERY_PARAM = `cursor`;
var EXPIRED_HANDLE_QUERY_PARAM = `expired_handle`;
var SHAPE_HANDLE_QUERY_PARAM = `handle`;
var LIVE_QUERY_PARAM = `live`;
var OFFSET_QUERY_PARAM = `offset`;
var TABLE_QUERY_PARAM = `table`;
var WHERE_QUERY_PARAM = `where`;
var REPLICA_PARAM = `replica`;
var WHERE_PARAMS_PARAM = `params`;
var EXPERIMENTAL_LIVE_SSE_QUERY_PARAM = `experimental_live_sse`;
var LIVE_SSE_QUERY_PARAM = `live_sse`;
var FORCE_DISCONNECT_AND_REFRESH = `force-disconnect-and-refresh`;
var PAUSE_STREAM = `pause-stream`;
var SYSTEM_WAKE = `system-wake`;
var LIVE_REQUEST_TIMEOUT = `live-request-timeout`;
var LOG_MODE_QUERY_PARAM = `log`;
var SUBSET_PARAM_WHERE = `subset__where`;
var SUBSET_PARAM_LIMIT = `subset__limit`;
var SUBSET_PARAM_OFFSET = `subset__offset`;
var SUBSET_PARAM_ORDER_BY = `subset__order_by`;
var SUBSET_PARAM_WHERE_PARAMS = `subset__params`;
var SUBSET_PARAM_WHERE_EXPR = `subset__where_expr`;
var SUBSET_PARAM_ORDER_BY_EXPR = `subset__order_by_expr`;
var CACHE_BUSTER_QUERY_PARAM = `cache-buster`;
var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
LIVE_QUERY_PARAM,
LIVE_SSE_QUERY_PARAM,
EXPERIMENTAL_LIVE_SSE_QUERY_PARAM,
SHAPE_HANDLE_QUERY_PARAM,
OFFSET_QUERY_PARAM,
LIVE_CACHE_BUSTER_QUERY_PARAM,
EXPIRED_HANDLE_QUERY_PARAM,
LOG_MODE_QUERY_PARAM,
SUBSET_PARAM_WHERE,
SUBSET_PARAM_LIMIT,
SUBSET_PARAM_OFFSET,
SUBSET_PARAM_ORDER_BY,
SUBSET_PARAM_WHERE_PARAMS,
SUBSET_PARAM_WHERE_EXPR,
SUBSET_PARAM_ORDER_BY_EXPR,
CACHE_BUSTER_QUERY_PARAM
];
// src/fetch.ts
var HTTP_RETRY_STATUS_CODES = [429];
var BackoffDefaults = {
initialDelay: 1e3,
maxDelay: 32e3,
multiplier: 2,
maxRetries: Infinity
// Retry forever - clients may go offline and come back
};
function parseRetryAfterHeader(retryAfter) {
if (!retryAfter) return 0;
const retryAfterSec = Number(retryAfter);
if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) {
return retryAfterSec * 1e3;
}
const retryDate = Date.parse(retryAfter);
if (!isNaN(retryDate)) {
const deltaMs = retryDate - Date.now();
return Math.max(0, Math.min(deltaMs, 36e5));
}
return 0;
}
async function abortableSleep(waitMs, signal) {
if (waitMs <= 0) return;
if (signal == null ? void 0 : signal.aborted) throw new FetchBackoffAbortError();
await new Promise((resolve, reject) => {
let settled = false;
const done = (err) => {
if (settled) return;
settled = true;
clearTimeout(timer);
signal == null ? void 0 : signal.removeEventListener(`abort`, onAbort);
err ? reject(err) : resolve();
};
const onAbort = () => done(new FetchBackoffAbortError());
const timer = setTimeout(() => done(), waitMs);
signal == null ? void 0 : signal.addEventListener(`abort`, onAbort, { once: true });
if (signal == null ? void 0 : signal.aborted) onAbort();
});
}
function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
const {
initialDelay,
maxDelay,
multiplier,
debug = false,
onFailedAttempt,
maxRetries = Infinity
} = backoffOptions;
return async (...args) => {
var _a, _b;
const url = args[0];
const options = args[1];
let delay = initialDelay;
let attempt = 0;
while (true) {
try {
const result = await fetchClient(...args);
if (result.ok) {
return result;
}
const err = await FetchError.fromResponse(result, url.toString());
throw err;
} catch (e) {
onFailedAttempt == null ? void 0 : onFailedAttempt();
if ((_a = options == null ? void 0 : options.signal) == null ? void 0 : _a.aborted) {
throw new FetchBackoffAbortError();
} else if (e instanceof FetchError && !HTTP_RETRY_STATUS_CODES.includes(e.status) && e.status >= 400 && e.status < 500) {
throw e;
} else {
attempt++;
if (attempt > maxRetries) {
if (debug) {
console.log(
`Max retries reached (${attempt}/${maxRetries}), giving up`
);
}
throw e;
}
const serverMinimumMs = e instanceof FetchError && e.headers ? parseRetryAfterHeader(e.headers[`retry-after`]) : 0;
const jitter = Math.random() * delay;
const clientBackoffMs = Math.min(jitter, maxDelay);
const waitMs = Math.max(serverMinimumMs, clientBackoffMs);
if (debug) {
const source = serverMinimumMs > 0 ? `server+client` : `client`;
console.log(
`Retry attempt #${attempt} after ${waitMs}ms (${source}, serverMin=${serverMinimumMs}ms, clientBackoff=${clientBackoffMs}ms)`
);
}
await abortableSleep(waitMs, (_b = options == null ? void 0 : options.signal) != null ? _b : void 0);
delay = Math.min(delay * multiplier, maxDelay);
}
}
}
};
}
var NO_BODY_STATUS_CODES = [201, 204, 205];
async function consumeResponseBody(res, url, signal) {
try {
if (res.status < 200 || NO_BODY_STATUS_CODES.includes(res.status)) {
return res;
}
const text = await res.text();
return new Response(text, res);
} catch (err) {
if (signal == null ? void 0 : signal.aborted) {
throw new FetchBackoffAbortError();
}
throw new FetchError(
res.status,
void 0,
void 0,
Object.fromEntries([...res.headers.entries()]),
url,
err instanceof Error ? err.message : typeof err === `string` ? err : `failed to read body`
);
}
}
function createFetchWithConsumedMessages(fetchClient) {
return async (...args) => {
var _a, _b;
const url = args[0];
const res = await fetchClient(...args);
return consumeResponseBody(
res,
url.toString(),
(_b = (_a = args[1]) == null ? void 0 : _a.signal) != null ? _b : void 0
);
};
}
var ChunkPrefetchDefaults = {
maxChunksToPrefetch: 2
};
function createFetchWithChunkBuffer(fetchClient, prefetchOptions = ChunkPrefetchDefaults) {
const { maxChunksToPrefetch } = prefetchOptions;
let prefetchQueue;
const prefetchClient = async (...args) => {
const url = args[0].toString();
const method = getRequestMethod(args[0], args[1]);
if (method !== `GET`) {
prefetchQueue == null ? void 0 : prefetchQueue.abort();
prefetchQueue = void 0;
return fetchClient(...args);
}
const prefetchedRequest = prefetchQueue == null ? void 0 : prefetchQueue.consume(...args);
if (prefetchedRequest) {
return prefetchedRequest;
}
prefetchQueue == null ? void 0 : prefetchQueue.abort();
prefetchQueue = void 0;
const response = await fetchClient(...args);
const nextUrl = getNextChunkUrl(url, response);
if (nextUrl) {
prefetchQueue = new PrefetchQueue({
fetchClient,
maxPrefetchedRequests: maxChunksToPrefetch,
url: nextUrl,
requestInit: args[1]
});
}
return response;
};
return prefetchClient;
}
var requiredElectricResponseHeaders = [
CHUNK_LAST_OFFSET_HEADER,
SHAPE_HANDLE_HEADER
];
var requiredLiveResponseHeaders = [LIVE_CACHE_BUSTER_HEADER];
var requiredNonLiveResponseHeaders = [SHAPE_SCHEMA_HEADER];
function createFetchWithResponseHeadersCheck(fetchClient) {
return async (...args) => {
const response = await fetchClient(...args);
if (response.ok) {
const headers = response.headers;
const missingHeaders = [];
const addMissingHeaders = (requiredHeaders) => missingHeaders.push(...requiredHeaders.filter((h) => !headers.has(h)));
const input = args[0];
const urlString = input.toString();
const url = new URL(urlString);
const isSnapshotRequest = [
SUBSET_PARAM_WHERE,
SUBSET_PARAM_WHERE_PARAMS,
SUBSET_PARAM_LIMIT,
SUBSET_PARAM_OFFSET,
SUBSET_PARAM_ORDER_BY
].some((p) => url.searchParams.has(p));
if (isSnapshotRequest) {
return response;
}
addMissingHeaders(requiredElectricResponseHeaders);
if (url.searchParams.get(LIVE_QUERY_PARAM) === `true`) {
addMissingHeaders(requiredLiveResponseHeaders);
}
if (!url.searchParams.has(LIVE_QUERY_PARAM) || url.searchParams.get(LIVE_QUERY_PARAM) === `false`) {
addMissingHeaders(requiredNonLiveResponseHeaders);
}
if (missingHeaders.length > 0) {
throw new MissingHeadersError(urlString, missingHeaders);
}
}
return response;
};
}
var _fetchClient, _maxPrefetchedRequests, _prefetchQueue, _queueHeadUrl, _queueTailUrl, _PrefetchQueue_instances, prefetch_fn;
var PrefetchQueue = class {
constructor(options) {
__privateAdd(this, _PrefetchQueue_instances);
__privateAdd(this, _fetchClient);
__privateAdd(this, _maxPrefetchedRequests);
__privateAdd(this, _prefetchQueue, /* @__PURE__ */ new Map());
__privateAdd(this, _queueHeadUrl);
__privateAdd(this, _queueTailUrl);
var _a;
__privateSet(this, _fetchClient, (_a = options.fetchClient) != null ? _a : (...args) => fetch(...args));
__privateSet(this, _maxPrefetchedRequests, options.maxPrefetchedRequests);
__privateSet(this, _queueHeadUrl, options.url.toString());
__privateSet(this, _queueTailUrl, __privateGet(this, _queueHeadUrl));
__privateMethod(this, _PrefetchQueue_instances, prefetch_fn).call(this, options.url, options.requestInit);
}
abort() {
__privateGet(this, _prefetchQueue).forEach(([_, aborter]) => aborter.abort());
__privateGet(this, _prefetchQueue).clear();
}
consume(...args) {
const url = args[0].toString();
const entry = __privateGet(this, _prefetchQueue).get(url);
if (!entry || url !== __privateGet(this, _queueHeadUrl)) return;
const [request, aborter] = entry;
if (aborter.signal.aborted) {
__privateGet(this, _prefetchQueue).delete(url);
return;
}
__privateGet(this, _prefetchQueue).delete(url);
request.then((response) => {
const nextUrl = getNextChunkUrl(url, response);
__privateSet(this, _queueHeadUrl, nextUrl);
if (__privateGet(this, _queueTailUrl) && !__privateGet(this, _prefetchQueue).has(__privateGet(this, _queueTailUrl))) {
__privateMethod(this, _PrefetchQueue_instances, prefetch_fn).call(this, __privateGet(this, _queueTailUrl), args[1]);
}
}).catch(() => {
});
return request;
}
};
_fetchClient = new WeakMap();
_maxPrefetchedRequests = new WeakMap();
_prefetchQueue = new WeakMap();
_queueHeadUrl = new WeakMap();
_queueTailUrl = new WeakMap();
_PrefetchQueue_instances = new WeakSet();
prefetch_fn = function(...args) {
var _a, _b;
const url = args[0].toString();
if (__privateGet(this, _prefetchQueue).size >= __privateGet(this, _maxPrefetchedRequests)) return;
const aborter = new AbortController();
try {
const { signal, cleanup } = chainAborter(aborter, (_a = args[1]) == null ? void 0 : _a.signal);
const request = __privateGet(this, _fetchClient).call(this, url, __spreadProps(__spreadValues({}, (_b = args[1]) != null ? _b : {}), { signal }));
__privateGet(this, _prefetchQueue).set(url, [request, aborter]);
request.then((response) => {
if (!response.ok || aborter.signal.aborted) return;
const nextUrl = getNextChunkUrl(url, response);
if (!nextUrl || nextUrl === url) {
__privateSet(this, _queueTailUrl, void 0);
return;
}
__privateSet(this, _queueTailUrl, nextUrl);
return __privateMethod(this, _PrefetchQueue_instances, prefetch_fn).call(this, nextUrl, args[1]);
}).catch(() => {
}).finally(cleanup);
} catch (_) {
}
};
function getNextChunkUrl(url, res) {
const shapeHandle = res.headers.get(SHAPE_HANDLE_HEADER);
const lastOffset = res.headers.get(CHUNK_LAST_OFFSET_HEADER);
const isUpToDate = res.headers.has(CHUNK_UP_TO_DATE_HEADER);
const isSnapshot = res.headers.get(SNAPSHOT_HEADER) === `true`;
if (!shapeHandle || !lastOffset || isUpToDate || isSnapshot) return;
const nextUrl = new URL(url);
if (nextUrl.searchParams.has(LIVE_QUERY_PARAM)) return;
const expiredHandle = nextUrl.searchParams.get(EXPIRED_HANDLE_QUERY_PARAM);
if (expiredHandle && shapeHandle === expiredHandle) {
console.warn(
`[Electric] Received stale cached response with expired shape handle. This should not happen and indicates a proxy/CDN caching misconfiguration. The response contained handle "${shapeHandle}" which was previously marked as expired. Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key. Skipping prefetch to prevent infinite 409 loop.`
);
return;
}
nextUrl.searchParams.set(SHAPE_HANDLE_QUERY_PARAM, shapeHandle);
nextUrl.searchParams.set(OFFSET_QUERY_PARAM, lastOffset);
nextUrl.searchParams.sort();
return nextUrl.toString();
}
function chainAborter(aborter, sourceSignal) {
let cleanup = noop;
if (!sourceSignal) {
} else if (sourceSignal.aborted) {
aborter.abort();
} else {
const abortParent = () => aborter.abort();
sourceSignal.addEventListener(`abort`, abortParent, {
once: true,
signal: aborter.signal
});
cleanup = () => sourceSignal.removeEventListener(`abort`, abortParent);
}
return {
signal: aborter.signal,
cleanup
};
}
function noop() {
}
function getRequestMethod(input, init) {
if (init == null ? void 0 : init.method) {
return init.method.toUpperCase();
}
if (typeof Request !== `undefined` && input instanceof Request) {
return input.method.toUpperCase();
}
return `GET`;
}
// src/expression-compiler.ts
function compileExpression(expr, columnMapper) {
switch (expr.type) {
case `ref`: {
const mappedColumn = columnMapper ? columnMapper(expr.column) : expr.column;
return quoteIdentifier(mappedColumn);
}
case `val`:
return `$${expr.paramIndex}`;
case `func`:
return compileFunction(expr, columnMapper);
default: {
const _exhaustive = expr;
throw new Error(`Unknown expression type: ${JSON.stringify(_exhaustive)}`);
}
}
}
function compileFunction(expr, columnMapper) {
const args = expr.args.map((arg) => compileExpression(arg, columnMapper));
switch (expr.name) {
// Binary comparison operators
case `eq`:
return `${args[0]} = ${args[1]}`;
case `gt`:
return `${args[0]} > ${args[1]}`;
case `gte`:
return `${args[0]} >= ${args[1]}`;
case `lt`:
return `${args[0]} < ${args[1]}`;
case `lte`:
return `${args[0]} <= ${args[1]}`;
// Logical operators
case `and`:
return args.map((a) => `(${a})`).join(` AND `);
case `or`:
return args.map((a) => `(${a})`).join(` OR `);
case `not`:
return `NOT (${args[0]})`;
// Special operators
case `in`:
return `${args[0]} = ANY(${args[1]})`;
case `like`:
return `${args[0]} LIKE ${args[1]}`;
case `ilike`:
return `${args[0]} ILIKE ${args[1]}`;
case `isNull`:
case `isUndefined`:
return `${args[0]} IS NULL`;
// String functions
case `upper`:
return `UPPER(${args[0]})`;
case `lower`:
return `LOWER(${args[0]})`;
case `length`:
return `LENGTH(${args[0]})`;
case `concat`:
return `CONCAT(${args.join(`, `)})`;
// Other functions
case `coalesce`:
return `COALESCE(${args.join(`, `)})`;
default:
throw new Error(`Unknown function: ${expr.name}`);
}
}
function compileOrderBy(clauses, columnMapper) {
return clauses.map((clause) => {
const mappedColumn = columnMapper ? columnMapper(clause.column) : clause.column;
let sql = quoteIdentifier(mappedColumn);
if (clause.direction === `desc`) sql += ` DESC`;
if (clause.nulls === `first`) sql += ` NULLS FIRST`;
if (clause.nulls === `last`) sql += ` NULLS LAST`;
return sql;
}).join(`, `);
}
// ../../node_modules/.pnpm/@microsoft+fetch-event-source@2.0.1_patch_hash=46f4e76dd960e002a542732bb4323817a24fce1673cb71e2f458fe09776fa188/node_modules/@microsoft/fetch-event-source/lib/esm/parse.js
async function getBytes(stream, onChunk) {
const reader = stream.getReader();
let result;
while (!(result = await reader.read()).done) {
onChunk(result.value);
}
}
function getLines(onLine) {
let buffer;
let position;
let fieldLength;
let discardTrailingNewline = false;
return function onChunk(arr) {
if (buffer === void 0) {
buffer = arr;
position = 0;
fieldLength = -1;
} else {
buffer = concat(buffer, arr);
}
const bufLength = buffer.length;
let lineStart = 0;
while (position < bufLength) {
if (discardTrailingNewline) {
if (buffer[position] === 10) {
lineStart = ++position;
}
discardTrailingNewline = false;
}
let lineEnd = -1;
for (; position < bufLength && lineEnd === -1; ++position) {
switch (buffer[position]) {
case 58:
if (fieldLength === -1) {
fieldLength = position - lineStart;
}
break;
case 13:
discardTrailingNewline = true;
case 10:
lineEnd = position;
break;
}
}
if (lineEnd === -1) {
break;
}
onLine(buffer.subarray(lineStart, lineEnd), fieldLength);
lineStart = position;
fieldLength = -1;
}
if (lineStart === bufLength) {
buffer = void 0;
} else if (lineStart !== 0) {
buffer = buffer.subarray(lineStart);
position -= lineStart;
}
};
}
function getMessages(onId, onRetry, onMessage) {
let message = newMessage();
const decoder = new TextDecoder();
return function onLine(line, fieldLength) {
if (line.length === 0) {
onMessage === null || onMessage === void 0 ? void 0 : onMessage(message);
message = newMessage();
} else if (fieldLength > 0) {
const field = decoder.decode(line.subarray(0, fieldLength));
const valueOffset = fieldLength + (line[fieldLength + 1] === 32 ? 2 : 1);
const value = decoder.decode(line.subarray(valueOffset));
switch (field) {
case "data":
message.data = message.data ? message.data + "\n" + value : value;
break;
case "event":
message.event = value;
break;
case "id":
onId(message.id = value);
break;
case "retry":
const retry = parseInt(value, 10);
if (!isNaN(retry)) {
onRetry(message.retry = retry);
}
break;
}
}
};
}
function concat(a, b) {
const res = new Uint8Array(a.length + b.length);
res.set(a);
res.set(b, a.length);
return res;
}
function newMessage() {
return {
data: "",
event: "",
id: "",
retry: void 0
};
}
// ../../node_modules/.pnpm/@microsoft+fetch-event-source@2.0.1_patch_hash=46f4e76dd960e002a542732bb4323817a24fce1673cb71e2f458fe09776fa188/node_modules/@microsoft/fetch-event-source/lib/esm/fetch.js
var __rest = function(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var EventStreamContentType = "text/event-stream";
var DefaultRetryInterval = 1e3;
var LastEventId = "last-event-id";
function fetchEventSource(input, _a) {
var { signal: inputSignal, headers: inputHeaders, onopen: inputOnOpen, onmessage, onclose, onerror, openWhenHidden, fetch: inputFetch } = _a, rest = __rest(_a, ["signal", "headers", "onopen", "onmessage", "onclose", "onerror", "openWhenHidden", "fetch"]);
return new Promise((resolve, reject) => {
const headers = Object.assign({}, inputHeaders);
if (!headers.accept) {
headers.accept = EventStreamContentType;
}
let curRequestController;
function onVisibilityChange() {
curRequestController.abort();
if (typeof document !== "undefined" && !document.hidden) {
create();
}
}
if (typeof document !== "undefined" && !openWhenHidden) {
document.addEventListener("visibilitychange", onVisibilityChange);
}
let retryInterval = DefaultRetryInterval;
let retryTimer = 0;
function dispose() {
if (typeof document !== "undefined") {
document.removeEventListener("visibilitychange", onVisibilityChange);
}
clearTimeout(retryTimer);
curRequestController.abort();
}
inputSignal === null || inputSignal === void 0 ? void 0 : inputSignal.addEventListener("abort", () => {
dispose();
});
const fetch2 = inputFetch !== null && inputFetch !== void 0 ? inputFetch : window.fetch;
const onopen = inputOnOpen !== null && inputOnOpen !== void 0 ? inputOnOpen : defaultOnOpen;
async function create() {
var _a2;
curRequestController = new AbortController();
const sig = inputSignal.aborted ? inputSignal : curRequestController.signal;
try {
const response = await fetch2(input, Object.assign(Object.assign({}, rest), { headers, signal: sig }));
await onopen(response);
await getBytes(response.body, getLines(getMessages((id) => {
if (id) {
headers[LastEventId] = id;
} else {
delete headers[LastEventId];
}
}, (retry) => {
retryInterval = retry;
}, onmessage)));
onclose === null || onclose === void 0 ? void 0 : onclose();
dispose();
resolve();
} catch (err) {
if (sig.aborted) {
dispose();
reject(err);
} else if (!curRequestController.signal.aborted) {
try {
const interval = (_a2 = onerror === null || onerror === void 0 ? void 0 : onerror(err)) !== null && _a2 !== void 0 ? _a2 : retryInterval;
clearTimeout(retryTimer);
retryTimer = setTimeout(create, interval);
} catch (innerErr) {
dispose();
reject(innerErr);
}
}
}
}
create();
});
}
function defaultOnOpen(response) {
const contentType = response.headers.get("content-type");
if (!(contentType === null || contentType === void 0 ? void 0 : contentType.startsWith(EventStreamContentType))) {
throw new Error(`Expected content-type to be ${EventStreamContentType}, Actual: ${contentType}`);
}
}
// src/expired-shapes-cache.ts
var ExpiredShapesCache = class {
constructor() {
this.data = {};
this.max = 250;
this.storageKey = `electric_expired_shapes`;
this.load();
}
getExpiredHandle(shapeUrl) {
const entry = this.data[shapeUrl];
if (entry) {
entry.lastUsed = Date.now();
this.save();
return entry.expiredHandle;
}
return null;
}
markExpired(shapeUrl, handle) {
this.data[shapeUrl] = { expiredHandle: handle, lastUsed: Date.now() };
const keys = Object.keys(this.data);
if (keys.length > this.max) {
const oldest = keys.reduce(
(min, k) => this.data[k].lastUsed < this.data[min].lastUsed ? k : min
);
delete this.data[oldest];
}
this.save();
}
save() {
if (typeof localStorage === `undefined`) return;
try {
localStorage.setItem(this.storageKey, JSON.stringify(this.data));
} catch (e) {
}
}
load() {
if (typeof localStorage === `undefined`) return;
try {
const stored = localStorage.getItem(this.storageKey);
if (stored) {
this.data = JSON.parse(stored);
}
} catch (e) {
this.data = {};
}
}
clear() {
this.data = {};
this.save();
}
delete(shapeUrl) {
delete this.data[shapeUrl];
this.save();
}
};
var expiredShapesCache = new ExpiredShapesCache();
// src/up-to-date-tracker.ts
var UpToDateTracker = class {
constructor() {
this.data = {};
this.storageKey = `electric_up_to_date_tracker`;
this.cacheTTL = 6e4;
// 60s to match typical CDN s-maxage cache duration
this.maxEntries = 250;
this.writeThrottleMs = 6e4;
// Throttle localStorage writes to once per 60s
this.lastWriteTime = 0;
this.load();
this.cleanup();
}
/**
* Records that a shape received an up-to-date message with a specific cursor.
* This timestamp and cursor are used to detect cache replay scenarios.
* Updates in-memory immediately, but throttles localStorage writes.
*/
recordUpToDate(shapeKey, cursor) {
this.data[shapeKey] = {
timestamp: Date.now(),
cursor
};
const keys = Object.keys(this.data);
if (keys.length > this.maxEntries) {
const oldest = keys.reduce(
(min, k) => this.data[k].timestamp < this.data[min].timestamp ? k : min
);
delete this.data[oldest];
}
this.scheduleSave();
}
/**
* Schedules a throttled save to localStorage.
* Writes immediately if enough time has passed, otherwise schedules for later.
*/
scheduleSave() {
const now = Date.now();
const timeSinceLastWrite = now - this.lastWriteTime;
if (timeSinceLastWrite >= this.writeThrottleMs) {
this.lastWriteTime = now;
this.save();
} else if (!this.pendingSaveTimer) {
const delay = this.writeThrottleMs - timeSinceLastWrite;
this.pendingSaveTimer = setTimeout(() => {
this.lastWriteTime = Date.now();
this.pendingSaveTimer = void 0;
this.save();
}, delay);
}
}
/**
* Checks if we should enter replay mode for this shape.
* Returns the last seen cursor if there's a recent up-to-date (< 60s),
* which means we'll likely be replaying cached responses.
* Returns null if no recent up-to-date exists.
*/
shouldEnterReplayMode(shapeKey) {
const entry = this.data[shapeKey];
if (!entry) {
return null;
}
const age = Date.now() - entry.timestamp;
if (age >= this.cacheTTL) {
return null;
}
return entry.cursor;
}
/**
* Cleans up expired entries from the cache.
* Called on initialization and can be called periodically.
*/
cleanup() {
const now = Date.now();
const keys = Object.keys(this.data);
let modified = false;
for (const key of keys) {
const age = now - this.data[key].timestamp;
if (age > this.cacheTTL) {
delete this.data[key];
modified = true;
}
}
if (modified) {
this.save();
}
}
save() {
if (typeof localStorage === `undefined`) return;
try {
localStorage.setItem(this.storageKey, JSON.stringify(this.data));
} catch (e) {
}
}
load() {
if (typeof localStorage === `undefined`) return;
try {
const stored = localStorage.getItem(this.storageKey);
if (stored) {
this.data = JSON.parse(stored);
}
} catch (e) {
this.data = {};
}
}
/**
* Clears all tracked up-to-date timestamps.
* Useful for testing or manual cache invalidation.
*/
clear() {
this.data = {};
if (this.pendingSaveTimer) {
clearTimeout(this.pendingSaveTimer);
this.pendingSaveTimer = void 0;
}
this.save();
}
delete(shapeKey) {
delete this.data[shapeKey];
this.save();
}
};
var upToDateTracker = new UpToDateTracker();
// src/snapshot-tracker.ts
var _SnapshotTracker_instances, detachFromReverseIndexes_fn, addToSet_fn, removeFromSet_fn, toXid8_fn, resolveLatestXid8_fn;
var SnapshotTracker = class {
constructor() {
__privateAdd(this, _SnapshotTracker_instances);
this.activeSnapshots = /* @__PURE__ */ new Map();
this.xmaxSnapshots = /* @__PURE__ */ new Map();
this.snapshotsByDatabaseLsn = /* @__PURE__ */ new Map();
}
/**
* Add a new snapshot for tracking
*/
addSnapshot(metadata, keys) {
__privateMethod(this, _SnapshotTracker_instances, detachFromReverseIndexes_fn).call(this, metadata.snapshot_mark);
const xmax = BigInt(metadata.xmax);
const databaseLsn = BigInt(metadata.database_lsn);
this.activeSnapshots.set(metadata.snapshot_mark, {
xmin: BigInt(metadata.xmin),
xmax,
xip_list: metadata.xip_list.map(BigInt),
keys,
databaseLsn
});
__privateMethod(this, _SnapshotTracker_instances, addToSet_fn).call(this, this.xmaxSnapshots, xmax, metadata.snapshot_mark);
__privateMethod(this, _SnapshotTracker_instances, addToSet_fn).call(this, this.snapshotsByDatabaseLsn, databaseLsn, metadata.snapshot_mark);
}
/**
* Remove a snapshot from tracking
*/
removeSnapshot(snapshotMark) {
__privateMethod(this, _SnapshotTracker_instances, detachFromReverseIndexes_fn).call(this, snapshotMark);
this.activeSnapshots.delete(snapshotMark);
}
/**
* Check if a change message should be filtered because its already in an active snapshot
* Returns true if the message should be filtered out (not processed)
*/
shouldRejectMessage(message) {
const txids = message.headers.txids || [];
if (txids.length === 0) return false;
for (const [xmax, snapshots] of this.xmaxSnapshots.entries()) {
const xid8 = __privateMethod(this, _SnapshotTracker_instances, resolveLatestXid8_fn).call(this, txids, xmax);
if (xid8 >= xmax) {
for (const snapshot of snapshots) {
this.removeSnapshot(snapshot);
}
}
}
return [...this.activeSnapshots.values()].some((snapshot) => {
if (!snapshot.keys.has(message.key)) return false;
const xid8 = __privateMethod(this, _SnapshotTracker_instances, resolveLatestXid8_fn).call(this, txids, snapshot.xmax);
return isVisibleInSnapshot(xid8, snapshot);
});
}
lastSeenUpdate(newDatabaseLsn) {
for (const [dbLsn, snapshots] of this.snapshotsByDatabaseLsn.entries()) {
if (dbLsn <= newDatabaseLsn) {
for (const snapshot of snapshots) {
this.removeSnapshot(snapshot);
}
}
}
}
};
_SnapshotTracker_instances = new WeakSet();
detachFromReverseIndexes_fn = function(snapshotMark) {
const existing = this.activeSnapshots.get(snapshotMark);
if (!existing) return;
__privateMethod(this, _SnapshotTracker_instances, removeFromSet_fn).call(this, this.xmaxSnapshots, existing.xmax, snapshotMark);
__privateMethod(this, _SnapshotTracker_instances, removeFromSet_fn).call(this, this.snapshotsByDatabaseLsn, existing.databaseLsn, snapshotMark);
};
addToSet_fn = function(map, key, value) {
const set = map.get(key);
if (set) {
set.add(value);
} else {
map.set(key, /* @__PURE__ */ new Set([value]));
}
};
removeFromSet_fn = function(map, key, value) {
const set = map.get(key);
if (!set) return;
set.delete(value);
if (set.size === 0) map.delete(key);
};
/**
* Resolves a 32-bit xid against an epoch-aware xid8.
*
* This signed modulo-2^32 calculation requires the reference and xid to be
* within 2^31 transactions. ShapeStream enforces that lifetime bound by
* retiring snapshots as global_last_seen_lsn passes their database_lsn.
*
* Mirrors `Electric.Postgres.Xid`
* (`packages/sync-service/lib/electric/postgres/xid.ex`).
*/
toXid8_fn = function(xid, referenceXid8) {
return referenceXid8 + BigInt.asIntN(32, BigInt(xid) - referenceXid8);
};
/**
* Resolves each contributing xid into the epoch nearest `referenceXid8` and
* returns the latest. Snapshot callers use `xmax` as the reference.
*/
resolveLatestXid8_fn = function(xids, referenceXid8) {
return xids.reduce((latest, xid) => {
const xid8 = __privateMethod(this, _SnapshotTracker_instances, toXid8_fn).call(this, xid, referenceXid8);
return xid8 > latest ? xid8 : latest;
}, BigInt(-1));
};
// src/shape-stream-state.ts
var ShapeStreamState = class {
// --- Derived booleans ---
get isUpToDate() {
return false;
}
// --- Per-state field defaults ---
get staleCacheBuster() {
return void 0;
}
get staleCacheRetryCount() {
return 0;
}
get sseFallbackToLongPolling() {
return false;
}
get consecutiveShortSseConnections() {
return 0;
}
get replayCursor() {
return void 0;
}
// --- Default no-op methods ---
canEnterReplayMode() {
return false;
}
enterReplayMode(_cursor) {
return this;
}
shouldUseSse(_opts) {
return false;
}
handleSseConnectionClosed(_input) {
return {
state: this,
fellBackToLongPolling: false,
wasShortConnection: false
};
}
// --- URL param application ---
/** Adds state-specific query parameters to the fetch URL. */
applyUrlParams(_url, _context) {
}
// --- Default response/message handlers (Paused/Error never receive these) ---
handleResponseMetadata(_input) {
return { action: `ignored`, state: this };
}
handleMessageBatch(_input) {
return { state: this, suppressUpToDate: false, becameUpToDate: false };
}
pause() {
return new PausedState(this);
}
toErrorState(error) {
return new ErrorState(this, error);
}
markMustRefetch(handle) {
return new InitialState({
handle,
offset: `-1`,
liveCacheBuster: ``,
lastSyncedAt: this.lastSyncedAt,
schema: void 0
});
}
};
var _shared;
var ActiveState = class extends ShapeStreamState {
constructor(shared) {
super();
__privateAdd(this, _shared);
__privateSet(this, _shared, shared);
}
get handle() {
return __privateGet(this, _shared).handle;
}
get offset() {
return __privateGet(this, _shared).offset;
}
get schema() {
return __privateGet(this, _shared).schema;
}
get liveCacheBuster() {
return __privateGet(this, _shared).liveCacheBuster;
}
get lastSyncedAt() {
return __privateGet(this, _shared).lastSyncedAt;
}
/** Expose shared fields to subclasses for spreading into new instances. */
get currentFields() {
return __privateGet(this, _shared);
}
// --- URL param application ---
applyUrlParams(url, _context) {
url.searchParams.set(OFFSET_QUERY_PARAM, __privateGet(this, _shared).offset);
if (__privateGet(this, _shared).handle) {
url.searchParams.set(SHAPE_HANDLE_QUERY_PARAM, __privateGet(this, _shared).handle);
}
}
// --- Helpers for subclass handleResponseMetadata implementations ---
/** Extracts updated SharedStateFields from response headers. */
parseResponseFields(input) {
var _a, _b, _c;
const responseHandle = input.responseHandle;
const handle = responseHandle && responseHandle !== input.expiredHandle ? responseHandle : __privateGet(this, _shared).handle;
const offset = (_a = input.responseOffset) != null ? _a : __privateGet(this, _shared).offset;
const liveCacheBuster = (_b = input.responseCursor) != null ? _b : __privateGet(this, _shared).liveCacheBuster;
const schema = (_c = __privateGet(this, _shared).schema) != null ? _c : input.responseSchema;
const lastSyncedAt = input.status === 204 ? input.now : __privateGet(this, _shared).lastSyncedAt;
return { handle, offset, schema, liveCacheBuster, lastSyncedAt };
}
/**
* Stale detection. Returns a transition if the response is stale,
* or null if it is not stale and the caller should proceed normally.
*/
checkStaleResponse(input) {
const responseHandle = input.responseHandle;
const expiredHandle = input.expiredHandle;
if (!responseHandle || responseHandle !== expiredHandle) {
return null;
}
const retryCount = this.staleCacheRetryCount + 1;
return {
action: `stale-retry`,
state: new StaleRetryState(__spreadProps(__spreadValues({}, this.currentFields), {
staleCacheBuster: input.createCacheBuster(),
staleCacheRetryCount: retryCount
})),
exceededMaxRetries: retryCount > input.maxStaleCacheRetries
};
}
// --- handleMessageBatch: template method with onUpToDate override point ---
handleMessageBatch(input) {
if (!input.hasMessages || !input.hasUpToDateMessage) {
return { state: this, suppressUpToDate: false, becameUpToDate: false };
}
let offset = __privateGet(this, _shared).offset;
if (input.isSse && input.upToDateOffset) {
offset = input.upToDateOffset;
}
const shared = {
handle: __privateGet(this, _shared).handle,
offset,
schema: __privateGet(this, _shared).schema,
liveCacheBuster: __privateGet(this, _shared).liveCacheBuster,
lastSyncedAt: input.now
};
return this.onUpToDate(shared, input);
}
/** Override point for up-to-date handling. Default → LiveState. */
onUpToDate(shared, _input) {
return {
state: new LiveState(shared),
suppressUpToDate: false,
becameUpToDate: true
};
}
};
_shared = new WeakMap();
var FetchingState = class extends ActiveState {
handleResponseMetadata(input) {
const staleResult = this.checkStaleResponse(input);
if (staleResult) return staleResult;
const shared = this.parseResponseFields(input);
if (input.status === 204) {
return {
action: `accepted`,
state: new LiveState(shared, { sseFallbackToLongPolling: true })
};
}
return { action: `accepted`, state: new SyncingState(shared) };
}
canEnterReplayMode() {
return true;
}
enterReplayMode(cursor) {
return new ReplayingState(__spreadProps(__spreadValues({}, this.currentFields), {
replayCursor: cursor
}));
}
};
var InitialState = class _InitialState extends FetchingState {
constructor(shared) {
super(shared);
this.kind = `initial`;
}
withHandle(handle) {
return new _InitialState(__spreadProps(__spreadValues({}, this.currentFields), { handle }));
}
};
var SyncingState = class _SyncingState extends FetchingState {
constructor(shared) {
super(shared);
this.kind = `syncing`;
}
withHandle(handle) {
return new _SyncingState(__spreadProps(__spreadValues({}, this.currentFields), { handle }));
}
};
var _staleCacheBuster, _staleCacheRetryCount;
var _StaleRetryState = class _StaleRetryState extends FetchingState {
constructor(fields) {
const _a = fields, { staleCacheBuster, staleCacheRetryCount } = _a, shared = __objRest(_a, ["staleCacheBuster", "staleCacheRetryCount"]);
super(shared);
this.kind = `stale-retry`;
__privateAdd(this, _staleCacheBuster);
__privateAdd(this, _staleCacheRetryCount);
__privateSet(this, _staleCacheBuster, staleCacheBuster);
__privateSet(this, _staleCacheRetryCount, staleCacheRetryCount);
}
get staleCacheBuster() {
return __privateGet(this, _staleCacheBuster);
}
get staleCacheRetryCount() {
return __privateGet(this, _staleCacheRetryCount);
}
// StaleRetryState must not enter replay mode — it would lose the retry count
canEnterReplayMode() {
return false;
}
withHandle(handle) {
return new _StaleRetryState(__spreadProps(__spreadValues({}, this.currentFields), {
handle,
staleCacheBuster: __privateGet(this, _staleCacheBuster),
staleCacheRetryCount: __privateGet(this, _staleCacheRetryCount)
}));
}
applyUrlParams(url, context) {
super.applyUrlParams(url, context);
url.searchParams.set(CACHE_BUSTER_QUERY_PARAM, __privateGet(this, _staleCacheBuster));
}
};
_staleCacheBuster = new WeakMap();
_staleCacheRetryCount = new WeakMap();
var StaleRetryState = _StaleRetryState;
var _consecutiveShortSseConnections, _sseFallbackToLongPolling;
var _LiveState = class _LiveState extends ActiveState {
constructor(shared, sseState) {
var _a, _b;
super(shared);
this.kind = `live`;
__privateAdd(this, _consecutiveShortSseConnections);
__privateAdd(this, _sseFallbackToLongPolling);
__privateSet(this, _consecutiveShortSseConnections, (_a = sseState == null ? void 0 : sseState.consecutiveShortSseConnections) != null ? _a : 0);
__privateSet(this, _sseFallbackToLongPolling, (_b = sseState == null ? void 0 : sseState.sseFallbackToLongPolling) != null ? _b : false);
}
get isUpToDate() {
return true;
}
get consecutiveShortSseConnections() {
return __privateGet(this, _consecutiveShortSseConnections);
}
get sseFallbackToLongPolling() {
return __privateGet(this, _sseFallbackToLongPolling);
}
withHandle(handle) {
return new _LiveState(__spreadProps(__spreadValues({}, this.currentFields), { handle }), this.sseState);
}
applyUrlParams(url, context) {
super.applyUrlParams(url, context);
if (!context.isSnapshotRequest) {
url.searchParams.set(LIVE_CACHE_BUSTER_QUERY_PARAM, this.liveCacheBuster);
if (context.canLongPoll) {
url.searchParams.set(LIVE_QUERY_PARAM, `true`);
}
}
}
get sseState() {
return {
consecutiveShortSseConnections: __privateGet(this, _consecutiveShortSseConnections),
sseFallbackToLongPolling: __privateGet(this, _sseFallbackToLongPolling)
};
}
handleResponseMetadata(input) {
const staleResult = this.checkStaleResponse(input);
if (staleResult) return staleResult;
const shared = this.parseResponseFields(input);
return {
action: `accepted`,
state: new _LiveState(shared, this.sseState)
};
}
onUpToDate(shared, _input) {
return {
state: new _LiveState(shared, this.sseState),
suppressUpToDate: false,
becameUpToDate: true
};
}
shouldUseSse(opts) {
return opts.liveSseEnabled && !opts.isRefreshing && !opts.resumingFromPause && !__privateGet(this, _sseFallbackToLongPolling);
}
handleSseConnectionClosed(input) {
let nextConsecutiveShort = __privateGet(this, _consecutiveShortSseConnections);
let nextFallback = __privateGet(this, _sseFallbackToLongPolling);
let fellBackToLongPolling = false;
let wasShortConnection = false;
if (input.connectionDuration < input.minConnectionDuration && !input.wasAborted) {
wasShortConnection = true;
nextConsecutiveShort = nextConsecutiveShort + 1;
if (nextConsecutiveShort >= input.maxShortConnections) {
nextFallback = true;
fellBackToLongPolling = true;
}
} else if (input.connectionDuration >= input.minConnectionDuration) {
nextConsecutiveShort = 0;
}
return {
state: new _LiveState(this.currentFields, {
consecutiveShortSseConnections: nextConsecutiveShort,
sseFallbackToLongPolling: nextFallback
}),
fellBackToLongPolling,
wasShortConnection
};
}
};
_consecutiveShortSseConnections = new WeakMap();
_sseFallbackToLongPolling = new WeakMap();
var LiveState = _LiveState;
var _replayCursor;
var _ReplayingState = class _ReplayingState extends ActiveState {
constructor(fields) {
const _a = fields, { replayCursor } = _a, shared = __objRest(_a, ["replayCursor"]);
super(shared);
this.kind = `replaying`;
__privateAdd(this, _replayCursor);
__privateSet(this, _replayCursor, replayCursor);
}
get replayCursor() {
return __privateGet(this, _replayCursor);
}
withHandle(handle) {
return new _ReplayingState(__spreadProps(__spreadValues({}, this.currentFields), {
handle,
replayCursor: __privateGet(this, _replayCursor)
}));
}
handleResponseMetadata(input) {
const staleResult = this.checkStaleResponse(input);
if (staleResult) return staleResult;
const shared = this.parseResponseFields(input);
return {
action: `accepted`,
state: new _ReplayingState(__spreadProps(__spreadValues({}, shared), {
replayCursor: __privateGet(this, _replayCursor)
}))
};
}
onUpToDate(shared, input) {
const suppressUpToDate = !input.isSse && __privateGet(this, _replayCursor) === input.currentCursor;
return {
state: new LiveState(shared),
suppressUpToDate,
becameUpToDate: true
};
}
};
_replayCursor = new WeakMap();
var ReplayingState = _ReplayingState;
var PausedState = class _PausedState extends ShapeStreamState {
constructor(previousState) {
super();
this.kind = `paused`;
this.previousState = previousState instanceof _PausedState ? previousState.previousState : previousState;
}
get handle() {
return this.previousState.handle;
}
get offset() {
return this.previousState.offset;
}
get schema() {
return this.previousState.schema;
}
get liveCacheBuster() {
return this.previousState.liveCacheBuster;
}
get lastSyncedAt() {
return this.previousState.lastSyncedAt;
}
get isUpToDate() {
return this.previousState.isUpToDate;
}
get staleCacheBuster() {
return this.previousState.staleCacheBuster;
}
get staleCacheRetryCount() {
return this.previousState.staleCacheRetryCount;
}
get sseFallbackToLongPolling() {
return this.previousState.sseFallbackToLongPolling;
}
get consecutiveShortSseConnections() {
return this.previousState.consecutiveShortSseConnections;
}
get replayCursor() {
return this.previousState.replayCursor;
}
handleResponseMetadata(input) {
const transition = this.previousState.handleResponseMetadata(input);
if (transition.action === `accepted`) {
return { action: `accepted`, state: new _PausedState(transition.state) };
}
if (transition.action === `ignored`) {
return { action: `ignored`, state: this };
}
if (transition.action === `stale-retry`) {
return {
action: `stale-retry`,
state: new _PausedState(transition.state),
exceededMaxRetries: transition.exceededMaxRetries
};
}
const _exhaustive = transition;
throw new Error(
`PausedState.handleResponseMetadata: unhandled transition action "${_exhaustive.action}"`
);
}
withHandle(handle) {
return new _PausedState(this.previousState.withHandle(handle));
}
applyUrlParams(url, context) {
this.previousState.applyUrlParams(url, context);
}
pause() {
return this;
}
resume() {
return this.previousState;
}
};
var ErrorState = class _ErrorState extends ShapeStreamState {
constructor(previousState, error) {
super();
this.kind = `error`;
this.previousState = previousState instanceof _ErrorState ? previousState.previousState : previousState;
this.error = error;
}
get handle() {
return this.previousState.handle;
}
get offset() {
return this.previousState.offset;
}
get schema() {
return this.previousState.schema;
}
get liveCacheBuster() {
return this.previousState.liveCacheBuster;
}
get lastSyncedAt() {
return this.previousState.lastSyncedAt;
}
get isUpToDate() {
return this.previousState.isUpToDate;
}
get staleCacheBuster() {
return this.previousState.staleCacheBuster;
}
get staleCacheRetryCount() {
return this.previousState.staleCacheRetryCount;
}
get sseFallbackToLongPolling() {
return this.previousState.sseFallbackToLongPolling;
}
get consecutiveShortSseConnections() {
return this.previousState.consecutiveShortSseConnections;
}
get replayCursor() {
return this.previousState.replayCursor;
}
withHandle(handle) {
return new _ErrorState(this.previousState.withHandle(handle), this.error);
}
applyUrlParams(url, context) {
this.previousState.applyUrlParams(url, context);
}
retry() {
return this.previousState;
}
reset(handle) {
return this.previousState.markMustRefetch(handle);
}
};
function createInitialState(opts) {
return new InitialState({
handle: opts.handle,
offset: opts.offset,
liveCacheBuster: ``,
lastSyncedAt: void 0,
schema: void 0
});
}
// src/pause-lock.ts
var _holders, _onAcquired, _onReleased;
var PauseLock = class {
constructor(callbacks) {
__privateAdd(this, _holders, /* @__PURE__ */ new Set());
__privateAdd(this, _onAcquired);
__privateAdd(this, _onReleased);
__privateSet(this, _onAcquired, callbacks.onAcquired);
__privateSet(this, _onReleased, callbacks.onReleased);
}
/**
* Acquire the lock for a given reason. Idempotent — acquiring the same
* reason twice is a no-op (but logs a warning since it likely indicates
* a caller bug).
*
* Fires `onAcquired` when the first reason is acquired (transition from
* unlocked to locked).
*/
acquire(reason) {
if (__privateGet(this, _holders).has(reason)) {
console.warn(
`[Electric] PauseLock: "${reason}" already held \u2014 ignoring duplicate acquire`
);
return;
}
const wasUnlocked = __privateGet(this, _holders).size === 0;
__privateGet(this, _holders).add(reason);
if (wasUnlocked) {
__privateGet(this, _onAcquired).call(this);
}
}
/**
* Release the lock for a given reason. Releasing a reason that isn't
* held logs a warning (likely indicates an acquire/release mismatch).
*
* Fires `onReleased` when the last reason is released (transition from
* locked to unlocked).
*/
release(reason) {
if (!__privateGet(this, _holders).delete(reason)) {
console.warn(
`[Electric] PauseLock: "${reason}" not held \u2014 ignoring release (possible acquire/release mismatch)`
);
return;
}
if (__privateGet(this, _holders).size === 0) {
__privateGet(this, _onReleased).call(this);
}
}
/**
* Whether the lock is currently held by any reason.
*/
get isPaused() {
return __privateGet(this, _holders).size > 0;
}
/**
* Check if a specific reason is holding the lock.
*/
isHeldBy(reason) {
return __privateGet(this, _holders).has(reason);
}
/**
* Release all reasons matching a prefix. Does NOT fire `onReleased` —
* this is for cleanup/reset paths where the stream state is being
* managed separately.
*
* This preserves reasons with different prefixes (e.g., 'visibility'
* is preserved when clearing 'snapshot-*' reasons).
*/
releaseAllMatching(prefix) {
for (const reason of __privateGet(this, _holders)) {
if (reason.startsWith(prefix)) {
__privateGet(this, _holders).delete(reason);
}
}
}
};
_holders = new WeakMap();
_onAcquired = new WeakMap();
_onReleased = new WeakMap();
// src/runtime-visibility.ts
var defaultRuntimeVisibilityAdapterFactory;
function getDefaultRuntimeVisibilityAdapterFactory() {
return defaultRuntimeVisibilityAdapterFactory;
}
// src/client.ts
var RESERVED_PARAMS = /* @__PURE__ */ new Set([
LIVE_CACHE_BUSTER_QUERY_PARAM,
SHAPE_HANDLE_QUERY_PARAM,
LIVE_QUERY_PARAM,
OFFSET_QUERY_PARAM,
CACHE_BUSTER_QUERY_PARAM
]);
var TROUBLESHOOTING_URL = `https://electric-sql.com/docs/guides/troubleshooting`;
function createCacheBuster() {
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
async function resolveValue(value) {
if (typeof value === `function`) {
return value();
}
return value;
}
async function toInternalParams(params) {
const entries = Object.entries(params);
const resolvedEntries = await Promise.all(
entries.map(async ([key, value]) => {
if (value === void 0) return [key, void 0];
const resolvedValue = await resolveValue(value);
return [
key,
Array.isArray(resolvedValue) ? resolvedValue.join(`,`) : resolvedValue
];
})
);
return Object.fromEntries(
resolvedEntries.filter(([_, value]) => value !== void 0)
);
}
async function resolveHeaders(headers) {
if (!headers) return {};
const entries = Object.entries(headers);
const resolvedEntries = await Promise.all(
entries.map(async ([key, value]) => [key, await resolveValue(value)])
);
return Object.fromEntries(resolvedEntries);
}
var reactNativeAppStateToVisibility = (state) => {
if (state === null) return void 0;
return state === `active` ? `visible` : `hidden`;
};
function createReactNativeRuntimeVisibilityAdapter(AppState) {
return {
getCurrentState: () => reactNativeAppStateToVisibility(AppState.currentState),
subscribe: (callback) => {
const subscription = AppState.addEventListener(`change`, (state) => {
const visibilityState = reactNativeAppStateToVisibility(state);
if (visibilityState) callback(visibilityState);
});
return () => subscription.remove();
}
};
}
function getDefaultRuntimeVisibilityAdapter() {
var _a;
return (_a = getDefaultRuntimeVisibilityAdapterFactory()) == null ? void 0 : _a();
}
function canonicalShapeKey(url) {
const cleanUrl = new URL(url.origin + url.pathname);
for (const [key, value] of url.searchParams) {
if (!ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(key)) {
cleanUrl.searchParams.append(key, value);
}
}
cleanUrl.searchParams.sort();
return cleanUrl.toString();
}
var _error, _fetchClient2, _sseFetchClient, _messageParser, _subscribers, _started, _syncState, _connected, _mode, _onError, _requestAbortController, _restartAbortControllers, _refreshCount, _refreshCatchUpWatchdogActive, _snapshotCounter, _ShapeStream_instances, isRefreshing_get, _tickPromise, _tickPromiseResolver, _tickPromiseRejecter, _messageChain, _isPublishing, _snapshotTracker, _pauseLock, _currentFetchUrl, _lastSseConnectionStartTime, _minSseConnectionDuration, _maxShortSseConnections, _sseBackoffBaseDelay, _sseBackoffMaxDelay, _liveRequestTimeoutMs, _unsubscribeFromVisibilityChanges, _unsubscribeFromWakeDetection, _maxStaleCacheRetries, _recentRequestEntries, _fastLoopWindowMs, _fastLoopThreshold, _fastLoopBackoffBaseMs, _fastLoopBackoffMaxMs, _fastLoopConsecutiveCount, _fastLoopMaxCount, _pendingRequestShapeCacheBuster, _maxSnapshotRetries, _expiredShapeRecoveryKey, _pendingSelfHealCheck, _consecutiveErrorRetries, _maxConsecutiveErrorRetries, _onErrorBackoff, start_fn, teardown_fn, backoffOnErrorRetry_fn, requestShape_fn, checkFastLoop_fn, constructUrl_fn, createAbortListener_fn, onInitialResponse_fn, onMessages_fn, fetchShape_fn, withRequestTimeout_fn, requestShapeLongPoll_fn, requestShapeSSE_fn, nextTick_fn, publish_fn, sendErrorToSubscribers_fn, hasBrowserVisibilityAPI_fn, setVisibilityPaused_fn, subscribeToVisibilityChanges_fn, forceDisconnectAndRefreshFromWake_fn, subscribeToWakeDetection_fn, reset_fn, fetchSnapshotWithRetry_fn, buildSubsetBody_fn;
var ShapeStream = class {
constructor(options) {
__privateAdd(this, _ShapeStream_instances);
__privateAdd(this, _error, null);
__privateAdd(this, _fetchClient2);
__privateAdd(this, _sseFetchClient);
__privateAdd(this, _messageParser);
__privateAdd(this, _subscribers, /* @__PURE__ */ new Map());
__privateAdd(this, _started, false);
__privateAdd(this, _syncState);
__privateAdd(this, _connected, false);
__privateAdd(this, _mode);
__privateAdd(this, _onError);
__privateAdd(this, _requestAbortController);
__privateAdd(this, _restartAbortControllers, /* @__PURE__ */ new WeakSet());
__privateAdd(this, _refreshCount, 0);
__privateAdd(this, _refreshCatchUpWatchdogActive, false);
__privateAdd(this, _snapshotCounter, 0);
__privateAdd(this, _tickPromise);
__privateAdd(this, _tickPromiseResolver);
__privateAdd(this, _tickPromiseRejecter);
__privateAdd(this, _messageChain, Promise.resolve([]));
// promise chain for incoming messages
// Tracks when subscriber callbacks are actively being delivered from
// #messageChain. requestSnapshot can inject a nested batch from inside a
// subscriber; in that reentrant case #publish uses this as an intentional
// escape hatch to deliver the nested snapshot batch immediately rather than
// queueing it behind the subscriber that is awaiting it.
__privateAdd(this, _isPublishing, false);
__privateAdd(this, _snapshotTracker, new SnapshotTracker());
__privateAdd(this, _pauseLock);
__privateAdd(this, _currentFetchUrl);
// Current fetch URL for computing shape key
__privateAdd(this, _lastSseConnectionStartTime);
__privateAdd(this, _minSseConnectionDuration, 1e3);
// Minimum expected SSE connection duration (1 second)
__privateAdd(this, _maxShortSseConnections, 3);
// Fall back to long polling after this many short connections
__privateAdd(this, _sseBackoffBaseDelay, 100);
// Base delay for exponential backoff (ms)
__privateAdd(this, _sseBackoffMaxDelay, 5e3);
// Maximum delay cap (ms)
__privateAdd(this, _liveRequestTimeoutMs);
__privateAdd(this, _unsubscribeFromVisibilityChanges);
__privateAdd(this, _unsubscribeFromWakeDetection);
__privateAdd(this, _maxStaleCacheRetries, 3);
// Fast-loop detection: track recent non-live requests to detect tight retry
// loops caused by proxy/CDN misconfiguration or stale client-side caches
__privateAdd(this, _recentRequestEntries, []);
__privateAdd(this, _fastLoopWindowMs, 500);
__privateAdd(this, _fastLoopThreshold, 5);
__privateAdd(this, _fastLoopBackoffBaseMs, 100);
__privateAdd(this, _fastLoopBackoffMaxMs, 5e3);
__privateAdd(this, _fastLoopConsecutiveCount, 0);
__privateAdd(this, _fastLoopMaxCount, 5);
__privateAdd(this, _pendingRequestShapeCacheBuster);
__privateAdd(this, _maxSnapshotRetries, 5);
__privateAdd(this, _expiredShapeRecoveryKey, null);
__privateAdd(this, _pendingSelfHealCheck, null);
__privateAdd(this, _consecutiveErrorRetries, 0);
__privateAdd(this, _maxConsecutiveErrorRetries, 50);
__privateAdd(this, _onErrorBackoff);
var _a, _b, _c, _d, _e;
this.options = __spreadValues({ subscribe: true }, options);
validateOptions(this.options);
__privateSet(this, _syncState, createInitialState({
offset: (_a = this.options.offset) != null ? _a : `-1`,
handle: this.options.handle
}));
__privateSet(this, _pauseLock, new PauseLock({
onAcquired: () => {
var _a2;
__privateSet(this, _syncState, __privateGet(this, _syncState).pause());
if (__privateGet(this, _started)) {
(_a2 = __privateGet(this, _requestAbortController)) == null ? void 0 : _a2.abort(PAUSE_STREAM);
}
},
onReleased: () => {
var _a2;
if (!__privateGet(this, _started)) return;
if ((_a2 = this.options.signal) == null ? void 0 : _a2.aborted) return;
__privateMethod(this, _ShapeStream_instances, start_fn).call(this).catch(() => {
});
}
}));
let transformer;
if (options.columnMapper) {
const applyColumnMapper = (row) => {
const result = {};
for (const [dbKey, value] of Object.entries(row)) {
const appKey = options.columnMapper.decode(dbKey);
result[appKey] = value;
}
return result;
};
transformer = options.transformer ? (row) => options.transformer(applyColumnMapper(row)) : applyColumnMapper;
} else {
transformer = options.transformer;
}
__privateSet(this, _messageParser, new MessageParser(options.parser, transformer));
__privateSet(this, _onError, this.options.onError);
__privateSet(this, _mode, (_b = this.options.log) != null ? _b : `full`);
__privateSet(this, _liveRequestTimeoutMs, (_c = this.options.liveRequestTimeoutMs) != null ? _c : 45e3);
const baseFetchClient = (_d = options.fetchClient) != null ? _d : (...args) => fetch(...args);
const backOffOpts = __spreadProps(__spreadValues({}, (_e = options.backoffOptions) != null ? _e : BackoffDefaults), {
onFailedAttempt: () => {
var _a2, _b2;
__privateSet(this, _connected, false);
(_b2 = (_a2 = options.backoffOptions) == null ? void 0 : _a2.onFailedAttempt) == null ? void 0 : _b2.call(_a2);
}
});
__privateSet(this, _onErrorBackoff, {
initialDelay: backOffOpts.initialDelay,
maxDelay: backOffOpts.maxDelay,
multiplier: backOffOpts.multiplier
});
const fetchWithBackoffClient = createFetchWithBackoff(
baseFetchClient,
backOffOpts
);
__privateSet(this, _sseFetchClient, createFetchWithResponseHeadersCheck(
createFetchWithChunkBuffer(fetchWithBackoffClient)
));
__privateSet(this, _fetchClient2, createFetchWithConsumedMessages(__privateGet(this, _sseFetchClient)));
__privateMethod(this, _ShapeStream_instances, subscribeToVisibilityChanges_fn).call(this);
}
get shapeHandle() {
return __privateGet(this, _syncState).handle;
}
get error() {
return __privateGet(this, _error);
}
get isUpToDate() {
return __privateGet(this, _syncState).isUpToDate;
}
get lastOffset() {
return __privateGet(this, _syncState).offset;
}
get mode() {
return __privateGet(this, _mode);
}
subscribe(callback, onError = () => {
}) {
const subscriptionId = {};
__privateGet(this, _subscribers).set(subscriptionId, [callback, onError]);
if (!__privateGet(this, _started)) {
__privateMethod(this, _ShapeStream_instances, start_fn).call(this).catch(() => {
});
}
return () => {
__privateGet(this, _subscribers).delete(subscriptionId);
};
}
unsubscribeAll() {
var _a, _b;
__privateGet(this, _subscribers).clear();
(_a = __privateGet(this, _unsubscribeFromVisibilityChanges)) == null ? void 0 : _a.call(this);
(_b = __privateGet(this, _unsubscribeFromWakeDetection)) == null ? void 0 : _b.call(this);
}
/** Unix time at which we last synced. Undefined until first successful up-to-date. */
lastSyncedAt() {
return __privateGet(this, _syncState).lastSyncedAt;
}
/** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */
lastSynced() {
if (__privateGet(this, _syncState).lastSyncedAt === void 0) return Infinity;
return Date.now() - __privateGet(this, _syncState).lastSyncedAt;
}
/** Indicates if we are connected to the Electric sync service. */
isConnected() {
return __privateGet(this, _connected);
}
/** True during initial fetch. False afterwards. */
isLoading() {
return !__privateGet(this, _syncState).isUpToDate;
}
hasStarted() {
return __privateGet(this, _started);
}
isPaused() {
return __privateGet(this, _pauseLock).isPaused;
}
/**
* Refreshes the shape stream.
* This preemptively aborts any ongoing long poll and reconnects without
* long polling, ensuring that the stream receives an up to date message with the
* latest LSN from Postgres at that point in time.
*/
async forceDisconnectAndRefresh() {
__privateWrapper(this, _refreshCount)._++;
__privateSet(this, _refreshCatchUpWatchdogActive, true);
try {
const requestAbortController = __privateGet(this, _requestAbortController);
if (__privateGet(this, _syncState).isUpToDate && requestAbortController && !requestAbortController.signal.aborted) {
__privateGet(this, _restartAbortControllers).add(requestAbortController);
requestAbortController.abort(FORCE_DISCONNECT_AND_REFRESH);
}
await __privateMethod(this, _ShapeStream_instances, nextTick_fn).call(this);
} finally {
__privateWrapper(this, _refreshCount)._--;
}
}
/**
* Request a snapshot for subset of data and inject it into the subscribed data stream.
*
* Only available when mode is `changes_only`.
* Returns the insertion point & the data, but more importantly injects the data
* into the subscribed data stream. Returned value is unlikely to be useful for the caller,
* unless the caller has complicated additional logic.
*
* Data will be injected in a way that's also tracking further incoming changes, and it'll
* skip the ones that are already in the snapshot.
*
* @param opts - The options for the snapshot request.
* @returns The metadata and the data for the snapshot.
*/
async requestSnapshot(opts) {
if (__privateGet(this, _mode) === `full`) {
throw new Error(
`Snapshot requests are not supported in ${__privateGet(this, _mode)} mode, as the consumer is guaranteed to observe all data`
);
}
if (!__privateGet(this, _started)) {
__privateMethod(this, _ShapeStream_instances, start_fn).call(this).catch(() => {
});
}
const snapshotReason = `snapshot-${++__privateWrapper(this, _snapshotCounter)._}`;
__privateGet(this, _pauseLock).acquire(snapshotReason);
const snapshotWarnTimer = setTimeout(() => {
console.warn(
`[Electric] Snapshot "${snapshotReason}" has held the pause lock for 30s \u2014 possible hung request or leaked lock. Current holders: ${[.../* @__PURE__ */ new Set([snapshotReason])].join(`, `)}`,
new Error(`stack trace`)
);
}, 3e4);
try {
const { metadata, data, responseOffset, responseHandle } = await this.fetchSnapshot(opts);
const dataWithEndBoundary = data.concat([
{ headers: __spreadValues({ control: `snapshot-end` }, metadata) },
{ headers: __spreadValues({ control: `subset-end` }, opts) }
]);
__privateGet(this, _snapshotTracker).addSnapshot(
metadata,
new Set(data.map((message) => message.key))
);
await __privateMethod(this, _ShapeStream_instances, onMessages_fn).call(this, dataWithEndBoundary, false, {
allowReentrantPublishBypass: true
});
if (responseOffset !== null || responseHandle !== null) {
const transition = __privateGet(this, _syncState).handleResponseMetadata({
status: 200,
responseHandle,
responseOffset,
responseCursor: null,
expiredHandle: null,
now: Date.now(),
maxStaleCacheRetries: __privateGet(this, _maxStaleCacheRetries),
createCacheBuster
});
if (transition.action === `accepted`) {
__privateSet(this, _syncState, transition.state);
} else {
console.warn(
`[Electric] Snapshot response metadata was not accepted by state "${__privateGet(this, _syncState).kind}" (action: ${transition.action}). Stream offset was not advanced from snapshot.`,
new Error(`stack trace`)
);
}
}
return {
metadata,
data
};
} finally {
clearTimeout(snapshotWarnTimer);
__privateGet(this, _pauseLock).release(snapshotReason);
}
}
/**
* Fetch a snapshot for subset of data.
* Returns the metadata and the data, but does not inject it into the subscribed data stream.
*
* By default, uses GET to send subset parameters as query parameters. This may hit URL length
* limits (HTTP 414) with large WHERE clauses or many parameters. Set `method: 'POST'` or use
* `subsetMethod: 'POST'` on the stream to send parameters in the request body instead.
*
* @param opts - The options for the snapshot request.
* @returns The metadata, data, and the response's offset/handle for state advancement.
*/
async fetchSnapshot(opts) {
return __privateMethod(this, _ShapeStream_instances, fetchSnapshotWithRetry_fn).call(this, opts, 0);
}
};
_error = new WeakMap();
_fetchClient2 = new WeakMap();
_sseFetchClient = new WeakMap();
_messageParser = new WeakMap();
_subscribers = new WeakMap();
_started = new WeakMap();
_syncState = new WeakMap();
_connected = new WeakMap();
_mode = new WeakMap();
_onError = new WeakMap();
_requestAbortController = new WeakMap();
_restartAbortControllers = new WeakMap();
_refreshCount = new WeakMap();
_refreshCatchUpWatchdogActive = new WeakMap();
_snapshotCounter = new WeakMap();
_ShapeStream_instances = new WeakSet();
isRefreshing_get = function() {
return __privateGet(this, _refreshCount) > 0;
};
_tickPromise = new WeakMap();
_tickPromiseResolver = new WeakMap();
_tickPromiseRejecter = new WeakMap();
_messageChain = new WeakMap();
_isPublishing = new WeakMap();
_snapshotTracker = new WeakMap();
_pauseLock = new WeakMap();
_currentFetchUrl = new WeakMap();
_lastSseConnectionStartTime = new WeakMap();
_minSseConnectionDuration = new WeakMap();
_maxShortSseConnections = new WeakMap();
_sseBackoffBaseDelay = new WeakMap();
_sseBackoffMaxDelay = new WeakMap();
_liveRequestTimeoutMs = new WeakMap();
_unsubscribeFromVisibilityChanges = new WeakMap();
_unsubscribeFromWakeDetection = new WeakMap();
_maxStaleCacheRetries = new WeakMap();
_recentRequestEntries = new WeakMap();
_fastLoopWindowMs = new WeakMap();
_fastLoopThreshold = new WeakMap();
_fastLoopBackoffBaseMs = new WeakMap();
_fastLoopBackoffMaxMs = new WeakMap();
_fastLoopConsecutiveCount = new WeakMap();
_fastLoopMaxCount = new WeakMap();
_pendingRequestShapeCacheBuster = new WeakMap();
_maxSnapshotRetries = new WeakMap();
_expiredShapeRecoveryKey = new WeakMap();
_pendingSelfHealCheck = new WeakMap();
_consecutiveErrorRetries = new WeakMap();
_maxConsecutiveErrorRetries = new WeakMap();
_onErrorBackoff = new WeakMap();
start_fn = async function() {
var _a, _b, _c;
__privateSet(this, _started, true);
__privateMethod(this, _ShapeStream_instances, subscribeToWakeDetection_fn).call(this);
try {
await __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
} catch (err) {
__privateSet(this, _error, err);
if (err instanceof Error) {
__privateSet(this, _syncState, __privateGet(this, _syncState).toErrorState(err));
}
if (__privateGet(this, _onError)) {
const retryOpts = await __privateGet(this, _onError).call(this, err);
const isRetryable = !(err instanceof MissingHeadersError);
if (retryOpts && typeof retryOpts === `object` && isRetryable) {
if (retryOpts.params) {
this.options.params = __spreadValues(__spreadValues({}, (_a = this.options.params) != null ? _a : {}), retryOpts.params);
}
if (retryOpts.headers) {
this.options.headers = __spreadValues(__spreadValues({}, (_b = this.options.headers) != null ? _b : {}), retryOpts.headers);
}
__privateWrapper(this, _consecutiveErrorRetries)._++;
if (__privateGet(this, _consecutiveErrorRetries) > __privateGet(this, _maxConsecutiveErrorRetries)) {
console.warn(
`[Electric] onError retry loop exhausted after ${__privateGet(this, _maxConsecutiveErrorRetries)} consecutive retries. The error was never resolved by the onError handler. Error: ${err instanceof Error ? err.message : String(err)}`,
new Error(`stack trace`)
);
if (err instanceof Error) {
__privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, err);
}
__privateMethod(this, _ShapeStream_instances, teardown_fn).call(this);
return;
}
__privateSet(this, _error, null);
if (__privateGet(this, _syncState) instanceof ErrorState) {
__privateSet(this, _syncState, __privateGet(this, _syncState).retry());
}
__privateSet(this, _fastLoopConsecutiveCount, 0);
__privateSet(this, _recentRequestEntries, []);
await __privateMethod(this, _ShapeStream_instances, backoffOnErrorRetry_fn).call(this, __privateGet(this, _consecutiveErrorRetries));
if ((_c = this.options.signal) == null ? void 0 : _c.aborted) {
__privateMethod(this, _ShapeStream_instances, teardown_fn).call(this);
return;
}
__privateSet(this, _started, false);
return __privateMethod(this, _ShapeStream_instances, start_fn).call(this);
}
if (err instanceof Error) {
__privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, err);
}
__privateMethod(this, _ShapeStream_instances, teardown_fn).call(this);
return;
}
if (err instanceof Error) {
__privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, err);
}
__privateMethod(this, _ShapeStream_instances, teardown_fn).call(this);
throw err;
}
__privateMethod(this, _ShapeStream_instances, teardown_fn).call(this);
};
teardown_fn = function() {
var _a, _b;
__privateSet(this, _connected, false);
(_a = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _a.call(this);
(_b = __privateGet(this, _unsubscribeFromWakeDetection)) == null ? void 0 : _b.call(this);
};
backoffOnErrorRetry_fn = async function(retryAttempt) {
const { initialDelay, maxDelay, multiplier } = __privateGet(this, _onErrorBackoff);
const cappedDelay = Math.min(
maxDelay,
initialDelay * Math.pow(multiplier, retryAttempt - 1)
// 1-indexed: first retry uses multiplier^0
);
const delayMs = Math.floor(Math.random() * cappedDelay);
const signal = this.options.signal;
if (delayMs <= 0 || (signal == null ? void 0 : signal.aborted)) return;
await new Promise((resolve) => {
let settled = false;
const done = () => {
if (settled) return;
settled = true;
clearTimeout(timer);
signal == null ? void 0 : signal.removeEventListener(`abort`, done);
resolve();
};
const timer = setTimeout(done, delayMs);
signal == null ? void 0 : signal.addEventListener(`abort`, done, { once: true });
if (signal == null ? void 0 : signal.aborted) done();
});
};
requestShape_fn = async function(requestShapeCacheBuster) {
var _a, _b, _c;
if ((_a = this.options.signal) == null ? void 0 : _a.aborted) {
__privateMethod(this, _ShapeStream_instances, teardown_fn).call(this);
return;
}
if (__privateGet(this, _syncState) instanceof ErrorState) {
throw __privateGet(this, _syncState).error;
}
const activeCacheBuster = requestShapeCacheBuster != null ? requestShapeCacheBuster : __privateGet(this, _pendingRequestShapeCacheBuster);
if (__privateGet(this, _pauseLock).isPaused) {
if (activeCacheBuster) {
__privateSet(this, _pendingRequestShapeCacheBuster, activeCacheBuster);
}
return;
}
if (!this.options.subscribe && (((_b = this.options.signal) == null ? void 0 : _b.aborted) || __privateGet(this, _syncState).isUpToDate)) {
return;
}
if (!__privateGet(this, _syncState).isUpToDate) {
await __privateMethod(this, _ShapeStream_instances, checkFastLoop_fn).call(this);
} else {
__privateSet(this, _fastLoopConsecutiveCount, 0);
__privateSet(this, _recentRequestEntries, []);
}
let resumingFromPause = false;
if (__privateGet(this, _syncState) instanceof PausedState) {
resumingFromPause = true;
__privateSet(this, _syncState, __privateGet(this, _syncState).resume());
}
const { url, signal } = this.options;
const { fetchUrl, requestHeaders } = await __privateMethod(this, _ShapeStream_instances, constructUrl_fn).call(this, url, resumingFromPause);
if (activeCacheBuster) {
fetchUrl.searchParams.set(CACHE_BUSTER_QUERY_PARAM, activeCacheBuster);
fetchUrl.searchParams.sort();
}
const abortListener = await __privateMethod(this, _ShapeStream_instances, createAbortListener_fn).call(this, signal);
const requestAbortController = __privateGet(this, _requestAbortController);
if (__privateGet(this, _pauseLock).isPaused) {
if (abortListener && signal) {
signal.removeEventListener(`abort`, abortListener);
}
if (activeCacheBuster) {
__privateSet(this, _pendingRequestShapeCacheBuster, activeCacheBuster);
}
__privateSet(this, _requestAbortController, void 0);
return;
}
__privateSet(this, _pendingRequestShapeCacheBuster, void 0);
try {
await __privateMethod(this, _ShapeStream_instances, fetchShape_fn).call(this, {
fetchUrl,
requestAbortController,
headers: requestHeaders,
resumingFromPause
});
} catch (e) {
const abortReason = requestAbortController.signal.reason;
const isMarkedRestartAbort = __privateGet(this, _restartAbortControllers).delete(
requestAbortController
);
const isRestartAbort = requestAbortController.signal.aborted && (isMarkedRestartAbort || abortReason === FORCE_DISCONNECT_AND_REFRESH || abortReason === SYSTEM_WAKE || abortReason === LIVE_REQUEST_TIMEOUT);
if ((e instanceof FetchError || e instanceof FetchBackoffAbortError) && isRestartAbort) {
return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
}
if (e instanceof FetchBackoffAbortError) {
return;
}
if (e instanceof StaleCacheError) {
return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
}
if (!(e instanceof FetchError)) throw e;
if (e.status == 409) {
if (__privateGet(this, _syncState).handle) {
const shapeKey = canonicalShapeKey(fetchUrl);
expiredShapesCache.markExpired(shapeKey, __privateGet(this, _syncState).handle);
}
const newShapeHandle = e.headers[SHAPE_HANDLE_HEADER];
if (!newShapeHandle) {
console.warn(
`[Electric] Received 409 response without a shape handle header. This likely indicates a proxy or CDN stripping required headers.`
);
}
const nextRequestShapeCacheBuster = createCacheBuster();
__privateMethod(this, _ShapeStream_instances, reset_fn).call(this, newShapeHandle);
await __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, [{ headers: { control: `must-refetch` } }]);
return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this, nextRequestShapeCacheBuster);
} else {
throw e;
}
} finally {
if (abortListener && signal) {
signal.removeEventListener(`abort`, abortListener);
}
__privateSet(this, _requestAbortController, void 0);
}
(_c = __privateGet(this, _tickPromiseResolver)) == null ? void 0 : _c.call(this);
return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
};
checkFastLoop_fn = async function() {
const now = Date.now();
const currentOffset = __privateGet(this, _syncState).offset;
__privateSet(this, _recentRequestEntries, __privateGet(this, _recentRequestEntries).filter(
(e) => now - e.timestamp < __privateGet(this, _fastLoopWindowMs)
));
__privateGet(this, _recentRequestEntries).push({ timestamp: now, offset: currentOffset });
const sameOffsetCount = __privateGet(this, _recentRequestEntries).filter(
(e) => e.offset === currentOffset
).length;
if (sameOffsetCount < __privateGet(this, _fastLoopThreshold)) return;
__privateWrapper(this, _fastLoopConsecutiveCount)._++;
if (__privateGet(this, _fastLoopConsecutiveCount) >= __privateGet(this, _fastLoopMaxCount)) {
throw new FetchError(
502,
void 0,
void 0,
{},
this.options.url,
`Client is stuck in a fast retry loop (${__privateGet(this, _fastLoopThreshold)} requests in ${__privateGet(this, _fastLoopWindowMs)}ms at the same offset, repeated ${__privateGet(this, _fastLoopMaxCount)} times). Client-side caches were cleared automatically on first detection, but the loop persists. This usually indicates a proxy or CDN misconfiguration. Common causes:
- Proxy is not including query parameters (handle, offset) in its cache key
- CDN is serving stale 409 responses
- Proxy is stripping required Electric headers from responses
For more information visit the troubleshooting guide: ${TROUBLESHOOTING_URL}`
);
}
if (__privateGet(this, _fastLoopConsecutiveCount) === 1) {
console.warn(
`[Electric] Detected fast retry loop (${__privateGet(this, _fastLoopThreshold)} requests in ${__privateGet(this, _fastLoopWindowMs)}ms at the same offset). Clearing client-side caches and resetting stream to recover. If this persists, check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key, and that required Electric headers are forwarded to the client. For more information visit the troubleshooting guide: ${TROUBLESHOOTING_URL}`,
new Error(`stack trace`)
);
if (__privateGet(this, _currentFetchUrl)) {
const shapeKey = canonicalShapeKey(__privateGet(this, _currentFetchUrl));
expiredShapesCache.delete(shapeKey);
upToDateTracker.delete(shapeKey);
} else {
expiredShapesCache.clear();
upToDateTracker.clear();
}
__privateMethod(this, _ShapeStream_instances, reset_fn).call(this);
__privateSet(this, _recentRequestEntries, []);
return;
}
const maxDelay = Math.min(
__privateGet(this, _fastLoopBackoffMaxMs),
__privateGet(this, _fastLoopBackoffBaseMs) * Math.pow(2, __privateGet(this, _fastLoopConsecutiveCount))
);
const delayMs = Math.floor(Math.random() * maxDelay);
await new Promise((resolve) => setTimeout(resolve, delayMs));
__privateSet(this, _recentRequestEntries, []);
};
constructUrl_fn = async function(url, resumingFromPause, subsetParams) {
var _a, _b, _c, _d, _e, _f;
const [requestHeaders, params] = await Promise.all([
resolveHeaders(this.options.headers),
this.options.params ? toInternalParams(convertWhereParamsToObj(this.options.params)) : void 0
]);
if (params) validateParams(params);
const fetchUrl = new URL(url);
if (params) {
if (params.table) setQueryParam(fetchUrl, TABLE_QUERY_PARAM, params.table);
if (params.where && typeof params.where === `string`) {
const encodedWhere = encodeWhereClause(
params.where,
(_a = this.options.columnMapper) == null ? void 0 : _a.encode
);
setQueryParam(fetchUrl, WHERE_QUERY_PARAM, encodedWhere);
}
if (params.columns) {
const originalColumns = await resolveValue((_b = this.options.params) == null ? void 0 : _b.columns);
if (Array.isArray(originalColumns)) {
let encodedColumns = originalColumns.map(String);
if (this.options.columnMapper) {
encodedColumns = encodedColumns.map(
this.options.columnMapper.encode
);
}
const serializedColumns = encodedColumns.map(quoteIdentifier).join(`,`);
setQueryParam(fetchUrl, COLUMNS_QUERY_PARAM, serializedColumns);
} else {
setQueryParam(fetchUrl, COLUMNS_QUERY_PARAM, params.columns);
}
}
if (params.replica) setQueryParam(fetchUrl, REPLICA_PARAM, params.replica);
if (params.params)
setQueryParam(fetchUrl, WHERE_PARAMS_PARAM, params.params);
const customParams = __spreadValues({}, params);
delete customParams.table;
delete customParams.where;
delete customParams.columns;
delete customParams.replica;
delete customParams.params;
for (const [key, value] of Object.entries(customParams)) {
setQueryParam(fetchUrl, key, value);
}
}
if (subsetParams) {
if (subsetParams.whereExpr) {
const compiledWhere = compileExpression(
subsetParams.whereExpr,
(_c = this.options.columnMapper) == null ? void 0 : _c.encode
);
setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, compiledWhere);
fetchUrl.searchParams.set(
SUBSET_PARAM_WHERE_EXPR,
JSON.stringify(subsetParams.whereExpr)
);
} else if (subsetParams.where && typeof subsetParams.where === `string`) {
const encodedWhere = encodeWhereClause(
subsetParams.where,
(_d = this.options.columnMapper) == null ? void 0 : _d.encode
);
setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, encodedWhere);
}
if (subsetParams.params)
fetchUrl.searchParams.set(
SUBSET_PARAM_WHERE_PARAMS,
bigintSafeStringify(subsetParams.params)
);
if (subsetParams.limit !== void 0)
setQueryParam(fetchUrl, SUBSET_PARAM_LIMIT, subsetParams.limit);
if (subsetParams.offset !== void 0)
setQueryParam(fetchUrl, SUBSET_PARAM_OFFSET, subsetParams.offset);
if (subsetParams.orderByExpr) {
const compiledOrderBy = compileOrderBy(
subsetParams.orderByExpr,
(_e = this.options.columnMapper) == null ? void 0 : _e.encode
);
setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, compiledOrderBy);
fetchUrl.searchParams.set(
SUBSET_PARAM_ORDER_BY_EXPR,
JSON.stringify(subsetParams.orderByExpr)
);
} else if (subsetParams.orderBy && typeof subsetParams.orderBy === `string`) {
const encodedOrderBy = encodeWhereClause(
subsetParams.orderBy,
(_f = this.options.columnMapper) == null ? void 0 : _f.encode
);
setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, encodedOrderBy);
}
}
__privateGet(this, _syncState).applyUrlParams(fetchUrl, {
isSnapshotRequest: subsetParams !== void 0,
// Don't long-poll when resuming from pause or refreshing — avoids
// a 20s hold during which `isConnected` would be false
canLongPoll: !__privateGet(this, _ShapeStream_instances, isRefreshing_get) && !resumingFromPause
});
fetchUrl.searchParams.set(LOG_MODE_QUERY_PARAM, __privateGet(this, _mode));
const shapeKey = canonicalShapeKey(fetchUrl);
const expiredHandle = expiredShapesCache.getExpiredHandle(shapeKey);
if (expiredHandle) {
fetchUrl.searchParams.set(EXPIRED_HANDLE_QUERY_PARAM, expiredHandle);
}
fetchUrl.searchParams.sort();
return {
fetchUrl,
requestHeaders
};
};
createAbortListener_fn = async function(signal) {
var _a;
__privateSet(this, _requestAbortController, new AbortController());
if (signal) {
const abortListener = () => {
var _a2;
(_a2 = __privateGet(this, _requestAbortController)) == null ? void 0 : _a2.abort(signal.reason);
};
signal.addEventListener(`abort`, abortListener, { once: true });
if (signal.aborted) {
(_a = __privateGet(this, _requestAbortController)) == null ? void 0 : _a.abort(signal.reason);
}
return abortListener;
}
};
onInitialResponse_fn = async function(response) {
var _a, _b, _c;
const { headers, status } = response;
const shapeHandle = headers.get(SHAPE_HANDLE_HEADER);
const shapeKey = __privateGet(this, _currentFetchUrl) ? canonicalShapeKey(__privateGet(this, _currentFetchUrl)) : null;
const expiredHandle = shapeKey ? expiredShapesCache.getExpiredHandle(shapeKey) : null;
if (__privateGet(this, _pendingSelfHealCheck)) {
const { shapeKey: healedKey, staleHandle } = __privateGet(this, _pendingSelfHealCheck);
__privateSet(this, _pendingSelfHealCheck, null);
if (shapeKey === healedKey && shapeHandle === staleHandle) {
console.warn(
`[Electric] Self-healing retry received the same handle "${staleHandle}" that was just marked expired. This means your proxy/CDN is serving a stale cached response and ignoring cache-buster query params. The client will proceed with this stale data to avoid a permanent failure, but it may be out of date until the cache refreshes. Fix: configure your proxy/CDN to include all query parameters (especially 'handle' and 'offset') in its cache key. For more information visit the troubleshooting guide: ${TROUBLESHOOTING_URL}`,
new Error(`stack trace`)
);
}
}
const transition = __privateGet(this, _syncState).handleResponseMetadata({
status,
responseHandle: shapeHandle,
responseOffset: headers.get(CHUNK_LAST_OFFSET_HEADER),
responseCursor: headers.get(LIVE_CACHE_BUSTER_HEADER),
responseSchema: getSchemaFromHeaders(headers),
expiredHandle,
now: Date.now(),
maxStaleCacheRetries: __privateGet(this, _maxStaleCacheRetries),
createCacheBuster
});
__privateSet(this, _syncState, transition.state);
if (status === 204) {
__privateSet(this, _expiredShapeRecoveryKey, null);
}
if (transition.action === `accepted` && status === 204) {
__privateSet(this, _consecutiveErrorRetries, 0);
}
if (transition.action === `stale-retry`) {
await ((_a = response.body) == null ? void 0 : _a.cancel());
if (transition.exceededMaxRetries) {
if (shapeKey) {
expiredShapesCache.delete(shapeKey);
if (__privateGet(this, _expiredShapeRecoveryKey) !== shapeKey) {
console.warn(
`[Electric] Stale cache retries exhausted (${__privateGet(this, _maxStaleCacheRetries)} attempts). Clearing expired handle entry and attempting self-healing retry without the expired_handle parameter. For more information visit the troubleshooting guide: ${TROUBLESHOOTING_URL}`,
new Error(`stack trace`)
);
__privateSet(this, _expiredShapeRecoveryKey, shapeKey);
if (shapeHandle) {
__privateSet(this, _pendingSelfHealCheck, {
shapeKey,
staleHandle: shapeHandle
});
}
__privateMethod(this, _ShapeStream_instances, reset_fn).call(this);
throw new StaleCacheError(
`Expired handle entry evicted for self-healing retry`
);
}
}
throw new FetchError(
502,
void 0,
void 0,
{},
(_c = (_b = __privateGet(this, _currentFetchUrl)) == null ? void 0 : _b.toString()) != null ? _c : ``,
`CDN continues serving stale cached responses after ${__privateGet(this, _maxStaleCacheRetries)} retry attempts. This indicates a severe proxy/CDN misconfiguration. Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key. For more information visit the troubleshooting guide: ${TROUBLESHOOTING_URL}`
);
}
console.warn(
`[Electric] Received stale cached response with expired shape handle. This should not happen and indicates a proxy/CDN caching misconfiguration. The response contained handle "${shapeHandle}" which was previously marked as expired. Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key. For more information visit the troubleshooting guide: ${TROUBLESHOOTING_URL} Retrying with a random cache buster to bypass the stale cache (attempt ${__privateGet(this, _syncState).staleCacheRetryCount}/${__privateGet(this, _maxStaleCacheRetries)}).`,
new Error(`stack trace`)
);
throw new StaleCacheError(
`Received stale cached response with expired handle "${shapeHandle}". This indicates a proxy/CDN caching misconfiguration. Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key.`
);
}
if (transition.action === `ignored`) {
console.warn(
`[Electric] Response was ignored by state "${__privateGet(this, _syncState).kind}". The response body will be skipped. This may indicate a proxy/CDN caching issue or a client state machine bug.`,
new Error(`stack trace`)
);
return false;
}
return true;
};
onMessages_fn = async function(batch, isSseMessage = false, opts = {}) {
if (!Array.isArray(batch)) {
console.warn(
`[Electric] #onMessages called with non-array argument (${typeof batch}). This is a client bug \u2014 please report it.`,
new Error(`stack trace`)
);
return;
}
if (batch.length === 0) return;
__privateSet(this, _consecutiveErrorRetries, 0);
const lastMessage = batch[batch.length - 1];
const hasUpToDateMessage = isUpToDateMessage(lastMessage);
const upToDateOffset = hasUpToDateMessage ? getOffset(lastMessage) : void 0;
const transition = __privateGet(this, _syncState).handleMessageBatch({
hasMessages: true,
hasUpToDateMessage,
isSse: isSseMessage,
upToDateOffset,
now: Date.now(),
currentCursor: __privateGet(this, _syncState).liveCacheBuster
});
__privateSet(this, _syncState, transition.state);
if (hasUpToDateMessage) {
__privateSet(this, _refreshCatchUpWatchdogActive, false);
if (!transition.suppressUpToDate && __privateGet(this, _currentFetchUrl)) {
const shapeKey = canonicalShapeKey(__privateGet(this, _currentFetchUrl));
upToDateTracker.recordUpToDate(
shapeKey,
__privateGet(this, _syncState).liveCacheBuster
);
__privateSet(this, _expiredShapeRecoveryKey, null);
}
}
const messagesToProcess = batch.filter((message) => {
if (isChangeMessage(message)) {
const changeLsn = message.headers.lsn;
if (typeof changeLsn === `string` && changeLsn) {
__privateGet(this, _snapshotTracker).lastSeenUpdate(BigInt(changeLsn));
}
return !__privateGet(this, _snapshotTracker).shouldRejectMessage(message);
}
if (isUpToDateMessage(message)) {
const lastSeenLsn = message.headers.global_last_seen_lsn;
if (typeof lastSeenLsn === `string` && lastSeenLsn) {
__privateGet(this, _snapshotTracker).lastSeenUpdate(BigInt(lastSeenLsn));
}
if (transition.suppressUpToDate) return false;
}
return true;
});
if (messagesToProcess.length === 0 && transition.suppressUpToDate) return;
await __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, messagesToProcess, {
allowReentrantBypass: opts.allowReentrantPublishBypass
});
};
fetchShape_fn = async function(opts) {
var _a;
__privateSet(this, _currentFetchUrl, opts.fetchUrl);
if (!__privateGet(this, _syncState).isUpToDate && __privateGet(this, _syncState).canEnterReplayMode()) {
const shapeKey = canonicalShapeKey(opts.fetchUrl);
const lastSeenCursor = upToDateTracker.shouldEnterReplayMode(shapeKey);
if (lastSeenCursor) {
__privateSet(this, _syncState, __privateGet(this, _syncState).enterReplayMode(lastSeenCursor));
}
}
const useSse = (_a = this.options.liveSse) != null ? _a : this.options.experimentalLiveSse;
if (__privateGet(this, _syncState).shouldUseSse({
liveSseEnabled: !!useSse,
isRefreshing: __privateGet(this, _ShapeStream_instances, isRefreshing_get),
resumingFromPause: !!opts.resumingFromPause
})) {
opts.fetchUrl.searchParams.set(EXPERIMENTAL_LIVE_SSE_QUERY_PARAM, `true`);
opts.fetchUrl.searchParams.set(LIVE_SSE_QUERY_PARAM, `true`);
return __privateMethod(this, _ShapeStream_instances, requestShapeSSE_fn).call(this, opts);
}
return __privateMethod(this, _ShapeStream_instances, requestShapeLongPoll_fn).call(this, opts);
};
withRequestTimeout_fn = async function(promise, requestAbortController, fetchUrl) {
const timeoutMs = __privateGet(this, _liveRequestTimeoutMs);
const isLiveRequest = fetchUrl.searchParams.get(LIVE_QUERY_PARAM) === `true`;
const isRefreshCatchUpRequest = __privateGet(this, _ShapeStream_instances, isRefreshing_get) || __privateGet(this, _refreshCatchUpWatchdogActive);
if (timeoutMs === false || !isLiveRequest && !isRefreshCatchUpRequest) {
return promise;
}
let timeout;
const timeoutPromise = new Promise((_, reject) => {
timeout = setTimeout(() => {
if (!requestAbortController.signal.aborted) {
__privateGet(this, _restartAbortControllers).add(requestAbortController);
requestAbortController.abort(LIVE_REQUEST_TIMEOUT);
}
reject(new FetchBackoffAbortError());
}, timeoutMs);
});
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
if (timeout) clearTimeout(timeout);
}
};
requestShapeLongPoll_fn = async function(opts) {
var _a;
const { fetchUrl, requestAbortController, headers } = opts;
const fetchUrlString = fetchUrl.toString();
const rawResponse = await __privateMethod(this, _ShapeStream_instances, withRequestTimeout_fn).call(this, __privateGet(this, _sseFetchClient).call(this, fetchUrlString, {
signal: requestAbortController.signal,
headers
}), requestAbortController, fetchUrl);
const response = await consumeResponseBody(
rawResponse,
fetchUrlString,
requestAbortController.signal
);
__privateSet(this, _connected, true);
const shouldProcessBody = await __privateMethod(this, _ShapeStream_instances, onInitialResponse_fn).call(this, response);
if (!shouldProcessBody) return;
const schema = __privateGet(this, _syncState).schema;
const res = await response.text();
const messages = res || `[]`;
const batch = __privateGet(this, _messageParser).parse(messages, schema);
if (!Array.isArray(batch)) {
const preview = (_a = bigintSafeStringify(batch)) == null ? void 0 : _a.slice(0, 200);
throw new FetchError(
response.status,
`Received non-array response body from shape endpoint. This may indicate a proxy or CDN is returning an unexpected response. Expected a JSON array, got ${typeof batch}: ${preview}`,
void 0,
Object.fromEntries(response.headers.entries()),
fetchUrl.toString()
);
}
await __privateMethod(this, _ShapeStream_instances, onMessages_fn).call(this, batch);
};
requestShapeSSE_fn = async function(opts) {
const { fetchUrl, requestAbortController, headers } = opts;
const fetch2 = __privateGet(this, _sseFetchClient);
__privateSet(this, _lastSseConnectionStartTime, Date.now());
const sseHeaders = __spreadProps(__spreadValues({}, headers), {
Accept: `text/event-stream`
});
let ignoredStaleResponse = false;
try {
let buffer = [];
await fetchEventSource(fetchUrl.toString(), {
headers: sseHeaders,
fetch: fetch2,
onopen: async (response) => {
__privateSet(this, _connected, true);
const shouldProcessBody = await __privateMethod(this, _ShapeStream_instances, onInitialResponse_fn).call(this, response);
if (!shouldProcessBody) {
ignoredStaleResponse = true;
throw new Error(`stale response ignored`);
}
},
onmessage: (event) => {
if (event.data) {
const schema = __privateGet(this, _syncState).schema;
const message = __privateGet(this, _messageParser).parse(
event.data,
schema
);
buffer.push(message);
if (isUpToDateMessage(message)) {
__privateMethod(this, _ShapeStream_instances, onMessages_fn).call(this, buffer, true);
buffer = [];
}
}
},
onerror: (error) => {
throw error;
},
signal: requestAbortController.signal
});
} catch (error) {
if (ignoredStaleResponse) {
return;
}
if (requestAbortController.signal.aborted) {
throw new FetchBackoffAbortError();
}
if (error instanceof FetchError || error instanceof StaleCacheError || error instanceof MissingHeadersError) {
throw error;
}
} finally {
const connectionDuration = Date.now() - __privateGet(this, _lastSseConnectionStartTime);
const wasAborted = requestAbortController.signal.aborted;
const transition = __privateGet(this, _syncState).handleSseConnectionClosed({
connectionDuration,
wasAborted,
minConnectionDuration: __privateGet(this, _minSseConnectionDuration),
maxShortConnections: __privateGet(this, _maxShortSseConnections)
});
__privateSet(this, _syncState, transition.state);
if (transition.fellBackToLongPolling) {
console.warn(
`[Electric] SSE connections are closing immediately (possibly due to proxy buffering or misconfiguration). Falling back to long polling. Your proxy must support streaming SSE responses (not buffer the complete response). Configuration: Nginx add 'X-Accel-Buffering: no', Caddy add 'flush_interval -1' to reverse_proxy. Note: Do NOT disable caching entirely - Electric uses cache headers to enable request collapsing for efficiency.`,
new Error(`stack trace`)
);
} else if (transition.wasShortConnection) {
const maxDelay = Math.min(
__privateGet(this, _sseBackoffMaxDelay),
__privateGet(this, _sseBackoffBaseDelay) * Math.pow(2, __privateGet(this, _syncState).consecutiveShortSseConnections)
);
const delayMs = Math.floor(Math.random() * maxDelay);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
};
nextTick_fn = async function() {
if (__privateGet(this, _pauseLock).isPaused) {
throw new Error(
`Cannot wait for next tick while PauseLock is held \u2014 this would deadlock because the request loop is paused`
);
}
if (__privateGet(this, _tickPromise)) {
return __privateGet(this, _tickPromise);
}
__privateSet(this, _tickPromise, new Promise((resolve, reject) => {
__privateSet(this, _tickPromiseResolver, resolve);
__privateSet(this, _tickPromiseRejecter, reject);
}));
__privateGet(this, _tickPromise).finally(() => {
__privateSet(this, _tickPromise, void 0);
__privateSet(this, _tickPromiseResolver, void 0);
__privateSet(this, _tickPromiseRejecter, void 0);
}).catch(() => {
});
return __privateGet(this, _tickPromise);
};
publish_fn = async function(messages, opts = {}) {
const deliver = () => Promise.all(
Array.from(__privateGet(this, _subscribers).values()).map(async ([callback, __]) => {
try {
await callback(messages);
} catch (err) {
queueMicrotask(() => {
throw err;
});
}
})
);
if (__privateGet(this, _isPublishing) && opts.allowReentrantBypass) {
return deliver();
}
__privateSet(this, _messageChain, __privateGet(this, _messageChain).then(async () => {
__privateSet(this, _isPublishing, true);
try {
return await deliver();
} finally {
__privateSet(this, _isPublishing, false);
}
}));
return __privateGet(this, _messageChain);
};
sendErrorToSubscribers_fn = function(error) {
__privateGet(this, _subscribers).forEach(([_, errorFn]) => {
errorFn == null ? void 0 : errorFn(error);
});
};
hasBrowserVisibilityAPI_fn = function() {
return typeof document === `object` && typeof document.hidden === `boolean` && typeof document.addEventListener === `function`;
};
setVisibilityPaused_fn = function(isHidden) {
if (isHidden) {
__privateGet(this, _pauseLock).acquire(`visibility`);
} else if (__privateGet(this, _pauseLock).isHeldBy(`visibility`)) {
__privateGet(this, _pauseLock).release(`visibility`);
}
};
subscribeToVisibilityChanges_fn = function() {
var _a, _b;
const runtimeVisibility = (_a = this.options.runtimeVisibility) != null ? _a : getDefaultRuntimeVisibilityAdapter();
if (runtimeVisibility) {
__privateMethod(this, _ShapeStream_instances, setVisibilityPaused_fn).call(this, ((_b = runtimeVisibility.getCurrentState) == null ? void 0 : _b.call(runtimeVisibility)) === `hidden`);
const unsubscribe = runtimeVisibility.subscribe((state) => {
__privateMethod(this, _ShapeStream_instances, setVisibilityPaused_fn).call(this, state === `hidden`);
});
__privateSet(this, _unsubscribeFromVisibilityChanges, () => {
unsubscribe();
__privateSet(this, _unsubscribeFromVisibilityChanges, void 0);
});
return;
}
if (__privateMethod(this, _ShapeStream_instances, hasBrowserVisibilityAPI_fn).call(this)) {
const visibilityHandler = () => {
__privateMethod(this, _ShapeStream_instances, setVisibilityPaused_fn).call(this, document.hidden);
};
visibilityHandler();
document.addEventListener(`visibilitychange`, visibilityHandler);
__privateSet(this, _unsubscribeFromVisibilityChanges, () => {
document.removeEventListener(`visibilitychange`, visibilityHandler);
__privateSet(this, _unsubscribeFromVisibilityChanges, void 0);
});
}
};
/**
* Detects system wake from sleep using timer gap detection.
* When the system sleeps, setInterval timers are paused. On wake,
* the elapsed wall-clock time since the last tick will be much larger
* than the interval period, indicating the system was asleep.
*
* Only active in non-browser environments (Bun, Node.js) where
* `document.visibilitychange` is not available. In browsers,
* `#subscribeToVisibilityChanges` handles this instead. Without wake
* detection, in-flight HTTP requests (long-poll or SSE) may hang until
* the OS TCP timeout.
*/
forceDisconnectAndRefreshFromWake_fn = function() {
var _a;
const requestAbortController = __privateGet(this, _requestAbortController);
if (__privateGet(this, _pauseLock).isPaused || !requestAbortController || requestAbortController.signal.aborted || ((_a = this.options.signal) == null ? void 0 : _a.aborted)) {
return;
}
__privateWrapper(this, _refreshCount)._++;
__privateSet(this, _refreshCatchUpWatchdogActive, true);
__privateGet(this, _restartAbortControllers).add(requestAbortController);
requestAbortController.abort(SYSTEM_WAKE);
__privateMethod(this, _ShapeStream_instances, nextTick_fn).call(this).catch(() => {
}).finally(() => {
__privateWrapper(this, _refreshCount)._--;
});
};
subscribeToWakeDetection_fn = function() {
if (__privateMethod(this, _ShapeStream_instances, hasBrowserVisibilityAPI_fn).call(this)) return;
if (__privateGet(this, _unsubscribeFromWakeDetection)) return;
const INTERVAL_MS = 2e3;
const WAKE_THRESHOLD_MS = 4e3;
let lastTickTime = Date.now();
const timer = setInterval(() => {
const now = Date.now();
const elapsed = now - lastTickTime;
lastTickTime = now;
if (elapsed > INTERVAL_MS + WAKE_THRESHOLD_MS) {
__privateMethod(this, _ShapeStream_instances, forceDisconnectAndRefreshFromWake_fn).call(this);
}
}, INTERVAL_MS);
if (typeof timer === `object` && `unref` in timer) {
timer.unref();
}
__privateSet(this, _unsubscribeFromWakeDetection, () => {
clearInterval(timer);
__privateSet(this, _unsubscribeFromWakeDetection, void 0);
});
};
/**
* Resets the state of the stream, optionally with a provided
* shape handle
*/
reset_fn = function(handle) {
__privateSet(this, _syncState, __privateGet(this, _syncState).markMustRefetch(handle));
__privateSet(this, _connected, false);
__privateGet(this, _pauseLock).releaseAllMatching(`snapshot`);
};
fetchSnapshotWithRetry_fn = async function(opts, retryCount, cacheBuster) {
var _a, _b, _c;
const method = (_b = (_a = opts.method) != null ? _a : this.options.subsetMethod) != null ? _b : `GET`;
const usePost = method === `POST`;
let fetchUrl;
let fetchOptions;
if (usePost) {
const result = await __privateMethod(this, _ShapeStream_instances, constructUrl_fn).call(this, this.options.url, true);
fetchUrl = result.fetchUrl;
fetchOptions = {
method: `POST`,
headers: __spreadProps(__spreadValues({}, result.requestHeaders), {
"Content-Type": `application/json`
}),
body: bigintSafeStringify(__privateMethod(this, _ShapeStream_instances, buildSubsetBody_fn).call(this, opts))
};
} else {
const result = await __privateMethod(this, _ShapeStream_instances, constructUrl_fn).call(this, this.options.url, true, opts);
fetchUrl = result.fetchUrl;
fetchOptions = { headers: result.requestHeaders };
}
if (cacheBuster) {
fetchUrl.searchParams.set(CACHE_BUSTER_QUERY_PARAM, cacheBuster);
fetchUrl.searchParams.sort();
}
const usedHandle = __privateGet(this, _syncState).handle;
let response;
try {
response = await __privateGet(this, _fetchClient2).call(this, fetchUrl.toString(), fetchOptions);
} catch (e) {
if (e instanceof FetchError && e.status === 409) {
const nextRetryCount = retryCount + 1;
if (nextRetryCount > __privateGet(this, _maxSnapshotRetries)) {
throw new FetchError(
502,
void 0,
void 0,
{},
fetchUrl.toString(),
`Snapshot request stuck in 409 retry loop after ${__privateGet(this, _maxSnapshotRetries)} attempts. This indicates a proxy/CDN misconfiguration. For more information visit the troubleshooting guide: ${TROUBLESHOOTING_URL}`
);
}
if (usedHandle) {
const shapeKey = canonicalShapeKey(fetchUrl);
expiredShapesCache.markExpired(shapeKey, usedHandle);
}
const nextHandle = e.headers[SHAPE_HANDLE_HEADER];
if (nextHandle) {
__privateSet(this, _syncState, __privateGet(this, _syncState).withHandle(nextHandle));
} else {
console.warn(
`[Electric] Received 409 response without a shape handle header. This likely indicates a proxy or CDN stripping required headers.`
);
}
const nextCacheBuster = createCacheBuster();
return __privateMethod(this, _ShapeStream_instances, fetchSnapshotWithRetry_fn).call(this, opts, nextRetryCount, nextCacheBuster);
}
throw e;
}
if (!response.ok) {
throw await FetchError.fromResponse(response, fetchUrl.toString());
}
const schema = (_c = __privateGet(this, _syncState).schema) != null ? _c : getSchemaFromHeaders(response.headers, {
required: true,
url: fetchUrl.toString()
});
const { metadata, data: rawData } = await response.json();
const data = __privateGet(this, _messageParser).parseSnapshotData(
rawData,
schema
);
const responseOffset = response.headers.get(CHUNK_LAST_OFFSET_HEADER) || null;
const responseHandle = response.headers.get(SHAPE_HANDLE_HEADER);
return { metadata, data, responseOffset, responseHandle };
};
buildSubsetBody_fn = function(opts) {
var _a, _b, _c, _d;
const body = {};
if (opts.whereExpr) {
body.where = compileExpression(
opts.whereExpr,
(_a = this.options.columnMapper) == null ? void 0 : _a.encode
);
body.where_expr = opts.whereExpr;
} else if (opts.where && typeof opts.where === `string`) {
body.where = encodeWhereClause(
opts.where,
(_b = this.options.columnMapper) == null ? void 0 : _b.encode
);
}
if (opts.params) {
body.params = opts.params;
}
if (opts.limit !== void 0) {
body.limit = opts.limit;
}
if (opts.offset !== void 0) {
body.offset = opts.offset;
}
if (opts.orderByExpr) {
body.order_by = compileOrderBy(
opts.orderByExpr,
(_c = this.options.columnMapper) == null ? void 0 : _c.encode
);
body.order_by_expr = opts.orderByExpr;
} else if (opts.orderBy && typeof opts.orderBy === `string`) {
body.order_by = encodeWhereClause(
opts.orderBy,
(_d = this.options.columnMapper) == null ? void 0 : _d.encode
);
}
return body;
};
ShapeStream.Replica = {
FULL: `full`,
DEFAULT: `default`
};
function getSchemaFromHeaders(headers, options) {
const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER);
if (!schemaHeader) {
if ((options == null ? void 0 : options.required) && (options == null ? void 0 : options.url)) {
throw new MissingHeadersError(options.url, [SHAPE_SCHEMA_HEADER]);
}
return {};
}
return JSON.parse(schemaHeader);
}
function validateParams(params) {
if (!params) return;
const reservedParams = Object.keys(params).filter(
(key) => RESERVED_PARAMS.has(key)
);
if (reservedParams.length > 0) {
throw new ReservedParamError(reservedParams);
}
}
var didWarnOnHttp = false;
function getNodeEnvSafely() {
var _a;
return typeof process !== `undefined` ? (_a = process.env) == null ? void 0 : _a.NODE_ENV : void 0;
}
function resolveUrlMaybe(url, base) {
try {
return new URL(url, base);
} catch (e) {
return void 0;
}
}
function isBrowserEnvironment() {
return typeof window !== `undefined`;
}
function getWindowLocationHref() {
if (isBrowserEnvironment() && typeof window.location !== `undefined`) {
return window.location.href;
}
return void 0;
}
function validateOptions(options) {
var _a;
if (!options.url) {
throw new MissingShapeUrlError();
}
if (options.signal && !(options.signal instanceof AbortSignal)) {
throw new InvalidSignalError();
}
if (options.liveRequestTimeoutMs !== void 0 && options.liveRequestTimeoutMs !== false && (!Number.isFinite(options.liveRequestTimeoutMs) || options.liveRequestTimeoutMs <= 0)) {
throw new InvalidShapeOptionsError(
`Invalid shape options: liveRequestTimeoutMs must be a positive finite number or false`
);
}
if (options.offset !== void 0 && options.offset !== `-1` && options.offset !== `now` && !options.handle) {
throw new MissingShapeHandleError();
}
validateParams(options.params);
const nodeEnv = getNodeEnvSafely();
const warnOnHttp = (_a = options.warnOnHttp) != null ? _a : nodeEnv !== `test`;
if (warnOnHttp && !didWarnOnHttp && isBrowserEnvironment()) {
if (typeof console !== `undefined`) {
const baseUrl = getWindowLocationHref();
const resolvedUrl = resolveUrlMaybe(options.url, baseUrl);
const isHttp = (resolvedUrl == null ? void 0 : resolvedUrl.protocol) === `http:`;
if (isHttp) {
didWarnOnHttp = true;
console.warn(
`[Electric] Using HTTP (not HTTPS) typically limits browsers to ~6 concurrent connections per origin under HTTP/1.1. This can cause slow shapes and app freezes with multiple shapes. Use HTTPS for HTTP/2 support. See: https://electric-sql.com/r/electric-http2`
);
}
}
}
return;
}
function _resetHttpWarningForTesting() {
didWarnOnHttp = false;
}
function setQueryParam(url, key, value) {
if (value === void 0 || value == null) {
return;
} else if (typeof value === `string`) {
url.searchParams.set(key, value);
} else if (typeof value === `object`) {
for (const [k, v] of Object.entries(value)) {
url.searchParams.set(`${key}[${k}]`, v);
}
} else {
url.searchParams.set(key, value.toString());
}
}
function convertWhereParamsToObj(allPgParams) {
if (Array.isArray(allPgParams.params)) {
return __spreadProps(__spreadValues({}, allPgParams), {
params: Object.fromEntries(allPgParams.params.map((v, i) => [i + 1, v]))
});
}
return allPgParams;
}
// src/shape.ts
var _data, _subscribers2, _insertedKeys, _requestedSubSnapshots, _reexecuteSnapshotsPending, _status, _error2, _Shape_instances, process_fn, reexecuteSnapshots_fn, surfaceReexecuteError_fn, awaitUpToDate_fn, updateShapeStatus_fn, handleError_fn, notify_fn;
var Shape = class {
constructor(stream) {
__privateAdd(this, _Shape_instances);
__privateAdd(this, _data, /* @__PURE__ */ new Map());
__privateAdd(this, _subscribers2, /* @__PURE__ */ new Map());
__privateAdd(this, _insertedKeys, /* @__PURE__ */ new Set());
__privateAdd(this, _requestedSubSnapshots, /* @__PURE__ */ new Set());
__privateAdd(this, _reexecuteSnapshotsPending, false);
__privateAdd(this, _status, `syncing`);
__privateAdd(this, _error2, false);
this.stream = stream;
this.stream.subscribe(
__privateMethod(this, _Shape_instances, process_fn).bind(this),
__privateMethod(this, _Shape_instances, handleError_fn).bind(this)
);
}
get isUpToDate() {
return __privateGet(this, _status) === `up-to-date`;
}
get lastOffset() {
return this.stream.lastOffset;
}
get handle() {
return this.stream.shapeHandle;
}
get rows() {
return this.value.then((v) => Array.from(v.values()));
}
get currentRows() {
return Array.from(this.currentValue.values());
}
get value() {
return new Promise((resolve, reject) => {
if (this.stream.isUpToDate) {
resolve(this.currentValue);
} else {
const unsubscribe = this.subscribe(({ value }) => {
unsubscribe();
if (__privateGet(this, _error2)) reject(__privateGet(this, _error2));
resolve(value);
});
}
});
}
get currentValue() {
return __privateGet(this, _data);
}
get error() {
return __privateGet(this, _error2);
}
/** Unix time at which we last synced. Undefined when `isLoading` is true. */
lastSyncedAt() {
return this.stream.lastSyncedAt();
}
/** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */
lastSynced() {
return this.stream.lastSynced();
}
/** True during initial fetch. False afterwise. */
isLoading() {
return this.stream.isLoading();
}
/** Indicates if we are connected to the Electric sync service. */
isConnected() {
return this.stream.isConnected();
}
/** Current log mode of the underlying stream */
get mode() {
return this.stream.mode;
}
/**
* Request a snapshot for subset of data. Only available when mode is changes_only.
* Returns void; data will be emitted via the stream and processed by this Shape.
*/
async requestSnapshot(params) {
const key = canonicalBigintSafeStringify(params);
__privateGet(this, _requestedSubSnapshots).add(key);
await __privateMethod(this, _Shape_instances, awaitUpToDate_fn).call(this);
await this.stream.requestSnapshot(params);
}
subscribe(callback) {
const subscriptionId = {};
__privateGet(this, _subscribers2).set(subscriptionId, callback);
return () => {
__privateGet(this, _subscribers2).delete(subscriptionId);
};
}
unsubscribeAll() {
__privateGet(this, _subscribers2).clear();
}
get numSubscribers() {
return __privateGet(this, _subscribers2).size;
}
};
_data = new WeakMap();
_subscribers2 = new WeakMap();
_insertedKeys = new WeakMap();
_requestedSubSnapshots = new WeakMap();
_reexecuteSnapshotsPending = new WeakMap();
_status = new WeakMap();
_error2 = new WeakMap();
_Shape_instances = new WeakSet();
process_fn = function(messages) {
let shouldNotify = false;
messages.forEach((message) => {
if (isChangeMessage(message)) {
const wasUpToDate = __privateGet(this, _status) === `up-to-date`;
__privateMethod(this, _Shape_instances, updateShapeStatus_fn).call(this, `syncing`);
if (this.mode === `full`) {
switch (message.headers.operation) {
case `insert`:
__privateGet(this, _data).set(message.key, message.value);
if (wasUpToDate) shouldNotify = true;
break;
case `update`:
__privateGet(this, _data).set(message.key, __spreadValues(__spreadValues({}, __privateGet(this, _data).get(message.key)), message.value));
if (wasUpToDate) shouldNotify = true;
break;
case `delete`:
__privateGet(this, _data).delete(message.key);
if (wasUpToDate) shouldNotify = true;
break;
}
} else {
switch (message.headers.operation) {
case `insert`:
__privateGet(this, _insertedKeys).add(message.key);
__privateGet(this, _data).set(message.key, message.value);
if (wasUpToDate) shouldNotify = true;
break;
case `update`:
if (__privateGet(this, _insertedKeys).has(message.key)) {
__privateGet(this, _data).set(message.key, __spreadValues(__spreadValues({}, __privateGet(this, _data).get(message.key)), message.value));
if (wasUpToDate) shouldNotify = true;
}
break;
case `delete`:
if (__privateGet(this, _insertedKeys).has(message.key)) {
__privateGet(this, _data).delete(message.key);
__privateGet(this, _insertedKeys).delete(message.key);
if (wasUpToDate) shouldNotify = true;
}
break;
}
}
}
if (isControlMessage(message)) {
switch (message.headers.control) {
case `up-to-date`:
if (__privateMethod(this, _Shape_instances, updateShapeStatus_fn).call(this, `up-to-date`)) shouldNotify = true;
if (__privateGet(this, _reexecuteSnapshotsPending)) {
__privateSet(this, _reexecuteSnapshotsPending, false);
void __privateMethod(this, _Shape_instances, reexecuteSnapshots_fn).call(this);
}
break;
case `must-refetch`:
__privateGet(this, _data).clear();
__privateGet(this, _insertedKeys).clear();
__privateSet(this, _error2, false);
__privateMethod(this, _Shape_instances, updateShapeStatus_fn).call(this, `syncing`);
__privateSet(this, _reexecuteSnapshotsPending, true);
break;
}
}
});
if (shouldNotify) __privateMethod(this, _Shape_instances, notify_fn).call(this);
};
reexecuteSnapshots_fn = async function() {
try {
await __privateMethod(this, _Shape_instances, awaitUpToDate_fn).call(this);
} catch (e) {
__privateMethod(this, _Shape_instances, surfaceReexecuteError_fn).call(this, e);
return;
}
const results = await Promise.all(
Array.from(__privateGet(this, _requestedSubSnapshots)).map(async (jsonParams) => {
try {
const snapshot = JSON.parse(jsonParams);
await this.stream.requestSnapshot(snapshot);
return void 0;
} catch (e) {
return e;
}
})
);
const firstError = results.find((e) => e !== void 0);
if (firstError !== void 0) __privateMethod(this, _Shape_instances, surfaceReexecuteError_fn).call(this, firstError);
};
surfaceReexecuteError_fn = function(e) {
if (e instanceof FetchError) {
__privateSet(this, _error2, e);
} else if (e instanceof Error) {
__privateSet(this, _error2, new FetchError(0, e.message, void 0, {}, ``, e.message));
} else {
__privateSet(this, _error2, new FetchError(0, String(e), void 0, {}, ``, String(e)));
}
__privateMethod(this, _Shape_instances, notify_fn).call(this);
};
awaitUpToDate_fn = async function() {
if (__privateGet(this, _error2)) throw __privateGet(this, _error2);
if (this.stream.isUpToDate) return;
if (this.stream.error) throw this.stream.error;
await new Promise((resolve, reject) => {
let settled = false;
let interval;
let unsub;
const done = (action) => {
if (settled) return;
settled = true;
clearInterval(interval);
unsub == null ? void 0 : unsub();
action();
};
const check = () => {
if (this.stream.isUpToDate) return done(resolve);
const streamError = this.stream.error;
if (streamError) return done(() => reject(streamError));
if (__privateGet(this, _error2)) {
const err = __privateGet(this, _error2);
return done(() => reject(err));
}
};
interval = setInterval(check, 10);
unsub = this.stream.subscribe(
() => check(),
(err) => done(() => reject(err))
);
check();
});
};
updateShapeStatus_fn = function(status) {
const stateChanged = __privateGet(this, _status) !== status;
__privateSet(this, _status, status);
return stateChanged && status === `up-to-date`;
};
handleError_fn = function(e) {
if (e instanceof FetchError) {
__privateSet(this, _error2, e);
__privateMethod(this, _Shape_instances, notify_fn).call(this);
}
};
notify_fn = function() {
__privateGet(this, _subscribers2).forEach((callback) => {
callback({ value: this.currentValue, rows: this.currentRows });
});
};
export {
BackoffDefaults,
ELECTRIC_PROTOCOL_QUERY_PARAMS,
FetchBackoffAbortError,
FetchError,
Shape,
ShapeStream,
_resetHttpWarningForTesting,
camelToSnake,
canonicalShapeKey,
compileExpression,
compileOrderBy,
createColumnMapper,
createReactNativeRuntimeVisibilityAdapter,
isChangeMessage,
isControlMessage,
isVisibleInSnapshot,
resolveValue,
snakeCamelMapper,
snakeToCamel
};
//# sourceMappingURL=index.legacy-esm.js.map