@electric-sql/client
Advanced tools
Comparing version 0.3.4 to 0.4.0
@@ -180,3 +180,5 @@ type Value = string | number | boolean | bigint | null | Value[] | { | ||
private messageParser; | ||
private lastSyncedAt?; | ||
isUpToDate: boolean; | ||
private connected; | ||
shapeId?: string; | ||
@@ -191,2 +193,5 @@ constructor(options: ShapeStreamOptions); | ||
unsubscribeAllUpToDateSubscribers(): void; | ||
/** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */ | ||
lastSynced(): number; | ||
isConnected(): boolean; | ||
private notifyUpToDateSubscribers; | ||
@@ -240,3 +245,4 @@ private sendErrorToUpToDateSubscribers; | ||
constructor(stream: ShapeStream<T>); | ||
get isUpToDate(): boolean; | ||
lastSynced(): number; | ||
isConnected(): boolean; | ||
get value(): Promise<ShapeData<T>>; | ||
@@ -243,0 +249,0 @@ get valueSync(): ShapeData<T>; |
@@ -207,3 +207,5 @@ var __defProp = Object.defineProperty; | ||
this.upToDateSubscribers = /* @__PURE__ */ new Map(); | ||
// unix time | ||
this.isUpToDate = false; | ||
this.connected = false; | ||
var _a, _b, _c; | ||
@@ -223,54 +225,64 @@ this.validateOptions(options); | ||
const { url, where, signal } = this.options; | ||
while (!(signal == null ? void 0 : signal.aborted) && !this.isUpToDate || this.options.subscribe) { | ||
const fetchUrl = new URL(url); | ||
if (where) fetchUrl.searchParams.set(`where`, where); | ||
fetchUrl.searchParams.set(`offset`, this.lastOffset); | ||
if (this.isUpToDate) { | ||
fetchUrl.searchParams.set(`live`, `true`); | ||
} | ||
if (this.shapeId) { | ||
fetchUrl.searchParams.set(`shape_id`, this.shapeId); | ||
} | ||
let response; | ||
try { | ||
const maybeResponse = await this.fetchWithBackoff(fetchUrl); | ||
if (maybeResponse) response = maybeResponse; | ||
else break; | ||
} catch (e) { | ||
if (!(e instanceof FetchError)) throw e; | ||
if (e.status == 409) { | ||
const newShapeId = e.headers[`x-electric-shape-id`]; | ||
this.reset(newShapeId); | ||
this.publish(e.json); | ||
continue; | ||
} else if (e.status >= 400 && e.status < 500) { | ||
this.sendErrorToUpToDateSubscribers(e); | ||
this.sendErrorToSubscribers(e); | ||
throw e; | ||
try { | ||
while (!(signal == null ? void 0 : signal.aborted) && !this.isUpToDate || this.options.subscribe) { | ||
const fetchUrl = new URL(url); | ||
if (where) fetchUrl.searchParams.set(`where`, where); | ||
fetchUrl.searchParams.set(`offset`, this.lastOffset); | ||
if (this.isUpToDate) { | ||
fetchUrl.searchParams.set(`live`, `true`); | ||
} | ||
} | ||
const { headers, status } = response; | ||
const shapeId = headers.get(`X-Electric-Shape-Id`); | ||
if (shapeId) { | ||
this.shapeId = shapeId; | ||
} | ||
const lastOffset = headers.get(`X-Electric-Chunk-Last-Offset`); | ||
if (lastOffset) { | ||
this.lastOffset = lastOffset; | ||
} | ||
const getSchema = () => { | ||
const schemaHeader = headers.get(`X-Electric-Schema`); | ||
return schemaHeader ? JSON.parse(schemaHeader) : {}; | ||
}; | ||
this.schema = (_a = this.schema) != null ? _a : getSchema(); | ||
const messages = status === 204 ? `[]` : await response.text(); | ||
const batch = this.messageParser.parse(messages, this.schema); | ||
if (batch.length > 0) { | ||
const lastMessage = batch[batch.length - 1]; | ||
if (isControlMessage(lastMessage) && lastMessage.headers.control === `up-to-date` && !this.isUpToDate) { | ||
this.isUpToDate = true; | ||
this.notifyUpToDateSubscribers(); | ||
if (this.shapeId) { | ||
fetchUrl.searchParams.set(`shape_id`, this.shapeId); | ||
} | ||
this.publish(batch); | ||
let response; | ||
try { | ||
const maybeResponse = await this.fetchWithBackoff(fetchUrl); | ||
if (maybeResponse) response = maybeResponse; | ||
else break; | ||
} catch (e) { | ||
if (!(e instanceof FetchError)) throw e; | ||
if (e.status == 409) { | ||
const newShapeId = e.headers[`x-electric-shape-id`]; | ||
this.reset(newShapeId); | ||
this.publish(e.json); | ||
continue; | ||
} else if (e.status >= 400 && e.status < 500) { | ||
this.sendErrorToUpToDateSubscribers(e); | ||
this.sendErrorToSubscribers(e); | ||
throw e; | ||
} | ||
} | ||
const { headers, status } = response; | ||
const shapeId = headers.get(`X-Electric-Shape-Id`); | ||
if (shapeId) { | ||
this.shapeId = shapeId; | ||
} | ||
const lastOffset = headers.get(`X-Electric-Chunk-Last-Offset`); | ||
if (lastOffset) { | ||
this.lastOffset = lastOffset; | ||
} | ||
const getSchema = () => { | ||
const schemaHeader = headers.get(`X-Electric-Schema`); | ||
return schemaHeader ? JSON.parse(schemaHeader) : {}; | ||
}; | ||
this.schema = (_a = this.schema) != null ? _a : getSchema(); | ||
const messages = status === 204 ? `[]` : await response.text(); | ||
if (status === 204) { | ||
this.lastSyncedAt = Date.now(); | ||
} | ||
const batch = this.messageParser.parse(messages, this.schema); | ||
if (batch.length > 0) { | ||
const lastMessage = batch[batch.length - 1]; | ||
if (isControlMessage(lastMessage) && lastMessage.headers.control === `up-to-date`) { | ||
this.lastSyncedAt = Date.now(); | ||
if (!this.isUpToDate) { | ||
this.isUpToDate = true; | ||
this.notifyUpToDateSubscribers(); | ||
} | ||
} | ||
this.publish(batch); | ||
} | ||
} | ||
} finally { | ||
this.connected = false; | ||
} | ||
@@ -309,2 +321,10 @@ } | ||
} | ||
/** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */ | ||
lastSynced() { | ||
if (this.lastSyncedAt === void 0) return Infinity; | ||
return Date.now() - this.lastSyncedAt; | ||
} | ||
isConnected() { | ||
return this.connected; | ||
} | ||
notifyUpToDateSubscribers() { | ||
@@ -328,2 +348,3 @@ this.upToDateSubscribers.forEach(([callback]) => { | ||
this.isUpToDate = false; | ||
this.connected = false; | ||
this.schema = void 0; | ||
@@ -354,5 +375,10 @@ } | ||
const result = await this.fetchClient(url.toString(), { signal }); | ||
if (result.ok) return result; | ||
else throw await FetchError.fromResponse(result, url.toString()); | ||
if (result.ok) { | ||
if (this.options.subscribe) { | ||
this.connected = true; | ||
} | ||
return result; | ||
} else throw await FetchError.fromResponse(result, url.toString()); | ||
} catch (e) { | ||
this.connected = false; | ||
if (signal == null ? void 0 : signal.aborted) { | ||
@@ -390,5 +416,8 @@ return void 0; | ||
} | ||
get isUpToDate() { | ||
return this.stream.isUpToDate; | ||
lastSynced() { | ||
return this.stream.lastSynced(); | ||
} | ||
isConnected() { | ||
return this.stream.isConnected(); | ||
} | ||
get value() { | ||
@@ -395,0 +424,0 @@ return new Promise((resolve) => { |
{ | ||
"name": "@electric-sql/client", | ||
"version": "0.3.4", | ||
"version": "0.4.0", | ||
"description": "Postgres everywhere - your data, in sync, wherever you need it.", | ||
@@ -31,3 +31,3 @@ "type": "module", | ||
}, | ||
"homepage": "https://next.electric-sql.com", | ||
"homepage": "https://electric-sql.com", | ||
"dependencies": {}, | ||
@@ -34,0 +34,0 @@ "devDependencies": { |
<p align="center"> | ||
<a href="https://next.electric-sql.com" target="_blank"> | ||
<a href="https://electric-sql.com" target="_blank"> | ||
<picture> | ||
@@ -30,3 +30,3 @@ <source media="(prefers-color-scheme: dark)" | ||
Electric provides an [HTTP interface](https://next.electric-sql.com/api/http) to Postgres to enable a massive number of clients to query and get real-time updates to subsets of the database, called [Shapes](https://next.electric-sql.com//guides/shapes). In this way, Electric turns Postgres into a real-time database. | ||
Electric provides an [HTTP interface](https://electric-sql.com/docs/api/http) to Postgres to enable a massive number of clients to query and get real-time updates to subsets of the database, called [Shapes](https://electric-sql.com//docs/guides/shapes). In this way, Electric turns Postgres into a real-time database. | ||
@@ -81,2 +81,2 @@ The TypeScript client helps ease reading Shapes from the HTTP API in the browser and other JavaScript environments, such as edge functions and server-side Node/Bun/Deno applications. It supports both fine-grained and coarse-grained reactivity patterns — you can subscribe to see every row that changes, or you can just subscribe to get the whole shape whenever it changes. | ||
See the [Docs](https://next.electric-sql.com) and [Examples](https://next.electric-sql.com/examples/basic) for more information. | ||
See the [Docs](https://electric-sql.com) and [Examples](https://electric-sql.com/examples/basic) for more information. |
@@ -189,3 +189,5 @@ import { Message, Offset, Schema, Row } from './types' | ||
private messageParser: MessageParser<T> | ||
private lastSyncedAt?: number // unix time | ||
public isUpToDate: boolean = false | ||
private connected: boolean = false | ||
@@ -214,76 +216,87 @@ shapeId?: string | ||
while ((!signal?.aborted && !this.isUpToDate) || this.options.subscribe) { | ||
const fetchUrl = new URL(url) | ||
if (where) fetchUrl.searchParams.set(`where`, where) | ||
fetchUrl.searchParams.set(`offset`, this.lastOffset) | ||
try { | ||
while ((!signal?.aborted && !this.isUpToDate) || this.options.subscribe) { | ||
const fetchUrl = new URL(url) | ||
if (where) fetchUrl.searchParams.set(`where`, where) | ||
fetchUrl.searchParams.set(`offset`, this.lastOffset) | ||
if (this.isUpToDate) { | ||
fetchUrl.searchParams.set(`live`, `true`) | ||
} | ||
if (this.isUpToDate) { | ||
fetchUrl.searchParams.set(`live`, `true`) | ||
} | ||
if (this.shapeId) { | ||
// This should probably be a header for better cache breaking? | ||
fetchUrl.searchParams.set(`shape_id`, this.shapeId!) | ||
} | ||
if (this.shapeId) { | ||
// This should probably be a header for better cache breaking? | ||
fetchUrl.searchParams.set(`shape_id`, this.shapeId!) | ||
} | ||
let response!: Response | ||
let response!: Response | ||
try { | ||
const maybeResponse = await this.fetchWithBackoff(fetchUrl) | ||
if (maybeResponse) response = maybeResponse | ||
else break | ||
} catch (e) { | ||
if (!(e instanceof FetchError)) throw e // should never happen | ||
if (e.status == 409) { | ||
// Upon receiving a 409, we should start from scratch | ||
// with the newly provided shape ID | ||
const newShapeId = e.headers[`x-electric-shape-id`] | ||
this.reset(newShapeId) | ||
this.publish(e.json as Message<T>[]) | ||
continue | ||
} else if (e.status >= 400 && e.status < 500) { | ||
// Notify subscribers | ||
this.sendErrorToUpToDateSubscribers(e) | ||
this.sendErrorToSubscribers(e) | ||
try { | ||
const maybeResponse = await this.fetchWithBackoff(fetchUrl) | ||
if (maybeResponse) response = maybeResponse | ||
else break | ||
} catch (e) { | ||
if (!(e instanceof FetchError)) throw e // should never happen | ||
if (e.status == 409) { | ||
// Upon receiving a 409, we should start from scratch | ||
// with the newly provided shape ID | ||
const newShapeId = e.headers[`x-electric-shape-id`] | ||
this.reset(newShapeId) | ||
this.publish(e.json as Message<T>[]) | ||
continue | ||
} else if (e.status >= 400 && e.status < 500) { | ||
// Notify subscribers | ||
this.sendErrorToUpToDateSubscribers(e) | ||
this.sendErrorToSubscribers(e) | ||
// 400 errors are not actionable without additional user input, so we're throwing them. | ||
throw e | ||
// 400 errors are not actionable without additional user input, so we're throwing them. | ||
throw e | ||
} | ||
} | ||
} | ||
const { headers, status } = response | ||
const shapeId = headers.get(`X-Electric-Shape-Id`) | ||
if (shapeId) { | ||
this.shapeId = shapeId | ||
} | ||
const { headers, status } = response | ||
const shapeId = headers.get(`X-Electric-Shape-Id`) | ||
if (shapeId) { | ||
this.shapeId = shapeId | ||
} | ||
const lastOffset = headers.get(`X-Electric-Chunk-Last-Offset`) | ||
if (lastOffset) { | ||
this.lastOffset = lastOffset as Offset | ||
} | ||
const lastOffset = headers.get(`X-Electric-Chunk-Last-Offset`) | ||
if (lastOffset) { | ||
this.lastOffset = lastOffset as Offset | ||
} | ||
const getSchema = (): Schema => { | ||
const schemaHeader = headers.get(`X-Electric-Schema`) | ||
return schemaHeader ? JSON.parse(schemaHeader) : {} | ||
} | ||
this.schema = this.schema ?? getSchema() | ||
const getSchema = (): Schema => { | ||
const schemaHeader = headers.get(`X-Electric-Schema`) | ||
return schemaHeader ? JSON.parse(schemaHeader) : {} | ||
} | ||
this.schema = this.schema ?? getSchema() | ||
const messages = status === 204 ? `[]` : await response.text() | ||
const messages = status === 204 ? `[]` : await response.text() | ||
const batch = this.messageParser.parse(messages, this.schema) | ||
if (status === 204) { | ||
// There's no content so we are live and up to date | ||
this.lastSyncedAt = Date.now() | ||
} | ||
// Update isUpToDate | ||
if (batch.length > 0) { | ||
const lastMessage = batch[batch.length - 1] | ||
if ( | ||
isControlMessage(lastMessage) && | ||
lastMessage.headers.control === `up-to-date` && | ||
!this.isUpToDate | ||
) { | ||
this.isUpToDate = true | ||
this.notifyUpToDateSubscribers() | ||
const batch = this.messageParser.parse(messages, this.schema) | ||
// Update isUpToDate | ||
if (batch.length > 0) { | ||
const lastMessage = batch[batch.length - 1] | ||
if ( | ||
isControlMessage(lastMessage) && | ||
lastMessage.headers.control === `up-to-date` | ||
) { | ||
this.lastSyncedAt = Date.now() | ||
if (!this.isUpToDate) { | ||
this.isUpToDate = true | ||
this.notifyUpToDateSubscribers() | ||
} | ||
} | ||
this.publish(batch) | ||
} | ||
this.publish(batch) | ||
} | ||
} finally { | ||
this.connected = false | ||
} | ||
@@ -339,2 +352,12 @@ } | ||
/** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */ | ||
lastSynced(): number { | ||
if (this.lastSyncedAt === undefined) return Infinity | ||
return Date.now() - this.lastSyncedAt | ||
} | ||
isConnected(): boolean { | ||
return this.connected | ||
} | ||
private notifyUpToDateSubscribers() { | ||
@@ -361,2 +384,3 @@ this.upToDateSubscribers.forEach(([callback]) => { | ||
this.isUpToDate = false | ||
this.connected = false | ||
this.schema = undefined | ||
@@ -397,5 +421,10 @@ } | ||
const result = await this.fetchClient(url.toString(), { signal }) | ||
if (result.ok) return result | ||
else throw await FetchError.fromResponse(result, url.toString()) | ||
if (result.ok) { | ||
if (this.options.subscribe) { | ||
this.connected = true | ||
} | ||
return result | ||
} else throw await FetchError.fromResponse(result, url.toString()) | ||
} catch (e) { | ||
this.connected = false | ||
if (signal?.aborted) { | ||
@@ -479,6 +508,10 @@ return undefined | ||
get isUpToDate(): boolean { | ||
return this.stream.isUpToDate | ||
lastSynced(): number { | ||
return this.stream.lastSynced() | ||
} | ||
isConnected(): boolean { | ||
return this.stream.isConnected() | ||
} | ||
get value(): Promise<ShapeData<T>> { | ||
@@ -485,0 +518,0 @@ return new Promise((resolve) => { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
246099
2751