@kontsedal/olas-core 0.0.5 → 0.1.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/index.cjs +0 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +109 -27
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +109 -27
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +0 -0
- package/dist/index.mjs.map +1 -1
- package/dist/{root-DqWolle_.mjs → root-Byq-QYTp.mjs} +1478 -347
- package/dist/root-Byq-QYTp.mjs.map +1 -0
- package/dist/{root-lBp7qziQ.cjs → root-CPSL5kpR.cjs} +1483 -346
- package/dist/root-CPSL5kpR.cjs.map +1 -0
- package/dist/testing.cjs +5 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +3 -2
- package/dist/testing.d.cts.map +1 -1
- package/dist/testing.d.mts +3 -2
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.mjs +5 -2
- package/dist/testing.mjs.map +1 -1
- package/dist/{types-BH1o6nYa.d.mts → types-Brp-4WaZ.d.mts} +244 -23
- package/dist/types-Brp-4WaZ.d.mts.map +1 -0
- package/dist/{types-C4Vtkxbn.d.cts → types-DzDIypPo.d.cts} +244 -23
- package/dist/types-DzDIypPo.d.cts.map +1 -0
- package/package.json +4 -1
- package/src/controller/instance.ts +360 -94
- package/src/controller/root.ts +58 -30
- package/src/controller/types.ts +66 -4
- package/src/emitter.ts +8 -2
- package/src/errors.ts +39 -3
- package/src/forms/field.ts +219 -25
- package/src/forms/form-types.ts +16 -3
- package/src/forms/form.ts +360 -38
- package/src/forms/index.ts +3 -1
- package/src/forms/types.ts +27 -1
- package/src/forms/validators.ts +45 -11
- package/src/index.ts +13 -7
- package/src/query/client.ts +237 -58
- package/src/query/define.ts +0 -0
- package/src/query/entry.ts +259 -15
- package/src/query/focus-online.ts +53 -11
- package/src/query/infinite.ts +351 -47
- package/src/query/keys.ts +33 -2
- package/src/query/local.ts +3 -0
- package/src/query/mutation.ts +27 -6
- package/src/query/plugin.ts +43 -4
- package/src/query/types.ts +76 -6
- package/src/query/use.ts +53 -10
- package/src/selection.ts +49 -12
- package/src/signals/readonly.ts +3 -0
- package/src/signals/runtime.ts +27 -0
- package/src/signals/types.ts +10 -0
- package/src/testing.ts +9 -0
- package/src/timing/debounced.ts +106 -23
- package/src/timing/index.ts +1 -1
- package/src/timing/throttled.ts +85 -28
- package/src/utils.ts +15 -2
- package/dist/root-DqWolle_.mjs.map +0 -1
- package/dist/root-lBp7qziQ.cjs.map +0 -1
- package/dist/types-BH1o6nYa.d.mts.map +0 -1
- package/dist/types-C4Vtkxbn.d.cts.map +0 -1
package/src/timing/debounced.ts
CHANGED
|
@@ -1,52 +1,135 @@
|
|
|
1
1
|
import { effect, signal } from '../signals'
|
|
2
|
+
import { readOnly } from '../signals/readonly'
|
|
2
3
|
import type { ReadSignal } from '../signals/types'
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* A `ReadSignal<T>` returned by `debounced` / `throttled`. Extends the
|
|
7
|
+
* subscription surface with manual `cancel()` and `flush()`.
|
|
8
|
+
*
|
|
9
|
+
* - `cancel()` drops any pending emission without firing. Useful when a
|
|
10
|
+
* navigation away from the screen should discard the latest in-flight
|
|
11
|
+
* draft instead of writing it through to the debounced output.
|
|
12
|
+
* - `flush()` immediately emits the pending value (if any). Useful at
|
|
13
|
+
* submit time: "commit whatever the user just typed without waiting
|
|
14
|
+
* for the debounce timer to fire."
|
|
15
|
+
*
|
|
16
|
+
* Both are no-ops when nothing is pending.
|
|
17
|
+
*/
|
|
18
|
+
export type TimingSignal<T> = ReadSignal<T> & {
|
|
19
|
+
cancel(): void
|
|
20
|
+
flush(): void
|
|
21
|
+
/**
|
|
22
|
+
* Tear down the internal effect (drop the subscription to `source`) and
|
|
23
|
+
* clear any pending timer. Idempotent. Call this when the timing signal
|
|
24
|
+
* has no `options.signal` tying its lifecycle to an AbortController —
|
|
25
|
+
* otherwise the effect keeps `source` subscribed forever.
|
|
26
|
+
*/
|
|
27
|
+
dispose(): void
|
|
28
|
+
}
|
|
29
|
+
|
|
4
30
|
/**
|
|
5
31
|
* Lag a signal by `ms`. The returned signal updates only after the source has
|
|
6
32
|
* been unchanged for `ms`. Each new write resets the timer.
|
|
7
33
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
34
|
+
* - `leading: true` (default `false`) emits immediately on the first write,
|
|
35
|
+
* then suppresses further writes until `ms` has passed since the last
|
|
36
|
+
* emission. Combine with trailing (default `true`) for "first + last"
|
|
37
|
+
* semantics.
|
|
38
|
+
* - `trailing: false` disables the trailing emission. Pair with
|
|
39
|
+
* `leading: true` for "only fire on the leading edge" semantics.
|
|
40
|
+
* - `options.signal` (`AbortSignal`) ties the internal effect to a
|
|
41
|
+
* lifecycle — when the signal aborts the effect disposes, the pending
|
|
42
|
+
* timer clears, and the subscriber chain on `source` drops.
|
|
13
43
|
*/
|
|
14
44
|
export function debounced<T>(
|
|
15
45
|
source: ReadSignal<T>,
|
|
16
46
|
ms: number,
|
|
17
|
-
options?: { signal?: AbortSignal },
|
|
18
|
-
):
|
|
47
|
+
options?: { signal?: AbortSignal; leading?: boolean; trailing?: boolean },
|
|
48
|
+
): TimingSignal<T> {
|
|
49
|
+
const leading = options?.leading ?? false
|
|
50
|
+
const trailing = options?.trailing ?? true
|
|
51
|
+
if (!leading && !trailing) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'[olas] debounced: at least one of `leading` or `trailing` must be true (both false never emits).',
|
|
54
|
+
)
|
|
55
|
+
}
|
|
19
56
|
const out = signal<T>(source.peek())
|
|
20
57
|
let timer: ReturnType<typeof setTimeout> | null = null
|
|
58
|
+
let pendingValue: T = source.peek()
|
|
59
|
+
let hasPending = false
|
|
21
60
|
let initial = true
|
|
61
|
+
let inCooldown = false
|
|
62
|
+
|
|
63
|
+
const fireTrailing = () => {
|
|
64
|
+
timer = null
|
|
65
|
+
inCooldown = false
|
|
66
|
+
if (hasPending && trailing) {
|
|
67
|
+
out.set(pendingValue)
|
|
68
|
+
hasPending = false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
22
71
|
|
|
23
|
-
const
|
|
72
|
+
const disposeEffect = effect(() => {
|
|
24
73
|
const value = source.value
|
|
25
74
|
if (initial) {
|
|
26
|
-
// The first effect run reads the source for tracking; we already
|
|
27
|
-
// initialized `out` to the same value, so skip scheduling.
|
|
28
75
|
initial = false
|
|
29
76
|
return
|
|
30
77
|
}
|
|
78
|
+
pendingValue = value
|
|
31
79
|
if (timer != null) clearTimeout(timer)
|
|
32
|
-
|
|
80
|
+
if (leading && !inCooldown) {
|
|
81
|
+
// Leading edge — emit now, start a cooldown timer that, if untouched
|
|
82
|
+
// by another write, fires the trailing edge with the same value.
|
|
33
83
|
out.set(value)
|
|
34
|
-
|
|
35
|
-
|
|
84
|
+
hasPending = false
|
|
85
|
+
inCooldown = true
|
|
86
|
+
timer = setTimeout(fireTrailing, ms)
|
|
87
|
+
} else {
|
|
88
|
+
// Pending only matters if a trailing emit can actually happen. With
|
|
89
|
+
// `trailing: false` the timer just resets the cooldown and must NOT
|
|
90
|
+
// leave a value for a later `flush()` to emit. (T2.7)
|
|
91
|
+
hasPending = trailing
|
|
92
|
+
timer = setTimeout(fireTrailing, ms)
|
|
93
|
+
}
|
|
36
94
|
})
|
|
37
95
|
|
|
96
|
+
const cancel = () => {
|
|
97
|
+
if (timer != null) {
|
|
98
|
+
clearTimeout(timer)
|
|
99
|
+
timer = null
|
|
100
|
+
}
|
|
101
|
+
hasPending = false
|
|
102
|
+
inCooldown = false
|
|
103
|
+
}
|
|
104
|
+
const flush = () => {
|
|
105
|
+
if (timer != null) {
|
|
106
|
+
clearTimeout(timer)
|
|
107
|
+
timer = null
|
|
108
|
+
}
|
|
109
|
+
if (hasPending) {
|
|
110
|
+
out.set(pendingValue)
|
|
111
|
+
hasPending = false
|
|
112
|
+
}
|
|
113
|
+
inCooldown = false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const dispose = () => {
|
|
117
|
+
cancel()
|
|
118
|
+
disposeEffect()
|
|
119
|
+
}
|
|
120
|
+
|
|
38
121
|
const sig = options?.signal
|
|
39
122
|
if (sig) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
clearTimeout(timer)
|
|
43
|
-
timer = null
|
|
44
|
-
}
|
|
45
|
-
dispose()
|
|
46
|
-
}
|
|
47
|
-
if (sig.aborted) stop()
|
|
48
|
-
else sig.addEventListener('abort', stop, { once: true })
|
|
123
|
+
if (sig.aborted) dispose()
|
|
124
|
+
else sig.addEventListener('abort', dispose, { once: true })
|
|
49
125
|
}
|
|
50
126
|
|
|
51
|
-
|
|
127
|
+
// Expose a read-only projection of `out` plus the control surface. The old
|
|
128
|
+
// `out as TimingSignal` cast leaked `out.set` to callers. (T2.7)
|
|
129
|
+
const handle = Object.assign(Object.create(readOnly(out)), {
|
|
130
|
+
cancel,
|
|
131
|
+
flush,
|
|
132
|
+
dispose,
|
|
133
|
+
}) as TimingSignal<T>
|
|
134
|
+
return handle
|
|
52
135
|
}
|
package/src/timing/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { debounced } from './debounced'
|
|
1
|
+
export { debounced, type TimingSignal } from './debounced'
|
|
2
2
|
export { throttled } from './throttled'
|
package/src/timing/throttled.ts
CHANGED
|
@@ -1,65 +1,122 @@
|
|
|
1
1
|
import { effect, signal } from '../signals'
|
|
2
|
+
import { readOnly } from '../signals/readonly'
|
|
2
3
|
import type { ReadSignal } from '../signals/types'
|
|
4
|
+
import type { TimingSignal } from './debounced'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Time source — `Date.now()`. Stays in lockstep with `vi.setSystemTime()`
|
|
8
|
+
* for tests that exercise time-jumps; downstream consumers that need a
|
|
9
|
+
* monotonic clock can pass `options.signal` and gate on system-time
|
|
10
|
+
* changes externally.
|
|
11
|
+
*/
|
|
12
|
+
function now(): number {
|
|
13
|
+
return Date.now()
|
|
14
|
+
}
|
|
3
15
|
|
|
4
16
|
/**
|
|
5
17
|
* Rate-limit a signal so it emits at most once per `ms` (leading + trailing).
|
|
6
18
|
* The first change passes through immediately. Subsequent changes within the
|
|
7
19
|
* window are coalesced; the latest value is emitted when the window expires.
|
|
8
20
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
21
|
+
* - `leading: false` (default `true`) skips the immediate leading-edge
|
|
22
|
+
* emission. Useful for "fire only after the window settles" semantics.
|
|
23
|
+
* - `trailing: false` (default `true`) skips the windowed trailing emit.
|
|
24
|
+
* Combine with `leading: true` for "only fire on the leading edge."
|
|
25
|
+
* - `options.signal` ties the internal effect to a lifecycle.
|
|
26
|
+
*
|
|
27
|
+
* The returned handle exposes `cancel()` / `flush()` — see `TimingSignal`.
|
|
12
28
|
*/
|
|
13
29
|
export function throttled<T>(
|
|
14
30
|
source: ReadSignal<T>,
|
|
15
31
|
ms: number,
|
|
16
|
-
options?: { signal?: AbortSignal },
|
|
17
|
-
):
|
|
32
|
+
options?: { signal?: AbortSignal; leading?: boolean; trailing?: boolean },
|
|
33
|
+
): TimingSignal<T> {
|
|
34
|
+
const leading = options?.leading ?? true
|
|
35
|
+
const trailing = options?.trailing ?? true
|
|
36
|
+
if (!leading && !trailing) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
'[olas] throttled: at least one of `leading` or `trailing` must be true (both false never emits).',
|
|
39
|
+
)
|
|
40
|
+
}
|
|
18
41
|
const out = signal<T>(source.peek())
|
|
19
42
|
let lastEmit = Number.NEGATIVE_INFINITY
|
|
20
43
|
let trailingTimer: ReturnType<typeof setTimeout> | null = null
|
|
21
44
|
let trailingValue: T = source.peek()
|
|
45
|
+
let hasPending = false
|
|
22
46
|
let initial = true
|
|
23
47
|
|
|
24
|
-
const
|
|
48
|
+
const fireTrailing = () => {
|
|
49
|
+
trailingTimer = null
|
|
50
|
+
if (hasPending && trailing) {
|
|
51
|
+
out.set(trailingValue)
|
|
52
|
+
lastEmit = now()
|
|
53
|
+
hasPending = false
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const disposeEffect = effect(() => {
|
|
25
58
|
const value = source.value
|
|
26
59
|
if (initial) {
|
|
27
60
|
initial = false
|
|
28
61
|
return
|
|
29
62
|
}
|
|
30
|
-
const
|
|
31
|
-
const elapsed =
|
|
32
|
-
if (elapsed >= ms) {
|
|
63
|
+
const t = now()
|
|
64
|
+
const elapsed = t - lastEmit
|
|
65
|
+
if (elapsed >= ms && leading) {
|
|
33
66
|
out.set(value)
|
|
34
|
-
lastEmit =
|
|
67
|
+
lastEmit = t
|
|
68
|
+
hasPending = false
|
|
69
|
+
// The leading emit consumed the value — drop any stale trailing-pending.
|
|
35
70
|
if (trailingTimer != null) {
|
|
36
71
|
clearTimeout(trailingTimer)
|
|
37
72
|
trailingTimer = null
|
|
38
73
|
}
|
|
39
|
-
} else {
|
|
74
|
+
} else if (trailing) {
|
|
75
|
+
// Coalesce into the trailing edge. With `trailing: false` we schedule
|
|
76
|
+
// nothing and never set `hasPending`, so a later `flush()` can't emit a
|
|
77
|
+
// value the option said should never fire. (T2.7)
|
|
40
78
|
trailingValue = value
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
lastEmit = Date.now()
|
|
45
|
-
trailingTimer = null
|
|
46
|
-
}, ms - elapsed)
|
|
47
|
-
}
|
|
79
|
+
hasPending = true
|
|
80
|
+
const delay = elapsed >= ms ? ms : ms - elapsed
|
|
81
|
+
if (trailingTimer == null) trailingTimer = setTimeout(fireTrailing, delay)
|
|
48
82
|
}
|
|
49
83
|
})
|
|
50
84
|
|
|
85
|
+
const cancel = () => {
|
|
86
|
+
if (trailingTimer != null) {
|
|
87
|
+
clearTimeout(trailingTimer)
|
|
88
|
+
trailingTimer = null
|
|
89
|
+
}
|
|
90
|
+
hasPending = false
|
|
91
|
+
}
|
|
92
|
+
const flush = () => {
|
|
93
|
+
if (trailingTimer != null) {
|
|
94
|
+
clearTimeout(trailingTimer)
|
|
95
|
+
trailingTimer = null
|
|
96
|
+
}
|
|
97
|
+
if (hasPending) {
|
|
98
|
+
out.set(trailingValue)
|
|
99
|
+
lastEmit = now()
|
|
100
|
+
hasPending = false
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const dispose = () => {
|
|
105
|
+
cancel()
|
|
106
|
+
disposeEffect()
|
|
107
|
+
}
|
|
108
|
+
|
|
51
109
|
const sig = options?.signal
|
|
52
110
|
if (sig) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
clearTimeout(trailingTimer)
|
|
56
|
-
trailingTimer = null
|
|
57
|
-
}
|
|
58
|
-
dispose()
|
|
59
|
-
}
|
|
60
|
-
if (sig.aborted) stop()
|
|
61
|
-
else sig.addEventListener('abort', stop, { once: true })
|
|
111
|
+
if (sig.aborted) dispose()
|
|
112
|
+
else sig.addEventListener('abort', dispose, { once: true })
|
|
62
113
|
}
|
|
63
114
|
|
|
64
|
-
|
|
115
|
+
// Read-only projection + control surface; the old cast leaked `out.set`. (T2.7)
|
|
116
|
+
const handle = Object.assign(Object.create(readOnly(out)), {
|
|
117
|
+
cancel,
|
|
118
|
+
flush,
|
|
119
|
+
dispose,
|
|
120
|
+
}) as TimingSignal<T>
|
|
121
|
+
return handle
|
|
65
122
|
}
|
package/src/utils.ts
CHANGED
|
@@ -24,7 +24,7 @@ export function isAbortError(err: unknown): boolean {
|
|
|
24
24
|
export function abortableSleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
25
25
|
return new Promise((resolve, reject) => {
|
|
26
26
|
if (signal.aborted) {
|
|
27
|
-
reject(
|
|
27
|
+
reject(abortReason(signal))
|
|
28
28
|
return
|
|
29
29
|
}
|
|
30
30
|
const timer = setTimeout(() => {
|
|
@@ -34,8 +34,21 @@ export function abortableSleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
|
34
34
|
const onAbort = () => {
|
|
35
35
|
clearTimeout(timer)
|
|
36
36
|
signal.removeEventListener('abort', onAbort)
|
|
37
|
-
reject(
|
|
37
|
+
reject(abortReason(signal))
|
|
38
38
|
}
|
|
39
39
|
signal.addEventListener('abort', onAbort, { once: true })
|
|
40
40
|
})
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read the caller-supplied abort reason if present, falling back to the
|
|
45
|
+
* canonical `DOMException('Aborted', 'AbortError')`. `signal.reason` lets
|
|
46
|
+
* users propagate context (e.g. "supersededByLatestWins") through abort
|
|
47
|
+
* chains — discarding it would force every caller to wrap with custom
|
|
48
|
+
* cancellation tokens.
|
|
49
|
+
*/
|
|
50
|
+
function abortReason(signal: AbortSignal): unknown {
|
|
51
|
+
const reason: unknown = (signal as { reason?: unknown }).reason
|
|
52
|
+
if (reason !== undefined) return reason
|
|
53
|
+
return new DOMException('Aborted', 'AbortError')
|
|
54
|
+
}
|