UNPKG

@electric-sql/client

Version:

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

1,317 lines (1,308 loc) 61.4 kB
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 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); } }; // 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) { const row = value; Object.keys(row).forEach((key2) => { row[key2] = this.parseRow(key2, row[key2], schema); }); if (this.transformer) value = this.transformer(value); } return value; }); } // 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/helpers.ts function isChangeMessage(message) { return `key` in message; } function isControlMessage(message) { return !isChangeMessage(message); } function isUpToDateMessage(message) { return isControlMessage(message) && message.headers.control === `up-to-date`; } function getOffset(message) { const lsn = message.headers.global_last_seen_lsn; if (!lsn) { return; } return `${lsn}_0`; } 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 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 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 ]; // src/fetch.ts var HTTP_RETRY_STATUS_CODES = [429]; var BackoffDefaults = { initialDelay: 100, maxDelay: 6e4, // Cap at 60s - reasonable for long-lived connections multiplier: 1.3, 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 prefetchedRequest = prefetchQueue == null ? void 0 : prefetchQueue.consume(...args); if (prefetchedRequest) { return prefetchedRequest; } prefetchQueue == null ? void 0 : prefetchQueue.abort(); 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 = [ `electric-offset`, `electric-handle` ]; var requiredLiveResponseHeaders = [`electric-cursor`]; var requiredNonLiveResponseHeaders = [`electric-schema`]; 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()); } consume(...args) { var _a; const url = args[0].toString(); const request = (_a = __privateGet(this, _prefetchQueue).get(url)) == null ? void 0 : _a[0]; if (!request || url !== __privateGet(this, _queueHeadUrl)) 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; 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() { } // src/client.ts import { fetchEventSource } from "@microsoft/fetch-event-source"; // 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(); } }; var expiredShapesCache = new ExpiredShapesCache(); // 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/client.ts var RESERVED_PARAMS = /* @__PURE__ */ new Set([ LIVE_CACHE_BUSTER_QUERY_PARAM, SHAPE_HANDLE_QUERY_PARAM, LIVE_QUERY_PARAM, OFFSET_QUERY_PARAM ]); 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); } 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.set(key, value); } } cleanUrl.searchParams.sort(); return cleanUrl.toString(); } var _error, _fetchClient2, _sseFetchClient, _messageParser, _subscribers, _started, _state, _lastOffset, _liveCacheBuster, _lastSyncedAt, _isUpToDate, _isMidStream, _connected, _shapeHandle, _mode, _schema, _onError, _requestAbortController, _isRefreshing, _tickPromise, _tickPromiseResolver, _tickPromiseRejecter, _messageChain, _snapshotTracker, _activeSnapshotRequests, _midStreamPromise, _midStreamPromiseResolver, _lastSseConnectionStartTime, _minSseConnectionDuration, _consecutiveShortSseConnections, _maxShortSseConnections, _sseFallbackToLongPolling, _sseBackoffBaseDelay, _sseBackoffMaxDelay, _ShapeStream_instances, start_fn, requestShape_fn, constructUrl_fn, createAbortListener_fn, onInitialResponse_fn, onMessages_fn, fetchShape_fn, requestShapeLongPoll_fn, requestShapeSSE_fn, pause_fn, resume_fn, nextTick_fn, waitForStreamEnd_fn, publish_fn, sendErrorToSubscribers_fn, subscribeToVisibilityChanges_fn, reset_fn, fetchSnapshot_fn; var ShapeStream = class { // Maximum delay cap (ms) 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, _state, `active`); __privateAdd(this, _lastOffset); __privateAdd(this, _liveCacheBuster); // Seconds since our Electric Epoch 😎 __privateAdd(this, _lastSyncedAt); // unix time __privateAdd(this, _isUpToDate, false); __privateAdd(this, _isMidStream, true); __privateAdd(this, _connected, false); __privateAdd(this, _shapeHandle); __privateAdd(this, _mode); __privateAdd(this, _schema); __privateAdd(this, _onError); __privateAdd(this, _requestAbortController); __privateAdd(this, _isRefreshing, false); __privateAdd(this, _tickPromise); __privateAdd(this, _tickPromiseResolver); __privateAdd(this, _tickPromiseRejecter); __privateAdd(this, _messageChain, Promise.resolve([])); // promise chain for incoming messages __privateAdd(this, _snapshotTracker, new SnapshotTracker()); __privateAdd(this, _activeSnapshotRequests, 0); // counter for concurrent snapshot requests __privateAdd(this, _midStreamPromise); __privateAdd(this, _midStreamPromiseResolver); __privateAdd(this, _lastSseConnectionStartTime); __privateAdd(this, _minSseConnectionDuration, 1e3); // Minimum expected SSE connection duration (1 second) __privateAdd(this, _consecutiveShortSseConnections, 0); __privateAdd(this, _maxShortSseConnections, 3); // Fall back to long polling after this many short connections __privateAdd(this, _sseFallbackToLongPolling, false); __privateAdd(this, _sseBackoffBaseDelay, 100); // Base delay for exponential backoff (ms) __privateAdd(this, _sseBackoffMaxDelay, 5e3); var _a, _b, _c, _d; this.options = __spreadValues({ subscribe: true }, options); validateOptions(this.options); __privateSet(this, _lastOffset, (_a = this.options.offset) != null ? _a : `-1`); __privateSet(this, _liveCacheBuster, ``); __privateSet(this, _shapeHandle, this.options.handle); __privateSet(this, _messageParser, new MessageParser( options.parser, options.transformer )); __privateSet(this, _onError, this.options.onError); __privateSet(this, _mode, (_b = this.options.log) != null ? _b : `full`); const baseFetchClient = (_c = options.fetchClient) != null ? _c : (...args) => fetch(...args); const backOffOpts = __spreadProps(__spreadValues({}, (_d = options.backoffOptions) != null ? _d : BackoffDefaults), { onFailedAttempt: () => { var _a2, _b2; __privateSet(this, _connected, false); (_b2 = (_a2 = options.backoffOptions) == null ? void 0 : _a2.onFailedAttempt) == null ? void 0 : _b2.call(_a2); } }); 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, _shapeHandle); } get error() { return __privateGet(this, _error); } get isUpToDate() { return __privateGet(this, _isUpToDate); } get lastOffset() { return __privateGet(this, _lastOffset); } get mode() { return __privateGet(this, _mode); } subscribe(callback, onError = () => { }) { const subscriptionId = Math.random(); __privateGet(this, _subscribers).set(subscriptionId, [callback, onError]); if (!__privateGet(this, _started)) __privateMethod(this, _ShapeStream_instances, start_fn).call(this); return () => { __privateGet(this, _subscribers).delete(subscriptionId); }; } unsubscribeAll() { __privateGet(this, _subscribers).clear(); } /** Unix time at which we last synced. Undefined when `isLoading` is true. */ lastSyncedAt() { return __privateGet(this, _lastSyncedAt); } /** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */ lastSynced() { if (__privateGet(this, _lastSyncedAt) === void 0) return Infinity; return Date.now() - __privateGet(this, _lastSyncedAt); } /** Indicates if we are connected to the Electric sync service. */ isConnected() { return __privateGet(this, _connected); } /** True during initial fetch. False afterwise. */ isLoading() { return !__privateGet(this, _isUpToDate); } hasStarted() { return __privateGet(this, _started); } isPaused() { return __privateGet(this, _state) === `paused`; } /** * 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() { var _a, _b; __privateSet(this, _isRefreshing, true); if (__privateGet(this, _isUpToDate) && !((_a = __privateGet(this, _requestAbortController)) == null ? void 0 : _a.signal.aborted)) { (_b = __privateGet(this, _requestAbortController)) == null ? void 0 : _b.abort(FORCE_DISCONNECT_AND_REFRESH); } await __privateMethod(this, _ShapeStream_instances, nextTick_fn).call(this); __privateSet(this, _isRefreshing, false); } /** * Request a snapshot for subset of data. * * 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)) await __privateMethod(this, _ShapeStream_instances, start_fn).call(this); await __privateMethod(this, _ShapeStream_instances, waitForStreamEnd_fn).call(this); __privateWrapper(this, _activeSnapshotRequests)._++; try { if (__privateGet(this, _activeSnapshotRequests) === 1) { __privateMethod(this, _ShapeStream_instances, pause_fn).call(this); } const { fetchUrl, requestHeaders } = await __privateMethod(this, _ShapeStream_instances, constructUrl_fn).call(this, this.options.url, true, opts); const { metadata, data } = await __privateMethod(this, _ShapeStream_instances, fetchSnapshot_fn).call(this, fetchUrl, requestHeaders); const dataWithEndBoundary = data.concat([ { headers: __spreadValues({ control: `snapshot-end` }, metadata) } ]); __privateGet(this, _snapshotTracker).addSnapshot( metadata, new Set(data.map((message) => message.key)) ); __privateMethod(this, _ShapeStream_instances, onMessages_fn).call(this, dataWithEndBoundary, false); return { metadata, data }; } finally { __privateWrapper(this, _activeSnapshotRequests)._--; if (__privateGet(this, _activeSnapshotRequests) === 0) { __privateMethod(this, _ShapeStream_instances, resume_fn).call(this); } } } }; _error = new WeakMap(); _fetchClient2 = new WeakMap(); _sseFetchClient = new WeakMap(); _messageParser = new WeakMap(); _subscribers = new WeakMap(); _started = new WeakMap(); _state = new WeakMap(); _lastOffset = new WeakMap(); _liveCacheBuster = new WeakMap(); _lastSyncedAt = new WeakMap(); _isUpToDate = new WeakMap(); _isMidStream = new WeakMap(); _connected = new WeakMap(); _shapeHandle = new WeakMap(); _mode = new WeakMap(); _schema = new WeakMap(); _onError = new WeakMap(); _requestAbortController = new WeakMap(); _isRefreshing = new WeakMap(); _tickPromise = new WeakMap(); _tickPromiseResolver = new WeakMap(); _tickPromiseRejecter = new WeakMap(); _messageChain = new WeakMap(); _snapshotTracker = new WeakMap(); _activeSnapshotRequests = new WeakMap(); _midStreamPromise = new WeakMap(); _midStreamPromiseResolver = new WeakMap(); _lastSseConnectionStartTime = new WeakMap(); _minSseConnectionDuration = new WeakMap(); _consecutiveShortSseConnections = new WeakMap(); _maxShortSseConnections = new WeakMap(); _sseFallbackToLongPolling = new WeakMap(); _sseBackoffBaseDelay = new WeakMap(); _sseBackoffMaxDelay = new WeakMap(); _ShapeStream_instances = new WeakSet(); start_fn = async function() { var _a, _b, _c, _d, _e; __privateSet(this, _started, true); try { await __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this); } catch (err) { __privateSet(this, _error, err); if (__privateGet(this, _onError)) { const retryOpts = await __privateGet(this, _onError).call(this, err); if (retryOpts && typeof retryOpts === `object`) { 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); } __privateSet(this, _error, null); __privateSet(this, _started, false); await __privateMethod(this, _ShapeStream_instances, start_fn).call(this); return; } if (err instanceof Error) { __privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, err); } __privateSet(this, _connected, false); (_c = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _c.call(this); return; } if (err instanceof Error) { __privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, err); } __privateSet(this, _connected, false); (_d = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _d.call(this); throw err; } __privateSet(this, _connected, false); (_e = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _e.call(this); }; requestShape_fn = async function() { var _a, _b; if (__privateGet(this, _state) === `pause-requested`) { __privateSet(this, _state, `paused`); return; } if (!this.options.subscribe && (((_a = this.options.signal) == null ? void 0 : _a.aborted) || __privateGet(this, _isUpToDate))) { return; } const resumingFromPause = __privateGet(this, _state) === `paused`; __privateSet(this, _state, `active`); const { url, signal } = this.options; const { fetchUrl, requestHeaders } = await __privateMethod(this, _ShapeStream_instances, constructUrl_fn).call(this, url, resumingFromPause); const abortListener = await __privateMethod(this, _ShapeStream_instances, createAbortListener_fn).call(this, signal); const requestAbortController = __privateGet(this, _requestAbortController); try { await __privateMethod(this, _ShapeStream_instances, fetchShape_fn).call(this, { fetchUrl, requestAbortController, headers: requestHeaders, resumingFromPause }); } catch (e) { if ((e instanceof FetchError || e instanceof FetchBackoffAbortError) && requestAbortController.signal.aborted && requestAbortController.signal.reason === FORCE_DISCONNECT_AND_REFRESH) { return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this); } if (e instanceof FetchBackoffAbortError) { if (requestAbortController.signal.aborted && requestAbortController.signal.reason === PAUSE_STREAM) { __privateSet(this, _state, `paused`); } return; } if (!(e instanceof FetchError)) throw e; if (e.status == 409) { if (__privateGet(this, _shapeHandle)) { const shapeKey = canonicalShapeKey(fetchUrl); expiredShapesCache.markExpired(shapeKey, __privateGet(this, _shapeHandle)); } const newShapeHandle = e.headers[SHAPE_HANDLE_HEADER] || `${__privateGet(this, _shapeHandle)}-next`; __privateMethod(this, _ShapeStream_instances, reset_fn).call(this, newShapeHandle); await __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, Array.isArray(e.json) ? e.json : [e.json]); return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this); } else { throw e; } } finally { if (abortListener && signal) { signal.removeEventListener(`abort`, abortListener); } __privateSet(this, _requestAbortController, void 0); } (_b = __privateGet(this, _tickPromiseResolver)) == null ? void 0 : _b.call(this); return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this); }; constructUrl_fn = async function(url, resumingFromPause, subsetParams) { 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) setQueryParam(fetchUrl, WHERE_QUERY_PARAM, params.where); if (params.columns) 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.where) setQueryParam(fetchUrl, SUBSET_PARAM_WHERE, subsetParams.where); if (subsetParams.params) setQueryParam(fetchUrl, SUBSET_PARAM_WHERE_PARAMS, subsetParams.params); if (subsetParams.limit) setQueryParam(fetchUrl, SUBSET_PARAM_LIMIT, subsetParams.limit); if (subsetParams.offset) setQueryParam(fetchUrl, SUBSET_PARAM_OFFSET, subsetParams.offset); if (subsetParams.orderBy) setQueryParam(fetchUrl, SUBSET_PARAM_ORDER_BY, subsetParams.orderBy); } fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, __privateGet(this, _lastOffset)); fetchUrl.searchParams.set(LOG_MODE_QUERY_PARAM, __privateGet(this, _mode)); if (__privateGet(this, _isUpToDate)) { if (!__privateGet(this, _isRefreshing) && !resumingFromPause) { fetchUrl.searchParams.set(LIVE_QUERY_PARAM, `true`); } fetchUrl.searchParams.set( LIVE_CACHE_BUSTER_QUERY_PARAM, __privateGet(this, _liveCacheBuster) ); } if (__privateGet(this, _shapeHandle)) { fetchUrl.searchParams.set(SHAPE_HANDLE_QUERY_PARAM, __privateGet(this, _shapeHandle)); } 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; const { headers, status } = response; const shapeHandle = headers.get(SHAPE_HANDLE_HEADER); if (shapeHandle) { __privateSet(this, _shapeHandle, shapeHandle); } const lastOffset = headers.get(CHUNK_LAST_OFFSET_HEADER); if (lastOffset) { __privateSet(this, _lastOffset, lastOffset); } const liveCacheBuster = headers.get(LIVE_CACHE_BUSTER_HEADER); if (liveCacheBuster) { __privateSet(this, _liveCacheBuster, liveCacheBuster); } const getSchema = () => { const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER); return schemaHeader ? JSON.parse(schemaHeader) : {}; }; __privateSet(this, _schema, (_a = __privateGet(this, _schema)) != null ? _a : getSchema()); if (status === 204) { __privateSet(this, _lastSyncedAt, Date.now()); } }; onMessages_fn = async function(batch, isSseMessage = false) { var _a; if (batch.length > 0) { __privateSet(this, _isMidStream, true); const lastMessage = batch[batch.length - 1]; if (isUpToDateMessage(lastMessage)) { if (isSseMessage) { const offset = getOffset(lastMessage); if (offset) { __privateSet(this, _lastOffset, offset); } } __privateSet(this, _lastSyncedAt, Date.now()); __privateSet(this, _isUpToDate, true); __privateSet(this, _isMidStream, false); (_a = __privateGet(this, _midStreamPromiseResolver)) == null ? void 0 : _a.call(this); } const messagesToProcess = batch.filter((message) => { if (isChangeMessage(message)) { return !__privateGet(this, _snapshotTracker).shouldRejectMessage(message); } return true; }); await __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, messagesToProcess); } }; fetchShape_fn = async function(opts) { var _a; const useSse = (_a = this.options.liveSse) != null ? _a : this.options.experimentalLiveSse; if (__privateGet(this, _isUpToDate) && useSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause && !__privateGet(this, _sseFallbackToLongPolling)) { 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); }; requestShapeLongPoll_fn = async function(opts) { const { fetchUrl, requestAbortController, headers } = opts; const response = await __privateGet(this, _fetchClient2).call(this, fetchUrl.toString(), { signal: requestAbortController.signal, headers }); __privateSet(this, _connected, true); await __privateMethod(this, _ShapeStream_instances, onInitialResponse_fn).call(this, response); const schema = __privateGet(this, _schema); const res = await response.text(); const messages = res || `[]`; const batch = __privateGet(this, _messageParser).parse(messages, schema); 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()); try { let buffer = []; await fetchEventSource(fetchUrl.toString(), { headers, fetch: fetch2, onopen: async (response) => { __privateSet(this, _connected, true); await __privateMethod(this, _ShapeStream_instances, onInitialResponse_fn).call(this, response); }, onmessage: (event) => { if (event.data) { const schema = __privateGet(this, _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 (requestAbortController.signal.aborted) { throw new FetchBackoffAbortError(); } throw error; } finally { const connectionDuration = Date.now() - __privateGet(this, _lastSseConnectionStartTime); const wasAborted = requestAbortController.signal.aborted; if (connectionDuration < __privateGet(this, _minSseConnectionDuration) && !wasAborted) { __privateWrapper(this, _consecutiveShortSseConnections)._++; if (__privateGet(this, _consecutiveShortSseConnections) >= __privateGet(this, _maxShortSseConnections)) { __privateSet(this, _sseFallbackToLongPolling, true); 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.` ); } else { const maxDelay = Math.min( __privateGet(this, _sseBackoffMaxDelay), __privateGet(this, _sseBackoffBaseDelay) * Math.pow(2, __privateGet(this, _consecutiveShortSseConnections)) ); const delayMs = Math.floor(Math.random() * maxDelay); await new Promise((resolve) => setTimeout(resolve, delayMs)); } } else if (connectionDuration >= __privateGet(this, _minSseConnectionDuration)) { __privateSet(this, _consecutiveShortSseConnections, 0); } } }; pause_fn = function() { var _a; if (__privateGet(this, _started) && __privateGet(this, _state) === `active`) { __privateSet(this, _state, `pause-requested`); (_a = __privateGet(this, _requestAbortController)) == null ? void 0 : _a.abort(PAUSE_STREAM); } }; resume_fn = function() { if (__privateGet(this, _started) && __privateGet(this, _state) === `paused`) { __privateMethod(this, _ShapeStream_instances, start_fn).call(this); } }; nextTick_fn = async function() { 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); }); return __privateGet(this, _tickPromise); }; waitForStreamEnd_fn = async function() { if (!__privateGet(this, _isMidStream)) { return; } if (__privateGet(this, _midStreamPromise)) { return __privateGet(this, _midStreamPromise); } __privateSet(this, _midStreamPromise, new Promise((resolve) => { __privateSet(this,