@electric-sql/client 1.0.10 → 1.0.11
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 +338 -24
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +90 -5
- package/dist/index.browser.mjs +3 -3
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +90 -5
- package/dist/index.legacy-esm.js +325 -24
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +337 -24
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +203 -6
- package/src/constants.ts +12 -0
- package/src/fetch.ts +19 -1
- package/src/helpers.ts +34 -1
- package/src/index.ts +5 -1
- package/src/shape.ts +104 -14
- package/src/snapshot-tracker.ts +88 -0
- package/src/types.ts +40 -6
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { isVisibleInSnapshot } from './helpers'
|
|
2
|
+
import { Row, SnapshotMetadata } from './types'
|
|
3
|
+
import { ChangeMessage } from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tracks active snapshots and filters out duplicate change messages that are already included in snapshots.
|
|
7
|
+
*
|
|
8
|
+
* When requesting a snapshot in changes_only mode, we need to track which transactions were included in the
|
|
9
|
+
* snapshot to avoid processing duplicate changes that arrive via the live stream. This class maintains that
|
|
10
|
+
* tracking state and provides methods to:
|
|
11
|
+
*
|
|
12
|
+
* - Add new snapshots for tracking via addSnapshot()
|
|
13
|
+
* - Remove completed snapshots via removeSnapshot()
|
|
14
|
+
* - Check if incoming changes should be filtered via shouldRejectMessage()
|
|
15
|
+
*/
|
|
16
|
+
export class SnapshotTracker {
|
|
17
|
+
private activeSnapshots: Map<
|
|
18
|
+
number,
|
|
19
|
+
{ xmin: bigint; xmax: bigint; xip_list: bigint[]; keys: Set<string> }
|
|
20
|
+
> = new Map()
|
|
21
|
+
private xmaxSnapshots: Map<bigint, Set<number>> = new Map()
|
|
22
|
+
private snapshotsByDatabaseLsn: Map<bigint, Set<number>> = new Map()
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Add a new snapshot for tracking
|
|
26
|
+
*/
|
|
27
|
+
addSnapshot(metadata: SnapshotMetadata, keys: Set<string>): void {
|
|
28
|
+
this.activeSnapshots.set(metadata.snapshot_mark, {
|
|
29
|
+
xmin: BigInt(metadata.xmin),
|
|
30
|
+
xmax: BigInt(metadata.xmax),
|
|
31
|
+
xip_list: metadata.xip_list.map(BigInt),
|
|
32
|
+
keys,
|
|
33
|
+
})
|
|
34
|
+
const xmaxSet =
|
|
35
|
+
this.xmaxSnapshots
|
|
36
|
+
.get(BigInt(metadata.xmax))
|
|
37
|
+
?.add(metadata.snapshot_mark) ?? new Set([metadata.snapshot_mark])
|
|
38
|
+
this.xmaxSnapshots.set(BigInt(metadata.xmax), xmaxSet)
|
|
39
|
+
const databaseLsnSet =
|
|
40
|
+
this.snapshotsByDatabaseLsn
|
|
41
|
+
.get(BigInt(metadata.database_lsn))
|
|
42
|
+
?.add(metadata.snapshot_mark) ?? new Set([metadata.snapshot_mark])
|
|
43
|
+
this.snapshotsByDatabaseLsn.set(
|
|
44
|
+
BigInt(metadata.database_lsn),
|
|
45
|
+
databaseLsnSet
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Remove a snapshot from tracking
|
|
51
|
+
*/
|
|
52
|
+
removeSnapshot(snapshotMark: number): void {
|
|
53
|
+
this.activeSnapshots.delete(snapshotMark)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a change message should be filtered because its already in an active snapshot
|
|
58
|
+
* Returns true if the message should be filtered out (not processed)
|
|
59
|
+
*/
|
|
60
|
+
shouldRejectMessage(message: ChangeMessage<Row<unknown>>): boolean {
|
|
61
|
+
const txids = message.headers.txids || []
|
|
62
|
+
if (txids.length === 0) return false
|
|
63
|
+
|
|
64
|
+
const xid = Math.max(...txids) // Use the maximum transaction ID
|
|
65
|
+
|
|
66
|
+
for (const [xmax, snapshots] of this.xmaxSnapshots.entries()) {
|
|
67
|
+
if (xid >= xmax) {
|
|
68
|
+
for (const snapshot of snapshots) {
|
|
69
|
+
this.removeSnapshot(snapshot)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return [...this.activeSnapshots.values()].some(
|
|
75
|
+
(x) => x.keys.has(message.key) && isVisibleInSnapshot(xid, x)
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
lastSeenUpdate(newDatabaseLsn: bigint): void {
|
|
80
|
+
for (const [dbLsn, snapshots] of this.snapshotsByDatabaseLsn.entries()) {
|
|
81
|
+
if (dbLsn <= newDatabaseLsn) {
|
|
82
|
+
for (const snapshot of snapshots) {
|
|
83
|
+
this.removeSnapshot(snapshot)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -23,7 +23,24 @@ export type GetExtensions<T> = [T] extends [Row<never>]
|
|
|
23
23
|
? E
|
|
24
24
|
: never
|
|
25
25
|
|
|
26
|
-
export type Offset =
|
|
26
|
+
export type Offset =
|
|
27
|
+
| `-1`
|
|
28
|
+
| `now`
|
|
29
|
+
| `${number}_${number}`
|
|
30
|
+
| `${bigint}_${number}`
|
|
31
|
+
|
|
32
|
+
/** Information about transaction visibility for a snapshot. All fields are encoded as strings, but should be treated as uint64. */
|
|
33
|
+
export type PostgresSnapshot = {
|
|
34
|
+
xmin: `${bigint}`
|
|
35
|
+
xmax: `${bigint}`
|
|
36
|
+
xip_list: `${bigint}`[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type NormalizedPgSnapshot = {
|
|
40
|
+
xmin: bigint
|
|
41
|
+
xmax: bigint
|
|
42
|
+
xip_list: bigint[]
|
|
43
|
+
}
|
|
27
44
|
|
|
28
45
|
interface Header {
|
|
29
46
|
[key: Exclude<string, `operation` | `control`>]: Value
|
|
@@ -32,17 +49,19 @@ interface Header {
|
|
|
32
49
|
export type Operation = `insert` | `update` | `delete`
|
|
33
50
|
|
|
34
51
|
export type ControlMessage = {
|
|
35
|
-
headers:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
headers:
|
|
53
|
+
| (Header & {
|
|
54
|
+
control: `up-to-date` | `must-refetch`
|
|
55
|
+
global_last_seen_lsn?: string
|
|
56
|
+
})
|
|
57
|
+
| (Header & { control: `snapshot-end` } & PostgresSnapshot)
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
export type ChangeMessage<T extends Row<unknown> = Row> = {
|
|
42
61
|
key: string
|
|
43
62
|
value: T
|
|
44
63
|
old_value?: Partial<T> // Only provided for updates if `replica` is `full`
|
|
45
|
-
headers: Header & { operation: Operation }
|
|
64
|
+
headers: Header & { operation: Operation; txids?: number[] }
|
|
46
65
|
}
|
|
47
66
|
|
|
48
67
|
// Define the type for a record
|
|
@@ -131,3 +150,18 @@ export type TypedMessages<T extends Row<unknown> = Row> = {
|
|
|
131
150
|
}
|
|
132
151
|
|
|
133
152
|
export type MaybePromise<T> = T | Promise<T>
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Metadata that allows the consumer to know which changes have been incorporated into this snapshot.
|
|
156
|
+
*
|
|
157
|
+
* For any data that has a known transaction ID `xid` (and e.g. a key that's part of the snapshot):
|
|
158
|
+
* - if `xid` < `xmin` - included, change can be skipped
|
|
159
|
+
* - if `xid` < `xmax` AND `xid` not in `xip` - included, change can be skipped
|
|
160
|
+
* - if `xid` < `xmax` AND `xid` in `xip` - parallel, not included, change must be processed
|
|
161
|
+
* - if `xid` >= `xmax` - not included, change must be processed, and we can stop filtering after we see this
|
|
162
|
+
*/
|
|
163
|
+
export type SnapshotMetadata = {
|
|
164
|
+
/** Random number that's reflected in the `snapshot_mark` header on the snapshot items. */
|
|
165
|
+
snapshot_mark: number
|
|
166
|
+
database_lsn: string
|
|
167
|
+
} & PostgresSnapshot
|