@electric-sql/client 1.1.3 → 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 +95 -41
- 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 +93 -39
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +95 -41
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +96 -34
- package/src/parser.ts +45 -7
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
|
/**
|
|
@@ -489,6 +489,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
489
489
|
#sseFallbackToLongPolling = false
|
|
490
490
|
#sseBackoffBaseDelay = 100 // Base delay for exponential backoff (ms)
|
|
491
491
|
#sseBackoffMaxDelay = 5000 // Maximum delay cap (ms)
|
|
492
|
+
#unsubscribeFromVisibilityChanges?: () => void
|
|
492
493
|
|
|
493
494
|
constructor(options: ShapeStreamOptions<GetExtensions<T>>) {
|
|
494
495
|
this.options = { subscribe: true, ...options }
|
|
@@ -656,9 +657,17 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
656
657
|
}
|
|
657
658
|
|
|
658
659
|
if (e instanceof FetchBackoffAbortError) {
|
|
660
|
+
// Check current state - it may have changed due to concurrent pause/resume calls
|
|
661
|
+
// from the visibility change handler during the async fetch operation.
|
|
662
|
+
// TypeScript's flow analysis doesn't account for concurrent state changes.
|
|
663
|
+
const currentState = this.#state as
|
|
664
|
+
| `active`
|
|
665
|
+
| `pause-requested`
|
|
666
|
+
| `paused`
|
|
659
667
|
if (
|
|
660
668
|
requestAbortController.signal.aborted &&
|
|
661
|
-
requestAbortController.signal.reason === PAUSE_STREAM
|
|
669
|
+
requestAbortController.signal.reason === PAUSE_STREAM &&
|
|
670
|
+
currentState === `pause-requested`
|
|
662
671
|
) {
|
|
663
672
|
this.#state = `paused`
|
|
664
673
|
}
|
|
@@ -765,7 +774,10 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
765
774
|
fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, this.#lastOffset)
|
|
766
775
|
fetchUrl.searchParams.set(LOG_MODE_QUERY_PARAM, this.#mode)
|
|
767
776
|
|
|
768
|
-
|
|
777
|
+
// Snapshot requests (with subsetParams) should never use live polling
|
|
778
|
+
const isSnapshotRequest = subsetParams !== undefined
|
|
779
|
+
|
|
780
|
+
if (this.#isUpToDate && !isSnapshotRequest) {
|
|
769
781
|
// If we are resuming from a paused state, we don't want to perform a live request
|
|
770
782
|
// because it could be a long poll that holds for 20sec
|
|
771
783
|
// and during all that time `isConnected` will be false
|
|
@@ -837,11 +849,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
837
849
|
this.#liveCacheBuster = liveCacheBuster
|
|
838
850
|
}
|
|
839
851
|
|
|
840
|
-
|
|
841
|
-
const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER)
|
|
842
|
-
return schemaHeader ? JSON.parse(schemaHeader) : {}
|
|
843
|
-
}
|
|
844
|
-
this.#schema = this.#schema ?? getSchema()
|
|
852
|
+
this.#schema = this.#schema ?? getSchemaFromHeaders(headers)
|
|
845
853
|
|
|
846
854
|
// NOTE: 204s are deprecated, the Electric server should not
|
|
847
855
|
// send these in latest versions but this is here for backwards
|
|
@@ -1045,7 +1053,15 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1045
1053
|
}
|
|
1046
1054
|
|
|
1047
1055
|
#resume() {
|
|
1048
|
-
if (
|
|
1056
|
+
if (
|
|
1057
|
+
this.#started &&
|
|
1058
|
+
(this.#state === `paused` || this.#state === `pause-requested`)
|
|
1059
|
+
) {
|
|
1060
|
+
// If we're resuming from pause-requested state, we need to set state back to active
|
|
1061
|
+
// to prevent the pause from completing
|
|
1062
|
+
if (this.#state === `pause-requested`) {
|
|
1063
|
+
this.#state = `active`
|
|
1064
|
+
}
|
|
1049
1065
|
this.#start()
|
|
1050
1066
|
}
|
|
1051
1067
|
}
|
|
@@ -1066,6 +1082,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1066
1082
|
|
|
1067
1083
|
unsubscribeAll(): void {
|
|
1068
1084
|
this.#subscribers.clear()
|
|
1085
|
+
this.#unsubscribeFromVisibilityChanges?.()
|
|
1069
1086
|
}
|
|
1070
1087
|
|
|
1071
1088
|
/** Unix time at which we last synced. Undefined when `isLoading` is true. */
|
|
@@ -1192,6 +1209,11 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1192
1209
|
}
|
|
1193
1210
|
|
|
1194
1211
|
document.addEventListener(`visibilitychange`, visibilityHandler)
|
|
1212
|
+
|
|
1213
|
+
// Store cleanup function to remove the event listener
|
|
1214
|
+
this.#unsubscribeFromVisibilityChanges = () => {
|
|
1215
|
+
document.removeEventListener(`visibilitychange`, visibilityHandler)
|
|
1216
|
+
}
|
|
1195
1217
|
}
|
|
1196
1218
|
}
|
|
1197
1219
|
|
|
@@ -1214,7 +1236,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1214
1236
|
}
|
|
1215
1237
|
|
|
1216
1238
|
/**
|
|
1217
|
-
* Request a snapshot for subset of data.
|
|
1239
|
+
* Request a snapshot for subset of data and inject it into the subscribed data stream.
|
|
1218
1240
|
*
|
|
1219
1241
|
* Only available when mode is `changes_only`.
|
|
1220
1242
|
* Returns the insertion point & the data, but more importantly injects the data
|
|
@@ -1252,16 +1274,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1252
1274
|
this.#pause()
|
|
1253
1275
|
}
|
|
1254
1276
|
|
|
1255
|
-
const {
|
|
1256
|
-
this.options.url,
|
|
1257
|
-
true,
|
|
1258
|
-
opts
|
|
1259
|
-
)
|
|
1260
|
-
|
|
1261
|
-
const { metadata, data } = await this.#fetchSnapshot(
|
|
1262
|
-
fetchUrl,
|
|
1263
|
-
requestHeaders
|
|
1264
|
-
)
|
|
1277
|
+
const { metadata, data } = await this.fetchSnapshot(opts)
|
|
1265
1278
|
|
|
1266
1279
|
const dataWithEndBoundary = (data as Array<Message<T>>).concat([
|
|
1267
1280
|
{ headers: { control: `snapshot-end`, ...metadata } },
|
|
@@ -1286,8 +1299,26 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1286
1299
|
}
|
|
1287
1300
|
}
|
|
1288
1301
|
|
|
1289
|
-
|
|
1290
|
-
|
|
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
|
+
})
|
|
1291
1322
|
|
|
1292
1323
|
if (!response.ok) {
|
|
1293
1324
|
throw new FetchError(
|
|
@@ -1295,21 +1326,52 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1295
1326
|
undefined,
|
|
1296
1327
|
undefined,
|
|
1297
1328
|
Object.fromEntries([...response.headers.entries()]),
|
|
1298
|
-
|
|
1329
|
+
fetchUrl.toString()
|
|
1299
1330
|
)
|
|
1300
1331
|
}
|
|
1301
1332
|
|
|
1302
|
-
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1305
|
-
|
|
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
|
|
1306
1345
|
)
|
|
1307
1346
|
|
|
1308
1347
|
return {
|
|
1309
1348
|
metadata,
|
|
1310
|
-
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])
|
|
1311
1371
|
}
|
|
1372
|
+
return {}
|
|
1312
1373
|
}
|
|
1374
|
+
return JSON.parse(schemaHeader)
|
|
1313
1375
|
}
|
|
1314
1376
|
|
|
1315
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,
|