UNPKG

@electric-sql/client

Version:

Postgres everywhere - your data, in sync, wherever you need it.

1,539 lines (1,525 loc) 120 kB
"use strict"; var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropNames = Object.getOwnPropertyNames; 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 __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); 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/index.ts var src_exports = {}; __export(src_exports, { BackoffDefaults: () => BackoffDefaults, ELECTRIC_PROTOCOL_QUERY_PARAMS: () => ELECTRIC_PROTOCOL_QUERY_PARAMS, FetchError: () => FetchError, Shape: () => Shape, ShapeStream: () => ShapeStream, camelToSnake: () => camelToSnake, compileExpression: () => compileExpression, compileOrderBy: () => compileOrderBy, createColumnMapper: () => createColumnMapper, isChangeMessage: () => isChangeMessage, isControlMessage: () => isControlMessage, isVisibleInSnapshot: () => isVisibleInSnapshot, resolveValue: () => resolveValue, snakeCamelMapper: () => snakeCamelMapper, snakeToCamel: () => snakeToCamel }); module.exports = __toCommonJS(src_exports); // 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 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, (_, letter) => 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 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 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 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, 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; } function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) { const { initialDelay, maxDelay, multiplier, debug = false, onFailedAttempt, maxRetries = Infinity } = backoffOptions; return async (...args) => { var _a; 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 new Promise((resolve) => setTimeout(resolve, waitMs)); delay = Math.min(delay * multiplier, maxDelay); } } } }; } var NO_BODY_STATUS_CODES = [201, 204, 205]; function createFetchWithConsumedMessages(fetchClient) { return async (...args) => { var _a, _b; const url = args[0]; const res = await fetchClient(...args); 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 ((_b = (_a = args[1]) == null ? void 0 : _a.signal) == null ? void 0 : _b.aborted) { throw new FetchBackoffAbortError(); } throw new FetchError( res.status, void 0, void 0, Object.fromEntries([...res.headers.entries()]), url.toString(), err instanceof Error ? err.message : typeof err === `string` ? err : `failed to read body` ); } }; } 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); if (!shapeHandle || !lastOffset || isUpToDate) 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 = class { constructor() { this.activeSnapshots = /* @__PURE__ */ new Map(); this.xmaxSnapshots = /* @__PURE__ */ new Map(); this.snapshotsByDatabaseLsn = /* @__PURE__ */ new Map(); } /** * Add a new snapshot for tracking */ addSnapshot(metadata, keys) { var _a, _b, _c, _d; this.activeSnapshots.set(metadata.snapshot_mark, { xmin: BigInt(metadata.xmin), xmax: BigInt(metadata.xmax), xip_list: metadata.xip_list.map(BigInt), keys }); const xmaxSet = (_b = (_a = this.xmaxSnapshots.get(BigInt(metadata.xmax))) == null ? void 0 : _a.add(metadata.snapshot_mark)) != null ? _b : /* @__PURE__ */ new Set([metadata.snapshot_mark]); this.xmaxSnapshots.set(BigInt(metadata.xmax), xmaxSet); const databaseLsnSet = (_d = (_c = this.snapshotsByDatabaseLsn.get(BigInt(metadata.database_lsn))) == null ? void 0 : _c.add(metadata.snapshot_mark)) != null ? _d : /* @__PURE__ */ new Set([metadata.snapshot_mark]); this.snapshotsByDatabaseLsn.set( BigInt(metadata.database_lsn), databaseLsnSet ); } /** * Remove a snapshot from tracking */ removeSnapshot(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; const xid = Math.max(...txids); for (const [xmax, snapshots] of this.xmaxSnapshots.entries()) { if (xid >= xmax) { for (const snapshot of snapshots) { this.removeSnapshot(snapshot); } } } return [...this.activeSnapshots.values()].some( (x) => x.keys.has(message.key) && isVisibleInSnapshot(xid, x) ); } lastSeenUpdate(newDatabaseLsn) { for (const [dbLsn, snapshots] of this.snapshotsByDatabaseLsn.entries()) { if (dbLsn <= newDatabaseLsn) { for (const snapshot of snapshots) { this.removeSnapshot(snapshot); } } } } }; // 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, suppressBatch: 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, suppressBatch: 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), suppressBatch: 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 = `init