@kontsedal/olas-core 0.0.1-rc.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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -0
  3. package/dist/index.cjs +363 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +178 -0
  6. package/dist/index.d.cts.map +1 -0
  7. package/dist/index.d.mts +178 -0
  8. package/dist/index.d.mts.map +1 -0
  9. package/dist/index.mjs +339 -0
  10. package/dist/index.mjs.map +1 -0
  11. package/dist/root-BImHnGj1.mjs +3270 -0
  12. package/dist/root-BImHnGj1.mjs.map +1 -0
  13. package/dist/root-Bazp5_Ik.cjs +3347 -0
  14. package/dist/root-Bazp5_Ik.cjs.map +1 -0
  15. package/dist/testing.cjs +81 -0
  16. package/dist/testing.cjs.map +1 -0
  17. package/dist/testing.d.cts +56 -0
  18. package/dist/testing.d.cts.map +1 -0
  19. package/dist/testing.d.mts +56 -0
  20. package/dist/testing.d.mts.map +1 -0
  21. package/dist/testing.mjs +78 -0
  22. package/dist/testing.mjs.map +1 -0
  23. package/dist/types-CAMgqCMz.d.mts +816 -0
  24. package/dist/types-CAMgqCMz.d.mts.map +1 -0
  25. package/dist/types-emq_lZd7.d.cts +816 -0
  26. package/dist/types-emq_lZd7.d.cts.map +1 -0
  27. package/package.json +47 -0
  28. package/src/__dev__.d.ts +8 -0
  29. package/src/controller/define.ts +50 -0
  30. package/src/controller/index.ts +12 -0
  31. package/src/controller/instance.ts +499 -0
  32. package/src/controller/root.ts +160 -0
  33. package/src/controller/types.ts +195 -0
  34. package/src/devtools.ts +0 -0
  35. package/src/emitter.ts +79 -0
  36. package/src/errors.ts +49 -0
  37. package/src/forms/field.ts +303 -0
  38. package/src/forms/form-types.ts +130 -0
  39. package/src/forms/form.ts +640 -0
  40. package/src/forms/index.ts +2 -0
  41. package/src/forms/types.ts +1 -0
  42. package/src/forms/validators.ts +70 -0
  43. package/src/index.ts +89 -0
  44. package/src/query/client.ts +934 -0
  45. package/src/query/define.ts +154 -0
  46. package/src/query/entry.ts +322 -0
  47. package/src/query/focus-online.ts +73 -0
  48. package/src/query/index.ts +3 -0
  49. package/src/query/infinite.ts +462 -0
  50. package/src/query/keys.ts +33 -0
  51. package/src/query/local.ts +113 -0
  52. package/src/query/mutation.ts +384 -0
  53. package/src/query/plugin.ts +135 -0
  54. package/src/query/types.ts +168 -0
  55. package/src/query/use.ts +321 -0
  56. package/src/scope.ts +42 -0
  57. package/src/selection.ts +146 -0
  58. package/src/signals/index.ts +3 -0
  59. package/src/signals/readonly.ts +22 -0
  60. package/src/signals/runtime.ts +115 -0
  61. package/src/signals/types.ts +31 -0
  62. package/src/testing.ts +142 -0
  63. package/src/timing/debounced.ts +32 -0
  64. package/src/timing/index.ts +2 -0
  65. package/src/timing/throttled.ts +46 -0
  66. package/src/utils.ts +13 -0
