@electric-sql/client 1.0.0-beta.4 → 1.0.0
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 +65 -37
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +12 -1
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.legacy-esm.js +65 -37
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +65 -37
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +58 -11
- package/src/constants.ts +1 -0
- package/src/fetch.ts +28 -10
- package/src/shape.ts +15 -23
package/src/client.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
SHAPE_HANDLE_QUERY_PARAM,
|
|
35
35
|
SHAPE_SCHEMA_HEADER,
|
|
36
36
|
WHERE_QUERY_PARAM,
|
|
37
|
+
WHERE_PARAMS_PARAM,
|
|
37
38
|
TABLE_QUERY_PARAM,
|
|
38
39
|
REPLICA_PARAM,
|
|
39
40
|
FORCE_DISCONNECT_AND_REFRESH,
|
|
@@ -64,6 +65,17 @@ export interface PostgresParams {
|
|
|
64
65
|
/** The where clauses for the shape */
|
|
65
66
|
where?: string
|
|
66
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Positional where clause paramater values. These will be passed to the server
|
|
70
|
+
* and will substitute `$i` parameters in the where clause.
|
|
71
|
+
*
|
|
72
|
+
* It can be an array (note that positional arguments start at 1, the array will be mapped
|
|
73
|
+
* accordingly), or an object with keys matching the used positional parameters in the where clause.
|
|
74
|
+
*
|
|
75
|
+
* If where clause is `id = $1 or id = $2`, params must have keys `"1"` and `"2"`, or be an array with length 2.
|
|
76
|
+
*/
|
|
77
|
+
params?: Record<`${number}`, string> | string[]
|
|
78
|
+
|
|
67
79
|
/**
|
|
68
80
|
* If `replica` is `default` (the default) then Electric will only send the
|
|
69
81
|
* changed columns in an update.
|
|
@@ -77,11 +89,10 @@ export interface PostgresParams {
|
|
|
77
89
|
*/
|
|
78
90
|
replica?: Replica
|
|
79
91
|
}
|
|
80
|
-
|
|
92
|
+
type SerializableParamValue = string | string[] | Record<string, string>
|
|
81
93
|
type ParamValue =
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
| (() => string | string[] | Promise<string | string[]>)
|
|
94
|
+
| SerializableParamValue
|
|
95
|
+
| (() => SerializableParamValue | Promise<SerializableParamValue>)
|
|
85
96
|
|
|
86
97
|
/**
|
|
87
98
|
* External params type - what users provide.
|
|
@@ -112,7 +123,9 @@ export type ExternalHeadersRecord = {
|
|
|
112
123
|
* All values are converted to strings.
|
|
113
124
|
*/
|
|
114
125
|
type InternalParamsRecord = {
|
|
115
|
-
[K in string as K extends ReservedParamKeys ? never : K]:
|
|
126
|
+
[K in string as K extends ReservedParamKeys ? never : K]:
|
|
127
|
+
| string
|
|
128
|
+
| Record<string, string>
|
|
116
129
|
}
|
|
117
130
|
|
|
118
131
|
/**
|
|
@@ -391,7 +404,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
391
404
|
const [requestHeaders, params] = await Promise.all([
|
|
392
405
|
resolveHeaders(this.options.headers),
|
|
393
406
|
this.options.params
|
|
394
|
-
? toInternalParams(this.options.params)
|
|
407
|
+
? toInternalParams(convertWhereParamsToObj(this.options.params))
|
|
395
408
|
: undefined,
|
|
396
409
|
])
|
|
397
410
|
|
|
@@ -405,13 +418,15 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
405
418
|
// Add PostgreSQL-specific parameters
|
|
406
419
|
if (params) {
|
|
407
420
|
if (params.table)
|
|
408
|
-
fetchUrl
|
|
421
|
+
setQueryParam(fetchUrl, TABLE_QUERY_PARAM, params.table)
|
|
409
422
|
if (params.where)
|
|
410
|
-
fetchUrl
|
|
423
|
+
setQueryParam(fetchUrl, WHERE_QUERY_PARAM, params.where)
|
|
411
424
|
if (params.columns)
|
|
412
|
-
fetchUrl
|
|
425
|
+
setQueryParam(fetchUrl, COLUMNS_QUERY_PARAM, params.columns)
|
|
413
426
|
if (params.replica)
|
|
414
|
-
fetchUrl
|
|
427
|
+
setQueryParam(fetchUrl, REPLICA_PARAM, params.replica)
|
|
428
|
+
if (params.params)
|
|
429
|
+
setQueryParam(fetchUrl, WHERE_PARAMS_PARAM, params.params)
|
|
415
430
|
|
|
416
431
|
// Add any remaining custom parameters
|
|
417
432
|
const customParams = { ...params }
|
|
@@ -419,9 +434,10 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
419
434
|
delete customParams.where
|
|
420
435
|
delete customParams.columns
|
|
421
436
|
delete customParams.replica
|
|
437
|
+
delete customParams.params
|
|
422
438
|
|
|
423
439
|
for (const [key, value] of Object.entries(customParams)) {
|
|
424
|
-
fetchUrl
|
|
440
|
+
setQueryParam(fetchUrl, key, value)
|
|
425
441
|
}
|
|
426
442
|
}
|
|
427
443
|
|
|
@@ -729,3 +745,34 @@ function validateOptions<T>(options: Partial<ShapeStreamOptions<T>>): void {
|
|
|
729
745
|
|
|
730
746
|
return
|
|
731
747
|
}
|
|
748
|
+
|
|
749
|
+
// `unknown` being in the value is a bit of defensive programming if user doesn't use TS
|
|
750
|
+
function setQueryParam(
|
|
751
|
+
url: URL,
|
|
752
|
+
key: string,
|
|
753
|
+
value: Record<string, string> | string | unknown
|
|
754
|
+
): void {
|
|
755
|
+
if (value === undefined || value == null) {
|
|
756
|
+
return
|
|
757
|
+
} else if (typeof value === `string`) {
|
|
758
|
+
url.searchParams.set(key, value)
|
|
759
|
+
} else if (typeof value === `object`) {
|
|
760
|
+
for (const [k, v] of Object.entries(value)) {
|
|
761
|
+
url.searchParams.set(`${key}[${k}]`, v)
|
|
762
|
+
}
|
|
763
|
+
} else {
|
|
764
|
+
url.searchParams.set(key, value.toString())
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function convertWhereParamsToObj(
|
|
769
|
+
allPgParams: ExternalParamsRecord
|
|
770
|
+
): ExternalParamsRecord {
|
|
771
|
+
if (Array.isArray(allPgParams.params)) {
|
|
772
|
+
return {
|
|
773
|
+
...allPgParams,
|
|
774
|
+
params: Object.fromEntries(allPgParams.params.map((v, i) => [i + 1, v])),
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return allPgParams
|
|
778
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -11,4 +11,5 @@ export const OFFSET_QUERY_PARAM = `offset`
|
|
|
11
11
|
export const TABLE_QUERY_PARAM = `table`
|
|
12
12
|
export const WHERE_QUERY_PARAM = `where`
|
|
13
13
|
export const REPLICA_PARAM = `replica`
|
|
14
|
+
export const WHERE_PARAMS_PARAM = `params`
|
|
14
15
|
export const FORCE_DISCONNECT_AND_REFRESH = `force-disconnect-and-refresh`
|
package/src/fetch.ts
CHANGED
|
@@ -264,10 +264,8 @@ class PrefetchQueue {
|
|
|
264
264
|
const aborter = new AbortController()
|
|
265
265
|
|
|
266
266
|
try {
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
signal: chainAborter(aborter, args[1]?.signal),
|
|
270
|
-
})
|
|
267
|
+
const { signal, cleanup } = chainAborter(aborter, args[1]?.signal)
|
|
268
|
+
const request = this.#fetchClient(url, { ...(args[1] ?? {}), signal })
|
|
271
269
|
this.#prefetchQueue.set(url, [request, aborter])
|
|
272
270
|
request
|
|
273
271
|
.then((response) => {
|
|
@@ -286,6 +284,7 @@ class PrefetchQueue {
|
|
|
286
284
|
return this.#prefetch(nextUrl, args[1])
|
|
287
285
|
})
|
|
288
286
|
.catch(() => {})
|
|
287
|
+
.finally(cleanup)
|
|
289
288
|
} catch (_) {
|
|
290
289
|
// ignore prefetch errors
|
|
291
290
|
}
|
|
@@ -324,12 +323,31 @@ function getNextChunkUrl(url: string, res: Response): string | void {
|
|
|
324
323
|
function chainAborter(
|
|
325
324
|
aborter: AbortController,
|
|
326
325
|
sourceSignal?: AbortSignal | null
|
|
327
|
-
):
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
326
|
+
): {
|
|
327
|
+
signal: AbortSignal
|
|
328
|
+
cleanup: () => void
|
|
329
|
+
} {
|
|
330
|
+
let cleanup = noop
|
|
331
|
+
if (!sourceSignal) {
|
|
332
|
+
// no-op, nothing to chain to
|
|
333
|
+
} else if (sourceSignal.aborted) {
|
|
334
|
+
// source signal is already aborted, abort immediately
|
|
335
|
+
aborter.abort()
|
|
336
|
+
} else {
|
|
337
|
+
// chain to source signal abort event, and add callback to unlink
|
|
338
|
+
// the aborter to avoid memory leaks
|
|
339
|
+
const abortParent = () => aborter.abort()
|
|
340
|
+
sourceSignal.addEventListener(`abort`, abortParent, {
|
|
332
341
|
once: true,
|
|
342
|
+
signal: aborter.signal,
|
|
333
343
|
})
|
|
334
|
-
|
|
344
|
+
cleanup = () => sourceSignal.removeEventListener(`abort`, abortParent)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
signal: aborter.signal,
|
|
349
|
+
cleanup,
|
|
350
|
+
}
|
|
335
351
|
}
|
|
352
|
+
|
|
353
|
+
function noop() {}
|
package/src/shape.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type ShapeChangedCallback<T extends Row<unknown> = Row> = (data: {
|
|
|
9
9
|
rows: T[]
|
|
10
10
|
}) => void
|
|
11
11
|
|
|
12
|
+
type ShapeStatus = `syncing` | `up-to-date`
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* A Shape is an object that subscribes to a shape log,
|
|
14
16
|
* keeps a materialised shape `.rows` in memory and
|
|
@@ -50,8 +52,7 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
50
52
|
|
|
51
53
|
readonly #data: ShapeData<T> = new Map()
|
|
52
54
|
readonly #subscribers = new Map<number, ShapeChangedCallback<T>>()
|
|
53
|
-
|
|
54
|
-
#hasNotifiedSubscribersUpToDate: boolean = false
|
|
55
|
+
#status: ShapeStatus = `syncing`
|
|
55
56
|
#error: FetchError | false = false
|
|
56
57
|
|
|
57
58
|
constructor(stream: ShapeStreamInterface<T>) {
|
|
@@ -63,7 +64,7 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
get isUpToDate(): boolean {
|
|
66
|
-
return this
|
|
67
|
+
return this.#status === `up-to-date`
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
get lastOffset(): Offset {
|
|
@@ -143,16 +144,11 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
#process(messages: Message<T>[]): void {
|
|
146
|
-
let
|
|
147
|
-
let isUpToDate = false
|
|
148
|
-
let newlyUpToDate = false
|
|
147
|
+
let shouldNotify = false
|
|
149
148
|
|
|
150
149
|
messages.forEach((message) => {
|
|
151
150
|
if (isChangeMessage(message)) {
|
|
152
|
-
|
|
153
|
-
message.headers.operation
|
|
154
|
-
)
|
|
155
|
-
|
|
151
|
+
shouldNotify = this.#updateShapeStatus(`syncing`)
|
|
156
152
|
switch (message.headers.operation) {
|
|
157
153
|
case `insert`:
|
|
158
154
|
this.#data.set(message.key, message.value)
|
|
@@ -172,28 +168,24 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
172
168
|
if (isControlMessage(message)) {
|
|
173
169
|
switch (message.headers.control) {
|
|
174
170
|
case `up-to-date`:
|
|
175
|
-
|
|
176
|
-
if (!this.#hasNotifiedSubscribersUpToDate) {
|
|
177
|
-
newlyUpToDate = true
|
|
178
|
-
}
|
|
171
|
+
shouldNotify = this.#updateShapeStatus(`up-to-date`)
|
|
179
172
|
break
|
|
180
173
|
case `must-refetch`:
|
|
181
174
|
this.#data.clear()
|
|
182
175
|
this.#error = false
|
|
183
|
-
|
|
184
|
-
isUpToDate = false
|
|
185
|
-
newlyUpToDate = false
|
|
176
|
+
shouldNotify = this.#updateShapeStatus(`syncing`)
|
|
186
177
|
break
|
|
187
178
|
}
|
|
188
179
|
}
|
|
189
180
|
})
|
|
190
181
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
182
|
+
if (shouldNotify) this.#notify()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#updateShapeStatus(status: ShapeStatus): boolean {
|
|
186
|
+
const stateChanged = this.#status !== status
|
|
187
|
+
this.#status = status
|
|
188
|
+
return stateChanged && status === `up-to-date`
|
|
197
189
|
}
|
|
198
190
|
|
|
199
191
|
#handleError(e: Error): void {
|