@electric-sql/client 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/parser.ts","../src/helpers.ts","../src/error.ts","../src/constants.ts","../src/fetch.ts","../src/client.ts","../src/shape.ts"],"sourcesContent":["import { ColumnInfo, Message, Row, Schema, Value } from './types'\n\ntype NullToken = null | `NULL`\ntype Token = Exclude<string, NullToken>\ntype NullableToken = Token | NullToken\nexport type ParseFunction = (\n value: Token,\n additionalInfo?: Omit<ColumnInfo, `type` | `dims`>\n) => Value\ntype NullableParseFunction = (\n value: NullableToken,\n additionalInfo?: Omit<ColumnInfo, `type` | `dims`>\n) => Value\nexport type Parser = { [key: string]: ParseFunction }\n\nconst parseNumber = (value: string) => Number(value)\nconst parseBool = (value: string) => value === `true` || value === `t`\nconst parseBigInt = (value: string) => BigInt(value)\nconst parseJson = (value: string) => JSON.parse(value)\nconst identityParser: ParseFunction = (v: string) => v\n\nexport const defaultParser: Parser = {\n int2: parseNumber,\n int4: parseNumber,\n int8: parseBigInt,\n bool: parseBool,\n float4: parseNumber,\n float8: parseNumber,\n json: parseJson,\n jsonb: parseJson,\n}\n\n// Taken from: https://github.com/electric-sql/pglite/blob/main/packages/pglite/src/types.ts#L233-L279\nexport function pgArrayParser(value: Token, parser?: ParseFunction): Value {\n let i = 0\n let char = null\n let str = ``\n let quoted = false\n let last = 0\n let p: string | undefined = undefined\n\n function loop(x: string): Value[] {\n const xs = []\n for (; i < x.length; i++) {\n char = x[i]\n if (quoted) {\n if (char === `\\\\`) {\n str += x[++i]\n } else if (char === `\"`) {\n xs.push(parser ? parser(str) : str)\n str = ``\n quoted = x[i + 1] === `\"`\n last = i + 2\n } else {\n str += char\n }\n } else if (char === `\"`) {\n quoted = true\n } else if (char === `{`) {\n last = ++i\n xs.push(loop(x))\n } else if (char === `}`) {\n quoted = false\n last < i &&\n xs.push(parser ? parser(x.slice(last, i)) : x.slice(last, i))\n last = i + 1\n break\n } else if (char === `,` && p !== `}` && p !== `\"`) {\n xs.push(parser ? parser(x.slice(last, i)) : x.slice(last, i))\n last = i + 1\n }\n p = char\n }\n last < i &&\n xs.push(parser ? parser(x.slice(last, i + 1)) : x.slice(last, i + 1))\n return xs\n }\n\n return loop(value)[0]\n}\n\nexport class MessageParser<T extends Row> {\n private parser: Parser\n constructor(parser?: Parser) {\n // Merge the provided parser with the default parser\n // to use the provided parser whenever defined\n // and otherwise fall back to the default parser\n this.parser = { ...defaultParser, ...parser }\n }\n\n parse(messages: string, schema: Schema): Message<T>[] {\n return JSON.parse(messages, (key, value) => {\n // typeof value === `object` && value !== null\n // is needed because there could be a column named `value`\n // and the value associated to that column will be a string or null.\n // But `typeof null === 'object'` so we need to make an explicit check.\n if (key === `value` && typeof value === `object` && value !== null) {\n // Parse the row values\n const row = value as Record<string, Value>\n Object.keys(row).forEach((key) => {\n row[key] = this.parseRow(key, row[key] as NullableToken, schema)\n })\n }\n return value\n }) as Message<T>[]\n }\n\n // Parses the message values using the provided parser based on the schema information\n private parseRow(key: string, value: NullableToken, schema: Schema): Value {\n const columnInfo = schema[key]\n if (!columnInfo) {\n // We don't have information about the value\n // so we just return it\n return value\n }\n\n // Copy the object but don't include `dimensions` and `type`\n const { type: typ, dims: dimensions, ...additionalInfo } = columnInfo\n\n // Pick the right parser for the type\n // and support parsing null values if needed\n // if no parser is provided for the given type, just return the value as is\n const typeParser = this.parser[typ] ?? identityParser\n const parser = makeNullableParser(typeParser, columnInfo, key)\n\n if (dimensions && dimensions > 0) {\n // It's an array\n const nullablePgArrayParser = makeNullableParser(\n (value, _) => pgArrayParser(value, parser),\n columnInfo,\n key\n )\n return nullablePgArrayParser(value)\n }\n\n return parser(value, additionalInfo)\n }\n}\n\nfunction makeNullableParser(\n parser: ParseFunction,\n columnInfo: ColumnInfo,\n columnName?: string\n): NullableParseFunction {\n const isNullable = !(columnInfo.not_null ?? false)\n // The sync service contains `null` value for a column whose value is NULL\n // but if the column value is an array that contains a NULL value\n // then it will be included in the array string as `NULL`, e.g.: `\"{1,NULL,3}\"`\n return (value: NullableToken) => {\n if (isPgNull(value)) {\n if (!isNullable) {\n throw new Error(`Column ${columnName ?? `unknown`} is not nullable`)\n }\n return null\n }\n return parser(value, columnInfo)\n }\n}\n\nfunction isPgNull(value: NullableToken): value is NullToken {\n return value === null || value === `NULL`\n}\n","import { ChangeMessage, ControlMessage, Message, Row } from './types'\n\n/**\n * Type guard for checking {@link Message} is {@link ChangeMessage}.\n *\n * See [TS docs](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards)\n * for information on how to use type guards.\n *\n * @param message - the message to check\n * @returns true if the message is a {@link ChangeMessage}\n *\n * @example\n * ```ts\n * if (isChangeMessage(message)) {\n * const msgChng: ChangeMessage = message // Ok\n * const msgCtrl: ControlMessage = message // Err, type mismatch\n * }\n * ```\n */\nexport function isChangeMessage<T extends Row = Row>(\n message: Message<T>\n): message is ChangeMessage<T> {\n return `key` in message\n}\n\n/**\n * Type guard for checking {@link Message} is {@link ControlMessage}.\n *\n * See [TS docs](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards)\n * for information on how to use type guards.\n *\n * @param message - the message to check\n * @returns true if the message is a {@link ControlMessage}\n *\n * * @example\n * ```ts\n * if (isControlMessage(message)) {\n * const msgChng: ChangeMessage = message // Err, type mismatch\n * const msgCtrl: ControlMessage = message // Ok\n * }\n * ```\n */\nexport function isControlMessage<T extends Row = Row>(\n message: Message<T>\n): message is ControlMessage {\n return !isChangeMessage(message)\n}\n\nexport function isUpToDateMessage<T extends Row = Row>(\n message: Message<T>\n): message is ControlMessage & { up_to_date: true } {\n return isControlMessage(message) && message.headers.control === `up-to-date`\n}\n","export class FetchError extends Error {\n status: number\n text?: string\n json?: object\n headers: Record<string, string>\n\n constructor(\n status: number,\n text: string | undefined,\n json: object | undefined,\n headers: Record<string, string>,\n public url: string,\n message?: string\n ) {\n super(\n message ||\n `HTTP Error ${status} at ${url}: ${text ?? JSON.stringify(json)}`\n )\n this.name = `FetchError`\n this.status = status\n this.text = text\n this.json = json\n this.headers = headers\n }\n\n static async fromResponse(\n response: Response,\n url: string\n ): Promise<FetchError> {\n const status = response.status\n const headers = Object.fromEntries([...response.headers.entries()])\n let text: string | undefined = undefined\n let json: object | undefined = undefined\n\n const contentType = response.headers.get(`content-type`)\n if (contentType && contentType.includes(`application/json`)) {\n json = (await response.json()) as object\n } else {\n text = await response.text()\n }\n\n return new FetchError(status, text, json, headers, url)\n }\n}\n\nexport class FetchBackoffAbortError extends Error {\n constructor() {\n super(`Fetch with backoff aborted`)\n }\n}\n","export const SHAPE_ID_HEADER = `electric-shape-id`\nexport const CHUNK_LAST_OFFSET_HEADER = `electric-chunk-last-offset`\nexport const CHUNK_UP_TO_DATE_HEADER = `electric-chunk-up-to-date`\nexport const SHAPE_SCHEMA_HEADER = `electric-schema`\nexport const SHAPE_ID_QUERY_PARAM = `shape_id`\nexport const OFFSET_QUERY_PARAM = `offset`\nexport const WHERE_QUERY_PARAM = `where`\nexport const LIVE_QUERY_PARAM = `live`\n","import {\n CHUNK_LAST_OFFSET_HEADER,\n CHUNK_UP_TO_DATE_HEADER,\n LIVE_QUERY_PARAM,\n OFFSET_QUERY_PARAM,\n SHAPE_ID_HEADER,\n SHAPE_ID_QUERY_PARAM,\n} from './constants'\nimport { FetchError, FetchBackoffAbortError } from './error'\n\nexport interface BackoffOptions {\n /**\n * Initial delay before retrying in milliseconds\n */\n initialDelay: number\n /**\n * Maximum retry delay in milliseconds\n */\n maxDelay: number\n multiplier: number\n onFailedAttempt?: () => void\n debug?: boolean\n}\n\nexport const BackoffDefaults = {\n initialDelay: 100,\n maxDelay: 10_000,\n multiplier: 1.3,\n}\n\nexport function createFetchWithBackoff(\n fetchClient: typeof fetch,\n backoffOptions: BackoffOptions = BackoffDefaults\n): typeof fetch {\n const {\n initialDelay,\n maxDelay,\n multiplier,\n debug = false,\n onFailedAttempt,\n } = backoffOptions\n return async (...args: Parameters<typeof fetch>): Promise<Response> => {\n const url = args[0]\n const options = args[1]\n\n let delay = initialDelay\n let attempt = 0\n\n /* eslint-disable no-constant-condition -- we re-fetch the shape log\n * continuously until we get a non-ok response. For recoverable errors,\n * we retry the fetch with exponential backoff. Users can pass in an\n * AbortController to abort the fetching an any point.\n * */\n while (true) {\n /* eslint-enable no-constant-condition */\n try {\n const result = await fetchClient(...args)\n if (result.ok) return result\n else throw await FetchError.fromResponse(result, url.toString())\n } catch (e) {\n onFailedAttempt?.()\n if (options?.signal?.aborted) {\n throw new FetchBackoffAbortError()\n } else if (\n e instanceof FetchError &&\n e.status >= 400 &&\n e.status < 500\n ) {\n // Any client errors cannot be backed off on, leave it to the caller to handle.\n throw e\n } else {\n // Exponentially backoff on errors.\n // Wait for the current delay duration\n await new Promise((resolve) => setTimeout(resolve, delay))\n\n // Increase the delay for the next attempt\n delay = Math.min(delay * multiplier, maxDelay)\n\n if (debug) {\n attempt++\n console.log(`Retry attempt #${attempt} after ${delay}ms`)\n }\n }\n }\n }\n }\n}\n\ninterface ChunkPrefetchOptions {\n maxChunksToPrefetch: number\n}\n\nconst ChunkPrefetchDefaults = {\n maxChunksToPrefetch: 2,\n}\n\n/**\n * Creates a fetch client that prefetches subsequent log chunks for\n * consumption by the shape stream without waiting for the chunk bodies\n * themselves to be loaded.\n *\n * @param fetchClient the client to wrap\n * @param prefetchOptions options to configure prefetching\n * @returns wrapped client with prefetch capabilities\n */\nexport function createFetchWithChunkBuffer(\n fetchClient: typeof fetch,\n prefetchOptions: ChunkPrefetchOptions = ChunkPrefetchDefaults\n): typeof fetch {\n const { maxChunksToPrefetch } = prefetchOptions\n\n let prefetchQueue: PrefetchQueue\n\n const prefetchClient = async (...args: Parameters<typeof fetchClient>) => {\n const url = args[0].toString()\n\n // try to consume from the prefetch queue first, and if request is\n // not present abort the prefetch queue as it must no longer be valid\n const prefetchedRequest = prefetchQueue?.consume(...args)\n if (prefetchedRequest) {\n return prefetchedRequest\n }\n\n prefetchQueue?.abort()\n\n // perform request and fire off prefetch queue if request is eligible\n const response = await fetchClient(...args)\n const nextUrl = getNextChunkUrl(url, response)\n if (nextUrl) {\n prefetchQueue = new PrefetchQueue({\n fetchClient,\n maxPrefetchedRequests: maxChunksToPrefetch,\n url: nextUrl,\n requestInit: args[1],\n })\n }\n\n return response\n }\n\n return prefetchClient\n}\n\nclass PrefetchQueue {\n readonly #fetchClient: typeof fetch\n readonly #maxPrefetchedRequests: number\n readonly #prefetchQueue = new Map<\n string,\n [Promise<Response>, AbortController]\n >()\n #queueHeadUrl: string | void\n #queueTailUrl: string\n\n constructor(options: {\n url: Parameters<typeof fetch>[0]\n requestInit: Parameters<typeof fetch>[1]\n maxPrefetchedRequests: number\n fetchClient?: typeof fetch\n }) {\n this.#fetchClient =\n options.fetchClient ??\n ((...args: Parameters<typeof fetch>) => fetch(...args))\n this.#maxPrefetchedRequests = options.maxPrefetchedRequests\n this.#queueHeadUrl = options.url.toString()\n this.#queueTailUrl = this.#queueHeadUrl\n this.#prefetch(options.url, options.requestInit)\n }\n\n abort(): void {\n this.#prefetchQueue.forEach(([_, aborter]) => aborter.abort())\n }\n\n consume(...args: Parameters<typeof fetch>): Promise<Response> | void {\n const url = args[0].toString()\n\n const request = this.#prefetchQueue.get(url)?.[0]\n // only consume if request is in queue and is the queue \"head\"\n // if request is in the queue but not the head, the queue is being\n // consumed out of order and should be restarted\n if (!request || url !== this.#queueHeadUrl) return\n this.#prefetchQueue.delete(url)\n\n // fire off new prefetch since request has been consumed\n request\n .then((response) => {\n const nextUrl = getNextChunkUrl(url, response)\n this.#queueHeadUrl = nextUrl\n if (!this.#prefetchQueue.has(this.#queueTailUrl)) {\n this.#prefetch(this.#queueTailUrl, args[1])\n }\n })\n .catch(() => {})\n\n return request\n }\n\n #prefetch(...args: Parameters<typeof fetch>): void {\n const url = args[0].toString()\n\n // only prefetch when queue is not full\n if (this.#prefetchQueue.size >= this.#maxPrefetchedRequests) return\n\n // initialize aborter per request, to avoid aborting consumed requests that\n // are still streaming their bodies to the consumer\n const aborter = new AbortController()\n\n try {\n const request = this.#fetchClient(url, {\n ...(args[1] ?? {}),\n signal: chainAborter(aborter, args[1]?.signal),\n })\n this.#prefetchQueue.set(url, [request, aborter])\n request\n .then((response) => {\n // only keep prefetching if response chain is uninterrupted\n if (!response.ok || aborter.signal.aborted) return\n\n const nextUrl = getNextChunkUrl(url, response)\n\n // only prefetch when there is a next URL\n if (!nextUrl || nextUrl === url) return\n\n this.#queueTailUrl = nextUrl\n return this.#prefetch(nextUrl, args[1])\n })\n .catch(() => {})\n } catch (_) {\n // ignore prefetch errors\n }\n }\n}\n\n/**\n * Generate the next chunk's URL if the url and response are valid\n */\nfunction getNextChunkUrl(url: string, res: Response): string | void {\n const shapeId = res.headers.get(SHAPE_ID_HEADER)\n const lastOffset = res.headers.get(CHUNK_LAST_OFFSET_HEADER)\n const isUpToDate = res.headers.has(CHUNK_UP_TO_DATE_HEADER)\n\n // only prefetch if shape ID and offset for next chunk are available, and\n // response is not already up-to-date\n if (!shapeId || !lastOffset || isUpToDate) return\n\n const nextUrl = new URL(url)\n\n // don't prefetch live requests, rushing them will only\n // potentially miss more recent data\n if (nextUrl.searchParams.has(LIVE_QUERY_PARAM)) return\n\n nextUrl.searchParams.set(SHAPE_ID_QUERY_PARAM, shapeId)\n nextUrl.searchParams.set(OFFSET_QUERY_PARAM, lastOffset)\n return nextUrl.toString()\n}\n\n/**\n * Chains an abort controller on an optional source signal's\n * aborted state - if the source signal is aborted, the provided abort\n * controller will also abort\n */\nfunction chainAborter(\n aborter: AbortController,\n sourceSignal?: AbortSignal\n): AbortSignal {\n if (!sourceSignal) return aborter.signal\n if (sourceSignal.aborted) aborter.abort()\n else\n sourceSignal.addEventListener(`abort`, () => aborter.abort(), {\n once: true,\n })\n return aborter.signal\n}\n","import { Message, Offset, Schema, Row, MaybePromise } from './types'\nimport { MessageParser, Parser } from './parser'\nimport { isUpToDateMessage } from './helpers'\nimport { FetchError, FetchBackoffAbortError } from './error'\nimport {\n BackoffDefaults,\n BackoffOptions,\n createFetchWithBackoff,\n createFetchWithChunkBuffer,\n} from './fetch'\nimport {\n CHUNK_LAST_OFFSET_HEADER,\n LIVE_QUERY_PARAM,\n OFFSET_QUERY_PARAM,\n SHAPE_ID_HEADER,\n SHAPE_ID_QUERY_PARAM,\n SHAPE_SCHEMA_HEADER,\n WHERE_QUERY_PARAM,\n} from './constants'\n\n/**\n * Options for constructing a ShapeStream.\n */\nexport interface ShapeStreamOptions {\n /**\n * The full URL to where the Shape is hosted. This can either be the Electric server\n * directly or a proxy. E.g. for a local Electric instance, you might set `http://localhost:3000/v1/shape/foo`\n */\n url: string\n /**\n * where clauses for the shape.\n */\n where?: string\n /**\n * The \"offset\" on the shape log. This is typically not set as the ShapeStream\n * will handle this automatically. A common scenario where you might pass an offset\n * is if you're maintaining a local cache of the log. If you've gone offline\n * and are re-starting a ShapeStream to catch-up to the latest state of the Shape,\n * you'd pass in the last offset and shapeId you'd seen from the Electric server\n * so it knows at what point in the shape to catch you up from.\n */\n offset?: Offset\n /**\n * Similar to `offset`, this isn't typically used unless you're maintaining\n * a cache of the shape log.\n */\n shapeId?: string\n backoffOptions?: BackoffOptions\n /**\n * Automatically fetch updates to the Shape. If you just want to sync the current\n * shape and stop, pass false.\n */\n subscribe?: boolean\n signal?: AbortSignal\n fetchClient?: typeof fetch\n parser?: Parser\n}\n\nexport interface ShapeStreamInterface<T extends Row = Row> {\n subscribe(\n callback: (messages: Message<T>[]) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ): void\n unsubscribeAllUpToDateSubscribers(): void\n unsubscribeAll(): void\n subscribeOnceToUpToDate(\n callback: () => MaybePromise<void>,\n error: (err: FetchError | Error) => void\n ): () => void\n\n isLoading(): boolean\n lastSyncedAt(): number | undefined\n lastSynced(): number\n isConnected(): boolean\n\n isUpToDate: boolean\n shapeId?: string\n}\n\n/**\n * Reads updates to a shape from Electric using HTTP requests and long polling. Notifies subscribers\n * when new messages come in. Doesn't maintain any history of the\n * log but does keep track of the offset position and is the best way\n * to consume the HTTP `GET /v1/shape` api.\n *\n * @constructor\n * @param {ShapeStreamOptions} options - configure the shape stream\n * @example\n * Register a callback function to subscribe to the messages.\n * ```\n * const stream = new ShapeStream(options)\n * stream.subscribe(messages => {\n * // messages is 1 or more row updates\n * })\n * ```\n *\n * To abort the stream, abort the `signal`\n * passed in via the `ShapeStreamOptions`.\n * ```\n * const aborter = new AbortController()\n * const issueStream = new ShapeStream({\n * url: `${BASE_URL}/${table}`\n * subscribe: true,\n * signal: aborter.signal,\n * })\n * // Later...\n * aborter.abort()\n * ```\n */\n\nexport class ShapeStream<T extends Row = Row>\n implements ShapeStreamInterface<T>\n{\n readonly options: ShapeStreamOptions\n\n readonly #fetchClient: typeof fetch\n readonly #messageParser: MessageParser<T>\n\n readonly #subscribers = new Map<\n number,\n [\n (messages: Message<T>[]) => MaybePromise<void>,\n ((error: Error) => void) | undefined,\n ]\n >()\n readonly #upToDateSubscribers = new Map<\n number,\n [() => void, (error: FetchError | Error) => void]\n >()\n\n #lastOffset: Offset\n #lastSyncedAt?: number // unix time\n #isUpToDate: boolean = false\n #connected: boolean = false\n #shapeId?: string\n #schema?: Schema\n\n constructor(options: ShapeStreamOptions) {\n validateOptions(options)\n this.options = { subscribe: true, ...options }\n this.#lastOffset = this.options.offset ?? `-1`\n this.#shapeId = this.options.shapeId\n this.#messageParser = new MessageParser<T>(options.parser)\n\n const baseFetchClient =\n options.fetchClient ??\n ((...args: Parameters<typeof fetch>) => fetch(...args))\n\n const fetchWithBackoffClient = createFetchWithBackoff(baseFetchClient, {\n ...(options.backoffOptions ?? BackoffDefaults),\n onFailedAttempt: () => {\n this.#connected = false\n options.backoffOptions?.onFailedAttempt?.()\n },\n })\n\n this.#fetchClient = createFetchWithChunkBuffer(fetchWithBackoffClient)\n\n this.start()\n }\n\n get shapeId() {\n return this.#shapeId\n }\n\n get isUpToDate() {\n return this.#isUpToDate\n }\n\n async start() {\n this.#isUpToDate = false\n\n const { url, where, signal } = this.options\n\n try {\n while (\n (!signal?.aborted && !this.#isUpToDate) ||\n this.options.subscribe\n ) {\n const fetchUrl = new URL(url)\n if (where) fetchUrl.searchParams.set(WHERE_QUERY_PARAM, where)\n fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, this.#lastOffset)\n\n if (this.#isUpToDate) {\n fetchUrl.searchParams.set(LIVE_QUERY_PARAM, `true`)\n }\n\n if (this.#shapeId) {\n // This should probably be a header for better cache breaking?\n fetchUrl.searchParams.set(SHAPE_ID_QUERY_PARAM, this.#shapeId!)\n }\n\n let response!: Response\n try {\n response = await this.#fetchClient(fetchUrl.toString(), { signal })\n this.#connected = true\n } catch (e) {\n if (e instanceof FetchBackoffAbortError) break // interrupted\n if (!(e instanceof FetchError)) throw e // should never happen\n if (e.status == 400) {\n // The request is invalid, most likely because the shape has been deleted.\n // We should start from scratch, this will force the shape to be recreated.\n this.#reset()\n await this.#publish(e.json as Message<T>[])\n continue\n } else if (e.status == 409) {\n // Upon receiving a 409, we should start from scratch\n // with the newly provided shape ID\n const newShapeId = e.headers[SHAPE_ID_HEADER]\n this.#reset(newShapeId)\n await this.#publish(e.json as Message<T>[])\n continue\n } else if (e.status >= 400 && e.status < 500) {\n // Notify subscribers\n this.#sendErrorToUpToDateSubscribers(e)\n this.#sendErrorToSubscribers(e)\n\n // 400 errors are not actionable without additional user input, so we're throwing them.\n throw e\n }\n }\n\n const { headers, status } = response\n const shapeId = headers.get(SHAPE_ID_HEADER)\n if (shapeId) {\n this.#shapeId = shapeId\n }\n\n const lastOffset = headers.get(CHUNK_LAST_OFFSET_HEADER)\n if (lastOffset) {\n this.#lastOffset = lastOffset as Offset\n }\n\n const getSchema = (): Schema => {\n const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER)\n return schemaHeader ? JSON.parse(schemaHeader) : {}\n }\n this.#schema = this.#schema ?? getSchema()\n\n const messages = status === 204 ? `[]` : await response.text()\n\n if (status === 204) {\n // There's no content so we are live and up to date\n this.#lastSyncedAt = Date.now()\n }\n\n const batch = this.#messageParser.parse(messages, this.#schema)\n\n // Update isUpToDate\n if (batch.length > 0) {\n const prevUpToDate = this.#isUpToDate\n const lastMessage = batch[batch.length - 1]\n if (isUpToDateMessage(lastMessage)) {\n this.#lastSyncedAt = Date.now()\n this.#isUpToDate = true\n }\n\n await this.#publish(batch)\n if (!prevUpToDate && this.#isUpToDate) {\n this.#notifyUpToDateSubscribers()\n }\n }\n }\n } finally {\n this.#connected = false\n }\n }\n\n subscribe(\n callback: (messages: Message<T>[]) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n\n this.#subscribers.set(subscriptionId, [callback, onError])\n\n return () => {\n this.#subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.#subscribers.clear()\n }\n\n subscribeOnceToUpToDate(\n callback: () => MaybePromise<void>,\n error: (err: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n\n this.#upToDateSubscribers.set(subscriptionId, [callback, error])\n\n return () => {\n this.#upToDateSubscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAllUpToDateSubscribers(): void {\n this.#upToDateSubscribers.clear()\n }\n\n /** Unix time at which we last synced. Undefined when `isLoading` is true. */\n lastSyncedAt(): number | undefined {\n return this.#lastSyncedAt\n }\n\n /** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */\n lastSynced(): number {\n if (this.#lastSyncedAt === undefined) return Infinity\n return Date.now() - this.#lastSyncedAt\n }\n\n /** Indicates if we are connected to the Electric sync service. */\n isConnected(): boolean {\n return this.#connected\n }\n\n /** True during initial fetch. False afterwise. */\n isLoading(): boolean {\n return !this.isUpToDate\n }\n\n async #publish(messages: Message<T>[]): Promise<void> {\n await Promise.all(\n Array.from(this.#subscribers.values()).map(async ([callback, __]) => {\n try {\n await callback(messages)\n } catch (err) {\n queueMicrotask(() => {\n throw err\n })\n }\n })\n )\n }\n\n #sendErrorToSubscribers(error: Error) {\n this.#subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n #notifyUpToDateSubscribers() {\n this.#upToDateSubscribers.forEach(([callback]) => {\n callback()\n })\n }\n\n #sendErrorToUpToDateSubscribers(error: FetchError | Error) {\n this.#upToDateSubscribers.forEach(([_, errorCallback]) =>\n errorCallback(error)\n )\n }\n\n /**\n * Resets the state of the stream, optionally with a provided\n * shape ID\n */\n #reset(shapeId?: string) {\n this.#lastOffset = `-1`\n this.#shapeId = shapeId\n this.#isUpToDate = false\n this.#connected = false\n this.#schema = undefined\n }\n}\n\nfunction validateOptions(options: Partial<ShapeStreamOptions>): void {\n if (!options.url) {\n throw new Error(`Invalid shape option. It must provide the url`)\n }\n if (options.signal && !(options.signal instanceof AbortSignal)) {\n throw new Error(\n `Invalid signal option. It must be an instance of AbortSignal.`\n )\n }\n\n if (\n options.offset !== undefined &&\n options.offset !== `-1` &&\n !options.shapeId\n ) {\n throw new Error(\n `shapeId is required if this isn't an initial fetch (i.e. offset > -1)`\n )\n }\n return\n}\n","import { Message, Row } from './types'\nimport { isChangeMessage, isControlMessage } from './helpers'\nimport { FetchError } from './error'\nimport { ShapeStreamInterface } from './client'\n\nexport type ShapeData<T extends Row = Row> = Map<string, T>\nexport type ShapeChangedCallback<T extends Row = Row> = (\n value: ShapeData<T>\n) => void\n\n/**\n * A Shape is an object that subscribes to a shape log,\n * keeps a materialised shape `.value` in memory and\n * notifies subscribers when the value has changed.\n *\n * It can be used without a framework and as a primitive\n * to simplify developing framework hooks.\n *\n * @constructor\n * @param {ShapeStream<T extends Row>} - the underlying shape stream\n * @example\n * ```\n * const shapeStream = new ShapeStream<{ foo: number }>(url: 'http://localhost:3000/v1/shape/foo'})\n * const shape = new Shape(shapeStream)\n * ```\n *\n * `value` returns a promise that resolves the Shape data once the Shape has been\n * fully loaded (and when resuming from being offline):\n *\n * const value = await shape.value\n *\n * `valueSync` returns the current data synchronously:\n *\n * const value = shape.valueSync\n *\n * Subscribe to updates. Called whenever the shape updates in Postgres.\n *\n * shape.subscribe(shapeData => {\n * console.log(shapeData)\n * })\n */\nexport class Shape<T extends Row = Row> {\n readonly #stream: ShapeStreamInterface<T>\n\n readonly #data: ShapeData<T> = new Map()\n readonly #subscribers = new Map<number, ShapeChangedCallback<T>>()\n\n #hasNotifiedSubscribersUpToDate: boolean = false\n #error: FetchError | false = false\n\n constructor(stream: ShapeStreamInterface<T>) {\n this.#stream = stream\n this.#stream.subscribe(\n this.#process.bind(this),\n this.#handleError.bind(this)\n )\n const unsubscribe = this.#stream.subscribeOnceToUpToDate(\n () => {\n unsubscribe()\n },\n (e) => {\n this.#handleError(e)\n throw e\n }\n )\n }\n\n get isUpToDate(): boolean {\n return this.#stream.isUpToDate\n }\n\n get value(): Promise<ShapeData<T>> {\n return new Promise((resolve, reject) => {\n if (this.#stream.isUpToDate) {\n resolve(this.valueSync)\n } else {\n const unsubscribe = this.subscribe((shapeData) => {\n unsubscribe()\n if (this.#error) reject(this.#error)\n resolve(shapeData)\n })\n }\n })\n }\n\n get valueSync() {\n return this.#data\n }\n\n get error() {\n return this.#error\n }\n\n /** Unix time at which we last synced. Undefined when `isLoading` is true. */\n lastSyncedAt(): number | undefined {\n return this.#stream.lastSyncedAt()\n }\n\n /** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */\n lastSynced() {\n return this.#stream.lastSynced()\n }\n\n /** True during initial fetch. False afterwise. */\n isLoading() {\n return this.#stream.isLoading()\n }\n\n /** Indicates if we are connected to the Electric sync service. */\n isConnected(): boolean {\n return this.#stream.isConnected()\n }\n\n subscribe(callback: ShapeChangedCallback<T>): () => void {\n const subscriptionId = Math.random()\n\n this.#subscribers.set(subscriptionId, callback)\n\n return () => {\n this.#subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.#subscribers.clear()\n }\n\n get numSubscribers() {\n return this.#subscribers.size\n }\n\n #process(messages: Message<T>[]): void {\n let dataMayHaveChanged = false\n let isUpToDate = false\n let newlyUpToDate = false\n\n messages.forEach((message) => {\n if (isChangeMessage(message)) {\n dataMayHaveChanged = [`insert`, `update`, `delete`].includes(\n message.headers.operation\n )\n\n switch (message.headers.operation) {\n case `insert`:\n this.#data.set(message.key, message.value)\n break\n case `update`:\n this.#data.set(message.key, {\n ...this.#data.get(message.key)!,\n ...message.value,\n })\n break\n case `delete`:\n this.#data.delete(message.key)\n break\n }\n }\n\n if (isControlMessage(message)) {\n switch (message.headers.control) {\n case `up-to-date`:\n isUpToDate = true\n if (!this.#hasNotifiedSubscribersUpToDate) {\n newlyUpToDate = true\n }\n break\n case `must-refetch`:\n this.#data.clear()\n this.#error = false\n isUpToDate = false\n newlyUpToDate = false\n break\n }\n }\n })\n\n // Always notify subscribers when the Shape first is up to date.\n // FIXME this would be cleaner with a simple state machine.\n if (newlyUpToDate || (isUpToDate && dataMayHaveChanged)) {\n this.#hasNotifiedSubscribersUpToDate = true\n this.#notify()\n }\n }\n\n #handleError(e: Error): void {\n if (e instanceof FetchError) {\n this.#error = e\n this.#notify()\n }\n }\n\n #notify(): void {\n this.#subscribers.forEach((callback) => {\n callback(this.valueSync)\n })\n }\n}\n"],"mappings":"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","names":["parseNumber","value","parseBool","parseBigInt","parseJson","identityParser","v","defaultParser","pgArrayParser","parser","i","char","str","quoted","last","p","loop","x","xs","MessageParser","__spreadValues","messages","schema","key","row","_b","columnInfo","_a","typ","dimensions","additionalInfo","__objRest","typeParser","makeNullableParser","_","columnName","isNullable","isPgNull","isChangeMessage","message","isControlMessage","isUpToDateMessage","FetchError","_FetchError","status","text","json","headers","url","message","response","__async","contentType","FetchBackoffAbortError","SHAPE_ID_HEADER","CHUNK_LAST_OFFSET_HEADER","CHUNK_UP_TO_DATE_HEADER","SHAPE_SCHEMA_HEADER","SHAPE_ID_QUERY_PARAM","OFFSET_QUERY_PARAM","WHERE_QUERY_PARAM","LIVE_QUERY_PARAM","BackoffDefaults","createFetchWithBackoff","fetchClient","backoffOptions","initialDelay","maxDelay","multiplier","debug","onFailedAttempt","args","__async","_a","url","options","delay","attempt","result","FetchError","e","FetchBackoffAbortError","resolve","ChunkPrefetchDefaults","createFetchWithChunkBuffer","prefetchOptions","maxChunksToPrefetch","prefetchQueue","prefetchedRequest","response","nextUrl","getNextChunkUrl","PrefetchQueue","_fetchClient","_maxPrefetchedRequests","_prefetchQueue","_queueHeadUrl","_queueTailUrl","_PrefetchQueue_instances","prefetch_fn","__privateAdd","__privateSet","__privateGet","__privateMethod","_","aborter","request","_b","__spreadProps","__spreadValues","chainAborter","res","shapeId","SHAPE_ID_HEADER","lastOffset","CHUNK_LAST_OFFSET_HEADER","isUpToDate","CHUNK_UP_TO_DATE_HEADER","LIVE_QUERY_PARAM","SHAPE_ID_QUERY_PARAM","OFFSET_QUERY_PARAM","sourceSignal","_fetchClient","_messageParser","_subscribers","_upToDateSubscribers","_lastOffset","_lastSyncedAt","_isUpToDate","_connected","_shapeId","_schema","_ShapeStream_instances","publish_fn","sendErrorToSubscribers_fn","notifyUpToDateSubscribers_fn","sendErrorToUpToDateSubscribers_fn","reset_fn","ShapeStream","options","__privateAdd","_a","_b","_c","validateOptions","__spreadValues","__privateSet","MessageParser","baseFetchClient","args","fetchWithBackoffClient","createFetchWithBackoff","__spreadProps","BackoffDefaults","createFetchWithChunkBuffer","__privateGet","__async","url","where","signal","fetchUrl","WHERE_QUERY_PARAM","OFFSET_QUERY_PARAM","LIVE_QUERY_PARAM","SHAPE_ID_QUERY_PARAM","response","e","FetchBackoffAbortError","FetchError","__privateMethod","newShapeId","SHAPE_ID_HEADER","headers","status","shapeId","lastOffset","CHUNK_LAST_OFFSET_HEADER","getSchema","schemaHeader","SHAPE_SCHEMA_HEADER","messages","batch","prevUpToDate","lastMessage","isUpToDateMessage","callback","onError","subscriptionId","error","_0","__","err","_","errorFn","errorCallback","_stream","_data","_subscribers","_hasNotifiedSubscribersUpToDate","_error","_Shape_instances","process_fn","handleError_fn","notify_fn","Shape","stream","__privateAdd","__privateSet","__privateGet","__privateMethod","unsubscribe","e","resolve","reject","shapeData","callback","subscriptionId","messages","dataMayHaveChanged","isUpToDate","newlyUpToDate","message","isChangeMessage","__spreadValues","isControlMessage","FetchError"]}
1
+ {"version":3,"sources":["../src/parser.ts","../src/helpers.ts","../src/error.ts","../src/constants.ts","../src/fetch.ts","../src/client.ts","../src/shape.ts"],"sourcesContent":["import { ColumnInfo, GetExtensions, Message, Row, Schema, Value } from './types'\n\ntype NullToken = null | `NULL`\ntype Token = Exclude<string, NullToken>\ntype NullableToken = Token | NullToken\nexport type ParseFunction<Extensions = never> = (\n value: Token,\n additionalInfo?: Omit<ColumnInfo, `type` | `dims`>\n) => Value<Extensions>\ntype NullableParseFunction<Extensions = never> = (\n value: NullableToken,\n additionalInfo?: Omit<ColumnInfo, `type` | `dims`>\n) => Value<Extensions>\n/**\n * @typeParam Extensions - Additional types that can be parsed by this parser beyond the standard SQL types.\n * Defaults to no additional types.\n */\nexport type Parser<Extensions = never> = {\n [key: string]: ParseFunction<Extensions>\n}\n\nconst parseNumber = (value: string) => Number(value)\nconst parseBool = (value: string) => value === `true` || value === `t`\nconst parseBigInt = (value: string) => BigInt(value)\nconst parseJson = (value: string) => JSON.parse(value)\nconst identityParser: ParseFunction = (v: string) => v\n\nexport const defaultParser: Parser = {\n int2: parseNumber,\n int4: parseNumber,\n int8: parseBigInt,\n bool: parseBool,\n float4: parseNumber,\n float8: parseNumber,\n json: parseJson,\n jsonb: parseJson,\n}\n\n// Taken from: https://github.com/electric-sql/pglite/blob/main/packages/pglite/src/types.ts#L233-L279\nexport function pgArrayParser<Extensions>(\n value: Token,\n parser?: ParseFunction<Extensions>\n): Value<Extensions> {\n let i = 0\n let char = null\n let str = ``\n let quoted = false\n let last = 0\n let p: string | undefined = undefined\n\n function loop(x: string): Array<Value<Extensions>> {\n const xs = []\n for (; i < x.length; i++) {\n char = x[i]\n if (quoted) {\n if (char === `\\\\`) {\n str += x[++i]\n } else if (char === `\"`) {\n xs.push(parser ? parser(str) : str)\n str = ``\n quoted = x[i + 1] === `\"`\n last = i + 2\n } else {\n str += char\n }\n } else if (char === `\"`) {\n quoted = true\n } else if (char === `{`) {\n last = ++i\n xs.push(loop(x))\n } else if (char === `}`) {\n quoted = false\n last < i &&\n xs.push(parser ? parser(x.slice(last, i)) : x.slice(last, i))\n last = i + 1\n break\n } else if (char === `,` && p !== `}` && p !== `\"`) {\n xs.push(parser ? parser(x.slice(last, i)) : x.slice(last, i))\n last = i + 1\n }\n p = char\n }\n last < i &&\n xs.push(parser ? parser(x.slice(last, i + 1)) : x.slice(last, i + 1))\n return xs\n }\n\n return loop(value)[0]\n}\n\nexport class MessageParser<T extends Row<unknown>> {\n private parser: Parser<GetExtensions<T>>\n constructor(parser?: Parser<GetExtensions<T>>) {\n // Merge the provided parser with the default parser\n // to use the provided parser whenever defined\n // and otherwise fall back to the default parser\n this.parser = { ...defaultParser, ...parser }\n }\n\n parse(messages: string, schema: Schema): Message<T>[] {\n return JSON.parse(messages, (key, value) => {\n // typeof value === `object` && value !== null\n // is needed because there could be a column named `value`\n // and the value associated to that column will be a string or null.\n // But `typeof null === 'object'` so we need to make an explicit check.\n if (key === `value` && typeof value === `object` && value !== null) {\n // Parse the row values\n const row = value as Record<string, Value<GetExtensions<T>>>\n Object.keys(row).forEach((key) => {\n row[key] = this.parseRow(key, row[key] as NullableToken, schema)\n })\n }\n return value\n }) as Message<T>[]\n }\n\n // Parses the message values using the provided parser based on the schema information\n private parseRow(\n key: string,\n value: NullableToken,\n schema: Schema\n ): Value<GetExtensions<T>> {\n const columnInfo = schema[key]\n if (!columnInfo) {\n // We don't have information about the value\n // so we just return it\n return value\n }\n\n // Copy the object but don't include `dimensions` and `type`\n const { type: typ, dims: dimensions, ...additionalInfo } = columnInfo\n\n // Pick the right parser for the type\n // and support parsing null values if needed\n // if no parser is provided for the given type, just return the value as is\n const typeParser = this.parser[typ] ?? identityParser\n const parser = makeNullableParser(typeParser, columnInfo, key)\n\n if (dimensions && dimensions > 0) {\n // It's an array\n const nullablePgArrayParser = makeNullableParser(\n (value, _) => pgArrayParser(value, parser),\n columnInfo,\n key\n )\n return nullablePgArrayParser(value)\n }\n\n return parser(value, additionalInfo)\n }\n}\n\nfunction makeNullableParser<Extensions>(\n parser: ParseFunction<Extensions>,\n columnInfo: ColumnInfo,\n columnName?: string\n): NullableParseFunction<Extensions> {\n const isNullable = !(columnInfo.not_null ?? false)\n // The sync service contains `null` value for a column whose value is NULL\n // but if the column value is an array that contains a NULL value\n // then it will be included in the array string as `NULL`, e.g.: `\"{1,NULL,3}\"`\n return (value: NullableToken) => {\n if (isPgNull(value)) {\n if (!isNullable) {\n throw new Error(`Column ${columnName ?? `unknown`} is not nullable`)\n }\n return null\n }\n return parser(value, columnInfo)\n }\n}\n\nfunction isPgNull(value: NullableToken): value is NullToken {\n return value === null || value === `NULL`\n}\n","import { ChangeMessage, ControlMessage, Message, Row } from './types'\n\n/**\n * Type guard for checking {@link Message} is {@link ChangeMessage}.\n *\n * See [TS docs](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards)\n * for information on how to use type guards.\n *\n * @param message - the message to check\n * @returns true if the message is a {@link ChangeMessage}\n *\n * @example\n * ```ts\n * if (isChangeMessage(message)) {\n * const msgChng: ChangeMessage = message // Ok\n * const msgCtrl: ControlMessage = message // Err, type mismatch\n * }\n * ```\n */\nexport function isChangeMessage<T extends Row<unknown> = Row>(\n message: Message<T>\n): message is ChangeMessage<T> {\n return `key` in message\n}\n\n/**\n * Type guard for checking {@link Message} is {@link ControlMessage}.\n *\n * See [TS docs](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards)\n * for information on how to use type guards.\n *\n * @param message - the message to check\n * @returns true if the message is a {@link ControlMessage}\n *\n * * @example\n * ```ts\n * if (isControlMessage(message)) {\n * const msgChng: ChangeMessage = message // Err, type mismatch\n * const msgCtrl: ControlMessage = message // Ok\n * }\n * ```\n */\nexport function isControlMessage<T extends Row<unknown> = Row>(\n message: Message<T>\n): message is ControlMessage {\n return !isChangeMessage(message)\n}\n\nexport function isUpToDateMessage<T extends Row<unknown> = Row>(\n message: Message<T>\n): message is ControlMessage & { up_to_date: true } {\n return isControlMessage(message) && message.headers.control === `up-to-date`\n}\n","export class FetchError extends Error {\n status: number\n text?: string\n json?: object\n headers: Record<string, string>\n\n constructor(\n status: number,\n text: string | undefined,\n json: object | undefined,\n headers: Record<string, string>,\n public url: string,\n message?: string\n ) {\n super(\n message ||\n `HTTP Error ${status} at ${url}: ${text ?? JSON.stringify(json)}`\n )\n this.name = `FetchError`\n this.status = status\n this.text = text\n this.json = json\n this.headers = headers\n }\n\n static async fromResponse(\n response: Response,\n url: string\n ): Promise<FetchError> {\n const status = response.status\n const headers = Object.fromEntries([...response.headers.entries()])\n let text: string | undefined = undefined\n let json: object | undefined = undefined\n\n const contentType = response.headers.get(`content-type`)\n if (contentType && contentType.includes(`application/json`)) {\n json = (await response.json()) as object\n } else {\n text = await response.text()\n }\n\n return new FetchError(status, text, json, headers, url)\n }\n}\n\nexport class FetchBackoffAbortError extends Error {\n constructor() {\n super(`Fetch with backoff aborted`)\n }\n}\n","export const SHAPE_ID_HEADER = `electric-shape-id`\nexport const LIVE_CACHE_BUSTER_HEADER = `electric-next-cursor`\nexport const LIVE_CACHE_BUSTER_QUERY_PARAM = `cursor`\nexport const CHUNK_LAST_OFFSET_HEADER = `electric-chunk-last-offset`\nexport const CHUNK_UP_TO_DATE_HEADER = `electric-chunk-up-to-date`\nexport const SHAPE_SCHEMA_HEADER = `electric-schema`\nexport const SHAPE_ID_QUERY_PARAM = `shape_id`\nexport const OFFSET_QUERY_PARAM = `offset`\nexport const WHERE_QUERY_PARAM = `where`\nexport const LIVE_QUERY_PARAM = `live`\n","import {\n CHUNK_LAST_OFFSET_HEADER,\n CHUNK_UP_TO_DATE_HEADER,\n LIVE_QUERY_PARAM,\n OFFSET_QUERY_PARAM,\n SHAPE_ID_HEADER,\n SHAPE_ID_QUERY_PARAM,\n} from './constants'\nimport { FetchError, FetchBackoffAbortError } from './error'\n\n// Some specific 4xx and 5xx HTTP status codes that we definitely\n// want to retry\nconst HTTP_RETRY_STATUS_CODES = [429]\n\nexport interface BackoffOptions {\n /**\n * Initial delay before retrying in milliseconds\n */\n initialDelay: number\n /**\n * Maximum retry delay in milliseconds\n */\n maxDelay: number\n multiplier: number\n onFailedAttempt?: () => void\n debug?: boolean\n}\n\nexport const BackoffDefaults = {\n initialDelay: 100,\n maxDelay: 10_000,\n multiplier: 1.3,\n}\n\nexport function createFetchWithBackoff(\n fetchClient: typeof fetch,\n backoffOptions: BackoffOptions = BackoffDefaults\n): typeof fetch {\n const {\n initialDelay,\n maxDelay,\n multiplier,\n debug = false,\n onFailedAttempt,\n } = backoffOptions\n return async (...args: Parameters<typeof fetch>): Promise<Response> => {\n const url = args[0]\n const options = args[1]\n\n let delay = initialDelay\n let attempt = 0\n\n /* eslint-disable no-constant-condition -- we re-fetch the shape log\n * continuously until we get a non-ok response. For recoverable errors,\n * we retry the fetch with exponential backoff. Users can pass in an\n * AbortController to abort the fetching an any point.\n * */\n while (true) {\n /* eslint-enable no-constant-condition */\n try {\n const result = await fetchClient(...args)\n if (result.ok) return result\n else throw await FetchError.fromResponse(result, url.toString())\n } catch (e) {\n onFailedAttempt?.()\n if (options?.signal?.aborted) {\n throw new FetchBackoffAbortError()\n } else if (\n e instanceof FetchError &&\n !HTTP_RETRY_STATUS_CODES.includes(e.status) &&\n e.status >= 400 &&\n e.status < 500\n ) {\n // Any client errors cannot be backed off on, leave it to the caller to handle.\n throw e\n } else {\n // Exponentially backoff on errors.\n // Wait for the current delay duration\n await new Promise((resolve) => setTimeout(resolve, delay))\n\n // Increase the delay for the next attempt\n delay = Math.min(delay * multiplier, maxDelay)\n\n if (debug) {\n attempt++\n console.log(`Retry attempt #${attempt} after ${delay}ms`)\n }\n }\n }\n }\n }\n}\n\ninterface ChunkPrefetchOptions {\n maxChunksToPrefetch: number\n}\n\nconst ChunkPrefetchDefaults = {\n maxChunksToPrefetch: 2,\n}\n\n/**\n * Creates a fetch client that prefetches subsequent log chunks for\n * consumption by the shape stream without waiting for the chunk bodies\n * themselves to be loaded.\n *\n * @param fetchClient the client to wrap\n * @param prefetchOptions options to configure prefetching\n * @returns wrapped client with prefetch capabilities\n */\nexport function createFetchWithChunkBuffer(\n fetchClient: typeof fetch,\n prefetchOptions: ChunkPrefetchOptions = ChunkPrefetchDefaults\n): typeof fetch {\n const { maxChunksToPrefetch } = prefetchOptions\n\n let prefetchQueue: PrefetchQueue\n\n const prefetchClient = async (...args: Parameters<typeof fetchClient>) => {\n const url = args[0].toString()\n\n // try to consume from the prefetch queue first, and if request is\n // not present abort the prefetch queue as it must no longer be valid\n const prefetchedRequest = prefetchQueue?.consume(...args)\n if (prefetchedRequest) {\n return prefetchedRequest\n }\n\n prefetchQueue?.abort()\n\n // perform request and fire off prefetch queue if request is eligible\n const response = await fetchClient(...args)\n const nextUrl = getNextChunkUrl(url, response)\n if (nextUrl) {\n prefetchQueue = new PrefetchQueue({\n fetchClient,\n maxPrefetchedRequests: maxChunksToPrefetch,\n url: nextUrl,\n requestInit: args[1],\n })\n }\n\n return response\n }\n\n return prefetchClient\n}\n\nclass PrefetchQueue {\n readonly #fetchClient: typeof fetch\n readonly #maxPrefetchedRequests: number\n readonly #prefetchQueue = new Map<\n string,\n [Promise<Response>, AbortController]\n >()\n #queueHeadUrl: string | void\n #queueTailUrl: string\n\n constructor(options: {\n url: Parameters<typeof fetch>[0]\n requestInit: Parameters<typeof fetch>[1]\n maxPrefetchedRequests: number\n fetchClient?: typeof fetch\n }) {\n this.#fetchClient =\n options.fetchClient ??\n ((...args: Parameters<typeof fetch>) => fetch(...args))\n this.#maxPrefetchedRequests = options.maxPrefetchedRequests\n this.#queueHeadUrl = options.url.toString()\n this.#queueTailUrl = this.#queueHeadUrl\n this.#prefetch(options.url, options.requestInit)\n }\n\n abort(): void {\n this.#prefetchQueue.forEach(([_, aborter]) => aborter.abort())\n }\n\n consume(...args: Parameters<typeof fetch>): Promise<Response> | void {\n const url = args[0].toString()\n\n const request = this.#prefetchQueue.get(url)?.[0]\n // only consume if request is in queue and is the queue \"head\"\n // if request is in the queue but not the head, the queue is being\n // consumed out of order and should be restarted\n if (!request || url !== this.#queueHeadUrl) return\n this.#prefetchQueue.delete(url)\n\n // fire off new prefetch since request has been consumed\n request\n .then((response) => {\n const nextUrl = getNextChunkUrl(url, response)\n this.#queueHeadUrl = nextUrl\n if (!this.#prefetchQueue.has(this.#queueTailUrl)) {\n this.#prefetch(this.#queueTailUrl, args[1])\n }\n })\n .catch(() => {})\n\n return request\n }\n\n #prefetch(...args: Parameters<typeof fetch>): void {\n const url = args[0].toString()\n\n // only prefetch when queue is not full\n if (this.#prefetchQueue.size >= this.#maxPrefetchedRequests) return\n\n // initialize aborter per request, to avoid aborting consumed requests that\n // are still streaming their bodies to the consumer\n const aborter = new AbortController()\n\n try {\n const request = this.#fetchClient(url, {\n ...(args[1] ?? {}),\n signal: chainAborter(aborter, args[1]?.signal),\n })\n this.#prefetchQueue.set(url, [request, aborter])\n request\n .then((response) => {\n // only keep prefetching if response chain is uninterrupted\n if (!response.ok || aborter.signal.aborted) return\n\n const nextUrl = getNextChunkUrl(url, response)\n\n // only prefetch when there is a next URL\n if (!nextUrl || nextUrl === url) return\n\n this.#queueTailUrl = nextUrl\n return this.#prefetch(nextUrl, args[1])\n })\n .catch(() => {})\n } catch (_) {\n // ignore prefetch errors\n }\n }\n}\n\n/**\n * Generate the next chunk's URL if the url and response are valid\n */\nfunction getNextChunkUrl(url: string, res: Response): string | void {\n const shapeId = res.headers.get(SHAPE_ID_HEADER)\n const lastOffset = res.headers.get(CHUNK_LAST_OFFSET_HEADER)\n const isUpToDate = res.headers.has(CHUNK_UP_TO_DATE_HEADER)\n\n // only prefetch if shape ID and offset for next chunk are available, and\n // response is not already up-to-date\n if (!shapeId || !lastOffset || isUpToDate) return\n\n const nextUrl = new URL(url)\n\n // don't prefetch live requests, rushing them will only\n // potentially miss more recent data\n if (nextUrl.searchParams.has(LIVE_QUERY_PARAM)) return\n\n nextUrl.searchParams.set(SHAPE_ID_QUERY_PARAM, shapeId)\n nextUrl.searchParams.set(OFFSET_QUERY_PARAM, lastOffset)\n return nextUrl.toString()\n}\n\n/**\n * Chains an abort controller on an optional source signal's\n * aborted state - if the source signal is aborted, the provided abort\n * controller will also abort\n */\nfunction chainAborter(\n aborter: AbortController,\n sourceSignal?: AbortSignal\n): AbortSignal {\n if (!sourceSignal) return aborter.signal\n if (sourceSignal.aborted) aborter.abort()\n else\n sourceSignal.addEventListener(`abort`, () => aborter.abort(), {\n once: true,\n })\n return aborter.signal\n}\n","import {\n Message,\n Offset,\n Schema,\n Row,\n MaybePromise,\n GetExtensions,\n} from './types'\nimport { MessageParser, Parser } from './parser'\nimport { isUpToDateMessage } from './helpers'\nimport { FetchError, FetchBackoffAbortError } from './error'\nimport {\n BackoffDefaults,\n BackoffOptions,\n createFetchWithBackoff,\n createFetchWithChunkBuffer,\n} from './fetch'\nimport {\n CHUNK_LAST_OFFSET_HEADER,\n LIVE_CACHE_BUSTER_HEADER,\n LIVE_CACHE_BUSTER_QUERY_PARAM,\n LIVE_QUERY_PARAM,\n OFFSET_QUERY_PARAM,\n SHAPE_ID_HEADER,\n SHAPE_ID_QUERY_PARAM,\n SHAPE_SCHEMA_HEADER,\n WHERE_QUERY_PARAM,\n} from './constants'\n\n/**\n * Options for constructing a ShapeStream.\n */\nexport interface ShapeStreamOptions<T = never> {\n /**\n * The full URL to where the Shape is hosted. This can either be the Electric server\n * directly or a proxy. E.g. for a local Electric instance, you might set `http://localhost:3000/v1/shape/foo`\n */\n url: string\n /**\n * where clauses for the shape.\n */\n where?: string\n /**\n * The \"offset\" on the shape log. This is typically not set as the ShapeStream\n * will handle this automatically. A common scenario where you might pass an offset\n * is if you're maintaining a local cache of the log. If you've gone offline\n * and are re-starting a ShapeStream to catch-up to the latest state of the Shape,\n * you'd pass in the last offset and shapeId you'd seen from the Electric server\n * so it knows at what point in the shape to catch you up from.\n */\n offset?: Offset\n /**\n * Similar to `offset`, this isn't typically used unless you're maintaining\n * a cache of the shape log.\n */\n shapeId?: string\n backoffOptions?: BackoffOptions\n\n /**\n * HTTP headers to attach to requests made by the client.\n * Can be used for adding authentication headers.\n */\n headers?: Record<string, string>\n\n /**\n * Automatically fetch updates to the Shape. If you just want to sync the current\n * shape and stop, pass false.\n */\n subscribe?: boolean\n signal?: AbortSignal\n fetchClient?: typeof fetch\n parser?: Parser<T>\n}\n\nexport interface ShapeStreamInterface<T extends Row<unknown> = Row> {\n subscribe(\n callback: (messages: Message<T>[]) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ): void\n unsubscribeAllUpToDateSubscribers(): void\n unsubscribeAll(): void\n subscribeOnceToUpToDate(\n callback: () => MaybePromise<void>,\n error: (err: FetchError | Error) => void\n ): () => void\n\n isLoading(): boolean\n lastSyncedAt(): number | undefined\n lastSynced(): number\n isConnected(): boolean\n\n isUpToDate: boolean\n shapeId?: string\n}\n\n/**\n * Reads updates to a shape from Electric using HTTP requests and long polling. Notifies subscribers\n * when new messages come in. Doesn't maintain any history of the\n * log but does keep track of the offset position and is the best way\n * to consume the HTTP `GET /v1/shape` api.\n *\n * @constructor\n * @param {ShapeStreamOptions} options - configure the shape stream\n * @example\n * Register a callback function to subscribe to the messages.\n * ```\n * const stream = new ShapeStream(options)\n * stream.subscribe(messages => {\n * // messages is 1 or more row updates\n * })\n * ```\n *\n * To abort the stream, abort the `signal`\n * passed in via the `ShapeStreamOptions`.\n * ```\n * const aborter = new AbortController()\n * const issueStream = new ShapeStream({\n * url: `${BASE_URL}/${table}`\n * subscribe: true,\n * signal: aborter.signal,\n * })\n * // Later...\n * aborter.abort()\n * ```\n */\n\nexport class ShapeStream<T extends Row<unknown> = Row>\n implements ShapeStreamInterface<T>\n{\n readonly options: ShapeStreamOptions<GetExtensions<T>>\n\n readonly #fetchClient: typeof fetch\n readonly #messageParser: MessageParser<T>\n\n readonly #subscribers = new Map<\n number,\n [\n (messages: Message<T>[]) => MaybePromise<void>,\n ((error: Error) => void) | undefined,\n ]\n >()\n readonly #upToDateSubscribers = new Map<\n number,\n [() => void, (error: FetchError | Error) => void]\n >()\n\n #lastOffset: Offset\n #liveCacheBuster: string // Seconds since our Electric Epoch 😎\n #lastSyncedAt?: number // unix time\n #isUpToDate: boolean = false\n #connected: boolean = false\n #shapeId?: string\n #schema?: Schema\n\n constructor(options: ShapeStreamOptions<GetExtensions<T>>) {\n validateOptions(options)\n this.options = { subscribe: true, ...options }\n this.#lastOffset = this.options.offset ?? `-1`\n this.#liveCacheBuster = ``\n this.#shapeId = this.options.shapeId\n this.#messageParser = new MessageParser<T>(options.parser)\n\n const baseFetchClient =\n options.fetchClient ??\n ((...args: Parameters<typeof fetch>) => fetch(...args))\n\n const fetchWithBackoffClient = createFetchWithBackoff(baseFetchClient, {\n ...(options.backoffOptions ?? BackoffDefaults),\n onFailedAttempt: () => {\n this.#connected = false\n options.backoffOptions?.onFailedAttempt?.()\n },\n })\n\n this.#fetchClient = createFetchWithChunkBuffer(fetchWithBackoffClient)\n\n this.start()\n }\n\n get shapeId() {\n return this.#shapeId\n }\n\n get isUpToDate() {\n return this.#isUpToDate\n }\n\n async start() {\n this.#isUpToDate = false\n\n const { url, where, signal } = this.options\n\n try {\n while (\n (!signal?.aborted && !this.#isUpToDate) ||\n this.options.subscribe\n ) {\n const fetchUrl = new URL(url)\n if (where) fetchUrl.searchParams.set(WHERE_QUERY_PARAM, where)\n fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, this.#lastOffset)\n\n if (this.#isUpToDate) {\n fetchUrl.searchParams.set(LIVE_QUERY_PARAM, `true`)\n fetchUrl.searchParams.set(\n LIVE_CACHE_BUSTER_QUERY_PARAM,\n this.#liveCacheBuster\n )\n }\n\n if (this.#shapeId) {\n // This should probably be a header for better cache breaking?\n fetchUrl.searchParams.set(SHAPE_ID_QUERY_PARAM, this.#shapeId!)\n }\n\n let response!: Response\n try {\n response = await this.#fetchClient(fetchUrl.toString(), {\n signal,\n headers: this.options.headers,\n })\n this.#connected = true\n } catch (e) {\n if (e instanceof FetchBackoffAbortError) break // interrupted\n if (!(e instanceof FetchError)) throw e // should never happen\n if (e.status == 400) {\n // The request is invalid, most likely because the shape has been deleted.\n // We should start from scratch, this will force the shape to be recreated.\n this.#reset()\n await this.#publish(e.json as Message<T>[])\n continue\n } else if (e.status == 409) {\n // Upon receiving a 409, we should start from scratch\n // with the newly provided shape ID\n const newShapeId = e.headers[SHAPE_ID_HEADER]\n this.#reset(newShapeId)\n await this.#publish(e.json as Message<T>[])\n continue\n } else if (e.status >= 400 && e.status < 500) {\n // Notify subscribers\n this.#sendErrorToUpToDateSubscribers(e)\n this.#sendErrorToSubscribers(e)\n\n // 400 errors are not actionable without additional user input, so we're throwing them.\n throw e\n }\n }\n\n const { headers, status } = response\n const shapeId = headers.get(SHAPE_ID_HEADER)\n if (shapeId) {\n this.#shapeId = shapeId\n }\n\n const lastOffset = headers.get(CHUNK_LAST_OFFSET_HEADER)\n if (lastOffset) {\n this.#lastOffset = lastOffset as Offset\n }\n\n const liveCacheBuster = headers.get(LIVE_CACHE_BUSTER_HEADER)\n if (liveCacheBuster) {\n this.#liveCacheBuster = liveCacheBuster\n }\n\n const getSchema = (): Schema => {\n const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER)\n return schemaHeader ? JSON.parse(schemaHeader) : {}\n }\n this.#schema = this.#schema ?? getSchema()\n\n const messages = status === 204 ? `[]` : await response.text()\n\n if (status === 204) {\n // There's no content so we are live and up to date\n this.#lastSyncedAt = Date.now()\n }\n\n const batch = this.#messageParser.parse(messages, this.#schema)\n\n // Update isUpToDate\n if (batch.length > 0) {\n const prevUpToDate = this.#isUpToDate\n const lastMessage = batch[batch.length - 1]\n if (isUpToDateMessage(lastMessage)) {\n this.#lastSyncedAt = Date.now()\n this.#isUpToDate = true\n }\n\n await this.#publish(batch)\n if (!prevUpToDate && this.#isUpToDate) {\n this.#notifyUpToDateSubscribers()\n }\n }\n }\n } finally {\n this.#connected = false\n }\n }\n\n subscribe(\n callback: (messages: Message<T>[]) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n\n this.#subscribers.set(subscriptionId, [callback, onError])\n\n return () => {\n this.#subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.#subscribers.clear()\n }\n\n subscribeOnceToUpToDate(\n callback: () => MaybePromise<void>,\n error: (err: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n\n this.#upToDateSubscribers.set(subscriptionId, [callback, error])\n\n return () => {\n this.#upToDateSubscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAllUpToDateSubscribers(): void {\n this.#upToDateSubscribers.clear()\n }\n\n /** Unix time at which we last synced. Undefined when `isLoading` is true. */\n lastSyncedAt(): number | undefined {\n return this.#lastSyncedAt\n }\n\n /** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */\n lastSynced(): number {\n if (this.#lastSyncedAt === undefined) return Infinity\n return Date.now() - this.#lastSyncedAt\n }\n\n /** Indicates if we are connected to the Electric sync service. */\n isConnected(): boolean {\n return this.#connected\n }\n\n /** True during initial fetch. False afterwise. */\n isLoading(): boolean {\n return !this.isUpToDate\n }\n\n async #publish(messages: Message<T>[]): Promise<void> {\n await Promise.all(\n Array.from(this.#subscribers.values()).map(async ([callback, __]) => {\n try {\n await callback(messages)\n } catch (err) {\n queueMicrotask(() => {\n throw err\n })\n }\n })\n )\n }\n\n #sendErrorToSubscribers(error: Error) {\n this.#subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n #notifyUpToDateSubscribers() {\n this.#upToDateSubscribers.forEach(([callback]) => {\n callback()\n })\n }\n\n #sendErrorToUpToDateSubscribers(error: FetchError | Error) {\n this.#upToDateSubscribers.forEach(([_, errorCallback]) =>\n errorCallback(error)\n )\n }\n\n /**\n * Resets the state of the stream, optionally with a provided\n * shape ID\n */\n #reset(shapeId?: string) {\n this.#lastOffset = `-1`\n this.#liveCacheBuster = ``\n this.#shapeId = shapeId\n this.#isUpToDate = false\n this.#connected = false\n this.#schema = undefined\n }\n}\n\nfunction validateOptions<T>(options: Partial<ShapeStreamOptions<T>>): void {\n if (!options.url) {\n throw new Error(`Invalid shape option. It must provide the url`)\n }\n if (options.signal && !(options.signal instanceof AbortSignal)) {\n throw new Error(\n `Invalid signal option. It must be an instance of AbortSignal.`\n )\n }\n\n if (\n options.offset !== undefined &&\n options.offset !== `-1` &&\n !options.shapeId\n ) {\n throw new Error(\n `shapeId is required if this isn't an initial fetch (i.e. offset > -1)`\n )\n }\n return\n}\n","import { Message, Row } from './types'\nimport { isChangeMessage, isControlMessage } from './helpers'\nimport { FetchError } from './error'\nimport { ShapeStreamInterface } from './client'\n\nexport type ShapeData<T extends Row<unknown> = Row> = Map<string, T>\nexport type ShapeChangedCallback<T extends Row<unknown> = Row> = (\n value: ShapeData<T>\n) => void\n\n/**\n * A Shape is an object that subscribes to a shape log,\n * keeps a materialised shape `.value` in memory and\n * notifies subscribers when the value has changed.\n *\n * It can be used without a framework and as a primitive\n * to simplify developing framework hooks.\n *\n * @constructor\n * @param {ShapeStream<T extends Row>} - the underlying shape stream\n * @example\n * ```\n * const shapeStream = new ShapeStream<{ foo: number }>(url: 'http://localhost:3000/v1/shape/foo'})\n * const shape = new Shape(shapeStream)\n * ```\n *\n * `value` returns a promise that resolves the Shape data once the Shape has been\n * fully loaded (and when resuming from being offline):\n *\n * const value = await shape.value\n *\n * `valueSync` returns the current data synchronously:\n *\n * const value = shape.valueSync\n *\n * Subscribe to updates. Called whenever the shape updates in Postgres.\n *\n * shape.subscribe(shapeData => {\n * console.log(shapeData)\n * })\n */\nexport class Shape<T extends Row<unknown> = Row> {\n readonly #stream: ShapeStreamInterface<T>\n\n readonly #data: ShapeData<T> = new Map()\n readonly #subscribers = new Map<number, ShapeChangedCallback<T>>()\n\n #hasNotifiedSubscribersUpToDate: boolean = false\n #error: FetchError | false = false\n\n constructor(stream: ShapeStreamInterface<T>) {\n this.#stream = stream\n this.#stream.subscribe(\n this.#process.bind(this),\n this.#handleError.bind(this)\n )\n const unsubscribe = this.#stream.subscribeOnceToUpToDate(\n () => {\n unsubscribe()\n },\n (e) => {\n this.#handleError(e)\n throw e\n }\n )\n }\n\n get isUpToDate(): boolean {\n return this.#stream.isUpToDate\n }\n\n get value(): Promise<ShapeData<T>> {\n return new Promise((resolve, reject) => {\n if (this.#stream.isUpToDate) {\n resolve(this.valueSync)\n } else {\n const unsubscribe = this.subscribe((shapeData) => {\n unsubscribe()\n if (this.#error) reject(this.#error)\n resolve(shapeData)\n })\n }\n })\n }\n\n get valueSync() {\n return this.#data\n }\n\n get error() {\n return this.#error\n }\n\n /** Unix time at which we last synced. Undefined when `isLoading` is true. */\n lastSyncedAt(): number | undefined {\n return this.#stream.lastSyncedAt()\n }\n\n /** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */\n lastSynced() {\n return this.#stream.lastSynced()\n }\n\n /** True during initial fetch. False afterwise. */\n isLoading() {\n return this.#stream.isLoading()\n }\n\n /** Indicates if we are connected to the Electric sync service. */\n isConnected(): boolean {\n return this.#stream.isConnected()\n }\n\n subscribe(callback: ShapeChangedCallback<T>): () => void {\n const subscriptionId = Math.random()\n\n this.#subscribers.set(subscriptionId, callback)\n\n return () => {\n this.#subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.#subscribers.clear()\n }\n\n get numSubscribers() {\n return this.#subscribers.size\n }\n\n #process(messages: Message<T>[]): void {\n let dataMayHaveChanged = false\n let isUpToDate = false\n let newlyUpToDate = false\n\n messages.forEach((message) => {\n if (isChangeMessage(message)) {\n dataMayHaveChanged = [`insert`, `update`, `delete`].includes(\n message.headers.operation\n )\n\n switch (message.headers.operation) {\n case `insert`:\n this.#data.set(message.key, message.value)\n break\n case `update`:\n this.#data.set(message.key, {\n ...this.#data.get(message.key)!,\n ...message.value,\n })\n break\n case `delete`:\n this.#data.delete(message.key)\n break\n }\n }\n\n if (isControlMessage(message)) {\n switch (message.headers.control) {\n case `up-to-date`:\n isUpToDate = true\n if (!this.#hasNotifiedSubscribersUpToDate) {\n newlyUpToDate = true\n }\n break\n case `must-refetch`:\n this.#data.clear()\n this.#error = false\n isUpToDate = false\n newlyUpToDate = false\n break\n }\n }\n })\n\n // Always notify subscribers when the Shape first is up to date.\n // FIXME this would be cleaner with a simple state machine.\n if (newlyUpToDate || (isUpToDate && dataMayHaveChanged)) {\n this.#hasNotifiedSubscribersUpToDate = true\n this.#notify()\n }\n }\n\n #handleError(e: Error): void {\n if (e instanceof FetchError) {\n this.#error = e\n this.#notify()\n }\n }\n\n #notify(): void {\n this.#subscribers.forEach((callback) => {\n callback(this.valueSync)\n })\n }\n}\n"],"mappings":"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","names":["parseNumber","value","parseBool","parseBigInt","parseJson","identityParser","v","defaultParser","pgArrayParser","parser","i","char","str","quoted","last","p","loop","x","xs","MessageParser","__spreadValues","messages","schema","key","row","_b","columnInfo","_a","typ","dimensions","additionalInfo","__objRest","typeParser","makeNullableParser","_","columnName","isNullable","isPgNull","isChangeMessage","message","isControlMessage","isUpToDateMessage","FetchError","_FetchError","status","text","json","headers","url","message","response","__async","contentType","FetchBackoffAbortError","SHAPE_ID_HEADER","LIVE_CACHE_BUSTER_HEADER","LIVE_CACHE_BUSTER_QUERY_PARAM","CHUNK_LAST_OFFSET_HEADER","CHUNK_UP_TO_DATE_HEADER","SHAPE_SCHEMA_HEADER","SHAPE_ID_QUERY_PARAM","OFFSET_QUERY_PARAM","WHERE_QUERY_PARAM","LIVE_QUERY_PARAM","HTTP_RETRY_STATUS_CODES","BackoffDefaults","createFetchWithBackoff","fetchClient","backoffOptions","initialDelay","maxDelay","multiplier","debug","onFailedAttempt","args","__async","_a","url","options","delay","attempt","result","FetchError","e","FetchBackoffAbortError","resolve","ChunkPrefetchDefaults","createFetchWithChunkBuffer","prefetchOptions","maxChunksToPrefetch","prefetchQueue","prefetchedRequest","response","nextUrl","getNextChunkUrl","PrefetchQueue","_fetchClient","_maxPrefetchedRequests","_prefetchQueue","_queueHeadUrl","_queueTailUrl","_PrefetchQueue_instances","prefetch_fn","__privateAdd","__privateSet","__privateGet","__privateMethod","_","aborter","request","_b","__spreadProps","__spreadValues","chainAborter","res","shapeId","SHAPE_ID_HEADER","lastOffset","CHUNK_LAST_OFFSET_HEADER","isUpToDate","CHUNK_UP_TO_DATE_HEADER","LIVE_QUERY_PARAM","SHAPE_ID_QUERY_PARAM","OFFSET_QUERY_PARAM","sourceSignal","_fetchClient","_messageParser","_subscribers","_upToDateSubscribers","_lastOffset","_liveCacheBuster","_lastSyncedAt","_isUpToDate","_connected","_shapeId","_schema","_ShapeStream_instances","publish_fn","sendErrorToSubscribers_fn","notifyUpToDateSubscribers_fn","sendErrorToUpToDateSubscribers_fn","reset_fn","ShapeStream","options","__privateAdd","_a","_b","_c","validateOptions","__spreadValues","__privateSet","MessageParser","baseFetchClient","args","fetchWithBackoffClient","createFetchWithBackoff","__spreadProps","BackoffDefaults","createFetchWithChunkBuffer","__privateGet","__async","url","where","signal","fetchUrl","WHERE_QUERY_PARAM","OFFSET_QUERY_PARAM","LIVE_QUERY_PARAM","LIVE_CACHE_BUSTER_QUERY_PARAM","SHAPE_ID_QUERY_PARAM","response","e","FetchBackoffAbortError","FetchError","__privateMethod","newShapeId","SHAPE_ID_HEADER","headers","status","shapeId","lastOffset","CHUNK_LAST_OFFSET_HEADER","liveCacheBuster","LIVE_CACHE_BUSTER_HEADER","getSchema","schemaHeader","SHAPE_SCHEMA_HEADER","messages","batch","prevUpToDate","lastMessage","isUpToDateMessage","callback","onError","subscriptionId","error","_0","__","err","_","errorFn","errorCallback","_stream","_data","_subscribers","_hasNotifiedSubscribersUpToDate","_error","_Shape_instances","process_fn","handleError_fn","notify_fn","Shape","stream","__privateAdd","__privateSet","__privateGet","__privateMethod","unsubscribe","e","resolve","reject","shapeData","callback","subscriptionId","messages","dataMayHaveChanged","isUpToDate","newlyUpToDate","message","isChangeMessage","__spreadValues","isControlMessage","FetchError"]}
package/dist/index.d.ts CHANGED
@@ -1,9 +1,12 @@
1
- type Value = string | number | boolean | bigint | null | Value[] | {
2
- [key: string]: Value;
3
- };
4
- type Row = {
5
- [key: string]: Value;
1
+ /**
2
+ * Default types for SQL but can be extended with additional types when using a custom parser.
3
+ * @typeParam Extensions - Additional value types.
4
+ */
5
+ type Value<Extensions = never> = string | number | boolean | bigint | null | Extensions | Value<Extensions>[] | {
6
+ [key: string]: Value<Extensions>;
6
7
  };
8
+ type Row<Extensions = never> = Record<string, Value<Extensions>>;
9
+ type GetExtensions<T extends Row<unknown>> = T extends Row<infer Extensions> ? Extensions : never;
7
10
  type Offset = `-1` | `${number}_${number}`;
8
11
  interface Header {
9
12
  [key: Exclude<string, `operation` | `control`>]: Value;
@@ -13,7 +16,7 @@ type ControlMessage = {
13
16
  control: `up-to-date` | `must-refetch`;
14
17
  };
15
18
  };
16
- type ChangeMessage<T extends Row = Row> = {
19
+ type ChangeMessage<T extends Row<unknown> = Row> = {
17
20
  key: string;
18
21
  value: T;
19
22
  headers: Header & {
@@ -21,7 +24,7 @@ type ChangeMessage<T extends Row = Row> = {
21
24
  };
22
25
  offset: Offset;
23
26
  };
24
- type Message<T extends Row = Row> = ControlMessage | ChangeMessage<T>;
27
+ type Message<T extends Row<unknown> = Row> = ControlMessage | ChangeMessage<T>;
25
28
  /**
26
29
  * Common properties for all columns.
27
30
  * `dims` is the number of dimensions of the column. Only provided if the column is an array.
@@ -68,7 +71,7 @@ type ColumnInfo = RegularColumn | VarcharColumn | BpcharColumn | TimeColumn | In
68
71
  type Schema = {
69
72
  [key: string]: ColumnInfo;
70
73
  };
71
- type TypedMessages<T extends Row = Row> = {
74
+ type TypedMessages<T extends Row<unknown> = Row> = {
72
75
  messages: Array<Message<T>>;
73
76
  schema: ColumnInfo;
74
77
  };
@@ -76,9 +79,13 @@ type MaybePromise<T> = T | Promise<T>;
76
79
 
77
80
  type NullToken = null | `NULL`;
78
81
  type Token = Exclude<string, NullToken>;
79
- type ParseFunction = (value: Token, additionalInfo?: Omit<ColumnInfo, `type` | `dims`>) => Value;
80
- type Parser = {
81
- [key: string]: ParseFunction;
82
+ type ParseFunction<Extensions = never> = (value: Token, additionalInfo?: Omit<ColumnInfo, `type` | `dims`>) => Value<Extensions>;
83
+ /**
84
+ * @typeParam Extensions - Additional types that can be parsed by this parser beyond the standard SQL types.
85
+ * Defaults to no additional types.
86
+ */
87
+ type Parser<Extensions = never> = {
88
+ [key: string]: ParseFunction<Extensions>;
82
89
  };
83
90
 
84
91
  declare class FetchError extends Error {
@@ -113,7 +120,7 @@ declare const BackoffDefaults: {
113
120
  /**
114
121
  * Options for constructing a ShapeStream.
115
122
  */
116
- interface ShapeStreamOptions {
123
+ interface ShapeStreamOptions<T = never> {
117
124
  /**
118
125
  * The full URL to where the Shape is hosted. This can either be the Electric server
119
126
  * directly or a proxy. E.g. for a local Electric instance, you might set `http://localhost:3000/v1/shape/foo`
@@ -138,6 +145,11 @@ interface ShapeStreamOptions {
138
145
  */
139
146
  shapeId?: string;
140
147
  backoffOptions?: BackoffOptions;
148
+ /**
149
+ * HTTP headers to attach to requests made by the client.
150
+ * Can be used for adding authentication headers.
151
+ */
152
+ headers?: Record<string, string>;
141
153
  /**
142
154
  * Automatically fetch updates to the Shape. If you just want to sync the current
143
155
  * shape and stop, pass false.
@@ -145,9 +157,9 @@ interface ShapeStreamOptions {
145
157
  subscribe?: boolean;
146
158
  signal?: AbortSignal;
147
159
  fetchClient?: typeof fetch;
148
- parser?: Parser;
160
+ parser?: Parser<T>;
149
161
  }
150
- interface ShapeStreamInterface<T extends Row = Row> {
162
+ interface ShapeStreamInterface<T extends Row<unknown> = Row> {
151
163
  subscribe(callback: (messages: Message<T>[]) => MaybePromise<void>, onError?: (error: FetchError | Error) => void): void;
152
164
  unsubscribeAllUpToDateSubscribers(): void;
153
165
  unsubscribeAll(): void;
@@ -189,10 +201,10 @@ interface ShapeStreamInterface<T extends Row = Row> {
189
201
  * aborter.abort()
190
202
  * ```
191
203
  */
192
- declare class ShapeStream<T extends Row = Row> implements ShapeStreamInterface<T> {
204
+ declare class ShapeStream<T extends Row<unknown> = Row> implements ShapeStreamInterface<T> {
193
205
  #private;
194
- readonly options: ShapeStreamOptions;
195
- constructor(options: ShapeStreamOptions);
206
+ readonly options: ShapeStreamOptions<GetExtensions<T>>;
207
+ constructor(options: ShapeStreamOptions<GetExtensions<T>>);
196
208
  get shapeId(): string;
197
209
  get isUpToDate(): boolean;
198
210
  start(): Promise<void>;
@@ -210,8 +222,8 @@ declare class ShapeStream<T extends Row = Row> implements ShapeStreamInterface<T
210
222
  isLoading(): boolean;
211
223
  }
212
224
 
213
- type ShapeData<T extends Row = Row> = Map<string, T>;
214
- type ShapeChangedCallback<T extends Row = Row> = (value: ShapeData<T>) => void;
225
+ type ShapeData<T extends Row<unknown> = Row> = Map<string, T>;
226
+ type ShapeChangedCallback<T extends Row<unknown> = Row> = (value: ShapeData<T>) => void;
215
227
  /**
216
228
  * A Shape is an object that subscribes to a shape log,
217
229
  * keeps a materialised shape `.value` in memory and
@@ -243,7 +255,7 @@ type ShapeChangedCallback<T extends Row = Row> = (value: ShapeData<T>) => void;
243
255
  * console.log(shapeData)
244
256
  * })
245
257
  */
246
- declare class Shape<T extends Row = Row> {
258
+ declare class Shape<T extends Row<unknown> = Row> {
247
259
  #private;
248
260
  constructor(stream: ShapeStreamInterface<T>);
249
261
  get isUpToDate(): boolean;
@@ -280,7 +292,7 @@ declare class Shape<T extends Row = Row> {
280
292
  * }
281
293
  * ```
282
294
  */
283
- declare function isChangeMessage<T extends Row = Row>(message: Message<T>): message is ChangeMessage<T>;
295
+ declare function isChangeMessage<T extends Row<unknown> = Row>(message: Message<T>): message is ChangeMessage<T>;
284
296
  /**
285
297
  * Type guard for checking {@link Message} is {@link ControlMessage}.
286
298
  *
@@ -298,6 +310,6 @@ declare function isChangeMessage<T extends Row = Row>(message: Message<T>): mess
298
310
  * }
299
311
  * ```
300
312
  */
301
- declare function isControlMessage<T extends Row = Row>(message: Message<T>): message is ControlMessage;
313
+ declare function isControlMessage<T extends Row<unknown> = Row>(message: Message<T>): message is ControlMessage;
302
314
 
303
- export { BackoffDefaults, type BackoffOptions, type BitColumn, type BpcharColumn, type ChangeMessage, type ColumnInfo, type CommonColumnProps, type ControlMessage, FetchError, type IntervalColumn, type IntervalColumnWithPrecision, type MaybePromise, type Message, type NumericColumn, type Offset, type RegularColumn, type Row, type Schema, Shape, type ShapeChangedCallback, type ShapeData, ShapeStream, type ShapeStreamInterface, type ShapeStreamOptions, type TimeColumn, type TypedMessages, type Value, type VarcharColumn, isChangeMessage, isControlMessage };
315
+ export { BackoffDefaults, type BackoffOptions, type BitColumn, type BpcharColumn, type ChangeMessage, type ColumnInfo, type CommonColumnProps, type ControlMessage, FetchError, type GetExtensions, type IntervalColumn, type IntervalColumnWithPrecision, type MaybePromise, type Message, type NumericColumn, type Offset, type RegularColumn, type Row, type Schema, Shape, type ShapeChangedCallback, type ShapeData, ShapeStream, type ShapeStreamInterface, type ShapeStreamOptions, type TimeColumn, type TypedMessages, type Value, type VarcharColumn, isChangeMessage, isControlMessage };
@@ -196,6 +196,8 @@ var FetchBackoffAbortError = class extends Error {
196
196
 
197
197
  // src/constants.ts
198
198
  var SHAPE_ID_HEADER = `electric-shape-id`;
199
+ var LIVE_CACHE_BUSTER_HEADER = `electric-next-cursor`;
200
+ var LIVE_CACHE_BUSTER_QUERY_PARAM = `cursor`;
199
201
  var CHUNK_LAST_OFFSET_HEADER = `electric-chunk-last-offset`;
200
202
  var CHUNK_UP_TO_DATE_HEADER = `electric-chunk-up-to-date`;
201
203
  var SHAPE_SCHEMA_HEADER = `electric-schema`;
@@ -205,6 +207,7 @@ var WHERE_QUERY_PARAM = `where`;
205
207
  var LIVE_QUERY_PARAM = `live`;
206
208
 
207
209
  // src/fetch.ts
210
+ var HTTP_RETRY_STATUS_CODES = [429];
208
211
  var BackoffDefaults = {
209
212
  initialDelay: 100,
210
213
  maxDelay: 1e4,
@@ -233,7 +236,7 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
233
236
  onFailedAttempt == null ? void 0 : onFailedAttempt();
234
237
  if ((_a = options == null ? void 0 : options.signal) == null ? void 0 : _a.aborted) {
235
238
  throw new FetchBackoffAbortError();
236
- } else if (e instanceof FetchError && e.status >= 400 && e.status < 500) {
239
+ } else if (e instanceof FetchError && !HTTP_RETRY_STATUS_CODES.includes(e.status) && e.status >= 400 && e.status < 500) {
237
240
  throw e;
238
241
  } else {
239
242
  await new Promise((resolve) => setTimeout(resolve, delay));
@@ -359,7 +362,7 @@ function chainAborter(aborter, sourceSignal) {
359
362
  }
360
363
 
361
364
  // src/client.ts
362
- var _fetchClient2, _messageParser, _subscribers, _upToDateSubscribers, _lastOffset, _lastSyncedAt, _isUpToDate, _connected, _shapeId, _schema, _ShapeStream_instances, publish_fn, sendErrorToSubscribers_fn, notifyUpToDateSubscribers_fn, sendErrorToUpToDateSubscribers_fn, reset_fn;
365
+ var _fetchClient2, _messageParser, _subscribers, _upToDateSubscribers, _lastOffset, _liveCacheBuster, _lastSyncedAt, _isUpToDate, _connected, _shapeId, _schema, _ShapeStream_instances, publish_fn, sendErrorToSubscribers_fn, notifyUpToDateSubscribers_fn, sendErrorToUpToDateSubscribers_fn, reset_fn;
363
366
  var ShapeStream = class {
364
367
  constructor(options) {
365
368
  __privateAdd(this, _ShapeStream_instances);
@@ -368,6 +371,8 @@ var ShapeStream = class {
368
371
  __privateAdd(this, _subscribers, /* @__PURE__ */ new Map());
369
372
  __privateAdd(this, _upToDateSubscribers, /* @__PURE__ */ new Map());
370
373
  __privateAdd(this, _lastOffset);
374
+ __privateAdd(this, _liveCacheBuster);
375
+ // Seconds since our Electric Epoch 😎
371
376
  __privateAdd(this, _lastSyncedAt);
372
377
  // unix time
373
378
  __privateAdd(this, _isUpToDate, false);
@@ -378,6 +383,7 @@ var ShapeStream = class {
378
383
  validateOptions(options);
379
384
  this.options = __spreadValues({ subscribe: true }, options);
380
385
  __privateSet(this, _lastOffset, (_a = this.options.offset) != null ? _a : `-1`);
386
+ __privateSet(this, _liveCacheBuster, ``);
381
387
  __privateSet(this, _shapeId, this.options.shapeId);
382
388
  __privateSet(this, _messageParser, new MessageParser(options.parser));
383
389
  const baseFetchClient = (_b = options.fetchClient) != null ? _b : (...args) => fetch(...args);
@@ -408,13 +414,20 @@ var ShapeStream = class {
408
414
  fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, __privateGet(this, _lastOffset));
409
415
  if (__privateGet(this, _isUpToDate)) {
410
416
  fetchUrl.searchParams.set(LIVE_QUERY_PARAM, `true`);
417
+ fetchUrl.searchParams.set(
418
+ LIVE_CACHE_BUSTER_QUERY_PARAM,
419
+ __privateGet(this, _liveCacheBuster)
420
+ );
411
421
  }
412
422
  if (__privateGet(this, _shapeId)) {
413
423
  fetchUrl.searchParams.set(SHAPE_ID_QUERY_PARAM, __privateGet(this, _shapeId));
414
424
  }
415
425
  let response;
416
426
  try {
417
- response = await __privateGet(this, _fetchClient2).call(this, fetchUrl.toString(), { signal });
427
+ response = await __privateGet(this, _fetchClient2).call(this, fetchUrl.toString(), {
428
+ signal,
429
+ headers: this.options.headers
430
+ });
418
431
  __privateSet(this, _connected, true);
419
432
  } catch (e) {
420
433
  if (e instanceof FetchBackoffAbortError) break;
@@ -443,6 +456,10 @@ var ShapeStream = class {
443
456
  if (lastOffset) {
444
457
  __privateSet(this, _lastOffset, lastOffset);
445
458
  }
459
+ const liveCacheBuster = headers.get(LIVE_CACHE_BUSTER_HEADER);
460
+ if (liveCacheBuster) {
461
+ __privateSet(this, _liveCacheBuster, liveCacheBuster);
462
+ }
446
463
  const getSchema = () => {
447
464
  const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER);
448
465
  return schemaHeader ? JSON.parse(schemaHeader) : {};
@@ -513,6 +530,7 @@ _messageParser = new WeakMap();
513
530
  _subscribers = new WeakMap();
514
531
  _upToDateSubscribers = new WeakMap();
515
532
  _lastOffset = new WeakMap();
533
+ _liveCacheBuster = new WeakMap();
516
534
  _lastSyncedAt = new WeakMap();
517
535
  _isUpToDate = new WeakMap();
518
536
  _connected = new WeakMap();
@@ -553,6 +571,7 @@ sendErrorToUpToDateSubscribers_fn = function(error) {
553
571
  */
554
572
  reset_fn = function(shapeId) {
555
573
  __privateSet(this, _lastOffset, `-1`);
574
+ __privateSet(this, _liveCacheBuster, ``);
556
575
  __privateSet(this, _shapeId, shapeId);
557
576
  __privateSet(this, _isUpToDate, false);
558
577
  __privateSet(this, _connected, false);