@@ -0,0 +1,321 @@
1
+ import { computed, effect, type Signal, signal, untracked } from '../signals'
2
+ import type { ReadSignal } from '../signals/types'
3
+ import type { ClientEntry, InfiniteClientEntry, QueryClient } from './client'
4
+ import type { InfiniteQuery, InfiniteQuerySpec, InfiniteQuerySubscription } from './infinite'
5
+ import type { AsyncStatus, Query, QuerySpec, QuerySubscription, UseOptions } from './types'
6
+
7
+ type QueryInternal<Args extends unknown[], T> = Query<Args, T> & {
8
+ readonly __spec: QuerySpec<Args, T>
9
+ }
10
+
11
+ class SubscriptionImpl<T> implements QuerySubscription<T> {
12
+ private readonly current$: Signal<ClientEntry<T> | null> = signal(null)
13
+ private readonly previousData$: Signal<T | undefined> = signal(undefined)
14
+
15
+ readonly data: ReadSignal<T | undefined>
16
+ readonly error: ReadSignal<unknown | undefined>
17
+ readonly status: ReadSignal<AsyncStatus>
18
+ readonly isLoading: ReadSignal<boolean>
19
+ readonly isFetching: ReadSignal<boolean>
20
+ readonly isStale: ReadSignal<boolean>
21
+ readonly lastUpdatedAt: ReadSignal<number | undefined>
22
+ readonly hasPendingMutations: ReadSignal<boolean>
23
+
24
+ constructor(private readonly keepPreviousData: boolean) {
25
+ this.data = computed(() => {
26
+ const cur = this.current$.value
27
+ const curData = cur?.entry.data.value
28
+ if (curData !== undefined) return curData
29
+ if (keepPreviousData) return this.previousData$.value
30
+ return undefined
31
+ })
32
+ this.error = computed(() => this.current$.value?.entry.error.value)
33
+ this.status = computed<AsyncStatus>(() => this.current$.value?.entry.status.value ?? 'idle')
34
+ this.isLoading = computed(() => {
35
+ const cur = this.current$.value
36
+ if (!cur) return false
37
+ if (keepPreviousData && this.previousData$.value !== undefined) return false
38
+ return cur.entry.isLoading.value
39
+ })
40
+ this.isFetching = computed(() => this.current$.value?.entry.isFetching.value ?? false)
41
+ this.isStale = computed(() => this.current$.value?.entry.isStale.value ?? true)
42
+ this.lastUpdatedAt = computed(() => this.current$.value?.entry.lastUpdatedAt.value)
43
+ this.hasPendingMutations = computed(
44
+ () => this.current$.value?.entry.hasPendingMutations.value ?? false,
45
+ )
46
+ }
47
+
48
+ attach(entry: ClientEntry<T>): void {
49
+ const prev = this.current$.peek()
50
+ if (prev === entry) return
51
+ if (prev && this.keepPreviousData) {
52
+ const prevData = prev.entry.data.peek()
53
+ if (prevData !== undefined) this.previousData$.set(prevData)
54
+ }
55
+ this.current$.set(entry)
56
+ }
57
+
58
+ detach(): void {
59
+ this.current$.set(null)
60
+ }
61
+
62
+ refetch = (): Promise<T> => {
63
+ const cur = this.current$.peek()
64
+ if (!cur) return Promise.reject(new Error('[olas] no active subscription'))
65
+ return cur.entry.refetch()
66
+ }
67
+
68
+ reset = (): void => {
69
+ this.current$.peek()?.entry.reset()
70
+ }
71
+
72
+ firstValue = (): Promise<T> => {
73
+ const cur = this.current$.peek()
74
+ if (!cur) return Promise.reject(new Error('[olas] no active subscription'))
75
+ return cur.entry.firstValue()
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Build a subscription + the effect that keeps it bound to the right entry.
81
+ * The controller container wires the disposer into the lifecycle.
82
+ */
83
+ export function createUse<Args extends unknown[], T>(
84
+ client: QueryClient,
85
+ query: Query<Args, T>,
86
+ keyOrOptions?: (() => Args) | UseOptions<Args>,
87
+ ): { subscription: QuerySubscription<T>; dispose: () => void } {
88
+ const internal = query as unknown as QueryInternal<Args, T>
89
+ const spec = internal.__spec
90
+ const keepPreviousData = spec.keepPreviousData ?? false
91
+
92
+ const keyFn = typeof keyOrOptions === 'function' ? keyOrOptions : keyOrOptions?.key
93
+ const enabledFn =
94
+ typeof keyOrOptions === 'object' && keyOrOptions !== null ? keyOrOptions.enabled : undefined
95
+
96
+ const sub = new SubscriptionImpl<T>(keepPreviousData)
97
+ let currentEntry: ClientEntry<T> | null = null
98
+
99
+ const effectDispose = effect(() => {
100
+ const isEnabled = enabledFn ? enabledFn() : true
101
+ if (!isEnabled) {
102
+ untracked(() => {
103
+ if (currentEntry) {
104
+ currentEntry.release()
105
+ currentEntry = null
106
+ }
107
+ sub.detach()
108
+ })
109
+ return
110
+ }
111
+
112
+ const args = (keyFn ? keyFn() : ([] as unknown as Args)) as Args
113
+
114
+ untracked(() => {
115
+ const entry = client.bindEntry<Args, T>(query, args)
116
+ if (currentEntry === entry) return
117
+ if (currentEntry) currentEntry.release()
118
+ entry.acquire()
119
+ currentEntry = entry
120
+ sub.attach(entry)
121
+
122
+ const status = entry.entry.status.peek()
123
+ const fetching = entry.entry.isFetching.peek()
124
+ if (!fetching && (status === 'idle' || entry.entry.isStaleNow() || status === 'error')) {
125
+ entry.entry.startFetch().catch(() => {
126
+ /* error captured on entry */
127
+ })
128
+ }
129
+ })
130
+ })
131
+
132
+ const dispose = () => {
133
+ effectDispose()
134
+ if (currentEntry) {
135
+ currentEntry.release()
136
+ currentEntry = null
137
+ }
138
+ sub.detach()
139
+ }
140
+
141
+ return { subscription: sub, dispose }
142
+ }
143
+
144
+ type InfiniteQueryInternal<Args extends unknown[], TPage, TItem> = InfiniteQuery<
145
+ Args,
146
+ TPage,
147
+ TItem
148
+ > & {
149
+ readonly __spec: InfiniteQuerySpec<Args, any, TPage, TItem>
150
+ }
151
+
152
+ class InfiniteSubscriptionImpl<TPage, TItem> implements InfiniteQuerySubscription<TPage, TItem> {
153
+ private readonly current$: Signal<InfiniteClientEntry<TPage, TItem, unknown> | null> =
154
+ signal(null)
155
+ private readonly previousPages$: Signal<TPage[] | undefined> = signal(undefined)
156
+
157
+ readonly data: ReadSignal<TPage[] | undefined>
158
+ readonly pages: ReadSignal<TPage[]>
159
+ readonly flat: ReadSignal<TItem[]>
160
+ readonly error: ReadSignal<unknown | undefined>
161
+ readonly status: ReadSignal<AsyncStatus>
162
+ readonly isLoading: ReadSignal<boolean>
163
+ readonly isFetching: ReadSignal<boolean>
164
+ readonly isStale: ReadSignal<boolean>
165
+ readonly lastUpdatedAt: ReadSignal<number | undefined>
166
+ readonly hasPendingMutations: ReadSignal<boolean>
167
+ readonly hasNextPage: ReadSignal<boolean>
168
+ readonly hasPreviousPage: ReadSignal<boolean>
169
+ readonly isFetchingNextPage: ReadSignal<boolean>
170
+ readonly isFetchingPreviousPage: ReadSignal<boolean>
171
+
172
+ constructor(private readonly keepPreviousData: boolean) {
173
+ this.pages = computed(() => {
174
+ const cur = this.current$.value
175
+ const ps = cur?.entry.pages.value
176
+ if (ps && ps.length > 0) return ps
177
+ if (keepPreviousData) return this.previousPages$.value ?? []
178
+ return ps ?? []
179
+ })
180
+ this.data = computed(() => {
181
+ const cur = this.current$.value
182
+ const ps = cur?.entry.pages.value
183
+ if (ps && ps.length > 0) return ps
184
+ if (keepPreviousData) {
185
+ const prev = this.previousPages$.value
186
+ if (prev && prev.length > 0) return prev
187
+ }
188
+ return undefined
189
+ })
190
+ this.flat = computed(() => this.current$.value?.entry.flat.value ?? [])
191
+ this.error = computed(() => this.current$.value?.entry.error.value)
192
+ this.status = computed<AsyncStatus>(() => this.current$.value?.entry.status.value ?? 'idle')
193
+ this.isLoading = computed(() => {
194
+ const cur = this.current$.value
195
+ if (!cur) return false
196
+ if (keepPreviousData) {
197
+ const prev = this.previousPages$.value
198
+ if (prev && prev.length > 0) return false
199
+ }
200
+ return cur.entry.isLoading.value
201
+ })
202
+ this.isFetching = computed(() => this.current$.value?.entry.isFetching.value ?? false)
203
+ this.isStale = computed(() => this.current$.value?.entry.isStale.value ?? true)
204
+ this.lastUpdatedAt = computed(() => this.current$.value?.entry.lastUpdatedAt.value)
205
+ this.hasPendingMutations = computed(
206
+ () => this.current$.value?.entry.hasPendingMutations.value ?? false,
207
+ )
208
+ this.hasNextPage = computed(() => this.current$.value?.entry.hasNextPage.value ?? false)
209
+ this.hasPreviousPage = computed(() => this.current$.value?.entry.hasPreviousPage.value ?? false)
210
+ this.isFetchingNextPage = computed(
211
+ () => this.current$.value?.entry.isFetchingNextPage.value ?? false,
212
+ )
213
+ this.isFetchingPreviousPage = computed(
214
+ () => this.current$.value?.entry.isFetchingPreviousPage.value ?? false,
215
+ )
216
+ }
217
+
218
+ attach(entry: InfiniteClientEntry<TPage, TItem, unknown>): void {
219
+ const prev = this.current$.peek()
220
+ if (prev === entry) return
221
+ if (prev && this.keepPreviousData) {
222
+ const prevPages = prev.entry.pages.peek()
223
+ if (prevPages.length > 0) this.previousPages$.set(prevPages)
224
+ }
225
+ this.current$.set(entry)
226
+ }
227
+
228
+ detach(): void {
229
+ this.current$.set(null)
230
+ }
231
+
232
+ refetch = (): Promise<TPage[]> => {
233
+ const cur = this.current$.peek()
234
+ if (!cur) return Promise.reject(new Error('[olas] no active subscription'))
235
+ return cur.entry.refetch().then(() => cur.entry.pages.peek())
236
+ }
237
+
238
+ reset = (): void => {
239
+ this.current$.peek()?.entry.reset()
240
+ }
241
+
242
+ firstValue = (): Promise<TPage[]> => {
243
+ const cur = this.current$.peek()
244
+ if (!cur) return Promise.reject(new Error('[olas] no active subscription'))
245
+ return cur.entry.firstValue()
246
+ }
247
+
248
+ fetchNextPage = (): Promise<void> => {
249
+ const cur = this.current$.peek()
250
+ if (!cur) return Promise.resolve()
251
+ return cur.entry.fetchNextPage()
252
+ }
253
+
254
+ fetchPreviousPage = (): Promise<void> => {
255
+ const cur = this.current$.peek()
256
+ if (!cur) return Promise.resolve()
257
+ return cur.entry.fetchPreviousPage()
258
+ }
259
+ }
260
+
261
+ export function createInfiniteUse<Args extends unknown[], TPage, TItem>(
262
+ client: QueryClient,
263
+ query: InfiniteQuery<Args, TPage, TItem>,
264
+ keyOrOptions?: (() => Args) | UseOptions<Args>,
265
+ ): {
266
+ subscription: InfiniteQuerySubscription<TPage, TItem>
267
+ dispose: () => void
268
+ } {
269
+ const spec = (query as unknown as InfiniteQueryInternal<Args, TPage, TItem>).__spec
270
+ const keepPreviousData = spec.keepPreviousData ?? false
271
+ const keyFn = typeof keyOrOptions === 'function' ? keyOrOptions : keyOrOptions?.key
272
+ const enabledFn =
273
+ typeof keyOrOptions === 'object' && keyOrOptions !== null ? keyOrOptions.enabled : undefined
274
+
275
+ const sub = new InfiniteSubscriptionImpl<TPage, TItem>(keepPreviousData)
276
+ let currentEntry: InfiniteClientEntry<TPage, TItem, unknown> | null = null
277
+
278
+ const effectDispose = effect(() => {
279
+ const isEnabled = enabledFn ? enabledFn() : true
280
+ if (!isEnabled) {
281
+ untracked(() => {
282
+ if (currentEntry) {
283
+ currentEntry.release()
284
+ currentEntry = null
285
+ }
286
+ sub.detach()
287
+ })
288
+ return
289
+ }
290
+
291
+ const args = (keyFn ? keyFn() : ([] as unknown as Args)) as Args
292
+
293
+ untracked(() => {
294
+ const entry = client.bindInfiniteEntry<Args, TPage, TItem>(query, args)
295
+ if (currentEntry === entry) return
296
+ if (currentEntry) currentEntry.release()
297
+ entry.acquire()
298
+ currentEntry = entry
299
+ sub.attach(entry)
300
+
301
+ const status = entry.entry.status.peek()
302
+ const fetching = entry.entry.isFetching.peek()
303
+ if (!fetching && (status === 'idle' || entry.entry.isStaleNow() || status === 'error')) {
304
+ entry.entry.startFetch().catch(() => {
305
+ /* error captured on entry */
306
+ })
307
+ }
308
+ })
309
+ })
310
+
311
+ const dispose = () => {
312
+ effectDispose()
313
+ if (currentEntry) {
314
+ currentEntry.release()
315
+ currentEntry = null
316
+ }
317
+ sub.detach()
318
+ }
319
+
320
+ return { subscription: sub, dispose }
321
+ }
package/src/scope.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Typed cross-tree data slot. Provided by an ancestor via `ctx.provide(scope, value)`
3
+ * and consumed anywhere in its subtree via `ctx.inject(scope)`. Defined at module
4
+ * scope so the identity is stable across calls. See spec §10.3.
5
+ */
6
+ export type Scope<T> = {
7
+ readonly __olas: 'scope'
8
+ /** Per-scope identity; matches across `provide` / `inject`. */
9
+ readonly __id: symbol
10
+ /** Optional human-readable name (used in error messages). */
11
+ readonly name?: string
12
+ /** Default value used when no provider exists; `undefined` if none was set. */
13
+ readonly default?: T
14
+ /** True iff `defineScope` was called with a `default` (even `default: undefined`). */
15
+ readonly hasDefault: boolean
16
+ // Phantom for inference — typed `T` is preserved through the scope's lifetime.
17
+ readonly __t?: T
18
+ }
19
+
20
+ export type ScopeOptions<T> = {
21
+ default?: T
22
+ name?: string
23
+ }
24
+
25
+ /**
26
+ * Create a scope. The returned value is the typed handle passed to
27
+ * `ctx.provide(scope, value)` and `ctx.inject(scope)`. Identity is keyed by
28
+ * an internal symbol so two `defineScope()` calls — even with identical
29
+ * options — yield distinct scopes.
30
+ */
31
+ export function defineScope<T>(options?: ScopeOptions<T>): Scope<T> {
32
+ const hasDefault = options !== undefined && 'default' in options
33
+ const name = options?.name
34
+ const scope: Scope<T> = {
35
+ __olas: 'scope',
36
+ __id: Symbol(name ?? 'scope'),
37
+ hasDefault,
38
+ ...(name !== undefined ? { name } : {}),
39
+ ...(hasDefault ? { default: options?.default as T } : {}),
40
+ }
41
+ return scope
42
+ }
@@ -0,0 +1,146 @@
1
+ import { computed, signal } from './signals'
2
+ import { readOnly } from './signals/readonly'
3
+ import type { ReadSignal } from './signals/types'
4
+
5
+ /**
6
+ * Multi-select state for tables / lists with bulk actions (spec §17.5).
7
+ *
8
+ * Plain function — not bound to `ctx`. Place it in a controller's closure so
9
+ * it dies with the closure. The phantom `T` parameter brands the selection by
10
+ * item type; IDs are always strings.
11
+ */
12
+ // biome-ignore lint/correctness/noUnusedVariables: phantom branding param (spec §17.5)
13
+ export type Selection<T = unknown> = {
14
+ selectedIds: ReadSignal<ReadonlySet<string>>
15
+ size: ReadSignal<number>
16
+ isSelected(id: string): ReadSignal<boolean>
17
+
18
+ select(id: string): void
19
+ deselect(id: string): void
20
+ toggle(id: string): void
21
+ clear(): void
22
+ selectAll(ids: readonly string[]): void
23
+
24
+ handleClick(
25
+ id: string,
26
+ mods: { shift?: boolean; meta?: boolean },
27
+ ordered: readonly string[],
28
+ ): void
29
+ }
30
+
31
+ /**
32
+ * Create a `Selection<T>`. Optional `initial` seeds the selected set.
33
+ *
34
+ * `handleClick` encapsulates the standard click semantics:
35
+ * - plain click → select only `id` (anchor moves to `id`)
36
+ * - meta-click → toggle `id` (anchor moves to `id` on add)
37
+ * - shift-click → range from anchor to `id` along `ordered` (anchor sticks,
38
+ * so subsequent shift-clicks extend from the same origin)
39
+ *
40
+ * Spec §16.5 / §17.5.
41
+ */
42
+ export function selection<T = unknown>(options?: { initial?: readonly string[] }): Selection<T> {
43
+ const ids = signal<ReadonlySet<string>>(new Set(options?.initial))
44
+ let anchor: string | null = options?.initial?.length
45
+ ? (options.initial[options.initial.length - 1] ?? null)
46
+ : null
47
+ // Snapshot of the selection just before the first shift-click of a run.
48
+ // Subsequent shift-clicks re-compute the range against this snapshot so the
49
+ // user can shrink or grow the range. Reset on any non-shift click.
50
+ let preShiftSelection: ReadonlySet<string> | null = null
51
+
52
+ const size = computed(() => ids.value.size)
53
+
54
+ const isSelected = (id: string): ReadSignal<boolean> => computed(() => ids.value.has(id))
55
+
56
+ const select = (id: string): void => {
57
+ const prev = ids.peek()
58
+ if (!prev.has(id)) {
59
+ const next = new Set(prev)
60
+ next.add(id)
61
+ ids.set(next)
62
+ }
63
+ anchor = id
64
+ }
65
+
66
+ const deselect = (id: string): void => {
67
+ const prev = ids.peek()
68
+ if (!prev.has(id)) return
69
+ const next = new Set(prev)
70
+ next.delete(id)
71
+ ids.set(next)
72
+ }
73
+
74
+ const toggle = (id: string): void => {
75
+ const prev = ids.peek()
76
+ const next = new Set(prev)
77
+ if (prev.has(id)) {
78
+ next.delete(id)
79
+ } else {
80
+ next.add(id)
81
+ anchor = id
82
+ }
83
+ ids.set(next)
84
+ }
85
+
86
+ const clear = (): void => {
87
+ if (ids.peek().size === 0) {
88
+ anchor = null
89
+ return
90
+ }
91
+ ids.set(new Set())
92
+ anchor = null
93
+ }
94
+
95
+ const selectAll = (incoming: readonly string[]): void => {
96
+ ids.set(new Set(incoming))
97
+ anchor = incoming.length > 0 ? (incoming[incoming.length - 1] ?? null) : null
98
+ }
99
+
100
+ const handleClick = (
101
+ id: string,
102
+ mods: { shift?: boolean; meta?: boolean },
103
+ ordered: readonly string[],
104
+ ): void => {
105
+ if (mods.shift && anchor !== null) {
106
+ const anchorIdx = ordered.indexOf(anchor)
107
+ const targetIdx = ordered.indexOf(id)
108
+ if (anchorIdx === -1 || targetIdx === -1) {
109
+ // anchor or target not visible — fall back to plain select
110
+ ids.set(new Set([id]))
111
+ anchor = id
112
+ preShiftSelection = null
113
+ return
114
+ }
115
+ if (preShiftSelection === null) {
116
+ preShiftSelection = ids.peek()
117
+ }
118
+ const [lo, hi] = anchorIdx < targetIdx ? [anchorIdx, targetIdx] : [targetIdx, anchorIdx]
119
+ const next = new Set(preShiftSelection)
120
+ for (const k of ordered.slice(lo, hi + 1)) next.add(k)
121
+ ids.set(next)
122
+ // Anchor stays — subsequent shift-clicks extend from the same origin.
123
+ return
124
+ }
125
+ // Any non-shift click ends the shift run.
126
+ preShiftSelection = null
127
+ if (mods.meta) {
128
+ toggle(id)
129
+ return
130
+ }
131
+ ids.set(new Set([id]))
132
+ anchor = id
133
+ }
134
+
135
+ return {
136
+ selectedIds: readOnly(ids),
137
+ size,
138
+ isSelected,
139
+ select,
140
+ deselect,
141
+ toggle,
142
+ clear,
143
+ selectAll,
144
+ handleClick,
145
+ }
146
+ }
@@ -0,0 +1,3 @@
1
+ export { readOnly } from './readonly'
2
+ export { batch, computed, effect, signal, untracked } from './runtime'
3
+ export type { Computed, ReadSignal, Signal } from './types'
@@ -0,0 +1,22 @@
1
+ import type { ReadSignal } from './types'
2
+
3
+ /**
4
+ * Project a Signal (or any object with a reactive `value` + `peek` + `subscribe`)
5
+ * as a `ReadSignal`. The returned object does not expose `set` / `update` /
6
+ * settable `value`, so it can be returned from APIs without callers mutating it.
7
+ *
8
+ * Internal helper — not exported from the package's public surface.
9
+ */
10
+ export function readOnly<T>(source: ReadSignal<T>): ReadSignal<T> {
11
+ return {
12
+ get value() {
13
+ return source.value
14
+ },
15
+ peek() {
16
+ return source.peek()
17
+ },
18
+ subscribe(handler) {
19
+ return source.subscribe(handler)
20
+ },
21
+ }
22
+ }
@@ -0,0 +1,115 @@
1
+ import {
2
+ batch as _batch,
3
+ computed as _computed,
4
+ effect as _effect,
5
+ signal as _signal,
6
+ untracked as _untracked,
7
+ type ReadonlySignal as PreactReadonlySignal,
8
+ type Signal as PreactSignal,
9
+ } from '@preact/signals-core'
10
+
11
+ import type { Computed, Signal } from './types'
12
+
13
+ class SignalImpl<T> implements Signal<T> {
14
+ private readonly inner: PreactSignal<T>
15
+
16
+ constructor(initial: T) {
17
+ this.inner = _signal<T>(initial)
18
+ }
19
+
20
+ get value(): T {
21
+ return this.inner.value
22
+ }
23
+
24
+ set value(next: T) {
25
+ this.inner.value = next
26
+ }
27
+
28
+ peek(): T {
29
+ return this.inner.peek()
30
+ }
31
+
32
+ subscribe(handler: (value: T) => void): () => void {
33
+ return this.inner.subscribe(handler)
34
+ }
35
+
36
+ set(value: T): void {
37
+ this.inner.value = value
38
+ }
39
+
40
+ update(fn: (prev: T) => T): void {
41
+ this.inner.value = fn(this.inner.peek())
42
+ }
43
+ }
44
+
45
+ class ComputedImpl<T> implements Computed<T> {
46
+ private readonly inner: PreactReadonlySignal<T>
47
+
48
+ constructor(fn: () => T) {
49
+ this.inner = _computed<T>(fn)
50
+ }
51
+
52
+ get value(): T {
53
+ return this.inner.value
54
+ }
55
+
56
+ peek(): T {
57
+ return this.inner.peek()
58
+ }
59
+
60
+ subscribe(handler: (value: T) => void): () => void {
61
+ return this.inner.subscribe(handler)
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Create a writable `Signal<T>`. Reads track the current auto-tracking scope
67
+ * (effect / computed); writes notify all subscribers (deduped via `Object.is`).
68
+ *
69
+ * Spec §20.1. For a single-pass non-tracked read use `signal.peek()`.
70
+ */
71
+ export function signal<T>(initial: T): Signal<T> {
72
+ return new SignalImpl(initial)
73
+ }
74
+
75
+ /**
76
+ * Create a `Computed<T>` — a read-only derived signal. The provided `fn` is
77
+ * re-evaluated whenever a signal it read during its last run changes; the
78
+ * resulting value is cached until then.
79
+ *
80
+ * Spec §20.1. The graph is glitch-free: a `computed` re-runs at most once per
81
+ * batched-write cycle.
82
+ */
83
+ export function computed<T>(fn: () => T): Computed<T> {
84
+ return new ComputedImpl(fn)
85
+ }
86
+
87
+ /**
88
+ * Run `fn` immediately and again whenever any signal it reads changes. If
89
+ * `fn` returns a function, that function is called as a cleanup before the
90
+ * next re-run and on dispose.
91
+ *
92
+ * Returns a `dispose` function. Inside a controller use `ctx.effect(...)`
93
+ * instead — that variant is auto-disposed with the controller.
94
+ */
95
+ export function effect(fn: () => void | (() => void)): () => void {
96
+ return _effect(fn)
97
+ }
98
+
99
+ /**
100
+ * Batch synchronous signal writes so subscribers see one notification at the
101
+ * end of the batch rather than one per write. Returns whatever `fn` returns.
102
+ */
103
+ export function batch<T>(fn: () => T): T {
104
+ return _batch(fn)
105
+ }
106
+
107
+ /**
108
+ * Run `fn` with auto-tracking suppressed — signals read inside don't become
109
+ * dependencies of the surrounding `computed` / `effect`. Useful for "read
110
+ * these signals once to log them" or for snapshotting state inside an effect
111
+ * without subscribing to it. For a single-signal peek, prefer `signal.peek()`.
112
+ */
113
+ export function untracked<T>(fn: () => T): T {
114
+ return _untracked(fn)
115
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Read-only reactive value. Reading `.value` inside a tracking scope
3
+ * (`computed` / `effect`) registers a dependency; `peek()` reads without
4
+ * tracking; `subscribe(handler)` fires `handler` immediately with the current
5
+ * value and on every change until the returned unsubscribe is called.
6
+ */
7
+ export type ReadSignal<T> = {
8
+ readonly value: T
9
+ /** Read the current value without registering a dependency. */
10
+ peek(): T
11
+ /**
12
+ * Subscribe to value changes. The handler is called synchronously with the
13
+ * current value on subscribe and on every change thereafter. Returns the
14
+ * unsubscribe function.
15
+ */
16
+ subscribe(handler: (value: T) => void): () => void
17
+ }
18
+
19
+ /**
20
+ * Writable reactive value. `value` is assignable; `set(value)` is the
21
+ * functional equivalent; `update(fn)` reads (peek) and writes the result of
22
+ * `fn(previous)`.
23
+ */
24
+ export type Signal<T> = ReadSignal<T> & {
25
+ value: T
26
+ set(value: T): void
27
+ update(fn: (prev: T) => T): void
28
+ }
29
+
30
+ /** A read-only derived signal — alias of `ReadSignal<T>`. */
31
+ export type Computed<T> = ReadSignal<T>