@electric-sql/client 1.5.2 → 1.5.4
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 +957 -274
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +2 -2
- package/dist/index.browser.mjs +4 -3
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.legacy-esm.js +956 -275
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +956 -275
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +304 -375
- package/src/pause-lock.ts +112 -0
- package/src/shape-stream-state.ts +781 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A set-based counting lock for coordinating multiple pause reasons.
|
|
3
|
+
*
|
|
4
|
+
* Multiple independent subsystems (tab visibility, snapshot requests, etc.)
|
|
5
|
+
* may each need the stream paused. A simple boolean flag or counter can't
|
|
6
|
+
* track *why* the stream is paused, leading to bugs where one subsystem's
|
|
7
|
+
* resume overrides another's pause.
|
|
8
|
+
*
|
|
9
|
+
* PauseLock uses a Set of reason strings. The stream is paused when any
|
|
10
|
+
* reason is held, and only resumes when all reasons are released.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const lock = new PauseLock({
|
|
15
|
+
* onAcquired: () => abortController.abort(),
|
|
16
|
+
* onReleased: () => startRequestLoop(),
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* // Tab hidden
|
|
20
|
+
* lock.acquire('visibility') // → onAcquired fires, stream pauses
|
|
21
|
+
*
|
|
22
|
+
* // Snapshot starts while tab hidden
|
|
23
|
+
* lock.acquire('snapshot-1') // → no-op, already paused
|
|
24
|
+
*
|
|
25
|
+
* // Snapshot finishes
|
|
26
|
+
* lock.release('snapshot-1') // → no-op, 'visibility' still held
|
|
27
|
+
*
|
|
28
|
+
* // Tab visible
|
|
29
|
+
* lock.release('visibility') // → onReleased fires, stream resumes
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class PauseLock {
|
|
33
|
+
#holders = new Set<string>()
|
|
34
|
+
#onAcquired: () => void
|
|
35
|
+
#onReleased: () => void
|
|
36
|
+
|
|
37
|
+
constructor(callbacks: { onAcquired: () => void; onReleased: () => void }) {
|
|
38
|
+
this.#onAcquired = callbacks.onAcquired
|
|
39
|
+
this.#onReleased = callbacks.onReleased
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Acquire the lock for a given reason. Idempotent — acquiring the same
|
|
44
|
+
* reason twice is a no-op (but logs a warning since it likely indicates
|
|
45
|
+
* a caller bug).
|
|
46
|
+
*
|
|
47
|
+
* Fires `onAcquired` when the first reason is acquired (transition from
|
|
48
|
+
* unlocked to locked).
|
|
49
|
+
*/
|
|
50
|
+
acquire(reason: string): void {
|
|
51
|
+
if (this.#holders.has(reason)) {
|
|
52
|
+
console.warn(
|
|
53
|
+
`[Electric] PauseLock: "${reason}" already held — ignoring duplicate acquire`
|
|
54
|
+
)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
const wasUnlocked = this.#holders.size === 0
|
|
58
|
+
this.#holders.add(reason)
|
|
59
|
+
if (wasUnlocked) {
|
|
60
|
+
this.#onAcquired()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Release the lock for a given reason. Releasing a reason that isn't
|
|
66
|
+
* held logs a warning (likely indicates an acquire/release mismatch).
|
|
67
|
+
*
|
|
68
|
+
* Fires `onReleased` when the last reason is released (transition from
|
|
69
|
+
* locked to unlocked).
|
|
70
|
+
*/
|
|
71
|
+
release(reason: string): void {
|
|
72
|
+
if (!this.#holders.delete(reason)) {
|
|
73
|
+
console.warn(
|
|
74
|
+
`[Electric] PauseLock: "${reason}" not held — ignoring release (possible acquire/release mismatch)`
|
|
75
|
+
)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
if (this.#holders.size === 0) {
|
|
79
|
+
this.#onReleased()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether the lock is currently held by any reason.
|
|
85
|
+
*/
|
|
86
|
+
get isPaused(): boolean {
|
|
87
|
+
return this.#holders.size > 0
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if a specific reason is holding the lock.
|
|
92
|
+
*/
|
|
93
|
+
isHeldBy(reason: string): boolean {
|
|
94
|
+
return this.#holders.has(reason)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Release all reasons matching a prefix. Does NOT fire `onReleased` —
|
|
99
|
+
* this is for cleanup/reset paths where the stream state is being
|
|
100
|
+
* managed separately.
|
|
101
|
+
*
|
|
102
|
+
* This preserves reasons with different prefixes (e.g., 'visibility'
|
|
103
|
+
* is preserved when clearing 'snapshot-*' reasons).
|
|
104
|
+
*/
|
|
105
|
+
releaseAllMatching(prefix: string): void {
|
|
106
|
+
for (const reason of this.#holders) {
|
|
107
|
+
if (reason.startsWith(prefix)) {
|
|
108
|
+
this.#holders.delete(reason)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|