@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.
Files changed (61) hide show
  1. package/dist/index.cjs +0 -0
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +109 -27
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +109 -27
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +0 -0
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/{root-DqWolle_.mjs → root-Byq-QYTp.mjs} +1478 -347
  10. package/dist/root-Byq-QYTp.mjs.map +1 -0
  11. package/dist/{root-lBp7qziQ.cjs → root-CPSL5kpR.cjs} +1483 -346
  12. package/dist/root-CPSL5kpR.cjs.map +1 -0
  13. package/dist/testing.cjs +5 -1
  14. package/dist/testing.cjs.map +1 -1
  15. package/dist/testing.d.cts +3 -2
  16. package/dist/testing.d.cts.map +1 -1
  17. package/dist/testing.d.mts +3 -2
  18. package/dist/testing.d.mts.map +1 -1
  19. package/dist/testing.mjs +5 -2
  20. package/dist/testing.mjs.map +1 -1
  21. package/dist/{types-BH1o6nYa.d.mts → types-Brp-4WaZ.d.mts} +244 -23
  22. package/dist/types-Brp-4WaZ.d.mts.map +1 -0
  23. package/dist/{types-C4Vtkxbn.d.cts → types-DzDIypPo.d.cts} +244 -23
  24. package/dist/types-DzDIypPo.d.cts.map +1 -0
  25. package/package.json +4 -1
  26. package/src/controller/instance.ts +360 -94
  27. package/src/controller/root.ts +58 -30
  28. package/src/controller/types.ts +66 -4
  29. package/src/emitter.ts +8 -2
  30. package/src/errors.ts +39 -3
  31. package/src/forms/field.ts +219 -25
  32. package/src/forms/form-types.ts +16 -3
  33. package/src/forms/form.ts +360 -38
  34. package/src/forms/index.ts +3 -1
  35. package/src/forms/types.ts +27 -1
  36. package/src/forms/validators.ts +45 -11
  37. package/src/index.ts +13 -7
  38. package/src/query/client.ts +237 -58
  39. package/src/query/define.ts +0 -0
  40. package/src/query/entry.ts +259 -15
  41. package/src/query/focus-online.ts +53 -11
  42. package/src/query/infinite.ts +351 -47
  43. package/src/query/keys.ts +33 -2
  44. package/src/query/local.ts +3 -0
  45. package/src/query/mutation.ts +27 -6
  46. package/src/query/plugin.ts +43 -4
  47. package/src/query/types.ts +76 -6
  48. package/src/query/use.ts +53 -10
  49. package/src/selection.ts +49 -12
  50. package/src/signals/readonly.ts +3 -0
  51. package/src/signals/runtime.ts +27 -0
  52. package/src/signals/types.ts +10 -0
  53. package/src/testing.ts +9 -0
  54. package/src/timing/debounced.ts +106 -23
  55. package/src/timing/index.ts +1 -1
  56. package/src/timing/throttled.ts +85 -28
  57. package/src/utils.ts +15 -2
  58. package/dist/root-DqWolle_.mjs.map +0 -1
  59. package/dist/root-lBp7qziQ.cjs.map +0 -1
  60. package/dist/types-BH1o6nYa.d.mts.map +0 -1
  61. package/dist/types-C4Vtkxbn.d.cts.map +0 -1
@@ -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
- * Pass `options.signal` (an `AbortSignal`) to tie the internal effect to a
9
- * lifecycle when the signal aborts the effect disposes, the pending timer
10
- * clears, and the subscriber chain on `source` drops. Without `signal`, the
11
- * effect lives as long as `source` does; pass a signal whenever the source
12
- * outlives the consumer.
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
- ): ReadSignal<T> {
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 dispose = effect(() => {
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
- timer = setTimeout(() => {
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
- timer = null
35
- }, ms)
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
- const stop = () => {
41
- if (timer != null) {
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
- return out
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
  }
@@ -1,2 +1,2 @@
1
- export { debounced } from './debounced'
1
+ export { debounced, type TimingSignal } from './debounced'
2
2
  export { throttled } from './throttled'
@@ -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
- * Pass `options.signal` to tie the internal effect to a lifecycle — when the
10
- * signal aborts the effect disposes and any pending trailing timer clears.
11
- * Without `signal`, the effect lives as long as `source` does.
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
- ): ReadSignal<T> {
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 dispose = effect(() => {
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 now = Date.now()
31
- const elapsed = now - lastEmit
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 = now
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
- if (trailingTimer == null) {
42
- trailingTimer = setTimeout(() => {
43
- out.set(trailingValue)
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
- const stop = () => {
54
- if (trailingTimer != null) {
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
- return out
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(new DOMException('Aborted', 'AbortError'))
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(new DOMException('Aborted', 'AbortError'))
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
+ }