@electric-sql/client 1.1.4 → 1.1.5
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 +81 -38
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +17 -8
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +17 -8
- package/dist/index.legacy-esm.js +79 -36
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +81 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +71 -32
- package/src/parser.ts +45 -7
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-sql/client",
|
|
3
3
|
"description": "Postgres everywhere - your data, in sync, wherever you need it.",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.5",
|
|
5
5
|
"author": "ElectricSQL team and contributors.",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/electric-sql/electric/issues"
|
package/src/client.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
InvalidSignalError,
|
|
18
18
|
MissingShapeHandleError,
|
|
19
19
|
ReservedParamError,
|
|
20
|
+
MissingHeadersError,
|
|
20
21
|
} from './error'
|
|
21
22
|
import {
|
|
22
23
|
BackoffDefaults,
|
|
@@ -368,16 +369,15 @@ export interface ShapeStreamInterface<T extends Row<unknown> = Row> {
|
|
|
368
369
|
|
|
369
370
|
forceDisconnectAndRefresh(): Promise<void>
|
|
370
371
|
|
|
371
|
-
requestSnapshot(params: {
|
|
372
|
-
where?: string
|
|
373
|
-
params?: Record<string, string>
|
|
374
|
-
limit: number
|
|
375
|
-
offset?: number
|
|
376
|
-
orderBy: string
|
|
377
|
-
}): Promise<{
|
|
372
|
+
requestSnapshot(params: SubsetParams): Promise<{
|
|
378
373
|
metadata: SnapshotMetadata
|
|
379
374
|
data: Array<Message<T>>
|
|
380
375
|
}>
|
|
376
|
+
|
|
377
|
+
fetchSnapshot(opts: SubsetParams): Promise<{
|
|
378
|
+
metadata: SnapshotMetadata
|
|
379
|
+
data: Array<ChangeMessage<T>>
|
|
380
|
+
}>
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
/**
|
|
@@ -774,7 +774,10 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
774
774
|
fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, this.#lastOffset)
|
|
775
775
|
fetchUrl.searchParams.set(LOG_MODE_QUERY_PARAM, this.#mode)
|
|
776
776
|
|
|
777
|
-
|
|
777
|
+
// Snapshot requests (with subsetParams) should never use live polling
|
|
778
|
+
const isSnapshotRequest = subsetParams !== undefined
|
|
779
|
+
|
|
780
|
+
if (this.#isUpToDate && !isSnapshotRequest) {
|
|
778
781
|
// If we are resuming from a paused state, we don't want to perform a live request
|
|
779
782
|
// because it could be a long poll that holds for 20sec
|
|
780
783
|
// and during all that time `isConnected` will be false
|
|
@@ -846,11 +849,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
846
849
|
this.#liveCacheBuster = liveCacheBuster
|
|
847
850
|
}
|
|
848
851
|
|
|
849
|
-
|
|
850
|
-
const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER)
|
|
851
|
-
return schemaHeader ? JSON.parse(schemaHeader) : {}
|
|
852
|
-
}
|
|
853
|
-
this.#schema = this.#schema ?? getSchema()
|
|
852
|
+
this.#schema = this.#schema ?? getSchemaFromHeaders(headers)
|
|
854
853
|
|
|
855
854
|
// NOTE: 204s are deprecated, the Electric server should not
|
|
856
855
|
// send these in latest versions but this is here for backwards
|
|
@@ -1237,7 +1236,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1237
1236
|
}
|
|
1238
1237
|
|
|
1239
1238
|
/**
|
|
1240
|
-
* Request a snapshot for subset of data.
|
|
1239
|
+
* Request a snapshot for subset of data and inject it into the subscribed data stream.
|
|
1241
1240
|
*
|
|
1242
1241
|
* Only available when mode is `changes_only`.
|
|
1243
1242
|
* Returns the insertion point & the data, but more importantly injects the data
|
|
@@ -1275,16 +1274,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1275
1274
|
this.#pause()
|
|
1276
1275
|
}
|
|
1277
1276
|
|
|
1278
|
-
const {
|
|
1279
|
-
this.options.url,
|
|
1280
|
-
true,
|
|
1281
|
-
opts
|
|
1282
|
-
)
|
|
1283
|
-
|
|
1284
|
-
const { metadata, data } = await this.#fetchSnapshot(
|
|
1285
|
-
fetchUrl,
|
|
1286
|
-
requestHeaders
|
|
1287
|
-
)
|
|
1277
|
+
const { metadata, data } = await this.fetchSnapshot(opts)
|
|
1288
1278
|
|
|
1289
1279
|
const dataWithEndBoundary = (data as Array<Message<T>>).concat([
|
|
1290
1280
|
{ headers: { control: `snapshot-end`, ...metadata } },
|
|
@@ -1309,8 +1299,26 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1309
1299
|
}
|
|
1310
1300
|
}
|
|
1311
1301
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1302
|
+
/**
|
|
1303
|
+
* Fetch a snapshot for subset of data.
|
|
1304
|
+
* Returns the metadata and the data, but does not inject it into the subscribed data stream.
|
|
1305
|
+
*
|
|
1306
|
+
* @param opts - The options for the snapshot request.
|
|
1307
|
+
* @returns The metadata and the data for the snapshot.
|
|
1308
|
+
*/
|
|
1309
|
+
async fetchSnapshot(opts: SubsetParams): Promise<{
|
|
1310
|
+
metadata: SnapshotMetadata
|
|
1311
|
+
data: Array<ChangeMessage<T>>
|
|
1312
|
+
}> {
|
|
1313
|
+
const { fetchUrl, requestHeaders } = await this.#constructUrl(
|
|
1314
|
+
this.options.url,
|
|
1315
|
+
true,
|
|
1316
|
+
opts
|
|
1317
|
+
)
|
|
1318
|
+
|
|
1319
|
+
const response = await this.#fetchClient(fetchUrl.toString(), {
|
|
1320
|
+
headers: requestHeaders,
|
|
1321
|
+
})
|
|
1314
1322
|
|
|
1315
1323
|
if (!response.ok) {
|
|
1316
1324
|
throw new FetchError(
|
|
@@ -1318,21 +1326,52 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1318
1326
|
undefined,
|
|
1319
1327
|
undefined,
|
|
1320
1328
|
Object.fromEntries([...response.headers.entries()]),
|
|
1321
|
-
|
|
1329
|
+
fetchUrl.toString()
|
|
1322
1330
|
)
|
|
1323
1331
|
}
|
|
1324
1332
|
|
|
1325
|
-
|
|
1326
|
-
const
|
|
1327
|
-
|
|
1328
|
-
|
|
1333
|
+
// Use schema from stream if available, otherwise extract from response header
|
|
1334
|
+
const schema: Schema =
|
|
1335
|
+
this.#schema ??
|
|
1336
|
+
getSchemaFromHeaders(response.headers, {
|
|
1337
|
+
required: true,
|
|
1338
|
+
url: fetchUrl.toString(),
|
|
1339
|
+
})
|
|
1340
|
+
|
|
1341
|
+
const { metadata, data: rawData } = await response.json()
|
|
1342
|
+
const data = this.#messageParser.parseSnapshotData<ChangeMessage<T>>(
|
|
1343
|
+
rawData,
|
|
1344
|
+
schema
|
|
1329
1345
|
)
|
|
1330
1346
|
|
|
1331
1347
|
return {
|
|
1332
1348
|
metadata,
|
|
1333
|
-
data
|
|
1349
|
+
data,
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Extracts the schema from response headers.
|
|
1356
|
+
* @param headers - The response headers
|
|
1357
|
+
* @param options - Options for schema extraction
|
|
1358
|
+
* @param options.required - If true, throws MissingHeadersError when header is missing. Defaults to false.
|
|
1359
|
+
* @param options.url - The URL to include in the error message if required is true
|
|
1360
|
+
* @returns The parsed schema, or an empty object if not required and header is missing
|
|
1361
|
+
* @throws {MissingHeadersError} if required is true and the header is missing
|
|
1362
|
+
*/
|
|
1363
|
+
function getSchemaFromHeaders(
|
|
1364
|
+
headers: Headers,
|
|
1365
|
+
options?: { required?: boolean; url?: string }
|
|
1366
|
+
): Schema {
|
|
1367
|
+
const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER)
|
|
1368
|
+
if (!schemaHeader) {
|
|
1369
|
+
if (options?.required && options?.url) {
|
|
1370
|
+
throw new MissingHeadersError(options.url, [SHAPE_SCHEMA_HEADER])
|
|
1334
1371
|
}
|
|
1372
|
+
return {}
|
|
1335
1373
|
}
|
|
1374
|
+
return JSON.parse(schemaHeader)
|
|
1336
1375
|
}
|
|
1337
1376
|
|
|
1338
1377
|
/**
|
package/src/parser.ts
CHANGED
|
@@ -122,18 +122,56 @@ export class MessageParser<T extends Row<unknown>> {
|
|
|
122
122
|
typeof value === `object` &&
|
|
123
123
|
value !== null
|
|
124
124
|
) {
|
|
125
|
-
|
|
126
|
-
const row = value as Record<string, Value<GetExtensions<T>>>
|
|
127
|
-
Object.keys(row).forEach((key) => {
|
|
128
|
-
row[key] = this.parseRow(key, row[key] as NullableToken, schema)
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
if (this.transformer) value = this.transformer(value)
|
|
125
|
+
return this.transformMessageValue(value, schema)
|
|
132
126
|
}
|
|
133
127
|
return value
|
|
134
128
|
}) as Result
|
|
135
129
|
}
|
|
136
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Parse an array of ChangeMessages from a snapshot response.
|
|
133
|
+
* Applies type parsing and transformations to the value and old_value properties.
|
|
134
|
+
*/
|
|
135
|
+
parseSnapshotData<Result>(
|
|
136
|
+
messages: Array<unknown>,
|
|
137
|
+
schema: Schema
|
|
138
|
+
): Array<Result> {
|
|
139
|
+
return messages.map((message) => {
|
|
140
|
+
const msg = message as Record<string, unknown>
|
|
141
|
+
|
|
142
|
+
// Transform the value property if it exists
|
|
143
|
+
if (msg.value && typeof msg.value === `object` && msg.value !== null) {
|
|
144
|
+
msg.value = this.transformMessageValue(msg.value, schema)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Transform the old_value property if it exists
|
|
148
|
+
if (
|
|
149
|
+
msg.old_value &&
|
|
150
|
+
typeof msg.old_value === `object` &&
|
|
151
|
+
msg.old_value !== null
|
|
152
|
+
) {
|
|
153
|
+
msg.old_value = this.transformMessageValue(msg.old_value, schema)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return msg as Result
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Transform a message value or old_value object by parsing its columns.
|
|
162
|
+
*/
|
|
163
|
+
private transformMessageValue(
|
|
164
|
+
value: unknown,
|
|
165
|
+
schema: Schema
|
|
166
|
+
): Row<GetExtensions<T>> {
|
|
167
|
+
const row = value as Record<string, Value<GetExtensions<T>>>
|
|
168
|
+
Object.keys(row).forEach((key) => {
|
|
169
|
+
row[key] = this.parseRow(key, row[key] as NullableToken, schema)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
return this.transformer ? this.transformer(row) : row
|
|
173
|
+
}
|
|
174
|
+
|
|
137
175
|
// Parses the message values using the provided parser based on the schema information
|
|
138
176
|
private parseRow(
|
|
139
177
|
key: string,
|