@electric-sql/client 0.3.3 → 0.3.4
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.
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +42 -45
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +49 -43
- package/src/helpers.ts +3 -3
- package/src/parser.ts +4 -4
- package/src/types.ts +5 -5
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/index.ts","../../src/parser.ts","../../src/helpers.ts","../../src/client.ts"],"sourcesContent":["export * from './client'\nexport * from './types'\nexport * from './helpers'\n","import { ColumnInfo, Message, 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 {\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[] {\n return JSON.parse(messages, (key, value) => {\n // typeof value === `object` is needed because\n // there could be a column named `value`\n // and the value associated to that column will be a string\n if (key === `value` && typeof value === `object`) {\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[]\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, Value } 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 Value = { [key: string]: Value }>(\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 Value = { [key: string]: Value }>(\n message: Message<T>\n): message is ControlMessage {\n return !isChangeMessage(message)\n}\n","import { Message, Value, Offset, Schema } from './types'\nimport { MessageParser, Parser } from './parser'\nimport { isChangeMessage, isControlMessage } from './helpers'\n\nexport type ShapeData = Map<string, { [key: string]: Value }>\nexport type ShapeChangedCallback = (value: ShapeData) => void\n\nexport interface BackoffOptions {\n initialDelay: number\n maxDelay: number\n multiplier: number\n}\n\nexport const BackoffDefaults = {\n initialDelay: 100,\n maxDelay: 10_000,\n multiplier: 1.3,\n}\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\n/**\n * Receives batches of `messages`, puts them on a queue and processes\n * them asynchronously by passing to a registered callback function.\n *\n * @constructor\n * @param {(messages: Message[]) => void} callback function\n */\nclass MessageProcessor {\n private messageQueue: Message[][] = []\n private isProcessing = false\n private callback: (messages: Message[]) => void | Promise<void>\n\n constructor(callback: (messages: Message[]) => void | Promise<void>) {\n this.callback = callback\n }\n\n process(messages: Message[]) {\n this.messageQueue.push(messages)\n\n if (!this.isProcessing) {\n this.processQueue()\n }\n }\n\n private async processQueue() {\n this.isProcessing = true\n\n while (this.messageQueue.length > 0) {\n const messages = this.messageQueue.shift()!\n\n await this.callback(messages)\n }\n\n this.isProcessing = false\n }\n}\n\nexport 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\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\n *\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 * 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 */\nexport class ShapeStream {\n private options: ShapeStreamOptions\n private backoffOptions: BackoffOptions\n private fetchClient: typeof fetch\n private schema?: Schema\n\n private subscribers = new Map<\n number,\n [MessageProcessor, ((error: Error) => void) | undefined]\n >()\n private upToDateSubscribers = new Map<\n number,\n [() => void, (error: FetchError | Error) => void]\n >()\n\n private lastOffset: Offset\n private messageParser: MessageParser\n public isUpToDate: boolean = false\n\n shapeId?: string\n\n constructor(options: ShapeStreamOptions) {\n this.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(options.parser)\n\n this.backoffOptions = options.backoffOptions ?? BackoffDefaults\n this.fetchClient =\n options.fetchClient ??\n ((...args: Parameters<typeof fetch>) => fetch(...args))\n\n this.start()\n }\n\n async start() {\n this.isUpToDate = false\n\n const { url, where, signal } = this.options\n\n while ((!signal?.aborted && !this.isUpToDate) || this.options.subscribe) {\n const fetchUrl = new URL(url)\n if (where) fetchUrl.searchParams.set(`where`, where)\n fetchUrl.searchParams.set(`offset`, this.lastOffset)\n\n if (this.isUpToDate) {\n fetchUrl.searchParams.set(`live`, `true`)\n }\n\n if (this.shapeId) {\n // This should probably be a header for better cache breaking?\n fetchUrl.searchParams.set(`shape_id`, this.shapeId!)\n }\n\n let response!: Response\n\n try {\n const maybeResponse = await this.fetchWithBackoff(fetchUrl)\n if (maybeResponse) response = maybeResponse\n else break\n } catch (e) {\n if (!(e instanceof FetchError)) throw e // should never happen\n 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[`x-electric-shape-id`]\n this.reset(newShapeId)\n this.publish(e.json as Message[])\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(`X-Electric-Shape-Id`)\n if (shapeId) {\n this.shapeId = shapeId\n }\n\n const lastOffset = headers.get(`X-Electric-Chunk-Last-Offset`)\n if (lastOffset) {\n this.lastOffset = lastOffset as Offset\n }\n\n const getSchema = (): Schema => {\n const schemaHeader = headers.get(`X-Electric-Schema`)\n return schemaHeader ? JSON.parse(schemaHeader) : {}\n }\n this.schema = this.schema ?? getSchema()\n\n const messages = status === 204 ? `[]` : await response.text()\n\n const batch = this.messageParser.parse(messages, this.schema)\n\n // Update isUpToDate\n if (batch.length > 0) {\n const lastMessage = batch[batch.length - 1]\n if (\n isControlMessage(lastMessage) &&\n lastMessage.headers.control === `up-to-date` &&\n !this.isUpToDate\n ) {\n this.isUpToDate = true\n this.notifyUpToDateSubscribers()\n }\n\n this.publish(batch)\n }\n }\n }\n\n subscribe(\n callback: (messages: Message[]) => void | Promise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n const subscriber = new MessageProcessor(callback)\n\n this.subscribers.set(subscriptionId, [subscriber, onError])\n\n return () => {\n this.subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.subscribers.clear()\n }\n\n private publish(messages: Message[]) {\n this.subscribers.forEach(([subscriber, _]) => {\n subscriber.process(messages)\n })\n }\n\n private sendErrorToSubscribers(error: Error) {\n this.subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n subscribeOnceToUpToDate(\n callback: () => void | Promise<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 private notifyUpToDateSubscribers() {\n this.upToDateSubscribers.forEach(([callback]) => {\n callback()\n })\n }\n\n private sendErrorToUpToDateSubscribers(error: FetchError | Error) {\n // eslint-disable-next-line\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 private reset(shapeId?: string) {\n this.lastOffset = `-1`\n this.shapeId = shapeId\n this.isUpToDate = false\n this.schema = undefined\n }\n\n private validateOptions(options: 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 }\n\n private async fetchWithBackoff(url: URL) {\n const { initialDelay, maxDelay, multiplier } = this.backoffOptions\n const signal = this.options.signal\n\n let delay = initialDelay\n let attempt = 0\n\n // eslint-disable-next-line no-constant-condition -- we're retrying with a lag until we get a non-500 response or the abort signal is triggered\n while (true) {\n try {\n const result = await this.fetchClient(url.toString(), { signal })\n if (result.ok) return result\n else throw await FetchError.fromResponse(result, url.toString())\n } catch (e) {\n if (signal?.aborted) {\n return undefined\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 attempt++\n console.log(`Retry attempt #${attempt} after ${delay}ms`)\n }\n }\n }\n }\n}\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 {Shape}\n *\n * const shapeStream = new ShapeStream(url: 'http://localhost:3000/v1/shape/foo'})\n * const shape = new Shape(shapeStream)\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 {\n private stream: ShapeStream\n\n private data: ShapeData = new Map()\n private subscribers = new Map<number, ShapeChangedCallback>()\n public error: FetchError | false = false\n private hasNotifiedSubscribersUpToDate: boolean = false\n\n constructor(stream: ShapeStream) {\n this.stream = stream\n this.stream.subscribe(this.process.bind(this), this.handleError.bind(this))\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> {\n return new Promise((resolve) => {\n if (this.stream.isUpToDate) {\n resolve(this.valueSync)\n } else {\n const unsubscribe = this.stream.subscribeOnceToUpToDate(\n () => {\n unsubscribe()\n resolve(this.valueSync)\n },\n (e) => {\n throw e\n }\n )\n }\n })\n }\n\n get valueSync() {\n return this.data\n }\n\n subscribe(callback: ShapeChangedCallback): () => 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 private process(messages: Message[]): 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 private handleError(e: Error): void {\n if (e instanceof FetchError) {\n this.error = e\n this.notify()\n }\n }\n\n private notify(): void {\n this.subscribers.forEach((callback) => {\n callback(this.valueSync)\n })\n }\n}\n"],"mappings":"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","names":["key","value"]}
|
|
1
|
+
{"version":3,"sources":["../../src/index.ts","../../src/parser.ts","../../src/helpers.ts","../../src/client.ts"],"sourcesContent":["export * from './client'\nexport * from './types'\nexport * from './helpers'\n","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` is needed because\n // there could be a column named `value`\n // and the value associated to that column will be a string\n if (key === `value` && typeof value === `object`) {\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","import { Message, Offset, Schema, Row } from './types'\nimport { MessageParser, Parser } from './parser'\nimport { isChangeMessage, isControlMessage } from './helpers'\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\nexport interface BackoffOptions {\n initialDelay: number\n maxDelay: number\n multiplier: number\n}\n\nexport const BackoffDefaults = {\n initialDelay: 100,\n maxDelay: 10_000,\n multiplier: 1.3,\n}\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\n/**\n * Receives batches of `messages`, puts them on a queue and processes\n * them asynchronously by passing to a registered callback function.\n *\n * @constructor\n * @param {(messages: Message[]) => void} callback function\n */\nclass MessageProcessor<T extends Row = Row> {\n private messageQueue: Message<T>[][] = []\n private isProcessing = false\n private callback: (messages: Message<T>[]) => void | Promise<void>\n\n constructor(callback: (messages: Message<T>[]) => void | Promise<void>) {\n this.callback = callback\n }\n\n process(messages: Message<T>[]) {\n this.messageQueue.push(messages)\n\n if (!this.isProcessing) {\n this.processQueue()\n }\n }\n\n private async processQueue() {\n this.isProcessing = true\n\n while (this.messageQueue.length > 0) {\n const messages = this.messageQueue.shift()!\n\n await this.callback(messages)\n }\n\n this.isProcessing = false\n }\n}\n\nexport 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\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 */\nexport class ShapeStream<T extends Row = Row> {\n private options: ShapeStreamOptions\n private backoffOptions: BackoffOptions\n private fetchClient: typeof fetch\n private schema?: Schema\n\n private subscribers = new Map<\n number,\n [MessageProcessor<T>, ((error: Error) => void) | undefined]\n >()\n private upToDateSubscribers = new Map<\n number,\n [() => void, (error: FetchError | Error) => void]\n >()\n\n private lastOffset: Offset\n private messageParser: MessageParser<T>\n public isUpToDate: boolean = false\n\n shapeId?: string\n\n constructor(options: ShapeStreamOptions) {\n this.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 this.backoffOptions = options.backoffOptions ?? BackoffDefaults\n this.fetchClient =\n options.fetchClient ??\n ((...args: Parameters<typeof fetch>) => fetch(...args))\n\n this.start()\n }\n\n async start() {\n this.isUpToDate = false\n\n const { url, where, signal } = this.options\n\n while ((!signal?.aborted && !this.isUpToDate) || this.options.subscribe) {\n const fetchUrl = new URL(url)\n if (where) fetchUrl.searchParams.set(`where`, where)\n fetchUrl.searchParams.set(`offset`, this.lastOffset)\n\n if (this.isUpToDate) {\n fetchUrl.searchParams.set(`live`, `true`)\n }\n\n if (this.shapeId) {\n // This should probably be a header for better cache breaking?\n fetchUrl.searchParams.set(`shape_id`, this.shapeId!)\n }\n\n let response!: Response\n\n try {\n const maybeResponse = await this.fetchWithBackoff(fetchUrl)\n if (maybeResponse) response = maybeResponse\n else break\n } catch (e) {\n if (!(e instanceof FetchError)) throw e // should never happen\n 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[`x-electric-shape-id`]\n this.reset(newShapeId)\n 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(`X-Electric-Shape-Id`)\n if (shapeId) {\n this.shapeId = shapeId\n }\n\n const lastOffset = headers.get(`X-Electric-Chunk-Last-Offset`)\n if (lastOffset) {\n this.lastOffset = lastOffset as Offset\n }\n\n const getSchema = (): Schema => {\n const schemaHeader = headers.get(`X-Electric-Schema`)\n return schemaHeader ? JSON.parse(schemaHeader) : {}\n }\n this.schema = this.schema ?? getSchema()\n\n const messages = status === 204 ? `[]` : await response.text()\n\n const batch = this.messageParser.parse(messages, this.schema)\n\n // Update isUpToDate\n if (batch.length > 0) {\n const lastMessage = batch[batch.length - 1]\n if (\n isControlMessage(lastMessage) &&\n lastMessage.headers.control === `up-to-date` &&\n !this.isUpToDate\n ) {\n this.isUpToDate = true\n this.notifyUpToDateSubscribers()\n }\n\n this.publish(batch)\n }\n }\n }\n\n subscribe(\n callback: (messages: Message<T>[]) => void | Promise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n const subscriber = new MessageProcessor(callback)\n\n this.subscribers.set(subscriptionId, [subscriber, onError])\n\n return () => {\n this.subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.subscribers.clear()\n }\n\n private publish(messages: Message<T>[]) {\n this.subscribers.forEach(([subscriber, _]) => {\n subscriber.process(messages)\n })\n }\n\n private sendErrorToSubscribers(error: Error) {\n this.subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n subscribeOnceToUpToDate(\n callback: () => void | Promise<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 private notifyUpToDateSubscribers() {\n this.upToDateSubscribers.forEach(([callback]) => {\n callback()\n })\n }\n\n private sendErrorToUpToDateSubscribers(error: FetchError | Error) {\n // eslint-disable-next-line\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 private reset(shapeId?: string) {\n this.lastOffset = `-1`\n this.shapeId = shapeId\n this.isUpToDate = false\n this.schema = undefined\n }\n\n private validateOptions(options: 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 }\n\n private async fetchWithBackoff(url: URL) {\n const { initialDelay, maxDelay, multiplier } = this.backoffOptions\n const signal = this.options.signal\n\n let delay = initialDelay\n let attempt = 0\n\n // eslint-disable-next-line no-constant-condition -- we're retrying with a lag until we get a non-500 response or the abort signal is triggered\n while (true) {\n try {\n const result = await this.fetchClient(url.toString(), { signal })\n if (result.ok) return result\n else throw await FetchError.fromResponse(result, url.toString())\n } catch (e) {\n if (signal?.aborted) {\n return undefined\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 attempt++\n console.log(`Retry attempt #${attempt} after ${delay}ms`)\n }\n }\n }\n }\n}\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 private stream: ShapeStream<T>\n\n private data: ShapeData<T> = new Map()\n private subscribers = new Map<number, ShapeChangedCallback<T>>()\n public error: FetchError | false = false\n private hasNotifiedSubscribersUpToDate: boolean = false\n\n constructor(stream: ShapeStream<T>) {\n this.stream = stream\n this.stream.subscribe(this.process.bind(this), this.handleError.bind(this))\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) => {\n if (this.stream.isUpToDate) {\n resolve(this.valueSync)\n } else {\n const unsubscribe = this.stream.subscribeOnceToUpToDate(\n () => {\n unsubscribe()\n resolve(this.valueSync)\n },\n (e) => {\n throw e\n }\n )\n }\n })\n }\n\n get valueSync() {\n return this.data\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 private 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 private handleError(e: Error): void {\n if (e instanceof FetchError) {\n this.error = e\n this.notify()\n }\n }\n\n private notify(): void {\n this.subscribers.forEach((callback) => {\n callback(this.valueSync)\n })\n }\n}\n"],"mappings":"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","names":["key","value"]}
|
package/dist/index.browser.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var x=Object.defineProperty;var m=Object.getOwnPropertySymbols;var D=Object.prototype.hasOwnProperty,E=Object.prototype.propertyIsEnumerable;var P=(r,e,s)=>e in r?x(r,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):r[e]=s,p=(r,e)=>{for(var s in e||(e={}))D.call(e,s)&&P(r,s,e[s]);if(m)for(var s of m(e))E.call(e,s)&&P(r,s,e[s]);return r};var O=(r,e)=>{var s={};for(var t in r)D.call(r,t)&&e.indexOf(t)<0&&(s[t]=r[t]);if(r!=null&&m)for(var t of m(r))e.indexOf(t)<0&&E.call(r,t)&&(s[t]=r[t]);return s};var d=(r,e,s)=>new Promise((t,a)=>{var i=n=>{try{l(s.next(n))}catch(c){a(c)}},o=n=>{try{l(s.throw(n))}catch(c){a(c)}},l=n=>n.done?t(n.value):Promise.resolve(n.value).then(i,o);l((s=s.apply(r,e)).next())});var
|
|
1
|
+
var x=Object.defineProperty;var m=Object.getOwnPropertySymbols;var D=Object.prototype.hasOwnProperty,E=Object.prototype.propertyIsEnumerable;var P=(r,e,s)=>e in r?x(r,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):r[e]=s,p=(r,e)=>{for(var s in e||(e={}))D.call(e,s)&&P(r,s,e[s]);if(m)for(var s of m(e))E.call(e,s)&&P(r,s,e[s]);return r};var O=(r,e)=>{var s={};for(var t in r)D.call(r,t)&&e.indexOf(t)<0&&(s[t]=r[t]);if(r!=null&&m)for(var t of m(r))e.indexOf(t)<0&&E.call(r,t)&&(s[t]=r[t]);return s};var d=(r,e,s)=>new Promise((t,a)=>{var i=n=>{try{l(s.next(n))}catch(c){a(c)}},o=n=>{try{l(s.throw(n))}catch(c){a(c)}},l=n=>n.done?t(n.value):Promise.resolve(n.value).then(i,o);l((s=s.apply(r,e)).next())});var T=r=>Number(r),N=r=>r==="true"||r==="t",j=r=>BigInt(r),M=r=>JSON.parse(r),A=r=>r,B={int2:T,int4:T,int8:j,bool:N,float4:T,float8:T,json:M,jsonb:M};function F(r,e){let s=0,t=null,a="",i=!1,o=0,l;function n(c){let u=[];for(;s<c.length;s++){if(t=c[s],i)t==="\\"?a+=c[++s]:t==='"'?(u.push(e?e(a):a),a="",i=c[s+1]==='"',o=s+2):a+=t;else if(t==='"')i=!0;else if(t==="{")o=++s,u.push(n(c));else if(t==="}"){i=!1,o<s&&u.push(e?e(c.slice(o,s)):c.slice(o,s)),o=s+1;break}else t===","&&l!=="}"&&l!=='"'&&(u.push(e?e(c.slice(o,s)):c.slice(o,s)),o=s+1);l=t}return o<s&&u.push(e?e(c.slice(o,s+1)):c.slice(o,s+1)),u}return n(r)[0]}var w=class{constructor(e){this.parser=p(p({},B),e)}parse(e,s){return JSON.parse(e,(t,a)=>{if(t==="value"&&typeof a=="object"){let i=a;Object.keys(i).forEach(o=>{i[o]=this.parseRow(o,i[o],s)})}return a})}parseRow(e,s,t){var g;let a=t[e];if(!a)return s;let u=a,{type:i,dims:o}=u,l=O(u,["type","dims"]),n=(g=this.parser[i])!=null?g:A,c=R(n,a,e);return o&&o>0?R((f,h)=>F(f,c),a,e)(s):c(s,l)}};function R(r,e,s){var a;let t=!((a=e.not_null)!=null&&a);return i=>{if(L(i)){if(!t)throw new Error(`Column ${s!=null?s:"unknown"} is not nullable`);return null}return r(i,e)}}function L(r){return r===null||r==="NULL"}function v(r){return"key"in r}function S(r){return!v(r)}var V={initialDelay:100,maxDelay:1e4,multiplier:1.3},y=class{constructor(e){this.messageQueue=[];this.isProcessing=!1;this.callback=e}process(e){this.messageQueue.push(e),this.isProcessing||this.processQueue()}processQueue(){return d(this,null,function*(){for(this.isProcessing=!0;this.messageQueue.length>0;){let e=this.messageQueue.shift();yield this.callback(e)}this.isProcessing=!1})}},b=class r extends Error{constructor(s,t,a,i,o,l){super(l||`HTTP Error ${s} at ${o}: ${t!=null?t:JSON.stringify(a)}`);this.url=o;this.name="FetchError",this.status=s,this.text=t,this.json=a,this.headers=i}static fromResponse(s,t){return d(this,null,function*(){let a=s.status,i=Object.fromEntries([...s.headers.entries()]),o,l,n=s.headers.get("content-type");return n&&n.includes("application/json")?l=yield s.json():o=yield s.text(),new r(a,o,l,i,t)})}},I=class{constructor(e){this.subscribers=new Map;this.upToDateSubscribers=new Map;this.isUpToDate=!1;var s,t,a;this.validateOptions(e),this.options=p({subscribe:!0},e),this.lastOffset=(s=this.options.offset)!=null?s:"-1",this.shapeId=this.options.shapeId,this.messageParser=new w(e.parser),this.backoffOptions=(t=e.backoffOptions)!=null?t:V,this.fetchClient=(a=e.fetchClient)!=null?a:(...i)=>fetch(...i),this.start()}start(){return d(this,null,function*(){var a;this.isUpToDate=!1;let{url:e,where:s,signal:t}=this.options;for(;!(t!=null&&t.aborted)&&!this.isUpToDate||this.options.subscribe;){let i=new URL(e);s&&i.searchParams.set("where",s),i.searchParams.set("offset",this.lastOffset),this.isUpToDate&&i.searchParams.set("live","true"),this.shapeId&&i.searchParams.set("shape_id",this.shapeId);let o;try{let h=yield this.fetchWithBackoff(i);if(h)o=h;else break}catch(h){if(!(h instanceof b))throw h;if(h.status==409){let C=h.headers["x-electric-shape-id"];this.reset(C),this.publish(h.json);continue}else if(h.status>=400&&h.status<500)throw this.sendErrorToUpToDateSubscribers(h),this.sendErrorToSubscribers(h),h}let{headers:l,status:n}=o,c=l.get("X-Electric-Shape-Id");c&&(this.shapeId=c);let u=l.get("X-Electric-Chunk-Last-Offset");u&&(this.lastOffset=u);let g=()=>{let h=l.get("X-Electric-Schema");return h?JSON.parse(h):{}};this.schema=(a=this.schema)!=null?a:g();let k=n===204?"[]":yield o.text(),f=this.messageParser.parse(k,this.schema);if(f.length>0){let h=f[f.length-1];S(h)&&h.headers.control==="up-to-date"&&!this.isUpToDate&&(this.isUpToDate=!0,this.notifyUpToDateSubscribers()),this.publish(f)}}})}subscribe(e,s){let t=Math.random(),a=new y(e);return this.subscribers.set(t,[a,s]),()=>{this.subscribers.delete(t)}}unsubscribeAll(){this.subscribers.clear()}publish(e){this.subscribers.forEach(([s,t])=>{s.process(e)})}sendErrorToSubscribers(e){this.subscribers.forEach(([s,t])=>{t==null||t(e)})}subscribeOnceToUpToDate(e,s){let t=Math.random();return this.upToDateSubscribers.set(t,[e,s]),()=>{this.upToDateSubscribers.delete(t)}}unsubscribeAllUpToDateSubscribers(){this.upToDateSubscribers.clear()}notifyUpToDateSubscribers(){this.upToDateSubscribers.forEach(([e])=>{e()})}sendErrorToUpToDateSubscribers(e){this.upToDateSubscribers.forEach(([s,t])=>t(e))}reset(e){this.lastOffset="-1",this.shapeId=e,this.isUpToDate=!1,this.schema=void 0}validateOptions(e){if(!e.url)throw new Error("Invalid shape option. It must provide the url");if(e.signal&&!(e.signal instanceof AbortSignal))throw new Error("Invalid signal option. It must be an instance of AbortSignal.");if(e.offset!==void 0&&e.offset!=="-1"&&!e.shapeId)throw new Error("shapeId is required if this isn't an initial fetch (i.e. offset > -1)")}fetchWithBackoff(e){return d(this,null,function*(){let{initialDelay:s,maxDelay:t,multiplier:a}=this.backoffOptions,i=this.options.signal,o=s,l=0;for(;;)try{let n=yield this.fetchClient(e.toString(),{signal:i});if(n.ok)return n;throw yield b.fromResponse(n,e.toString())}catch(n){if(i!=null&&i.aborted)return;if(n instanceof b&&n.status>=400&&n.status<500)throw n;yield new Promise(c=>setTimeout(c,o)),o=Math.min(o*a,t),l++,console.log(`Retry attempt #${l} after ${o}ms`)}})}},U=class{constructor(e){this.data=new Map;this.subscribers=new Map;this.error=!1;this.hasNotifiedSubscribersUpToDate=!1;this.stream=e,this.stream.subscribe(this.process.bind(this),this.handleError.bind(this));let s=this.stream.subscribeOnceToUpToDate(()=>{s()},t=>{throw this.handleError(t),t})}get isUpToDate(){return this.stream.isUpToDate}get value(){return new Promise(e=>{if(this.stream.isUpToDate)e(this.valueSync);else{let s=this.stream.subscribeOnceToUpToDate(()=>{s(),e(this.valueSync)},t=>{throw t})}})}get valueSync(){return this.data}subscribe(e){let s=Math.random();return this.subscribers.set(s,e),()=>{this.subscribers.delete(s)}}unsubscribeAll(){this.subscribers.clear()}get numSubscribers(){return this.subscribers.size}process(e){let s=!1,t=!1,a=!1;e.forEach(i=>{if(v(i))switch(s=["insert","update","delete"].includes(i.headers.operation),i.headers.operation){case"insert":this.data.set(i.key,i.value);break;case"update":this.data.set(i.key,p(p({},this.data.get(i.key)),i.value));break;case"delete":this.data.delete(i.key);break}if(S(i))switch(i.headers.control){case"up-to-date":t=!0,this.hasNotifiedSubscribersUpToDate||(a=!0);break;case"must-refetch":this.data.clear(),this.error=!1,t=!1,a=!1;break}}),(a||t&&s)&&(this.hasNotifiedSubscribersUpToDate=!0,this.notify())}handleError(e){e instanceof b&&(this.error=e,this.notify())}notify(){this.subscribers.forEach(e=>{e(this.valueSync)})}};export{V as BackoffDefaults,b as FetchError,U as Shape,I as ShapeStream,v as isChangeMessage,S as isControlMessage};
|
|
2
2
|
//# sourceMappingURL=index.browser.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/parser.ts","../src/helpers.ts","../src/client.ts"],"sourcesContent":["import { ColumnInfo, Message, 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 {\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[] {\n return JSON.parse(messages, (key, value) => {\n // typeof value === `object` is needed because\n // there could be a column named `value`\n // and the value associated to that column will be a string\n if (key === `value` && typeof value === `object`) {\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[]\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, Value } 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 Value = { [key: string]: Value }>(\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 Value = { [key: string]: Value }>(\n message: Message<T>\n): message is ControlMessage {\n return !isChangeMessage(message)\n}\n","import { Message, Value, Offset, Schema } from './types'\nimport { MessageParser, Parser } from './parser'\nimport { isChangeMessage, isControlMessage } from './helpers'\n\nexport type ShapeData = Map<string, { [key: string]: Value }>\nexport type ShapeChangedCallback = (value: ShapeData) => void\n\nexport interface BackoffOptions {\n initialDelay: number\n maxDelay: number\n multiplier: number\n}\n\nexport const BackoffDefaults = {\n initialDelay: 100,\n maxDelay: 10_000,\n multiplier: 1.3,\n}\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\n/**\n * Receives batches of `messages`, puts them on a queue and processes\n * them asynchronously by passing to a registered callback function.\n *\n * @constructor\n * @param {(messages: Message[]) => void} callback function\n */\nclass MessageProcessor {\n private messageQueue: Message[][] = []\n private isProcessing = false\n private callback: (messages: Message[]) => void | Promise<void>\n\n constructor(callback: (messages: Message[]) => void | Promise<void>) {\n this.callback = callback\n }\n\n process(messages: Message[]) {\n this.messageQueue.push(messages)\n\n if (!this.isProcessing) {\n this.processQueue()\n }\n }\n\n private async processQueue() {\n this.isProcessing = true\n\n while (this.messageQueue.length > 0) {\n const messages = this.messageQueue.shift()!\n\n await this.callback(messages)\n }\n\n this.isProcessing = false\n }\n}\n\nexport 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\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\n *\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 * 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 */\nexport class ShapeStream {\n private options: ShapeStreamOptions\n private backoffOptions: BackoffOptions\n private fetchClient: typeof fetch\n private schema?: Schema\n\n private subscribers = new Map<\n number,\n [MessageProcessor, ((error: Error) => void) | undefined]\n >()\n private upToDateSubscribers = new Map<\n number,\n [() => void, (error: FetchError | Error) => void]\n >()\n\n private lastOffset: Offset\n private messageParser: MessageParser\n public isUpToDate: boolean = false\n\n shapeId?: string\n\n constructor(options: ShapeStreamOptions) {\n this.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(options.parser)\n\n this.backoffOptions = options.backoffOptions ?? BackoffDefaults\n this.fetchClient =\n options.fetchClient ??\n ((...args: Parameters<typeof fetch>) => fetch(...args))\n\n this.start()\n }\n\n async start() {\n this.isUpToDate = false\n\n const { url, where, signal } = this.options\n\n while ((!signal?.aborted && !this.isUpToDate) || this.options.subscribe) {\n const fetchUrl = new URL(url)\n if (where) fetchUrl.searchParams.set(`where`, where)\n fetchUrl.searchParams.set(`offset`, this.lastOffset)\n\n if (this.isUpToDate) {\n fetchUrl.searchParams.set(`live`, `true`)\n }\n\n if (this.shapeId) {\n // This should probably be a header for better cache breaking?\n fetchUrl.searchParams.set(`shape_id`, this.shapeId!)\n }\n\n let response!: Response\n\n try {\n const maybeResponse = await this.fetchWithBackoff(fetchUrl)\n if (maybeResponse) response = maybeResponse\n else break\n } catch (e) {\n if (!(e instanceof FetchError)) throw e // should never happen\n 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[`x-electric-shape-id`]\n this.reset(newShapeId)\n this.publish(e.json as Message[])\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(`X-Electric-Shape-Id`)\n if (shapeId) {\n this.shapeId = shapeId\n }\n\n const lastOffset = headers.get(`X-Electric-Chunk-Last-Offset`)\n if (lastOffset) {\n this.lastOffset = lastOffset as Offset\n }\n\n const getSchema = (): Schema => {\n const schemaHeader = headers.get(`X-Electric-Schema`)\n return schemaHeader ? JSON.parse(schemaHeader) : {}\n }\n this.schema = this.schema ?? getSchema()\n\n const messages = status === 204 ? `[]` : await response.text()\n\n const batch = this.messageParser.parse(messages, this.schema)\n\n // Update isUpToDate\n if (batch.length > 0) {\n const lastMessage = batch[batch.length - 1]\n if (\n isControlMessage(lastMessage) &&\n lastMessage.headers.control === `up-to-date` &&\n !this.isUpToDate\n ) {\n this.isUpToDate = true\n this.notifyUpToDateSubscribers()\n }\n\n this.publish(batch)\n }\n }\n }\n\n subscribe(\n callback: (messages: Message[]) => void | Promise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n const subscriber = new MessageProcessor(callback)\n\n this.subscribers.set(subscriptionId, [subscriber, onError])\n\n return () => {\n this.subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.subscribers.clear()\n }\n\n private publish(messages: Message[]) {\n this.subscribers.forEach(([subscriber, _]) => {\n subscriber.process(messages)\n })\n }\n\n private sendErrorToSubscribers(error: Error) {\n this.subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n subscribeOnceToUpToDate(\n callback: () => void | Promise<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 private notifyUpToDateSubscribers() {\n this.upToDateSubscribers.forEach(([callback]) => {\n callback()\n })\n }\n\n private sendErrorToUpToDateSubscribers(error: FetchError | Error) {\n // eslint-disable-next-line\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 private reset(shapeId?: string) {\n this.lastOffset = `-1`\n this.shapeId = shapeId\n this.isUpToDate = false\n this.schema = undefined\n }\n\n private validateOptions(options: 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 }\n\n private async fetchWithBackoff(url: URL) {\n const { initialDelay, maxDelay, multiplier } = this.backoffOptions\n const signal = this.options.signal\n\n let delay = initialDelay\n let attempt = 0\n\n // eslint-disable-next-line no-constant-condition -- we're retrying with a lag until we get a non-500 response or the abort signal is triggered\n while (true) {\n try {\n const result = await this.fetchClient(url.toString(), { signal })\n if (result.ok) return result\n else throw await FetchError.fromResponse(result, url.toString())\n } catch (e) {\n if (signal?.aborted) {\n return undefined\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 attempt++\n console.log(`Retry attempt #${attempt} after ${delay}ms`)\n }\n }\n }\n }\n}\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 {Shape}\n *\n * const shapeStream = new ShapeStream(url: 'http://localhost:3000/v1/shape/foo'})\n * const shape = new Shape(shapeStream)\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 {\n private stream: ShapeStream\n\n private data: ShapeData = new Map()\n private subscribers = new Map<number, ShapeChangedCallback>()\n public error: FetchError | false = false\n private hasNotifiedSubscribersUpToDate: boolean = false\n\n constructor(stream: ShapeStream) {\n this.stream = stream\n this.stream.subscribe(this.process.bind(this), this.handleError.bind(this))\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> {\n return new Promise((resolve) => {\n if (this.stream.isUpToDate) {\n resolve(this.valueSync)\n } else {\n const unsubscribe = this.stream.subscribeOnceToUpToDate(\n () => {\n unsubscribe()\n resolve(this.valueSync)\n },\n (e) => {\n throw e\n }\n )\n }\n })\n }\n\n get valueSync() {\n return this.data\n }\n\n subscribe(callback: ShapeChangedCallback): () => 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 private process(messages: Message[]): 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 private handleError(e: Error): void {\n if (e instanceof FetchError) {\n this.error = e\n this.notify()\n }\n }\n\n private 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","BackoffDefaults","MessageProcessor","callback","messages","__async","FetchError","_FetchError","status","text","json","headers","url","message","response","contentType","ShapeStream","options","_a","_b","_c","__spreadValues","MessageParser","args","where","signal","fetchUrl","maybeResponse","e","newShapeId","shapeId","lastOffset","getSchema","schemaHeader","batch","lastMessage","isControlMessage","onError","subscriptionId","subscriber","_","error","errorFn","errorCallback","initialDelay","maxDelay","multiplier","delay","attempt","result","resolve","Shape","stream","unsubscribe","dataMayHaveChanged","isUpToDate","newlyUpToDate","isChangeMessage"]}
|
|
1
|
+
{"version":3,"sources":["../src/parser.ts","../src/helpers.ts","../src/client.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` is needed because\n // there could be a column named `value`\n // and the value associated to that column will be a string\n if (key === `value` && typeof value === `object`) {\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","import { Message, Offset, Schema, Row } from './types'\nimport { MessageParser, Parser } from './parser'\nimport { isChangeMessage, isControlMessage } from './helpers'\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\nexport interface BackoffOptions {\n initialDelay: number\n maxDelay: number\n multiplier: number\n}\n\nexport const BackoffDefaults = {\n initialDelay: 100,\n maxDelay: 10_000,\n multiplier: 1.3,\n}\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\n/**\n * Receives batches of `messages`, puts them on a queue and processes\n * them asynchronously by passing to a registered callback function.\n *\n * @constructor\n * @param {(messages: Message[]) => void} callback function\n */\nclass MessageProcessor<T extends Row = Row> {\n private messageQueue: Message<T>[][] = []\n private isProcessing = false\n private callback: (messages: Message<T>[]) => void | Promise<void>\n\n constructor(callback: (messages: Message<T>[]) => void | Promise<void>) {\n this.callback = callback\n }\n\n process(messages: Message<T>[]) {\n this.messageQueue.push(messages)\n\n if (!this.isProcessing) {\n this.processQueue()\n }\n }\n\n private async processQueue() {\n this.isProcessing = true\n\n while (this.messageQueue.length > 0) {\n const messages = this.messageQueue.shift()!\n\n await this.callback(messages)\n }\n\n this.isProcessing = false\n }\n}\n\nexport 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\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 */\nexport class ShapeStream<T extends Row = Row> {\n private options: ShapeStreamOptions\n private backoffOptions: BackoffOptions\n private fetchClient: typeof fetch\n private schema?: Schema\n\n private subscribers = new Map<\n number,\n [MessageProcessor<T>, ((error: Error) => void) | undefined]\n >()\n private upToDateSubscribers = new Map<\n number,\n [() => void, (error: FetchError | Error) => void]\n >()\n\n private lastOffset: Offset\n private messageParser: MessageParser<T>\n public isUpToDate: boolean = false\n\n shapeId?: string\n\n constructor(options: ShapeStreamOptions) {\n this.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 this.backoffOptions = options.backoffOptions ?? BackoffDefaults\n this.fetchClient =\n options.fetchClient ??\n ((...args: Parameters<typeof fetch>) => fetch(...args))\n\n this.start()\n }\n\n async start() {\n this.isUpToDate = false\n\n const { url, where, signal } = this.options\n\n while ((!signal?.aborted && !this.isUpToDate) || this.options.subscribe) {\n const fetchUrl = new URL(url)\n if (where) fetchUrl.searchParams.set(`where`, where)\n fetchUrl.searchParams.set(`offset`, this.lastOffset)\n\n if (this.isUpToDate) {\n fetchUrl.searchParams.set(`live`, `true`)\n }\n\n if (this.shapeId) {\n // This should probably be a header for better cache breaking?\n fetchUrl.searchParams.set(`shape_id`, this.shapeId!)\n }\n\n let response!: Response\n\n try {\n const maybeResponse = await this.fetchWithBackoff(fetchUrl)\n if (maybeResponse) response = maybeResponse\n else break\n } catch (e) {\n if (!(e instanceof FetchError)) throw e // should never happen\n 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[`x-electric-shape-id`]\n this.reset(newShapeId)\n 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(`X-Electric-Shape-Id`)\n if (shapeId) {\n this.shapeId = shapeId\n }\n\n const lastOffset = headers.get(`X-Electric-Chunk-Last-Offset`)\n if (lastOffset) {\n this.lastOffset = lastOffset as Offset\n }\n\n const getSchema = (): Schema => {\n const schemaHeader = headers.get(`X-Electric-Schema`)\n return schemaHeader ? JSON.parse(schemaHeader) : {}\n }\n this.schema = this.schema ?? getSchema()\n\n const messages = status === 204 ? `[]` : await response.text()\n\n const batch = this.messageParser.parse(messages, this.schema)\n\n // Update isUpToDate\n if (batch.length > 0) {\n const lastMessage = batch[batch.length - 1]\n if (\n isControlMessage(lastMessage) &&\n lastMessage.headers.control === `up-to-date` &&\n !this.isUpToDate\n ) {\n this.isUpToDate = true\n this.notifyUpToDateSubscribers()\n }\n\n this.publish(batch)\n }\n }\n }\n\n subscribe(\n callback: (messages: Message<T>[]) => void | Promise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n const subscriber = new MessageProcessor(callback)\n\n this.subscribers.set(subscriptionId, [subscriber, onError])\n\n return () => {\n this.subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.subscribers.clear()\n }\n\n private publish(messages: Message<T>[]) {\n this.subscribers.forEach(([subscriber, _]) => {\n subscriber.process(messages)\n })\n }\n\n private sendErrorToSubscribers(error: Error) {\n this.subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n subscribeOnceToUpToDate(\n callback: () => void | Promise<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 private notifyUpToDateSubscribers() {\n this.upToDateSubscribers.forEach(([callback]) => {\n callback()\n })\n }\n\n private sendErrorToUpToDateSubscribers(error: FetchError | Error) {\n // eslint-disable-next-line\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 private reset(shapeId?: string) {\n this.lastOffset = `-1`\n this.shapeId = shapeId\n this.isUpToDate = false\n this.schema = undefined\n }\n\n private validateOptions(options: 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 }\n\n private async fetchWithBackoff(url: URL) {\n const { initialDelay, maxDelay, multiplier } = this.backoffOptions\n const signal = this.options.signal\n\n let delay = initialDelay\n let attempt = 0\n\n // eslint-disable-next-line no-constant-condition -- we're retrying with a lag until we get a non-500 response or the abort signal is triggered\n while (true) {\n try {\n const result = await this.fetchClient(url.toString(), { signal })\n if (result.ok) return result\n else throw await FetchError.fromResponse(result, url.toString())\n } catch (e) {\n if (signal?.aborted) {\n return undefined\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 attempt++\n console.log(`Retry attempt #${attempt} after ${delay}ms`)\n }\n }\n }\n }\n}\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 private stream: ShapeStream<T>\n\n private data: ShapeData<T> = new Map()\n private subscribers = new Map<number, ShapeChangedCallback<T>>()\n public error: FetchError | false = false\n private hasNotifiedSubscribersUpToDate: boolean = false\n\n constructor(stream: ShapeStream<T>) {\n this.stream = stream\n this.stream.subscribe(this.process.bind(this), this.handleError.bind(this))\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) => {\n if (this.stream.isUpToDate) {\n resolve(this.valueSync)\n } else {\n const unsubscribe = this.stream.subscribeOnceToUpToDate(\n () => {\n unsubscribe()\n resolve(this.valueSync)\n },\n (e) => {\n throw e\n }\n )\n }\n })\n }\n\n get valueSync() {\n return this.data\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 private 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 private handleError(e: Error): void {\n if (e instanceof FetchError) {\n this.error = e\n this.notify()\n }\n }\n\n private 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","BackoffDefaults","MessageProcessor","callback","messages","__async","FetchError","_FetchError","status","text","json","headers","url","message","response","contentType","ShapeStream","options","_a","_b","_c","__spreadValues","MessageParser","args","where","signal","fetchUrl","maybeResponse","e","newShapeId","shapeId","lastOffset","getSchema","schemaHeader","batch","lastMessage","isControlMessage","onError","subscriptionId","subscriber","_","error","errorFn","errorCallback","initialDelay","maxDelay","multiplier","delay","attempt","result","resolve","Shape","stream","unsubscribe","dataMayHaveChanged","isUpToDate","newlyUpToDate","isChangeMessage"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
type Value = string | number | boolean | bigint | null | Value[] | {
|
|
2
2
|
[key: string]: Value;
|
|
3
3
|
};
|
|
4
|
+
type Row = {
|
|
5
|
+
[key: string]: Value;
|
|
6
|
+
};
|
|
4
7
|
type Offset = `-1` | `${number}_${number}`;
|
|
5
8
|
interface Header {
|
|
6
9
|
[key: Exclude<string, `operation` | `control`>]: Value;
|
|
@@ -10,7 +13,7 @@ type ControlMessage = {
|
|
|
10
13
|
control: `up-to-date` | `must-refetch`;
|
|
11
14
|
};
|
|
12
15
|
};
|
|
13
|
-
type ChangeMessage<T> = {
|
|
16
|
+
type ChangeMessage<T extends Row = Row> = {
|
|
14
17
|
key: string;
|
|
15
18
|
value: T;
|
|
16
19
|
headers: Header & {
|
|
@@ -18,9 +21,7 @@ type ChangeMessage<T> = {
|
|
|
18
21
|
};
|
|
19
22
|
offset: Offset;
|
|
20
23
|
};
|
|
21
|
-
type Message<T extends
|
|
22
|
-
[key: string]: Value;
|
|
23
|
-
}> = ControlMessage | ChangeMessage<T>;
|
|
24
|
+
type Message<T extends Row = Row> = ControlMessage | ChangeMessage<T>;
|
|
24
25
|
/**
|
|
25
26
|
* Common properties for all columns.
|
|
26
27
|
* `dims` is the number of dimensions of the column. Only provided if the column is an array.
|
|
@@ -67,9 +68,7 @@ type ColumnInfo = RegularColumn | VarcharColumn | BpcharColumn | TimeColumn | In
|
|
|
67
68
|
type Schema = {
|
|
68
69
|
[key: string]: ColumnInfo;
|
|
69
70
|
};
|
|
70
|
-
type TypedMessages<T extends
|
|
71
|
-
[key: string]: Value;
|
|
72
|
-
}> = {
|
|
71
|
+
type TypedMessages<T extends Row = Row> = {
|
|
73
72
|
messages: Array<Message<T>>;
|
|
74
73
|
schema: ColumnInfo;
|
|
75
74
|
};
|
|
@@ -81,10 +80,8 @@ type Parser = {
|
|
|
81
80
|
[key: string]: ParseFunction;
|
|
82
81
|
};
|
|
83
82
|
|
|
84
|
-
type ShapeData = Map<string,
|
|
85
|
-
|
|
86
|
-
}>;
|
|
87
|
-
type ShapeChangedCallback = (value: ShapeData) => void;
|
|
83
|
+
type ShapeData<T extends Row = Row> = Map<string, T>;
|
|
84
|
+
type ShapeChangedCallback<T extends Row = Row> = (value: ShapeData<T>) => void;
|
|
88
85
|
interface BackoffOptions {
|
|
89
86
|
initialDelay: number;
|
|
90
87
|
maxDelay: number;
|
|
@@ -148,28 +145,30 @@ declare class FetchError extends Error {
|
|
|
148
145
|
* to consume the HTTP `GET /v1/shape` api.
|
|
149
146
|
*
|
|
150
147
|
* @constructor
|
|
151
|
-
* @param {ShapeStreamOptions} options
|
|
152
|
-
*
|
|
148
|
+
* @param {ShapeStreamOptions} options - configure the shape stream
|
|
149
|
+
* @example
|
|
153
150
|
* Register a callback function to subscribe to the messages.
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
151
|
+
* ```
|
|
152
|
+
* const stream = new ShapeStream(options)
|
|
153
|
+
* stream.subscribe(messages => {
|
|
154
|
+
* // messages is 1 or more row updates
|
|
155
|
+
* })
|
|
156
|
+
* ```
|
|
159
157
|
*
|
|
160
158
|
* To abort the stream, abort the `signal`
|
|
161
159
|
* passed in via the `ShapeStreamOptions`.
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
160
|
+
* ```
|
|
161
|
+
* const aborter = new AbortController()
|
|
162
|
+
* const issueStream = new ShapeStream({
|
|
163
|
+
* url: `${BASE_URL}/${table}`
|
|
164
|
+
* subscribe: true,
|
|
165
|
+
* signal: aborter.signal,
|
|
166
|
+
* })
|
|
167
|
+
* // Later...
|
|
168
|
+
* aborter.abort()
|
|
169
|
+
* ```
|
|
171
170
|
*/
|
|
172
|
-
declare class ShapeStream {
|
|
171
|
+
declare class ShapeStream<T extends Row = Row> {
|
|
173
172
|
private options;
|
|
174
173
|
private backoffOptions;
|
|
175
174
|
private fetchClient;
|
|
@@ -182,7 +181,7 @@ declare class ShapeStream {
|
|
|
182
181
|
shapeId?: string;
|
|
183
182
|
constructor(options: ShapeStreamOptions);
|
|
184
183
|
start(): Promise<void>;
|
|
185
|
-
subscribe(callback: (messages: Message[]) => void | Promise<void>, onError?: (error: FetchError | Error) => void): () => void;
|
|
184
|
+
subscribe(callback: (messages: Message<T>[]) => void | Promise<void>, onError?: (error: FetchError | Error) => void): () => void;
|
|
186
185
|
unsubscribeAll(): void;
|
|
187
186
|
private publish;
|
|
188
187
|
private sendErrorToSubscribers;
|
|
@@ -207,10 +206,12 @@ declare class ShapeStream {
|
|
|
207
206
|
* to simplify developing framework hooks.
|
|
208
207
|
*
|
|
209
208
|
* @constructor
|
|
210
|
-
* @param {
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
209
|
+
* @param {ShapeStream<T extends Row>} - the underlying shape stream
|
|
210
|
+
* @example
|
|
211
|
+
* ```
|
|
212
|
+
* const shapeStream = new ShapeStream<{ foo: number }>(url: 'http://localhost:3000/v1/shape/foo'})
|
|
213
|
+
* const shape = new Shape(shapeStream)
|
|
214
|
+
* ```
|
|
214
215
|
*
|
|
215
216
|
* `value` returns a promise that resolves the Shape data once the Shape has been
|
|
216
217
|
* fully loaded (and when resuming from being offline):
|
|
@@ -227,17 +228,17 @@ declare class ShapeStream {
|
|
|
227
228
|
* console.log(shapeData)
|
|
228
229
|
* })
|
|
229
230
|
*/
|
|
230
|
-
declare class Shape {
|
|
231
|
+
declare class Shape<T extends Row = Row> {
|
|
231
232
|
private stream;
|
|
232
233
|
private data;
|
|
233
234
|
private subscribers;
|
|
234
235
|
error: FetchError | false;
|
|
235
236
|
private hasNotifiedSubscribersUpToDate;
|
|
236
|
-
constructor(stream: ShapeStream);
|
|
237
|
+
constructor(stream: ShapeStream<T>);
|
|
237
238
|
get isUpToDate(): boolean;
|
|
238
|
-
get value(): Promise<ShapeData
|
|
239
|
-
get valueSync(): ShapeData
|
|
240
|
-
subscribe(callback: ShapeChangedCallback): () => void;
|
|
239
|
+
get value(): Promise<ShapeData<T>>;
|
|
240
|
+
get valueSync(): ShapeData<T>;
|
|
241
|
+
subscribe(callback: ShapeChangedCallback<T>): () => void;
|
|
241
242
|
unsubscribeAll(): void;
|
|
242
243
|
get numSubscribers(): number;
|
|
243
244
|
private process;
|
|
@@ -262,9 +263,7 @@ declare class Shape {
|
|
|
262
263
|
* }
|
|
263
264
|
* ```
|
|
264
265
|
*/
|
|
265
|
-
declare function isChangeMessage<T extends
|
|
266
|
-
[key: string]: Value;
|
|
267
|
-
}>(message: Message<T>): message is ChangeMessage<T>;
|
|
266
|
+
declare function isChangeMessage<T extends Row = Row>(message: Message<T>): message is ChangeMessage<T>;
|
|
268
267
|
/**
|
|
269
268
|
* Type guard for checking {@link Message} is {@link ControlMessage}.
|
|
270
269
|
*
|
|
@@ -282,8 +281,6 @@ declare function isChangeMessage<T extends Value = {
|
|
|
282
281
|
* }
|
|
283
282
|
* ```
|
|
284
283
|
*/
|
|
285
|
-
declare function isControlMessage<T extends
|
|
286
|
-
[key: string]: Value;
|
|
287
|
-
}>(message: Message<T>): message is ControlMessage;
|
|
284
|
+
declare function isControlMessage<T extends Row = Row>(message: Message<T>): message is ControlMessage;
|
|
288
285
|
|
|
289
|
-
export { BackoffDefaults, type BackoffOptions, type BitColumn, type BpcharColumn, type ChangeMessage, type ColumnInfo, type CommonColumnProps, type ControlMessage, FetchError, type IntervalColumn, type IntervalColumnWithPrecision, type Message, type NumericColumn, type Offset, type RegularColumn, type Schema, Shape, type ShapeChangedCallback, type ShapeData, ShapeStream, type ShapeStreamOptions, type TimeColumn, type TypedMessages, type Value, type VarcharColumn, isChangeMessage, isControlMessage };
|
|
286
|
+
export { BackoffDefaults, type BackoffOptions, type BitColumn, type BpcharColumn, type ChangeMessage, type ColumnInfo, type CommonColumnProps, type ControlMessage, FetchError, type IntervalColumn, type IntervalColumnWithPrecision, type Message, type NumericColumn, type Offset, type RegularColumn, type Row, type Schema, Shape, type ShapeChangedCallback, type ShapeData, ShapeStream, type ShapeStreamOptions, type TimeColumn, type TypedMessages, type Value, type VarcharColumn, isChangeMessage, isControlMessage };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/parser.ts","../src/helpers.ts","../src/client.ts"],"sourcesContent":["import { ColumnInfo, Message, 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 {\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[] {\n return JSON.parse(messages, (key, value) => {\n // typeof value === `object` is needed because\n // there could be a column named `value`\n // and the value associated to that column will be a string\n if (key === `value` && typeof value === `object`) {\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[]\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, Value } 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 Value = { [key: string]: Value }>(\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 Value = { [key: string]: Value }>(\n message: Message<T>\n): message is ControlMessage {\n return !isChangeMessage(message)\n}\n","import { Message, Value, Offset, Schema } from './types'\nimport { MessageParser, Parser } from './parser'\nimport { isChangeMessage, isControlMessage } from './helpers'\n\nexport type ShapeData = Map<string, { [key: string]: Value }>\nexport type ShapeChangedCallback = (value: ShapeData) => void\n\nexport interface BackoffOptions {\n initialDelay: number\n maxDelay: number\n multiplier: number\n}\n\nexport const BackoffDefaults = {\n initialDelay: 100,\n maxDelay: 10_000,\n multiplier: 1.3,\n}\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\n/**\n * Receives batches of `messages`, puts them on a queue and processes\n * them asynchronously by passing to a registered callback function.\n *\n * @constructor\n * @param {(messages: Message[]) => void} callback function\n */\nclass MessageProcessor {\n private messageQueue: Message[][] = []\n private isProcessing = false\n private callback: (messages: Message[]) => void | Promise<void>\n\n constructor(callback: (messages: Message[]) => void | Promise<void>) {\n this.callback = callback\n }\n\n process(messages: Message[]) {\n this.messageQueue.push(messages)\n\n if (!this.isProcessing) {\n this.processQueue()\n }\n }\n\n private async processQueue() {\n this.isProcessing = true\n\n while (this.messageQueue.length > 0) {\n const messages = this.messageQueue.shift()!\n\n await this.callback(messages)\n }\n\n this.isProcessing = false\n }\n}\n\nexport 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\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\n *\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 * 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 */\nexport class ShapeStream {\n private options: ShapeStreamOptions\n private backoffOptions: BackoffOptions\n private fetchClient: typeof fetch\n private schema?: Schema\n\n private subscribers = new Map<\n number,\n [MessageProcessor, ((error: Error) => void) | undefined]\n >()\n private upToDateSubscribers = new Map<\n number,\n [() => void, (error: FetchError | Error) => void]\n >()\n\n private lastOffset: Offset\n private messageParser: MessageParser\n public isUpToDate: boolean = false\n\n shapeId?: string\n\n constructor(options: ShapeStreamOptions) {\n this.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(options.parser)\n\n this.backoffOptions = options.backoffOptions ?? BackoffDefaults\n this.fetchClient =\n options.fetchClient ??\n ((...args: Parameters<typeof fetch>) => fetch(...args))\n\n this.start()\n }\n\n async start() {\n this.isUpToDate = false\n\n const { url, where, signal } = this.options\n\n while ((!signal?.aborted && !this.isUpToDate) || this.options.subscribe) {\n const fetchUrl = new URL(url)\n if (where) fetchUrl.searchParams.set(`where`, where)\n fetchUrl.searchParams.set(`offset`, this.lastOffset)\n\n if (this.isUpToDate) {\n fetchUrl.searchParams.set(`live`, `true`)\n }\n\n if (this.shapeId) {\n // This should probably be a header for better cache breaking?\n fetchUrl.searchParams.set(`shape_id`, this.shapeId!)\n }\n\n let response!: Response\n\n try {\n const maybeResponse = await this.fetchWithBackoff(fetchUrl)\n if (maybeResponse) response = maybeResponse\n else break\n } catch (e) {\n if (!(e instanceof FetchError)) throw e // should never happen\n 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[`x-electric-shape-id`]\n this.reset(newShapeId)\n this.publish(e.json as Message[])\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(`X-Electric-Shape-Id`)\n if (shapeId) {\n this.shapeId = shapeId\n }\n\n const lastOffset = headers.get(`X-Electric-Chunk-Last-Offset`)\n if (lastOffset) {\n this.lastOffset = lastOffset as Offset\n }\n\n const getSchema = (): Schema => {\n const schemaHeader = headers.get(`X-Electric-Schema`)\n return schemaHeader ? JSON.parse(schemaHeader) : {}\n }\n this.schema = this.schema ?? getSchema()\n\n const messages = status === 204 ? `[]` : await response.text()\n\n const batch = this.messageParser.parse(messages, this.schema)\n\n // Update isUpToDate\n if (batch.length > 0) {\n const lastMessage = batch[batch.length - 1]\n if (\n isControlMessage(lastMessage) &&\n lastMessage.headers.control === `up-to-date` &&\n !this.isUpToDate\n ) {\n this.isUpToDate = true\n this.notifyUpToDateSubscribers()\n }\n\n this.publish(batch)\n }\n }\n }\n\n subscribe(\n callback: (messages: Message[]) => void | Promise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n const subscriber = new MessageProcessor(callback)\n\n this.subscribers.set(subscriptionId, [subscriber, onError])\n\n return () => {\n this.subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.subscribers.clear()\n }\n\n private publish(messages: Message[]) {\n this.subscribers.forEach(([subscriber, _]) => {\n subscriber.process(messages)\n })\n }\n\n private sendErrorToSubscribers(error: Error) {\n this.subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n subscribeOnceToUpToDate(\n callback: () => void | Promise<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 private notifyUpToDateSubscribers() {\n this.upToDateSubscribers.forEach(([callback]) => {\n callback()\n })\n }\n\n private sendErrorToUpToDateSubscribers(error: FetchError | Error) {\n // eslint-disable-next-line\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 private reset(shapeId?: string) {\n this.lastOffset = `-1`\n this.shapeId = shapeId\n this.isUpToDate = false\n this.schema = undefined\n }\n\n private validateOptions(options: 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 }\n\n private async fetchWithBackoff(url: URL) {\n const { initialDelay, maxDelay, multiplier } = this.backoffOptions\n const signal = this.options.signal\n\n let delay = initialDelay\n let attempt = 0\n\n // eslint-disable-next-line no-constant-condition -- we're retrying with a lag until we get a non-500 response or the abort signal is triggered\n while (true) {\n try {\n const result = await this.fetchClient(url.toString(), { signal })\n if (result.ok) return result\n else throw await FetchError.fromResponse(result, url.toString())\n } catch (e) {\n if (signal?.aborted) {\n return undefined\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 attempt++\n console.log(`Retry attempt #${attempt} after ${delay}ms`)\n }\n }\n }\n }\n}\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 {Shape}\n *\n * const shapeStream = new ShapeStream(url: 'http://localhost:3000/v1/shape/foo'})\n * const shape = new Shape(shapeStream)\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 {\n private stream: ShapeStream\n\n private data: ShapeData = new Map()\n private subscribers = new Map<number, ShapeChangedCallback>()\n public error: FetchError | false = false\n private hasNotifiedSubscribersUpToDate: boolean = false\n\n constructor(stream: ShapeStream) {\n this.stream = stream\n this.stream.subscribe(this.process.bind(this), this.handleError.bind(this))\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> {\n return new Promise((resolve) => {\n if (this.stream.isUpToDate) {\n resolve(this.valueSync)\n } else {\n const unsubscribe = this.stream.subscribeOnceToUpToDate(\n () => {\n unsubscribe()\n resolve(this.valueSync)\n },\n (e) => {\n throw e\n }\n )\n }\n })\n }\n\n get valueSync() {\n return this.data\n }\n\n subscribe(callback: ShapeChangedCallback): () => 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 private process(messages: Message[]): 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 private handleError(e: Error): void {\n if (e instanceof FetchError) {\n this.error = e\n this.notify()\n }\n }\n\n private notify(): void {\n this.subscribers.forEach((callback) => {\n callback(this.valueSync)\n })\n }\n}\n"],"mappings":"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","names":["key","value"]}
|
|
1
|
+
{"version":3,"sources":["../src/parser.ts","../src/helpers.ts","../src/client.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` is needed because\n // there could be a column named `value`\n // and the value associated to that column will be a string\n if (key === `value` && typeof value === `object`) {\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","import { Message, Offset, Schema, Row } from './types'\nimport { MessageParser, Parser } from './parser'\nimport { isChangeMessage, isControlMessage } from './helpers'\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\nexport interface BackoffOptions {\n initialDelay: number\n maxDelay: number\n multiplier: number\n}\n\nexport const BackoffDefaults = {\n initialDelay: 100,\n maxDelay: 10_000,\n multiplier: 1.3,\n}\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\n/**\n * Receives batches of `messages`, puts them on a queue and processes\n * them asynchronously by passing to a registered callback function.\n *\n * @constructor\n * @param {(messages: Message[]) => void} callback function\n */\nclass MessageProcessor<T extends Row = Row> {\n private messageQueue: Message<T>[][] = []\n private isProcessing = false\n private callback: (messages: Message<T>[]) => void | Promise<void>\n\n constructor(callback: (messages: Message<T>[]) => void | Promise<void>) {\n this.callback = callback\n }\n\n process(messages: Message<T>[]) {\n this.messageQueue.push(messages)\n\n if (!this.isProcessing) {\n this.processQueue()\n }\n }\n\n private async processQueue() {\n this.isProcessing = true\n\n while (this.messageQueue.length > 0) {\n const messages = this.messageQueue.shift()!\n\n await this.callback(messages)\n }\n\n this.isProcessing = false\n }\n}\n\nexport 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\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 */\nexport class ShapeStream<T extends Row = Row> {\n private options: ShapeStreamOptions\n private backoffOptions: BackoffOptions\n private fetchClient: typeof fetch\n private schema?: Schema\n\n private subscribers = new Map<\n number,\n [MessageProcessor<T>, ((error: Error) => void) | undefined]\n >()\n private upToDateSubscribers = new Map<\n number,\n [() => void, (error: FetchError | Error) => void]\n >()\n\n private lastOffset: Offset\n private messageParser: MessageParser<T>\n public isUpToDate: boolean = false\n\n shapeId?: string\n\n constructor(options: ShapeStreamOptions) {\n this.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 this.backoffOptions = options.backoffOptions ?? BackoffDefaults\n this.fetchClient =\n options.fetchClient ??\n ((...args: Parameters<typeof fetch>) => fetch(...args))\n\n this.start()\n }\n\n async start() {\n this.isUpToDate = false\n\n const { url, where, signal } = this.options\n\n while ((!signal?.aborted && !this.isUpToDate) || this.options.subscribe) {\n const fetchUrl = new URL(url)\n if (where) fetchUrl.searchParams.set(`where`, where)\n fetchUrl.searchParams.set(`offset`, this.lastOffset)\n\n if (this.isUpToDate) {\n fetchUrl.searchParams.set(`live`, `true`)\n }\n\n if (this.shapeId) {\n // This should probably be a header for better cache breaking?\n fetchUrl.searchParams.set(`shape_id`, this.shapeId!)\n }\n\n let response!: Response\n\n try {\n const maybeResponse = await this.fetchWithBackoff(fetchUrl)\n if (maybeResponse) response = maybeResponse\n else break\n } catch (e) {\n if (!(e instanceof FetchError)) throw e // should never happen\n 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[`x-electric-shape-id`]\n this.reset(newShapeId)\n 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(`X-Electric-Shape-Id`)\n if (shapeId) {\n this.shapeId = shapeId\n }\n\n const lastOffset = headers.get(`X-Electric-Chunk-Last-Offset`)\n if (lastOffset) {\n this.lastOffset = lastOffset as Offset\n }\n\n const getSchema = (): Schema => {\n const schemaHeader = headers.get(`X-Electric-Schema`)\n return schemaHeader ? JSON.parse(schemaHeader) : {}\n }\n this.schema = this.schema ?? getSchema()\n\n const messages = status === 204 ? `[]` : await response.text()\n\n const batch = this.messageParser.parse(messages, this.schema)\n\n // Update isUpToDate\n if (batch.length > 0) {\n const lastMessage = batch[batch.length - 1]\n if (\n isControlMessage(lastMessage) &&\n lastMessage.headers.control === `up-to-date` &&\n !this.isUpToDate\n ) {\n this.isUpToDate = true\n this.notifyUpToDateSubscribers()\n }\n\n this.publish(batch)\n }\n }\n }\n\n subscribe(\n callback: (messages: Message<T>[]) => void | Promise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n const subscriber = new MessageProcessor(callback)\n\n this.subscribers.set(subscriptionId, [subscriber, onError])\n\n return () => {\n this.subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.subscribers.clear()\n }\n\n private publish(messages: Message<T>[]) {\n this.subscribers.forEach(([subscriber, _]) => {\n subscriber.process(messages)\n })\n }\n\n private sendErrorToSubscribers(error: Error) {\n this.subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n subscribeOnceToUpToDate(\n callback: () => void | Promise<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 private notifyUpToDateSubscribers() {\n this.upToDateSubscribers.forEach(([callback]) => {\n callback()\n })\n }\n\n private sendErrorToUpToDateSubscribers(error: FetchError | Error) {\n // eslint-disable-next-line\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 private reset(shapeId?: string) {\n this.lastOffset = `-1`\n this.shapeId = shapeId\n this.isUpToDate = false\n this.schema = undefined\n }\n\n private validateOptions(options: 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 }\n\n private async fetchWithBackoff(url: URL) {\n const { initialDelay, maxDelay, multiplier } = this.backoffOptions\n const signal = this.options.signal\n\n let delay = initialDelay\n let attempt = 0\n\n // eslint-disable-next-line no-constant-condition -- we're retrying with a lag until we get a non-500 response or the abort signal is triggered\n while (true) {\n try {\n const result = await this.fetchClient(url.toString(), { signal })\n if (result.ok) return result\n else throw await FetchError.fromResponse(result, url.toString())\n } catch (e) {\n if (signal?.aborted) {\n return undefined\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 attempt++\n console.log(`Retry attempt #${attempt} after ${delay}ms`)\n }\n }\n }\n }\n}\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 private stream: ShapeStream<T>\n\n private data: ShapeData<T> = new Map()\n private subscribers = new Map<number, ShapeChangedCallback<T>>()\n public error: FetchError | false = false\n private hasNotifiedSubscribersUpToDate: boolean = false\n\n constructor(stream: ShapeStream<T>) {\n this.stream = stream\n this.stream.subscribe(this.process.bind(this), this.handleError.bind(this))\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) => {\n if (this.stream.isUpToDate) {\n resolve(this.valueSync)\n } else {\n const unsubscribe = this.stream.subscribeOnceToUpToDate(\n () => {\n unsubscribe()\n resolve(this.valueSync)\n },\n (e) => {\n throw e\n }\n )\n }\n })\n }\n\n get valueSync() {\n return this.data\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 private 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 private handleError(e: Error): void {\n if (e instanceof FetchError) {\n this.error = e\n this.notify()\n }\n }\n\n private notify(): void {\n this.subscribers.forEach((callback) => {\n callback(this.valueSync)\n })\n }\n}\n"],"mappings":"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","names":["key","value"]}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/parser.ts","../src/helpers.ts","../src/client.ts"],"sourcesContent":["import { ColumnInfo, Message, 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 {\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[] {\n return JSON.parse(messages, (key, value) => {\n // typeof value === `object` is needed because\n // there could be a column named `value`\n // and the value associated to that column will be a string\n if (key === `value` && typeof value === `object`) {\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[]\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, Value } 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 Value = { [key: string]: Value }>(\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 Value = { [key: string]: Value }>(\n message: Message<T>\n): message is ControlMessage {\n return !isChangeMessage(message)\n}\n","import { Message, Value, Offset, Schema } from './types'\nimport { MessageParser, Parser } from './parser'\nimport { isChangeMessage, isControlMessage } from './helpers'\n\nexport type ShapeData = Map<string, { [key: string]: Value }>\nexport type ShapeChangedCallback = (value: ShapeData) => void\n\nexport interface BackoffOptions {\n initialDelay: number\n maxDelay: number\n multiplier: number\n}\n\nexport const BackoffDefaults = {\n initialDelay: 100,\n maxDelay: 10_000,\n multiplier: 1.3,\n}\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\n/**\n * Receives batches of `messages`, puts them on a queue and processes\n * them asynchronously by passing to a registered callback function.\n *\n * @constructor\n * @param {(messages: Message[]) => void} callback function\n */\nclass MessageProcessor {\n private messageQueue: Message[][] = []\n private isProcessing = false\n private callback: (messages: Message[]) => void | Promise<void>\n\n constructor(callback: (messages: Message[]) => void | Promise<void>) {\n this.callback = callback\n }\n\n process(messages: Message[]) {\n this.messageQueue.push(messages)\n\n if (!this.isProcessing) {\n this.processQueue()\n }\n }\n\n private async processQueue() {\n this.isProcessing = true\n\n while (this.messageQueue.length > 0) {\n const messages = this.messageQueue.shift()!\n\n await this.callback(messages)\n }\n\n this.isProcessing = false\n }\n}\n\nexport 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\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\n *\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 * 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 */\nexport class ShapeStream {\n private options: ShapeStreamOptions\n private backoffOptions: BackoffOptions\n private fetchClient: typeof fetch\n private schema?: Schema\n\n private subscribers = new Map<\n number,\n [MessageProcessor, ((error: Error) => void) | undefined]\n >()\n private upToDateSubscribers = new Map<\n number,\n [() => void, (error: FetchError | Error) => void]\n >()\n\n private lastOffset: Offset\n private messageParser: MessageParser\n public isUpToDate: boolean = false\n\n shapeId?: string\n\n constructor(options: ShapeStreamOptions) {\n this.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(options.parser)\n\n this.backoffOptions = options.backoffOptions ?? BackoffDefaults\n this.fetchClient =\n options.fetchClient ??\n ((...args: Parameters<typeof fetch>) => fetch(...args))\n\n this.start()\n }\n\n async start() {\n this.isUpToDate = false\n\n const { url, where, signal } = this.options\n\n while ((!signal?.aborted && !this.isUpToDate) || this.options.subscribe) {\n const fetchUrl = new URL(url)\n if (where) fetchUrl.searchParams.set(`where`, where)\n fetchUrl.searchParams.set(`offset`, this.lastOffset)\n\n if (this.isUpToDate) {\n fetchUrl.searchParams.set(`live`, `true`)\n }\n\n if (this.shapeId) {\n // This should probably be a header for better cache breaking?\n fetchUrl.searchParams.set(`shape_id`, this.shapeId!)\n }\n\n let response!: Response\n\n try {\n const maybeResponse = await this.fetchWithBackoff(fetchUrl)\n if (maybeResponse) response = maybeResponse\n else break\n } catch (e) {\n if (!(e instanceof FetchError)) throw e // should never happen\n 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[`x-electric-shape-id`]\n this.reset(newShapeId)\n this.publish(e.json as Message[])\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(`X-Electric-Shape-Id`)\n if (shapeId) {\n this.shapeId = shapeId\n }\n\n const lastOffset = headers.get(`X-Electric-Chunk-Last-Offset`)\n if (lastOffset) {\n this.lastOffset = lastOffset as Offset\n }\n\n const getSchema = (): Schema => {\n const schemaHeader = headers.get(`X-Electric-Schema`)\n return schemaHeader ? JSON.parse(schemaHeader) : {}\n }\n this.schema = this.schema ?? getSchema()\n\n const messages = status === 204 ? `[]` : await response.text()\n\n const batch = this.messageParser.parse(messages, this.schema)\n\n // Update isUpToDate\n if (batch.length > 0) {\n const lastMessage = batch[batch.length - 1]\n if (\n isControlMessage(lastMessage) &&\n lastMessage.headers.control === `up-to-date` &&\n !this.isUpToDate\n ) {\n this.isUpToDate = true\n this.notifyUpToDateSubscribers()\n }\n\n this.publish(batch)\n }\n }\n }\n\n subscribe(\n callback: (messages: Message[]) => void | Promise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n const subscriber = new MessageProcessor(callback)\n\n this.subscribers.set(subscriptionId, [subscriber, onError])\n\n return () => {\n this.subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.subscribers.clear()\n }\n\n private publish(messages: Message[]) {\n this.subscribers.forEach(([subscriber, _]) => {\n subscriber.process(messages)\n })\n }\n\n private sendErrorToSubscribers(error: Error) {\n this.subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n subscribeOnceToUpToDate(\n callback: () => void | Promise<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 private notifyUpToDateSubscribers() {\n this.upToDateSubscribers.forEach(([callback]) => {\n callback()\n })\n }\n\n private sendErrorToUpToDateSubscribers(error: FetchError | Error) {\n // eslint-disable-next-line\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 private reset(shapeId?: string) {\n this.lastOffset = `-1`\n this.shapeId = shapeId\n this.isUpToDate = false\n this.schema = undefined\n }\n\n private validateOptions(options: 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 }\n\n private async fetchWithBackoff(url: URL) {\n const { initialDelay, maxDelay, multiplier } = this.backoffOptions\n const signal = this.options.signal\n\n let delay = initialDelay\n let attempt = 0\n\n // eslint-disable-next-line no-constant-condition -- we're retrying with a lag until we get a non-500 response or the abort signal is triggered\n while (true) {\n try {\n const result = await this.fetchClient(url.toString(), { signal })\n if (result.ok) return result\n else throw await FetchError.fromResponse(result, url.toString())\n } catch (e) {\n if (signal?.aborted) {\n return undefined\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 attempt++\n console.log(`Retry attempt #${attempt} after ${delay}ms`)\n }\n }\n }\n }\n}\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 {Shape}\n *\n * const shapeStream = new ShapeStream(url: 'http://localhost:3000/v1/shape/foo'})\n * const shape = new Shape(shapeStream)\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 {\n private stream: ShapeStream\n\n private data: ShapeData = new Map()\n private subscribers = new Map<number, ShapeChangedCallback>()\n public error: FetchError | false = false\n private hasNotifiedSubscribersUpToDate: boolean = false\n\n constructor(stream: ShapeStream) {\n this.stream = stream\n this.stream.subscribe(this.process.bind(this), this.handleError.bind(this))\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> {\n return new Promise((resolve) => {\n if (this.stream.isUpToDate) {\n resolve(this.valueSync)\n } else {\n const unsubscribe = this.stream.subscribeOnceToUpToDate(\n () => {\n unsubscribe()\n resolve(this.valueSync)\n },\n (e) => {\n throw e\n }\n )\n }\n })\n }\n\n get valueSync() {\n return this.data\n }\n\n subscribe(callback: ShapeChangedCallback): () => 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 private process(messages: Message[]): 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 private handleError(e: Error): void {\n if (e instanceof FetchError) {\n this.error = e\n this.notify()\n }\n }\n\n private notify(): void {\n this.subscribers.forEach((callback) => {\n callback(this.valueSync)\n })\n }\n}\n"],"mappings":"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","names":["key","value"]}
|
|
1
|
+
{"version":3,"sources":["../src/parser.ts","../src/helpers.ts","../src/client.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` is needed because\n // there could be a column named `value`\n // and the value associated to that column will be a string\n if (key === `value` && typeof value === `object`) {\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","import { Message, Offset, Schema, Row } from './types'\nimport { MessageParser, Parser } from './parser'\nimport { isChangeMessage, isControlMessage } from './helpers'\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\nexport interface BackoffOptions {\n initialDelay: number\n maxDelay: number\n multiplier: number\n}\n\nexport const BackoffDefaults = {\n initialDelay: 100,\n maxDelay: 10_000,\n multiplier: 1.3,\n}\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\n/**\n * Receives batches of `messages`, puts them on a queue and processes\n * them asynchronously by passing to a registered callback function.\n *\n * @constructor\n * @param {(messages: Message[]) => void} callback function\n */\nclass MessageProcessor<T extends Row = Row> {\n private messageQueue: Message<T>[][] = []\n private isProcessing = false\n private callback: (messages: Message<T>[]) => void | Promise<void>\n\n constructor(callback: (messages: Message<T>[]) => void | Promise<void>) {\n this.callback = callback\n }\n\n process(messages: Message<T>[]) {\n this.messageQueue.push(messages)\n\n if (!this.isProcessing) {\n this.processQueue()\n }\n }\n\n private async processQueue() {\n this.isProcessing = true\n\n while (this.messageQueue.length > 0) {\n const messages = this.messageQueue.shift()!\n\n await this.callback(messages)\n }\n\n this.isProcessing = false\n }\n}\n\nexport 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\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 */\nexport class ShapeStream<T extends Row = Row> {\n private options: ShapeStreamOptions\n private backoffOptions: BackoffOptions\n private fetchClient: typeof fetch\n private schema?: Schema\n\n private subscribers = new Map<\n number,\n [MessageProcessor<T>, ((error: Error) => void) | undefined]\n >()\n private upToDateSubscribers = new Map<\n number,\n [() => void, (error: FetchError | Error) => void]\n >()\n\n private lastOffset: Offset\n private messageParser: MessageParser<T>\n public isUpToDate: boolean = false\n\n shapeId?: string\n\n constructor(options: ShapeStreamOptions) {\n this.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 this.backoffOptions = options.backoffOptions ?? BackoffDefaults\n this.fetchClient =\n options.fetchClient ??\n ((...args: Parameters<typeof fetch>) => fetch(...args))\n\n this.start()\n }\n\n async start() {\n this.isUpToDate = false\n\n const { url, where, signal } = this.options\n\n while ((!signal?.aborted && !this.isUpToDate) || this.options.subscribe) {\n const fetchUrl = new URL(url)\n if (where) fetchUrl.searchParams.set(`where`, where)\n fetchUrl.searchParams.set(`offset`, this.lastOffset)\n\n if (this.isUpToDate) {\n fetchUrl.searchParams.set(`live`, `true`)\n }\n\n if (this.shapeId) {\n // This should probably be a header for better cache breaking?\n fetchUrl.searchParams.set(`shape_id`, this.shapeId!)\n }\n\n let response!: Response\n\n try {\n const maybeResponse = await this.fetchWithBackoff(fetchUrl)\n if (maybeResponse) response = maybeResponse\n else break\n } catch (e) {\n if (!(e instanceof FetchError)) throw e // should never happen\n 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[`x-electric-shape-id`]\n this.reset(newShapeId)\n 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(`X-Electric-Shape-Id`)\n if (shapeId) {\n this.shapeId = shapeId\n }\n\n const lastOffset = headers.get(`X-Electric-Chunk-Last-Offset`)\n if (lastOffset) {\n this.lastOffset = lastOffset as Offset\n }\n\n const getSchema = (): Schema => {\n const schemaHeader = headers.get(`X-Electric-Schema`)\n return schemaHeader ? JSON.parse(schemaHeader) : {}\n }\n this.schema = this.schema ?? getSchema()\n\n const messages = status === 204 ? `[]` : await response.text()\n\n const batch = this.messageParser.parse(messages, this.schema)\n\n // Update isUpToDate\n if (batch.length > 0) {\n const lastMessage = batch[batch.length - 1]\n if (\n isControlMessage(lastMessage) &&\n lastMessage.headers.control === `up-to-date` &&\n !this.isUpToDate\n ) {\n this.isUpToDate = true\n this.notifyUpToDateSubscribers()\n }\n\n this.publish(batch)\n }\n }\n }\n\n subscribe(\n callback: (messages: Message<T>[]) => void | Promise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n const subscriber = new MessageProcessor(callback)\n\n this.subscribers.set(subscriptionId, [subscriber, onError])\n\n return () => {\n this.subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.subscribers.clear()\n }\n\n private publish(messages: Message<T>[]) {\n this.subscribers.forEach(([subscriber, _]) => {\n subscriber.process(messages)\n })\n }\n\n private sendErrorToSubscribers(error: Error) {\n this.subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n subscribeOnceToUpToDate(\n callback: () => void | Promise<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 private notifyUpToDateSubscribers() {\n this.upToDateSubscribers.forEach(([callback]) => {\n callback()\n })\n }\n\n private sendErrorToUpToDateSubscribers(error: FetchError | Error) {\n // eslint-disable-next-line\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 private reset(shapeId?: string) {\n this.lastOffset = `-1`\n this.shapeId = shapeId\n this.isUpToDate = false\n this.schema = undefined\n }\n\n private validateOptions(options: 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 }\n\n private async fetchWithBackoff(url: URL) {\n const { initialDelay, maxDelay, multiplier } = this.backoffOptions\n const signal = this.options.signal\n\n let delay = initialDelay\n let attempt = 0\n\n // eslint-disable-next-line no-constant-condition -- we're retrying with a lag until we get a non-500 response or the abort signal is triggered\n while (true) {\n try {\n const result = await this.fetchClient(url.toString(), { signal })\n if (result.ok) return result\n else throw await FetchError.fromResponse(result, url.toString())\n } catch (e) {\n if (signal?.aborted) {\n return undefined\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 attempt++\n console.log(`Retry attempt #${attempt} after ${delay}ms`)\n }\n }\n }\n }\n}\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 private stream: ShapeStream<T>\n\n private data: ShapeData<T> = new Map()\n private subscribers = new Map<number, ShapeChangedCallback<T>>()\n public error: FetchError | false = false\n private hasNotifiedSubscribersUpToDate: boolean = false\n\n constructor(stream: ShapeStream<T>) {\n this.stream = stream\n this.stream.subscribe(this.process.bind(this), this.handleError.bind(this))\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) => {\n if (this.stream.isUpToDate) {\n resolve(this.valueSync)\n } else {\n const unsubscribe = this.stream.subscribeOnceToUpToDate(\n () => {\n unsubscribe()\n resolve(this.valueSync)\n },\n (e) => {\n throw e\n }\n )\n }\n })\n }\n\n get valueSync() {\n return this.data\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 private 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 private handleError(e: Error): void {\n if (e instanceof FetchError) {\n this.error = e\n this.notify()\n }\n }\n\n private notify(): void {\n this.subscribers.forEach((callback) => {\n callback(this.valueSync)\n })\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeA,IAAM,cAAc,CAAC,UAAkB,OAAO,KAAK;AACnD,IAAM,YAAY,CAAC,UAAkB,UAAU,UAAU,UAAU;AACnE,IAAM,cAAc,CAAC,UAAkB,OAAO,KAAK;AACnD,IAAM,YAAY,CAAC,UAAkB,KAAK,MAAM,KAAK;AACrD,IAAM,iBAAgC,CAAC,MAAc;AAE9C,IAAM,gBAAwB;AAAA,EACnC,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,OAAO;AACT;AAGO,SAAS,cAAc,OAAc,QAA+B;AACzE,MAAI,IAAI;AACR,MAAI,OAAO;AACX,MAAI,MAAM;AACV,MAAI,SAAS;AACb,MAAI,OAAO;AACX,MAAI,IAAwB;AAE5B,WAAS,KAAK,GAAoB;AAChC,UAAM,KAAK,CAAC;AACZ,WAAO,IAAI,EAAE,QAAQ,KAAK;AACxB,aAAO,EAAE,CAAC;AACV,UAAI,QAAQ;AACV,YAAI,SAAS,MAAM;AACjB,iBAAO,EAAE,EAAE,CAAC;AAAA,QACd,WAAW,SAAS,KAAK;AACvB,aAAG,KAAK,SAAS,OAAO,GAAG,IAAI,GAAG;AAClC,gBAAM;AACN,mBAAS,EAAE,IAAI,CAAC,MAAM;AACtB,iBAAO,IAAI;AAAA,QACb,OAAO;AACL,iBAAO;AAAA,QACT;AAAA,MACF,WAAW,SAAS,KAAK;AACvB,iBAAS;AAAA,MACX,WAAW,SAAS,KAAK;AACvB,eAAO,EAAE;AACT,WAAG,KAAK,KAAK,CAAC,CAAC;AAAA,MACjB,WAAW,SAAS,KAAK;AACvB,iBAAS;AACT,eAAO,KACL,GAAG,KAAK,SAAS,OAAO,EAAE,MAAM,MAAM,CAAC,CAAC,IAAI,EAAE,MAAM,MAAM,CAAC,CAAC;AAC9D,eAAO,IAAI;AACX;AAAA,MACF,WAAW,SAAS,OAAO,MAAM,OAAO,MAAM,KAAK;AACjD,WAAG,KAAK,SAAS,OAAO,EAAE,MAAM,MAAM,CAAC,CAAC,IAAI,EAAE,MAAM,MAAM,CAAC,CAAC;AAC5D,eAAO,IAAI;AAAA,MACb;AACA,UAAI;AAAA,IACN;AACA,WAAO,KACL,GAAG,KAAK,SAAS,OAAO,EAAE,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI,EAAE,MAAM,MAAM,IAAI,CAAC,CAAC;AACtE,WAAO;AAAA,EACT;AAEA,SAAO,KAAK,KAAK,EAAE,CAAC;AACtB;AAEO,IAAM,gBAAN,MAAmC;AAAA,EAExC,YAAY,QAAiB;AAI3B,SAAK,SAAS,kCAAK,gBAAkB;AAAA,EACvC;AAAA,EAEA,MAAM,UAAkB,QAA8B;AACpD,WAAO,KAAK,MAAM,UAAU,CAAC,KAAK,UAAU;AAI1C,UAAI,QAAQ,WAAW,OAAO,UAAU,UAAU;AAEhD,cAAM,MAAM;AACZ,eAAO,KAAK,GAAG,EAAE,QAAQ,CAACA,SAAQ;AAChC,cAAIA,IAAG,IAAI,KAAK,SAASA,MAAK,IAAIA,IAAG,GAAoB,MAAM;AAAA,QACjE,CAAC;AAAA,MACH;AACA,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAAA;AAAA,EAGQ,SAAS,KAAa,OAAsB,QAAuB;AA3G7E;AA4GI,UAAM,aAAa,OAAO,GAAG;AAC7B,QAAI,CAAC,YAAY;AAGf,aAAO;AAAA,IACT;AAGA,UAA2D,iBAAnD,QAAM,KAAK,MAAM,WApH7B,IAoH+D,IAAnB,2BAAmB,IAAnB,CAAhC,QAAW;AAKnB,UAAM,cAAa,UAAK,OAAO,GAAG,MAAf,YAAoB;AACvC,UAAM,SAAS,mBAAmB,YAAY,YAAY,GAAG;AAE7D,QAAI,cAAc,aAAa,GAAG;AAEhC,YAAM,wBAAwB;AAAA,QAC5B,CAACC,QAAO,MAAM,cAAcA,QAAO,MAAM;AAAA,QACzC;AAAA,QACA;AAAA,MACF;AACA,aAAO,sBAAsB,KAAK;AAAA,IACpC;AAEA,WAAO,OAAO,OAAO,cAAc;AAAA,EACrC;AACF;AAEA,SAAS,mBACP,QACA,YACA,YACuB;AA9IzB;AA+IE,QAAM,aAAa,GAAE,gBAAW,aAAX,YAAuB;AAI5C,SAAO,CAAC,UAAyB;AAC/B,QAAI,SAAS,KAAK,GAAG;AACnB,UAAI,CAAC,YAAY;AACf,cAAM,IAAI,MAAM,UAAU,kCAAc,SAAS,kBAAkB;AAAA,MACrE;AACA,aAAO;AAAA,IACT;AACA,WAAO,OAAO,OAAO,UAAU;AAAA,EACjC;AACF;AAEA,SAAS,SAAS,OAA0C;AAC1D,SAAO,UAAU,QAAQ,UAAU;AACrC;;;AC7IO,SAAS,gBACd,SAC6B;AAC7B,SAAO,SAAS;AAClB;AAmBO,SAAS,iBACd,SAC2B;AAC3B,SAAO,CAAC,gBAAgB,OAAO;AACjC;;;AC/BO,IAAM,kBAAkB;AAAA,EAC7B,cAAc;AAAA,EACd,UAAU;AAAA,EACV,YAAY;AACd;AA+CA,IAAM,mBAAN,MAA4C;AAAA,EAK1C,YAAY,UAA4D;AAJxE,SAAQ,eAA+B,CAAC;AACxC,SAAQ,eAAe;AAIrB,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,QAAQ,UAAwB;AAC9B,SAAK,aAAa,KAAK,QAAQ;AAE/B,QAAI,CAAC,KAAK,cAAc;AACtB,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AAAA,EAEc,eAAe;AAAA;AAC3B,WAAK,eAAe;AAEpB,aAAO,KAAK,aAAa,SAAS,GAAG;AACnC,cAAM,WAAW,KAAK,aAAa,MAAM;AAEzC,cAAM,KAAK,SAAS,QAAQ;AAAA,MAC9B;AAEA,WAAK,eAAe;AAAA,IACtB;AAAA;AACF;AAEO,IAAM,aAAN,MAAM,oBAAmB,MAAM;AAAA,EAMpC,YACE,QACA,MACA,MACA,SACO,KACP,SACA;AACA;AAAA,MACE,WACE,cAAc,MAAM,OAAO,GAAG,KAAK,sBAAQ,KAAK,UAAU,IAAI,CAAC;AAAA,IACnE;AANO;AAOP,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,OAAa,aACX,UACA,KACqB;AAAA;AACrB,YAAM,SAAS,SAAS;AACxB,YAAM,UAAU,OAAO,YAAY,CAAC,GAAG,SAAS,QAAQ,QAAQ,CAAC,CAAC;AAClE,UAAI,OAA2B;AAC/B,UAAI,OAA2B;AAE/B,YAAM,cAAc,SAAS,QAAQ,IAAI,cAAc;AACvD,UAAI,eAAe,YAAY,SAAS,kBAAkB,GAAG;AAC3D,eAAQ,MAAM,SAAS,KAAK;AAAA,MAC9B,OAAO;AACL,eAAO,MAAM,SAAS,KAAK;AAAA,MAC7B;AAEA,aAAO,IAAI,YAAW,QAAQ,MAAM,MAAM,SAAS,GAAG;AAAA,IACxD;AAAA;AACF;AAgCO,IAAM,cAAN,MAAuC;AAAA,EAqB5C,YAAY,SAA6B;AAfzC,SAAQ,cAAc,oBAAI,IAGxB;AACF,SAAQ,sBAAsB,oBAAI,IAGhC;AAIF,SAAO,aAAsB;AA5L/B;AAiMI,SAAK,gBAAgB,OAAO;AAC5B,SAAK,UAAU,iBAAE,WAAW,QAAS;AACrC,SAAK,cAAa,UAAK,QAAQ,WAAb,YAAuB;AACzC,SAAK,UAAU,KAAK,QAAQ;AAC5B,SAAK,gBAAgB,IAAI,cAAiB,QAAQ,MAAM;AAExD,SAAK,kBAAiB,aAAQ,mBAAR,YAA0B;AAChD,SAAK,eACH,aAAQ,gBAAR,YACC,IAAI,SAAmC,MAAM,GAAG,IAAI;AAEvD,SAAK,MAAM;AAAA,EACb;AAAA,EAEM,QAAQ;AAAA;AA/MhB;AAgNI,WAAK,aAAa;AAElB,YAAM,EAAE,KAAK,OAAO,OAAO,IAAI,KAAK;AAEpC,aAAQ,EAAC,iCAAQ,YAAW,CAAC,KAAK,cAAe,KAAK,QAAQ,WAAW;AACvE,cAAM,WAAW,IAAI,IAAI,GAAG;AAC5B,YAAI,MAAO,UAAS,aAAa,IAAI,SAAS,KAAK;AACnD,iBAAS,aAAa,IAAI,UAAU,KAAK,UAAU;AAEnD,YAAI,KAAK,YAAY;AACnB,mBAAS,aAAa,IAAI,QAAQ,MAAM;AAAA,QAC1C;AAEA,YAAI,KAAK,SAAS;AAEhB,mBAAS,aAAa,IAAI,YAAY,KAAK,OAAQ;AAAA,QACrD;AAEA,YAAI;AAEJ,YAAI;AACF,gBAAM,gBAAgB,MAAM,KAAK,iBAAiB,QAAQ;AAC1D,cAAI,cAAe,YAAW;AAAA,cACzB;AAAA,QACP,SAAS,GAAG;AACV,cAAI,EAAE,aAAa,YAAa,OAAM;AACtC,cAAI,EAAE,UAAU,KAAK;AAGnB,kBAAM,aAAa,EAAE,QAAQ,qBAAqB;AAClD,iBAAK,MAAM,UAAU;AACrB,iBAAK,QAAQ,EAAE,IAAoB;AACnC;AAAA,UACF,WAAW,EAAE,UAAU,OAAO,EAAE,SAAS,KAAK;AAE5C,iBAAK,+BAA+B,CAAC;AACrC,iBAAK,uBAAuB,CAAC;AAG7B,kBAAM;AAAA,UACR;AAAA,QACF;AAEA,cAAM,EAAE,SAAS,OAAO,IAAI;AAC5B,cAAM,UAAU,QAAQ,IAAI,qBAAqB;AACjD,YAAI,SAAS;AACX,eAAK,UAAU;AAAA,QACjB;AAEA,cAAM,aAAa,QAAQ,IAAI,8BAA8B;AAC7D,YAAI,YAAY;AACd,eAAK,aAAa;AAAA,QACpB;AAEA,cAAM,YAAY,MAAc;AAC9B,gBAAM,eAAe,QAAQ,IAAI,mBAAmB;AACpD,iBAAO,eAAe,KAAK,MAAM,YAAY,IAAI,CAAC;AAAA,QACpD;AACA,aAAK,UAAS,UAAK,WAAL,YAAe,UAAU;AAEvC,cAAM,WAAW,WAAW,MAAM,OAAO,MAAM,SAAS,KAAK;AAE7D,cAAM,QAAQ,KAAK,cAAc,MAAM,UAAU,KAAK,MAAM;AAG5D,YAAI,MAAM,SAAS,GAAG;AACpB,gBAAM,cAAc,MAAM,MAAM,SAAS,CAAC;AAC1C,cACE,iBAAiB,WAAW,KAC5B,YAAY,QAAQ,YAAY,gBAChC,CAAC,KAAK,YACN;AACA,iBAAK,aAAa;AAClB,iBAAK,0BAA0B;AAAA,UACjC;AAEA,eAAK,QAAQ,KAAK;AAAA,QACpB;AAAA,MACF;AAAA,IACF;AAAA;AAAA,EAEA,UACE,UACA,SACA;AACA,UAAM,iBAAiB,KAAK,OAAO;AACnC,UAAM,aAAa,IAAI,iBAAiB,QAAQ;AAEhD,SAAK,YAAY,IAAI,gBAAgB,CAAC,YAAY,OAAO,CAAC;AAE1D,WAAO,MAAM;AACX,WAAK,YAAY,OAAO,cAAc;AAAA,IACxC;AAAA,EACF;AAAA,EAEA,iBAAuB;AACrB,SAAK,YAAY,MAAM;AAAA,EACzB;AAAA,EAEQ,QAAQ,UAAwB;AACtC,SAAK,YAAY,QAAQ,CAAC,CAAC,YAAY,CAAC,MAAM;AAC5C,iBAAW,QAAQ,QAAQ;AAAA,IAC7B,CAAC;AAAA,EACH;AAAA,EAEQ,uBAAuB,OAAc;AAC3C,SAAK,YAAY,QAAQ,CAAC,CAAC,GAAG,OAAO,MAAM;AACzC,yCAAU;AAAA,IACZ,CAAC;AAAA,EACH;AAAA,EAEA,wBACE,UACA,OACA;AACA,UAAM,iBAAiB,KAAK,OAAO;AAEnC,SAAK,oBAAoB,IAAI,gBAAgB,CAAC,UAAU,KAAK,CAAC;AAE9D,WAAO,MAAM;AACX,WAAK,oBAAoB,OAAO,cAAc;AAAA,IAChD;AAAA,EACF;AAAA,EAEA,oCAA0C;AACxC,SAAK,oBAAoB,MAAM;AAAA,EACjC;AAAA,EAEQ,4BAA4B;AAClC,SAAK,oBAAoB,QAAQ,CAAC,CAAC,QAAQ,MAAM;AAC/C,eAAS;AAAA,IACX,CAAC;AAAA,EACH;AAAA,EAEQ,+BAA+B,OAA2B;AAEhE,SAAK,oBAAoB;AAAA,MAAQ,CAAC,CAAC,GAAG,aAAa,MACjD,cAAc,KAAK;AAAA,IACrB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,MAAM,SAAkB;AAC9B,SAAK,aAAa;AAClB,SAAK,UAAU;AACf,SAAK,aAAa;AAClB,SAAK,SAAS;AAAA,EAChB;AAAA,EAEQ,gBAAgB,SAAmC;AACzD,QAAI,CAAC,QAAQ,KAAK;AAChB,YAAM,IAAI,MAAM,+CAA+C;AAAA,IACjE;AACA,QAAI,QAAQ,UAAU,EAAE,QAAQ,kBAAkB,cAAc;AAC9D,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,QACE,QAAQ,WAAW,UACnB,QAAQ,WAAW,QACnB,CAAC,QAAQ,SACT;AACA,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEc,iBAAiB,KAAU;AAAA;AACvC,YAAM,EAAE,cAAc,UAAU,WAAW,IAAI,KAAK;AACpD,YAAM,SAAS,KAAK,QAAQ;AAE5B,UAAI,QAAQ;AACZ,UAAI,UAAU;AAGd,aAAO,MAAM;AACX,YAAI;AACF,gBAAM,SAAS,MAAM,KAAK,YAAY,IAAI,SAAS,GAAG,EAAE,OAAO,CAAC;AAChE,cAAI,OAAO,GAAI,QAAO;AAAA,cACjB,OAAM,MAAM,WAAW,aAAa,QAAQ,IAAI,SAAS,CAAC;AAAA,QACjE,SAAS,GAAG;AACV,cAAI,iCAAQ,SAAS;AACnB,mBAAO;AAAA,UACT,WACE,aAAa,cACb,EAAE,UAAU,OACZ,EAAE,SAAS,KACX;AAEA,kBAAM;AAAA,UACR,OAAO;AAGL,kBAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,KAAK,CAAC;AAGzD,oBAAQ,KAAK,IAAI,QAAQ,YAAY,QAAQ;AAE7C;AACA,oBAAQ,IAAI,kBAAkB,OAAO,UAAU,KAAK,IAAI;AAAA,UAC1D;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA;AACF;AAiCO,IAAM,QAAN,MAAiC;AAAA,EAQtC,YAAY,QAAwB;AALpC,SAAQ,OAAqB,oBAAI,IAAI;AACrC,SAAQ,cAAc,oBAAI,IAAqC;AAC/D,SAAO,QAA4B;AACnC,SAAQ,iCAA0C;AAGhD,SAAK,SAAS;AACd,SAAK,OAAO,UAAU,KAAK,QAAQ,KAAK,IAAI,GAAG,KAAK,YAAY,KAAK,IAAI,CAAC;AAC1E,UAAM,cAAc,KAAK,OAAO;AAAA,MAC9B,MAAM;AACJ,oBAAY;AAAA,MACd;AAAA,MACA,CAAC,MAAM;AACL,aAAK,YAAY,CAAC;AAClB,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA,EAEA,IAAI,aAAsB;AACxB,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA,EAEA,IAAI,QAA+B;AACjC,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAI,KAAK,OAAO,YAAY;AAC1B,gBAAQ,KAAK,SAAS;AAAA,MACxB,OAAO;AACL,cAAM,cAAc,KAAK,OAAO;AAAA,UAC9B,MAAM;AACJ,wBAAY;AACZ,oBAAQ,KAAK,SAAS;AAAA,UACxB;AAAA,UACA,CAAC,MAAM;AACL,kBAAM;AAAA,UACR;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,IAAI,YAAY;AACd,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,UAAU,UAA+C;AACvD,UAAM,iBAAiB,KAAK,OAAO;AAEnC,SAAK,YAAY,IAAI,gBAAgB,QAAQ;AAE7C,WAAO,MAAM;AACX,WAAK,YAAY,OAAO,cAAc;AAAA,IACxC;AAAA,EACF;AAAA,EAEA,iBAAuB;AACrB,SAAK,YAAY,MAAM;AAAA,EACzB;AAAA,EAEA,IAAI,iBAAiB;AACnB,WAAO,KAAK,YAAY;AAAA,EAC1B;AAAA,EAEQ,QAAQ,UAA8B;AAC5C,QAAI,qBAAqB;AACzB,QAAI,aAAa;AACjB,QAAI,gBAAgB;AAEpB,aAAS,QAAQ,CAAC,YAAY;AAC5B,UAAI,gBAAgB,OAAO,GAAG;AAC5B,6BAAqB,CAAC,UAAU,UAAU,QAAQ,EAAE;AAAA,UAClD,QAAQ,QAAQ;AAAA,QAClB;AAEA,gBAAQ,QAAQ,QAAQ,WAAW;AAAA,UACjC,KAAK;AACH,iBAAK,KAAK,IAAI,QAAQ,KAAK,QAAQ,KAAK;AACxC;AAAA,UACF,KAAK;AACH,iBAAK,KAAK,IAAI,QAAQ,KAAK,kCACtB,KAAK,KAAK,IAAI,QAAQ,GAAG,IACzB,QAAQ,MACZ;AACD;AAAA,UACF,KAAK;AACH,iBAAK,KAAK,OAAO,QAAQ,GAAG;AAC5B;AAAA,QACJ;AAAA,MACF;AAEA,UAAI,iBAAiB,OAAO,GAAG;AAC7B,gBAAQ,QAAQ,QAAQ,SAAS;AAAA,UAC/B,KAAK;AACH,yBAAa;AACb,gBAAI,CAAC,KAAK,gCAAgC;AACxC,8BAAgB;AAAA,YAClB;AACA;AAAA,UACF,KAAK;AACH,iBAAK,KAAK,MAAM;AAChB,iBAAK,QAAQ;AACb,yBAAa;AACb,4BAAgB;AAChB;AAAA,QACJ;AAAA,MACF;AAAA,IACF,CAAC;AAID,QAAI,iBAAkB,cAAc,oBAAqB;AACvD,WAAK,iCAAiC;AACtC,WAAK,OAAO;AAAA,IACd;AAAA,EACF;AAAA,EAEQ,YAAY,GAAgB;AAClC,QAAI,aAAa,YAAY;AAC3B,WAAK,QAAQ;AACb,WAAK,OAAO;AAAA,IACd;AAAA,EACF;AAAA,EAEQ,SAAe;AACrB,SAAK,YAAY,QAAQ,CAAC,aAAa;AACrC,eAAS,KAAK,SAAS;AAAA,IACzB,CAAC;AAAA,EACH;AACF;","names":["key","value"]}
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Message,
|
|
1
|
+
import { Message, Offset, Schema, Row } from './types'
|
|
2
2
|
import { MessageParser, Parser } from './parser'
|
|
3
3
|
import { isChangeMessage, isControlMessage } from './helpers'
|
|
4
4
|
|
|
5
|
-
export type ShapeData = Map<string,
|
|
6
|
-
export type ShapeChangedCallback =
|
|
5
|
+
export type ShapeData<T extends Row = Row> = Map<string, T>
|
|
6
|
+
export type ShapeChangedCallback<T extends Row = Row> = (
|
|
7
|
+
value: ShapeData<T>
|
|
8
|
+
) => void
|
|
7
9
|
|
|
8
10
|
export interface BackoffOptions {
|
|
9
11
|
initialDelay: number
|
|
@@ -62,16 +64,16 @@ export interface ShapeStreamOptions {
|
|
|
62
64
|
* @constructor
|
|
63
65
|
* @param {(messages: Message[]) => void} callback function
|
|
64
66
|
*/
|
|
65
|
-
class MessageProcessor {
|
|
66
|
-
private messageQueue: Message[][] = []
|
|
67
|
+
class MessageProcessor<T extends Row = Row> {
|
|
68
|
+
private messageQueue: Message<T>[][] = []
|
|
67
69
|
private isProcessing = false
|
|
68
|
-
private callback: (messages: Message[]) => void | Promise<void>
|
|
70
|
+
private callback: (messages: Message<T>[]) => void | Promise<void>
|
|
69
71
|
|
|
70
|
-
constructor(callback: (messages: Message[]) => void | Promise<void>) {
|
|
72
|
+
constructor(callback: (messages: Message<T>[]) => void | Promise<void>) {
|
|
71
73
|
this.callback = callback
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
process(messages: Message[]) {
|
|
76
|
+
process(messages: Message<T>[]) {
|
|
75
77
|
this.messageQueue.push(messages)
|
|
76
78
|
|
|
77
79
|
if (!this.isProcessing) {
|
|
@@ -144,28 +146,30 @@ export class FetchError extends Error {
|
|
|
144
146
|
* to consume the HTTP `GET /v1/shape` api.
|
|
145
147
|
*
|
|
146
148
|
* @constructor
|
|
147
|
-
* @param {ShapeStreamOptions} options
|
|
148
|
-
*
|
|
149
|
+
* @param {ShapeStreamOptions} options - configure the shape stream
|
|
150
|
+
* @example
|
|
149
151
|
* Register a callback function to subscribe to the messages.
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
152
|
+
* ```
|
|
153
|
+
* const stream = new ShapeStream(options)
|
|
154
|
+
* stream.subscribe(messages => {
|
|
155
|
+
* // messages is 1 or more row updates
|
|
156
|
+
* })
|
|
157
|
+
* ```
|
|
155
158
|
*
|
|
156
159
|
* To abort the stream, abort the `signal`
|
|
157
160
|
* passed in via the `ShapeStreamOptions`.
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
161
|
+
* ```
|
|
162
|
+
* const aborter = new AbortController()
|
|
163
|
+
* const issueStream = new ShapeStream({
|
|
164
|
+
* url: `${BASE_URL}/${table}`
|
|
165
|
+
* subscribe: true,
|
|
166
|
+
* signal: aborter.signal,
|
|
167
|
+
* })
|
|
168
|
+
* // Later...
|
|
169
|
+
* aborter.abort()
|
|
170
|
+
* ```
|
|
167
171
|
*/
|
|
168
|
-
export class ShapeStream {
|
|
172
|
+
export class ShapeStream<T extends Row = Row> {
|
|
169
173
|
private options: ShapeStreamOptions
|
|
170
174
|
private backoffOptions: BackoffOptions
|
|
171
175
|
private fetchClient: typeof fetch
|
|
@@ -173,7 +177,7 @@ export class ShapeStream {
|
|
|
173
177
|
|
|
174
178
|
private subscribers = new Map<
|
|
175
179
|
number,
|
|
176
|
-
[MessageProcessor
|
|
180
|
+
[MessageProcessor<T>, ((error: Error) => void) | undefined]
|
|
177
181
|
>()
|
|
178
182
|
private upToDateSubscribers = new Map<
|
|
179
183
|
number,
|
|
@@ -181,7 +185,7 @@ export class ShapeStream {
|
|
|
181
185
|
>()
|
|
182
186
|
|
|
183
187
|
private lastOffset: Offset
|
|
184
|
-
private messageParser: MessageParser
|
|
188
|
+
private messageParser: MessageParser<T>
|
|
185
189
|
public isUpToDate: boolean = false
|
|
186
190
|
|
|
187
191
|
shapeId?: string
|
|
@@ -191,7 +195,7 @@ export class ShapeStream {
|
|
|
191
195
|
this.options = { subscribe: true, ...options }
|
|
192
196
|
this.lastOffset = this.options.offset ?? `-1`
|
|
193
197
|
this.shapeId = this.options.shapeId
|
|
194
|
-
this.messageParser = new MessageParser(options.parser)
|
|
198
|
+
this.messageParser = new MessageParser<T>(options.parser)
|
|
195
199
|
|
|
196
200
|
this.backoffOptions = options.backoffOptions ?? BackoffDefaults
|
|
197
201
|
this.fetchClient =
|
|
@@ -233,7 +237,7 @@ export class ShapeStream {
|
|
|
233
237
|
// with the newly provided shape ID
|
|
234
238
|
const newShapeId = e.headers[`x-electric-shape-id`]
|
|
235
239
|
this.reset(newShapeId)
|
|
236
|
-
this.publish(e.json as Message[])
|
|
240
|
+
this.publish(e.json as Message<T>[])
|
|
237
241
|
continue
|
|
238
242
|
} else if (e.status >= 400 && e.status < 500) {
|
|
239
243
|
// Notify subscribers
|
|
@@ -284,7 +288,7 @@ export class ShapeStream {
|
|
|
284
288
|
}
|
|
285
289
|
|
|
286
290
|
subscribe(
|
|
287
|
-
callback: (messages: Message[]) => void | Promise<void>,
|
|
291
|
+
callback: (messages: Message<T>[]) => void | Promise<void>,
|
|
288
292
|
onError?: (error: FetchError | Error) => void
|
|
289
293
|
) {
|
|
290
294
|
const subscriptionId = Math.random()
|
|
@@ -301,7 +305,7 @@ export class ShapeStream {
|
|
|
301
305
|
this.subscribers.clear()
|
|
302
306
|
}
|
|
303
307
|
|
|
304
|
-
private publish(messages: Message[]) {
|
|
308
|
+
private publish(messages: Message<T>[]) {
|
|
305
309
|
this.subscribers.forEach(([subscriber, _]) => {
|
|
306
310
|
subscriber.process(messages)
|
|
307
311
|
})
|
|
@@ -423,10 +427,12 @@ export class ShapeStream {
|
|
|
423
427
|
* to simplify developing framework hooks.
|
|
424
428
|
*
|
|
425
429
|
* @constructor
|
|
426
|
-
* @param {
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
+
* @param {ShapeStream<T extends Row>} - the underlying shape stream
|
|
431
|
+
* @example
|
|
432
|
+
* ```
|
|
433
|
+
* const shapeStream = new ShapeStream<{ foo: number }>(url: 'http://localhost:3000/v1/shape/foo'})
|
|
434
|
+
* const shape = new Shape(shapeStream)
|
|
435
|
+
* ```
|
|
430
436
|
*
|
|
431
437
|
* `value` returns a promise that resolves the Shape data once the Shape has been
|
|
432
438
|
* fully loaded (and when resuming from being offline):
|
|
@@ -443,15 +449,15 @@ export class ShapeStream {
|
|
|
443
449
|
* console.log(shapeData)
|
|
444
450
|
* })
|
|
445
451
|
*/
|
|
446
|
-
export class Shape {
|
|
447
|
-
private stream: ShapeStream
|
|
452
|
+
export class Shape<T extends Row = Row> {
|
|
453
|
+
private stream: ShapeStream<T>
|
|
448
454
|
|
|
449
|
-
private data: ShapeData = new Map()
|
|
450
|
-
private subscribers = new Map<number, ShapeChangedCallback
|
|
455
|
+
private data: ShapeData<T> = new Map()
|
|
456
|
+
private subscribers = new Map<number, ShapeChangedCallback<T>>()
|
|
451
457
|
public error: FetchError | false = false
|
|
452
458
|
private hasNotifiedSubscribersUpToDate: boolean = false
|
|
453
459
|
|
|
454
|
-
constructor(stream: ShapeStream) {
|
|
460
|
+
constructor(stream: ShapeStream<T>) {
|
|
455
461
|
this.stream = stream
|
|
456
462
|
this.stream.subscribe(this.process.bind(this), this.handleError.bind(this))
|
|
457
463
|
const unsubscribe = this.stream.subscribeOnceToUpToDate(
|
|
@@ -469,7 +475,7 @@ export class Shape {
|
|
|
469
475
|
return this.stream.isUpToDate
|
|
470
476
|
}
|
|
471
477
|
|
|
472
|
-
get value(): Promise<ShapeData
|
|
478
|
+
get value(): Promise<ShapeData<T>> {
|
|
473
479
|
return new Promise((resolve) => {
|
|
474
480
|
if (this.stream.isUpToDate) {
|
|
475
481
|
resolve(this.valueSync)
|
|
@@ -491,7 +497,7 @@ export class Shape {
|
|
|
491
497
|
return this.data
|
|
492
498
|
}
|
|
493
499
|
|
|
494
|
-
subscribe(callback: ShapeChangedCallback): () => void {
|
|
500
|
+
subscribe(callback: ShapeChangedCallback<T>): () => void {
|
|
495
501
|
const subscriptionId = Math.random()
|
|
496
502
|
|
|
497
503
|
this.subscribers.set(subscriptionId, callback)
|
|
@@ -509,7 +515,7 @@ export class Shape {
|
|
|
509
515
|
return this.subscribers.size
|
|
510
516
|
}
|
|
511
517
|
|
|
512
|
-
private process(messages: Message[]): void {
|
|
518
|
+
private process(messages: Message<T>[]): void {
|
|
513
519
|
let dataMayHaveChanged = false
|
|
514
520
|
let isUpToDate = false
|
|
515
521
|
let newlyUpToDate = false
|
package/src/helpers.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChangeMessage, ControlMessage, Message,
|
|
1
|
+
import { ChangeMessage, ControlMessage, Message, Row } from './types'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Type guard for checking {@link Message} is {@link ChangeMessage}.
|
|
@@ -17,7 +17,7 @@ import { ChangeMessage, ControlMessage, Message, Value } from './types'
|
|
|
17
17
|
* }
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
|
-
export function isChangeMessage<T extends
|
|
20
|
+
export function isChangeMessage<T extends Row = Row>(
|
|
21
21
|
message: Message<T>
|
|
22
22
|
): message is ChangeMessage<T> {
|
|
23
23
|
return `key` in message
|
|
@@ -40,7 +40,7 @@ export function isChangeMessage<T extends Value = { [key: string]: Value }>(
|
|
|
40
40
|
* }
|
|
41
41
|
* ```
|
|
42
42
|
*/
|
|
43
|
-
export function isControlMessage<T extends
|
|
43
|
+
export function isControlMessage<T extends Row = Row>(
|
|
44
44
|
message: Message<T>
|
|
45
45
|
): message is ControlMessage {
|
|
46
46
|
return !isChangeMessage(message)
|
package/src/parser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ColumnInfo, Message, Schema, Value } from './types'
|
|
1
|
+
import { ColumnInfo, Message, Row, Schema, Value } from './types'
|
|
2
2
|
|
|
3
3
|
type NullToken = null | `NULL`
|
|
4
4
|
type Token = Exclude<string, NullToken>
|
|
@@ -79,7 +79,7 @@ export function pgArrayParser(value: Token, parser?: ParseFunction): Value {
|
|
|
79
79
|
return loop(value)[0]
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export class MessageParser {
|
|
82
|
+
export class MessageParser<T extends Row> {
|
|
83
83
|
private parser: Parser
|
|
84
84
|
constructor(parser?: Parser) {
|
|
85
85
|
// Merge the provided parser with the default parser
|
|
@@ -88,7 +88,7 @@ export class MessageParser {
|
|
|
88
88
|
this.parser = { ...defaultParser, ...parser }
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
parse(messages: string, schema: Schema): Message[] {
|
|
91
|
+
parse(messages: string, schema: Schema): Message<T>[] {
|
|
92
92
|
return JSON.parse(messages, (key, value) => {
|
|
93
93
|
// typeof value === `object` is needed because
|
|
94
94
|
// there could be a column named `value`
|
|
@@ -101,7 +101,7 @@ export class MessageParser {
|
|
|
101
101
|
})
|
|
102
102
|
}
|
|
103
103
|
return value
|
|
104
|
-
}) as Message[]
|
|
104
|
+
}) as Message<T>[]
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// Parses the message values using the provided parser based on the schema information
|
package/src/types.ts
CHANGED
|
@@ -7,6 +7,8 @@ export type Value =
|
|
|
7
7
|
| Value[]
|
|
8
8
|
| { [key: string]: Value }
|
|
9
9
|
|
|
10
|
+
export type Row = { [key: string]: Value }
|
|
11
|
+
|
|
10
12
|
export type Offset = `-1` | `${number}_${number}`
|
|
11
13
|
|
|
12
14
|
interface Header {
|
|
@@ -17,7 +19,7 @@ export type ControlMessage = {
|
|
|
17
19
|
headers: Header & { control: `up-to-date` | `must-refetch` }
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
export type ChangeMessage<T> = {
|
|
22
|
+
export type ChangeMessage<T extends Row = Row> = {
|
|
21
23
|
key: string
|
|
22
24
|
value: T
|
|
23
25
|
headers: Header & { operation: `insert` | `update` | `delete` }
|
|
@@ -25,9 +27,7 @@ export type ChangeMessage<T> = {
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
// Define the type for a record
|
|
28
|
-
export type Message<T extends
|
|
29
|
-
| ControlMessage
|
|
30
|
-
| ChangeMessage<T>
|
|
30
|
+
export type Message<T extends Row = Row> = ControlMessage | ChangeMessage<T>
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Common properties for all columns.
|
|
@@ -104,7 +104,7 @@ export type ColumnInfo =
|
|
|
104
104
|
|
|
105
105
|
export type Schema = { [key: string]: ColumnInfo }
|
|
106
106
|
|
|
107
|
-
export type TypedMessages<T extends
|
|
107
|
+
export type TypedMessages<T extends Row = Row> = {
|
|
108
108
|
messages: Array<Message<T>>
|
|
109
109
|
schema: ColumnInfo
|
|
110
110
|
}
|