@electric-sql/client 0.3.0 → 0.3.1
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 +14 -8
- 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 +18 -17
- package/dist/index.legacy-esm.js +14 -8
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +14 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/parser.ts +25 -12
- package/src/types.ts +18 -16
package/dist/cjs/index.cjs
CHANGED
|
@@ -147,22 +147,28 @@ var MessageParser = class {
|
|
|
147
147
|
}
|
|
148
148
|
// Parses the message values using the provided parser based on the schema information
|
|
149
149
|
parseRow(key, value, schema) {
|
|
150
|
+
var _b;
|
|
150
151
|
const columnInfo = schema[key];
|
|
151
152
|
if (!columnInfo) {
|
|
152
153
|
return value;
|
|
153
154
|
}
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (!parser) {
|
|
161
|
-
return value;
|
|
155
|
+
const _a = columnInfo, { type: typ, dims: dimensions } = _a, additionalInfo = __objRest(_a, ["type", "dims"]);
|
|
156
|
+
const identityParser = (v) => v;
|
|
157
|
+
const typParser = (_b = this.parser[typ]) != null ? _b : identityParser;
|
|
158
|
+
const parser = makeNullableParser(typParser, columnInfo.not_null);
|
|
159
|
+
if (dimensions && dimensions > 0) {
|
|
160
|
+
return pgArrayParser(value, parser);
|
|
162
161
|
}
|
|
163
162
|
return parser(value, additionalInfo);
|
|
164
163
|
}
|
|
165
164
|
};
|
|
165
|
+
function makeNullableParser(parser, notNullable) {
|
|
166
|
+
const isNullable = !(notNullable != null ? notNullable : false);
|
|
167
|
+
if (isNullable) {
|
|
168
|
+
return (value) => value === null || value === `NULL` ? null : parser(value);
|
|
169
|
+
}
|
|
170
|
+
return parser;
|
|
171
|
+
}
|
|
166
172
|
|
|
167
173
|
// src/client.ts
|
|
168
174
|
var BackoffDefaults = {
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/index.ts","../../src/parser.ts","../../src/client.ts"],"sourcesContent":["export * from './client'\nexport * from './types'\n","import { ColumnInfo, Message, Schema, Value } from './types'\n\nexport type ParseFunction = (\n value: string,\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)\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(\n value: string,\n parser?: (s: string) => Value\n): 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 string, 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: string, 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 // Pick the right parser for the type\n const parser = this.parser[columnInfo.type]\n\n // Copy the object but don't include `dimensions` and `type`\n const { type: _typ, dims: dimensions, ...additionalInfo } = columnInfo\n\n if (dimensions > 0) {\n // It's an array\n const identityParser = (v: string) => v\n return pgArrayParser(value, parser ?? identityParser)\n }\n\n if (!parser) {\n // No parser was provided for this type of values\n return value\n }\n\n return parser(value, additionalInfo)\n }\n}\n","import { ArgumentsType } from 'vitest'\nimport { Message, Value, Offset, Schema } from './types'\nimport { MessageParser, Parser } from './parser'\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: ArgumentsType<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 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 (`key` in 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 (message.headers?.[`control`] === `up-to-date`) {\n isUpToDate = true\n if (!this.hasNotifiedSubscribersUpToDate) {\n newlyUpToDate = true\n }\n }\n\n if (message.headers?.[`control`] === `must-refetch`) {\n this.data.clear()\n this.error = false\n isUpToDate = false\n newlyUpToDate = false\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 }\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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/index.ts","../../src/parser.ts","../../src/client.ts"],"sourcesContent":["export * from './client'\nexport * from './types'\n","import { ColumnInfo, Message, Schema, Value } from './types'\n\nexport type ParseFunction = (\n value: string,\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)\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(\n value: string,\n parser?: (s: string) => Value\n): 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 string, 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: string, 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 identityParser = (v: string) => v\n const typParser = this.parser[typ] ?? identityParser\n const parser = makeNullableParser(typParser, columnInfo.not_null)\n\n if (dimensions && dimensions > 0) {\n // It's an array\n return pgArrayParser(value, parser)\n }\n\n return parser(value, additionalInfo)\n }\n}\n\nfunction makeNullableParser(\n parser: ParseFunction,\n notNullable?: boolean\n): ParseFunction {\n const isNullable = !(notNullable ?? false)\n if (isNullable) {\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: string) =>\n value === null || value === `NULL` ? null : parser(value)\n }\n return parser\n}\n","import { ArgumentsType } from 'vitest'\nimport { Message, Value, Offset, Schema } from './types'\nimport { MessageParser, Parser } from './parser'\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: ArgumentsType<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 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 (`key` in 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 (message.headers?.[`control`] === `up-to-date`) {\n isUpToDate = true\n if (!this.hasNotifiedSubscribersUpToDate) {\n newlyUpToDate = true\n }\n }\n\n if (message.headers?.[`control`] === `must-refetch`) {\n this.data.clear()\n this.error = false\n isUpToDate = false\n newlyUpToDate = false\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 }\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"]}
|
package/dist/index.browser.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var C=Object.defineProperty;var
|
|
1
|
+
var C=Object.defineProperty;var v=Object.getOwnPropertySymbols;var E=Object.prototype.hasOwnProperty,P=Object.prototype.propertyIsEnumerable;var D=(a,e,s)=>e in a?C(a,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):a[e]=s,p=(a,e)=>{for(var s in e||(e={}))E.call(e,s)&&D(a,s,e[s]);if(v)for(var s of v(e))P.call(e,s)&&D(a,s,e[s]);return a};var O=(a,e)=>{var s={};for(var t in a)E.call(a,t)&&e.indexOf(t)<0&&(s[t]=a[t]);if(a!=null&&v)for(var t of v(a))e.indexOf(t)<0&&P.call(a,t)&&(s[t]=a[t]);return s};var d=(a,e,s)=>new Promise((t,o)=>{var i=n=>{try{c(s.next(n))}catch(h){o(h)}},r=n=>{try{c(s.throw(n))}catch(h){o(h)}},c=n=>n.done?t(n.value):Promise.resolve(n.value).then(i,r);c((s=s.apply(a,e)).next())});var S=a=>Number(a),j=a=>a==="true"||a==="t",R=a=>BigInt(a),k=a=>JSON.parse(a),x={int2:S,int4:S,int8:R,bool:j,float4:S,float8:S,json:k,jsonb:k};function A(a,e){let s=0,t=null,o="",i=!1,r=0,c;function n(h){let l=[];for(;s<h.length;s++){if(t=h[s],i)t==="\\"?o+=h[++s]:t==='"'?(l.push(e?e(o):o),o="",i=h[s+1]==='"',r=s+2):o+=t;else if(t==='"')i=!0;else if(t==="{")r=++s,l.push(n(h));else if(t==="}"){i=!1,r<s&&l.push(e?e(h.slice(r,s)):h.slice(r,s)),r=s+1;break}else t===","&&c!=="}"&&c!=='"'&&(l.push(e?e(h.slice(r,s)):h.slice(r,s)),r=s+1);c=t}return r<s&&l.push(e?e(h.slice(r,s+1)):h.slice(r,s+1)),l}return n(a)[0]}var y=class{constructor(e){this.parser=p(p({},x),e)}parse(e,s){return JSON.parse(e,(t,o)=>{if(t==="value"&&typeof o=="object"){let i=o;Object.keys(i).forEach(r=>{i[r]=this.parseRow(r,i[r],s)})}return o})}parseRow(e,s,t){var m;let o=t[e];if(!o)return s;let b=o,{type:i,dims:r}=b,c=O(b,["type","dims"]),n=w=>w,h=(m=this.parser[i])!=null?m:n,l=B(h,o.not_null);return r&&r>0?A(s,l):l(s,c)}};function B(a,e){return e!=null&&e?a:t=>t===null||t==="NULL"?null:a(t)}var N={initialDelay:100,maxDelay:1e4,multiplier:1.3},T=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})}},f=class a extends Error{constructor(s,t,o,i,r,c){super(c||`HTTP Error ${s} at ${r}: ${t!=null?t:JSON.stringify(o)}`);this.url=r;this.name="FetchError",this.status=s,this.text=t,this.json=o,this.headers=i}static fromResponse(s,t){return d(this,null,function*(){let o=s.status,i=Object.fromEntries([...s.headers.entries()]),r,c,n=s.headers.get("content-type");return n&&n.includes("application/json")?c=yield s.json():r=yield s.text(),new a(o,r,c,i,t)})}},I=class{constructor(e){this.subscribers=new Map;this.upToDateSubscribers=new Map;this.isUpToDate=!1;var s,t,o;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 y(e.parser),this.backoffOptions=(t=e.backoffOptions)!=null?t:N,this.fetchClient=(o=e.fetchClient)!=null?o:(...i)=>fetch(...i),this.start()}start(){return d(this,null,function*(){var o,i;this.isUpToDate=!1;let{url:e,where:s,signal:t}=this.options;for(;!(t!=null&&t.aborted)&&!this.isUpToDate||this.options.subscribe;){let r=new URL(e);s&&r.searchParams.set("where",s),r.searchParams.set("offset",this.lastOffset),this.isUpToDate&&r.searchParams.set("live","true"),this.shapeId&&r.searchParams.set("shape_id",this.shapeId);let c;try{let u=yield this.fetchWithBackoff(r);if(u)c=u;else break}catch(u){if(!(u instanceof f))throw u;if(u.status==409){let M=u.headers["x-electric-shape-id"];this.reset(M),this.publish(u.json);continue}else if(u.status>=400&&u.status<500)throw this.sendErrorToUpToDateSubscribers(u),this.sendErrorToSubscribers(u),u}let{headers:n,status:h}=c,l=n.get("X-Electric-Shape-Id");l&&(this.shapeId=l);let b=n.get("X-Electric-Chunk-Last-Offset");b&&(this.lastOffset=b);let m=()=>{let u=n.get("X-Electric-Schema");return u?JSON.parse(u):{}};this.schema=(o=this.schema)!=null?o:m();let w=h===204?"[]":yield c.text(),g=this.messageParser.parse(w,this.schema);g.length>0&&(((i=g[g.length-1].headers)==null?void 0:i.control)==="up-to-date"&&!this.isUpToDate&&(this.isUpToDate=!0,this.notifyUpToDateSubscribers()),this.publish(g))}})}subscribe(e,s){let t=Math.random(),o=new T(e);return this.subscribers.set(t,[o,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:o}=this.backoffOptions,i=this.options.signal,r=s,c=0;for(;;)try{let n=yield this.fetchClient(e.toString(),{signal:i});if(n.ok)return n;throw yield f.fromResponse(n,e.toString())}catch(n){if(i!=null&&i.aborted)return;if(n instanceof f&&n.status>=400&&n.status<500)throw n;yield new Promise(h=>setTimeout(h,r)),r=Math.min(r*o,t),c++,console.log(`Retry attempt #${c} after ${r}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,o=!1;e.forEach(i=>{var r,c;if("key"in 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}((r=i.headers)==null?void 0:r.control)==="up-to-date"&&(t=!0,this.hasNotifiedSubscribersUpToDate||(o=!0)),((c=i.headers)==null?void 0:c.control)==="must-refetch"&&(this.data.clear(),this.error=!1,t=!1,o=!1)}),(o||t&&s)&&(this.hasNotifiedSubscribersUpToDate=!0,this.notify())}handleError(e){e instanceof f&&(this.error=e)}notify(){this.subscribers.forEach(e=>{e(this.valueSync)})}};export{N as BackoffDefaults,f as FetchError,U as Shape,I as ShapeStream};
|
|
2
2
|
//# sourceMappingURL=index.browser.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/parser.ts","../src/client.ts"],"sourcesContent":["import { ColumnInfo, Message, Schema, Value } from './types'\n\nexport type ParseFunction = (\n value: string,\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)\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(\n value: string,\n parser?: (s: string) => Value\n): 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 string, 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: string, 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 // Pick the right parser for the type\n const parser = this.parser[columnInfo.type]\n\n // Copy the object but don't include `dimensions` and `type`\n const { type: _typ, dims: dimensions, ...additionalInfo } = columnInfo\n\n if (dimensions > 0) {\n // It's an array\n const identityParser = (v: string) => v\n return pgArrayParser(value, parser ?? identityParser)\n }\n\n if (!parser) {\n // No parser was provided for this type of values\n return value\n }\n\n return parser(value, additionalInfo)\n }\n}\n","import { ArgumentsType } from 'vitest'\nimport { Message, Value, Offset, Schema } from './types'\nimport { MessageParser, Parser } from './parser'\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: ArgumentsType<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 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 (`key` in 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 (message.headers?.[`control`] === `up-to-date`) {\n isUpToDate = true\n if (!this.hasNotifiedSubscribersUpToDate) {\n newlyUpToDate = true\n }\n }\n\n if (message.headers?.[`control`] === `must-refetch`) {\n this.data.clear()\n this.error = false\n isUpToDate = false\n newlyUpToDate = false\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 }\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","defaultParser","pgArrayParser","parser","i","char","str","quoted","last","p","loop","x","xs","MessageParser","__spreadValues","messages","schema","key","row","columnInfo","_a","_typ","dimensions","additionalInfo","__objRest","identityParser","v","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","onError","subscriptionId","subscriber","_","error","errorFn","errorCallback","initialDelay","maxDelay","multiplier","delay","attempt","result","resolve","Shape","stream","unsubscribe","dataMayHaveChanged","isUpToDate","newlyUpToDate"]}
|
|
1
|
+
{"version":3,"sources":["../src/parser.ts","../src/client.ts"],"sourcesContent":["import { ColumnInfo, Message, Schema, Value } from './types'\n\nexport type ParseFunction = (\n value: string,\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)\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(\n value: string,\n parser?: (s: string) => Value\n): 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 string, 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: string, 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 identityParser = (v: string) => v\n const typParser = this.parser[typ] ?? identityParser\n const parser = makeNullableParser(typParser, columnInfo.not_null)\n\n if (dimensions && dimensions > 0) {\n // It's an array\n return pgArrayParser(value, parser)\n }\n\n return parser(value, additionalInfo)\n }\n}\n\nfunction makeNullableParser(\n parser: ParseFunction,\n notNullable?: boolean\n): ParseFunction {\n const isNullable = !(notNullable ?? false)\n if (isNullable) {\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: string) =>\n value === null || value === `NULL` ? null : parser(value)\n }\n return parser\n}\n","import { ArgumentsType } from 'vitest'\nimport { Message, Value, Offset, Schema } from './types'\nimport { MessageParser, Parser } from './parser'\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: ArgumentsType<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 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 (`key` in 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 (message.headers?.[`control`] === `up-to-date`) {\n isUpToDate = true\n if (!this.hasNotifiedSubscribersUpToDate) {\n newlyUpToDate = true\n }\n }\n\n if (message.headers?.[`control`] === `must-refetch`) {\n this.data.clear()\n this.error = false\n isUpToDate = false\n newlyUpToDate = false\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 }\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","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","identityParser","v","typParser","makeNullableParser","notNullable","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","onError","subscriptionId","subscriber","_","error","errorFn","errorCallback","initialDelay","maxDelay","multiplier","delay","attempt","result","resolve","Shape","stream","unsubscribe","dataMayHaveChanged","isUpToDate","newlyUpToDate"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -19,47 +19,48 @@ type ChangeMessage<T> = {
|
|
|
19
19
|
type Message<T extends Value = {
|
|
20
20
|
[key: string]: Value;
|
|
21
21
|
}> = ControlMessage | ChangeMessage<T>;
|
|
22
|
+
/**
|
|
23
|
+
* Common properties for all columns.
|
|
24
|
+
* `dims` is the number of dimensions of the column. Only provided if the column is an array.
|
|
25
|
+
* `not_null` is true if the column has a `NOT NULL` constraint and is omitted otherwise.
|
|
26
|
+
*/
|
|
27
|
+
type CommonColumnProps = {
|
|
28
|
+
dims?: number;
|
|
29
|
+
not_null?: boolean;
|
|
30
|
+
};
|
|
22
31
|
type RegularColumn = {
|
|
23
32
|
type: string;
|
|
24
|
-
|
|
25
|
-
};
|
|
33
|
+
} & CommonColumnProps;
|
|
26
34
|
type VarcharColumn = {
|
|
27
35
|
type: `varchar`;
|
|
28
|
-
dims: number;
|
|
29
36
|
max_length?: number;
|
|
30
|
-
};
|
|
37
|
+
} & CommonColumnProps;
|
|
31
38
|
type BpcharColumn = {
|
|
32
39
|
type: `bpchar`;
|
|
33
|
-
dims: number;
|
|
34
40
|
length?: number;
|
|
35
|
-
};
|
|
41
|
+
} & CommonColumnProps;
|
|
36
42
|
type TimeColumn = {
|
|
37
43
|
type: `time` | `timetz` | `timestamp` | `timestamptz`;
|
|
38
|
-
dims: number;
|
|
39
44
|
precision?: number;
|
|
40
|
-
};
|
|
45
|
+
} & CommonColumnProps;
|
|
41
46
|
type IntervalColumn = {
|
|
42
47
|
type: `interval`;
|
|
43
|
-
dims: number;
|
|
44
48
|
fields?: `YEAR` | `MONTH` | `DAY` | `HOUR` | `MINUTE` | `YEAR TO MONTH` | `DAY TO HOUR` | `DAY TO MINUTE` | `DAY TO SECOND` | `HOUR TO MINUTE` | `HOUR TO SECOND` | `MINUTE TO SECOND`;
|
|
45
|
-
};
|
|
49
|
+
} & CommonColumnProps;
|
|
46
50
|
type IntervalColumnWithPrecision = {
|
|
47
51
|
type: `interval`;
|
|
48
|
-
dims: number;
|
|
49
52
|
precision?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
50
53
|
fields?: `SECOND`;
|
|
51
|
-
};
|
|
54
|
+
} & CommonColumnProps;
|
|
52
55
|
type BitColumn = {
|
|
53
56
|
type: `bit`;
|
|
54
|
-
dims: number;
|
|
55
57
|
length: number;
|
|
56
|
-
};
|
|
58
|
+
} & CommonColumnProps;
|
|
57
59
|
type NumericColumn = {
|
|
58
60
|
type: `numeric`;
|
|
59
|
-
dims: number;
|
|
60
61
|
precision?: number;
|
|
61
62
|
scale?: number;
|
|
62
|
-
};
|
|
63
|
+
} & CommonColumnProps;
|
|
63
64
|
type ColumnInfo = RegularColumn | VarcharColumn | BpcharColumn | TimeColumn | IntervalColumn | IntervalColumnWithPrecision | BitColumn | NumericColumn;
|
|
64
65
|
type Schema = {
|
|
65
66
|
[key: string]: ColumnInfo;
|
|
@@ -240,4 +241,4 @@ declare class Shape {
|
|
|
240
241
|
private notify;
|
|
241
242
|
}
|
|
242
243
|
|
|
243
|
-
export { BackoffDefaults, type BackoffOptions, type BitColumn, type BpcharColumn, type ChangeMessage, type ColumnInfo, 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 };
|
|
244
|
+
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 };
|
package/dist/index.legacy-esm.js
CHANGED
|
@@ -102,22 +102,28 @@ var MessageParser = class {
|
|
|
102
102
|
}
|
|
103
103
|
// Parses the message values using the provided parser based on the schema information
|
|
104
104
|
parseRow(key, value, schema) {
|
|
105
|
+
var _b;
|
|
105
106
|
const columnInfo = schema[key];
|
|
106
107
|
if (!columnInfo) {
|
|
107
108
|
return value;
|
|
108
109
|
}
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (!parser) {
|
|
116
|
-
return value;
|
|
110
|
+
const _a = columnInfo, { type: typ, dims: dimensions } = _a, additionalInfo = __objRest(_a, ["type", "dims"]);
|
|
111
|
+
const identityParser = (v) => v;
|
|
112
|
+
const typParser = (_b = this.parser[typ]) != null ? _b : identityParser;
|
|
113
|
+
const parser = makeNullableParser(typParser, columnInfo.not_null);
|
|
114
|
+
if (dimensions && dimensions > 0) {
|
|
115
|
+
return pgArrayParser(value, parser);
|
|
117
116
|
}
|
|
118
117
|
return parser(value, additionalInfo);
|
|
119
118
|
}
|
|
120
119
|
};
|
|
120
|
+
function makeNullableParser(parser, notNullable) {
|
|
121
|
+
const isNullable = !(notNullable != null ? notNullable : false);
|
|
122
|
+
if (isNullable) {
|
|
123
|
+
return (value) => value === null || value === `NULL` ? null : parser(value);
|
|
124
|
+
}
|
|
125
|
+
return parser;
|
|
126
|
+
}
|
|
121
127
|
|
|
122
128
|
// src/client.ts
|
|
123
129
|
var BackoffDefaults = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/parser.ts","../src/client.ts"],"sourcesContent":["import { ColumnInfo, Message, Schema, Value } from './types'\n\nexport type ParseFunction = (\n value: string,\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)\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(\n value: string,\n parser?: (s: string) => Value\n): 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 string, 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: string, 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 // Pick the right parser for the type\n const parser = this.parser[columnInfo.type]\n\n // Copy the object but don't include `dimensions` and `type`\n const { type: _typ, dims: dimensions, ...additionalInfo } = columnInfo\n\n if (dimensions > 0) {\n // It's an array\n const identityParser = (v: string) => v\n return pgArrayParser(value, parser ?? identityParser)\n }\n\n if (!parser) {\n // No parser was provided for this type of values\n return value\n }\n\n return parser(value, additionalInfo)\n }\n}\n","import { ArgumentsType } from 'vitest'\nimport { Message, Value, Offset, Schema } from './types'\nimport { MessageParser, Parser } from './parser'\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: ArgumentsType<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 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 (`key` in 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 (message.headers?.[`control`] === `up-to-date`) {\n isUpToDate = true\n if (!this.hasNotifiedSubscribersUpToDate) {\n newlyUpToDate = true\n }\n }\n\n if (message.headers?.[`control`] === `must-refetch`) {\n this.data.clear()\n this.error = false\n isUpToDate = false\n newlyUpToDate = false\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 }\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/parser.ts","../src/client.ts"],"sourcesContent":["import { ColumnInfo, Message, Schema, Value } from './types'\n\nexport type ParseFunction = (\n value: string,\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)\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(\n value: string,\n parser?: (s: string) => Value\n): 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 string, 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: string, 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 identityParser = (v: string) => v\n const typParser = this.parser[typ] ?? identityParser\n const parser = makeNullableParser(typParser, columnInfo.not_null)\n\n if (dimensions && dimensions > 0) {\n // It's an array\n return pgArrayParser(value, parser)\n }\n\n return parser(value, additionalInfo)\n }\n}\n\nfunction makeNullableParser(\n parser: ParseFunction,\n notNullable?: boolean\n): ParseFunction {\n const isNullable = !(notNullable ?? false)\n if (isNullable) {\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: string) =>\n value === null || value === `NULL` ? null : parser(value)\n }\n return parser\n}\n","import { ArgumentsType } from 'vitest'\nimport { Message, Value, Offset, Schema } from './types'\nimport { MessageParser, Parser } from './parser'\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: ArgumentsType<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 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 (`key` in 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 (message.headers?.[`control`] === `up-to-date`) {\n isUpToDate = true\n if (!this.hasNotifiedSubscribersUpToDate) {\n newlyUpToDate = true\n }\n }\n\n if (message.headers?.[`control`] === `must-refetch`) {\n this.data.clear()\n this.error = false\n isUpToDate = false\n newlyUpToDate = false\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 }\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"]}
|
package/dist/index.mjs
CHANGED
|
@@ -122,22 +122,28 @@ var MessageParser = class {
|
|
|
122
122
|
}
|
|
123
123
|
// Parses the message values using the provided parser based on the schema information
|
|
124
124
|
parseRow(key, value, schema) {
|
|
125
|
+
var _b;
|
|
125
126
|
const columnInfo = schema[key];
|
|
126
127
|
if (!columnInfo) {
|
|
127
128
|
return value;
|
|
128
129
|
}
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (!parser) {
|
|
136
|
-
return value;
|
|
130
|
+
const _a = columnInfo, { type: typ, dims: dimensions } = _a, additionalInfo = __objRest(_a, ["type", "dims"]);
|
|
131
|
+
const identityParser = (v) => v;
|
|
132
|
+
const typParser = (_b = this.parser[typ]) != null ? _b : identityParser;
|
|
133
|
+
const parser = makeNullableParser(typParser, columnInfo.not_null);
|
|
134
|
+
if (dimensions && dimensions > 0) {
|
|
135
|
+
return pgArrayParser(value, parser);
|
|
137
136
|
}
|
|
138
137
|
return parser(value, additionalInfo);
|
|
139
138
|
}
|
|
140
139
|
};
|
|
140
|
+
function makeNullableParser(parser, notNullable) {
|
|
141
|
+
const isNullable = !(notNullable != null ? notNullable : false);
|
|
142
|
+
if (isNullable) {
|
|
143
|
+
return (value) => value === null || value === `NULL` ? null : parser(value);
|
|
144
|
+
}
|
|
145
|
+
return parser;
|
|
146
|
+
}
|
|
141
147
|
|
|
142
148
|
// src/client.ts
|
|
143
149
|
var BackoffDefaults = {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/parser.ts","../src/client.ts"],"sourcesContent":["import { ColumnInfo, Message, Schema, Value } from './types'\n\nexport type ParseFunction = (\n value: string,\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)\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(\n value: string,\n parser?: (s: string) => Value\n): 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 string, 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: string, 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 // Pick the right parser for the type\n const parser = this.parser[columnInfo.type]\n\n // Copy the object but don't include `dimensions` and `type`\n const { type: _typ, dims: dimensions, ...additionalInfo } = columnInfo\n\n if (dimensions > 0) {\n // It's an array\n const identityParser = (v: string) => v\n return pgArrayParser(value, parser ?? identityParser)\n }\n\n if (!parser) {\n // No parser was provided for this type of values\n return value\n }\n\n return parser(value, additionalInfo)\n }\n}\n","import { ArgumentsType } from 'vitest'\nimport { Message, Value, Offset, Schema } from './types'\nimport { MessageParser, Parser } from './parser'\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: ArgumentsType<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 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 (`key` in 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 (message.headers?.[`control`] === `up-to-date`) {\n isUpToDate = true\n if (!this.hasNotifiedSubscribersUpToDate) {\n newlyUpToDate = true\n }\n }\n\n if (message.headers?.[`control`] === `must-refetch`) {\n this.data.clear()\n this.error = false\n isUpToDate = false\n newlyUpToDate = false\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 }\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/parser.ts","../src/client.ts"],"sourcesContent":["import { ColumnInfo, Message, Schema, Value } from './types'\n\nexport type ParseFunction = (\n value: string,\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)\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(\n value: string,\n parser?: (s: string) => Value\n): 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 string, 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: string, 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 identityParser = (v: string) => v\n const typParser = this.parser[typ] ?? identityParser\n const parser = makeNullableParser(typParser, columnInfo.not_null)\n\n if (dimensions && dimensions > 0) {\n // It's an array\n return pgArrayParser(value, parser)\n }\n\n return parser(value, additionalInfo)\n }\n}\n\nfunction makeNullableParser(\n parser: ParseFunction,\n notNullable?: boolean\n): ParseFunction {\n const isNullable = !(notNullable ?? false)\n if (isNullable) {\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: string) =>\n value === null || value === `NULL` ? null : parser(value)\n }\n return parser\n}\n","import { ArgumentsType } from 'vitest'\nimport { Message, Value, Offset, Schema } from './types'\nimport { MessageParser, Parser } from './parser'\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: ArgumentsType<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 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 (`key` in 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 (message.headers?.[`control`] === `up-to-date`) {\n isUpToDate = true\n if (!this.hasNotifiedSubscribersUpToDate) {\n newlyUpToDate = true\n }\n }\n\n if (message.headers?.[`control`] === `must-refetch`) {\n this.data.clear()\n this.error = false\n isUpToDate = false\n newlyUpToDate = false\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 }\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"]}
|
package/package.json
CHANGED
package/src/parser.ts
CHANGED
|
@@ -108,23 +108,36 @@ export class MessageParser {
|
|
|
108
108
|
return value
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
// Pick the right parser for the type
|
|
112
|
-
const parser = this.parser[columnInfo.type]
|
|
113
|
-
|
|
114
111
|
// Copy the object but don't include `dimensions` and `type`
|
|
115
|
-
const { type:
|
|
112
|
+
const { type: typ, dims: dimensions, ...additionalInfo } = columnInfo
|
|
116
113
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
114
|
+
// Pick the right parser for the type
|
|
115
|
+
// and support parsing null values if needed
|
|
116
|
+
// if no parser is provided for the given type, just return the value as is
|
|
117
|
+
const identityParser = (v: string) => v
|
|
118
|
+
const typParser = this.parser[typ] ?? identityParser
|
|
119
|
+
const parser = makeNullableParser(typParser, columnInfo.not_null)
|
|
122
120
|
|
|
123
|
-
if (
|
|
124
|
-
//
|
|
125
|
-
return value
|
|
121
|
+
if (dimensions && dimensions > 0) {
|
|
122
|
+
// It's an array
|
|
123
|
+
return pgArrayParser(value, parser)
|
|
126
124
|
}
|
|
127
125
|
|
|
128
126
|
return parser(value, additionalInfo)
|
|
129
127
|
}
|
|
130
128
|
}
|
|
129
|
+
|
|
130
|
+
function makeNullableParser(
|
|
131
|
+
parser: ParseFunction,
|
|
132
|
+
notNullable?: boolean
|
|
133
|
+
): ParseFunction {
|
|
134
|
+
const isNullable = !(notNullable ?? false)
|
|
135
|
+
if (isNullable) {
|
|
136
|
+
// The sync service contains `null` value for a column whose value is NULL
|
|
137
|
+
// but if the column value is an array that contains a NULL value
|
|
138
|
+
// then it will be included in the array string as `NULL`, e.g.: `"{1,NULL,3}"`
|
|
139
|
+
return (value: string) =>
|
|
140
|
+
value === null || value === `NULL` ? null : parser(value)
|
|
141
|
+
}
|
|
142
|
+
return parser
|
|
143
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -29,32 +29,37 @@ export type Message<T extends Value = { [key: string]: Value }> =
|
|
|
29
29
|
| ControlMessage
|
|
30
30
|
| ChangeMessage<T>
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Common properties for all columns.
|
|
34
|
+
* `dims` is the number of dimensions of the column. Only provided if the column is an array.
|
|
35
|
+
* `not_null` is true if the column has a `NOT NULL` constraint and is omitted otherwise.
|
|
36
|
+
*/
|
|
37
|
+
export type CommonColumnProps = {
|
|
38
|
+
dims?: number
|
|
39
|
+
not_null?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
export type RegularColumn = {
|
|
33
43
|
type: string
|
|
34
|
-
|
|
35
|
-
}
|
|
44
|
+
} & CommonColumnProps
|
|
36
45
|
|
|
37
46
|
export type VarcharColumn = {
|
|
38
47
|
type: `varchar`
|
|
39
|
-
dims: number
|
|
40
48
|
max_length?: number
|
|
41
|
-
}
|
|
49
|
+
} & CommonColumnProps
|
|
42
50
|
|
|
43
51
|
export type BpcharColumn = {
|
|
44
52
|
type: `bpchar`
|
|
45
|
-
dims: number
|
|
46
53
|
length?: number
|
|
47
|
-
}
|
|
54
|
+
} & CommonColumnProps
|
|
48
55
|
|
|
49
56
|
export type TimeColumn = {
|
|
50
57
|
type: `time` | `timetz` | `timestamp` | `timestamptz`
|
|
51
|
-
dims: number
|
|
52
58
|
precision?: number
|
|
53
|
-
}
|
|
59
|
+
} & CommonColumnProps
|
|
54
60
|
|
|
55
61
|
export type IntervalColumn = {
|
|
56
62
|
type: `interval`
|
|
57
|
-
dims: number
|
|
58
63
|
fields?:
|
|
59
64
|
| `YEAR`
|
|
60
65
|
| `MONTH`
|
|
@@ -68,27 +73,24 @@ export type IntervalColumn = {
|
|
|
68
73
|
| `HOUR TO MINUTE`
|
|
69
74
|
| `HOUR TO SECOND`
|
|
70
75
|
| `MINUTE TO SECOND`
|
|
71
|
-
}
|
|
76
|
+
} & CommonColumnProps
|
|
72
77
|
|
|
73
78
|
export type IntervalColumnWithPrecision = {
|
|
74
79
|
type: `interval`
|
|
75
|
-
dims: number
|
|
76
80
|
precision?: 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
77
81
|
fields?: `SECOND`
|
|
78
|
-
}
|
|
82
|
+
} & CommonColumnProps
|
|
79
83
|
|
|
80
84
|
export type BitColumn = {
|
|
81
85
|
type: `bit`
|
|
82
|
-
dims: number
|
|
83
86
|
length: number
|
|
84
|
-
}
|
|
87
|
+
} & CommonColumnProps
|
|
85
88
|
|
|
86
89
|
export type NumericColumn = {
|
|
87
90
|
type: `numeric`
|
|
88
|
-
dims: number
|
|
89
91
|
precision?: number
|
|
90
92
|
scale?: number
|
|
91
|
-
}
|
|
93
|
+
} & CommonColumnProps
|
|
92
94
|
|
|
93
95
|
export type ColumnInfo =
|
|
94
96
|
| RegularColumn
|