@streetui/state 1.0.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.
@@ -0,0 +1,217 @@
1
+ /**
2
+ * StreetUI reactive signals — framework-owned reactivity, no external libraries.
3
+ *
4
+ * Architecture:
5
+ * Signal<T> — writable, holds a value, notifies on change
6
+ * DerivedSignal<T> — read-only, lazily computed from other signals
7
+ * effect() — side-effect that re-runs when dependencies change
8
+ * batch() — run multiple updates before notifying
9
+ */
10
+ type Subscriber<T> = (value: T) => void;
11
+ type Unsubscribe = () => void;
12
+ /**
13
+ * Any reactive source that can have downstream consumers attached.
14
+ * Both Signal and DerivedSignal implement this.
15
+ */
16
+ interface ReactiveSource<T> {
17
+ get(): T;
18
+ peek(): T;
19
+ subscribe(fn: Subscriber<T>): Unsubscribe;
20
+ /** Internal: remove a downstream consumer. */
21
+ _removeConsumer(consumer: ReactiveConsumer): void;
22
+ }
23
+ /**
24
+ * A downstream consumer (DerivedSignal or Effect) that can be invalidated
25
+ * and can register itself as depending on a source.
26
+ */
27
+ interface ReactiveConsumer {
28
+ _invalidate(): void;
29
+ /** Internal: called by a source to register a dependency. */
30
+ _addSource(src: ReactiveSource<unknown>): void;
31
+ }
32
+ interface ReadonlySignal<T> {
33
+ get(): T;
34
+ peek(): T;
35
+ subscribe(fn: Subscriber<T>): Unsubscribe;
36
+ }
37
+ declare class Signal<T> implements ReactiveSource<T>, ReadonlySignal<T> {
38
+ protected _value: T;
39
+ private readonly _subscribers;
40
+ private readonly _consumers;
41
+ constructor(initial: T);
42
+ get(): T;
43
+ peek(): T;
44
+ set(value: T): void;
45
+ update(fn: (current: T) => T): void;
46
+ subscribe(fn: Subscriber<T>): Unsubscribe;
47
+ _removeConsumer(consumer: ReactiveConsumer): void;
48
+ /**
49
+ * Called by the batch machinery after the batch has completed.
50
+ * Notifies subscribers with the final coalesced value.
51
+ */
52
+ _flushBatch(value: unknown): void;
53
+ private _flush;
54
+ /**
55
+ * @internal DevTools inspection only. The number of live observers
56
+ * (direct subscribers plus derived/effect consumers). Read-only; never
57
+ * mutates reactive state.
58
+ */
59
+ _observerCount(): number;
60
+ }
61
+ declare class DerivedSignal<T> implements ReactiveSource<T>, ReactiveConsumer, ReadonlySignal<T> {
62
+ private _value;
63
+ private _dirty;
64
+ private _disposed;
65
+ private readonly _fn;
66
+ private readonly _subscribers;
67
+ /** All upstream sources this derived currently reads from. */
68
+ private readonly _sources;
69
+ /** Downstream consumers that depend on this derived. */
70
+ private readonly _consumers;
71
+ constructor(fn: () => T);
72
+ get(): T;
73
+ peek(): T;
74
+ subscribe(fn: Subscriber<T>): Unsubscribe;
75
+ _addSource(src: ReactiveSource<unknown>): void;
76
+ _removeConsumer(consumer: ReactiveConsumer): void;
77
+ _invalidate(): void;
78
+ private _recompute;
79
+ dispose(): void;
80
+ /**
81
+ * @internal DevTools inspection only. Live observers (subscribers plus
82
+ * downstream consumers). Read-only.
83
+ */
84
+ _observerCount(): number;
85
+ }
86
+ declare function signal<T>(initial: T): Signal<T>;
87
+ declare function derived<T>(fn: () => T): DerivedSignal<T>;
88
+ declare function effect(fn: () => void | (() => void)): Unsubscribe;
89
+ /**
90
+ * Run multiple signal updates as an atomic batch.
91
+ *
92
+ * Within the callback, calls to signal.set() are deferred — each signal
93
+ * accumulates its latest value. When the outermost batch() returns,
94
+ * each modified signal fires its subscribers exactly once with the final
95
+ * value. Nested batch() calls are supported; the flush only runs when the
96
+ * outermost batch exits.
97
+ *
98
+ * Example:
99
+ * batch(() => {
100
+ * count.set(1);
101
+ * count.set(2);
102
+ * count.set(3);
103
+ * });
104
+ * // subscribers see count = 3 exactly once
105
+ */
106
+ declare function batch(fn: () => void): void;
107
+ /** True when inside a batch() call. Useful for advanced scheduling integration. */
108
+ declare function isBatching(): boolean;
109
+ /** Whether a signal is writable (`signal()`) or computed (`derived()`). */
110
+ type SignalKind = 'writable' | 'derived';
111
+ /** Classify a reactive value as writable or derived. */
112
+ declare function signalKind(source: ReadonlySignal<unknown>): SignalKind;
113
+ /**
114
+ * The number of live observers on a signal — direct subscribers plus derived
115
+ * or effect consumers — or `undefined` if the source does not expose the count.
116
+ * Read-only; safe for DevTools. Never mutates reactive state.
117
+ */
118
+ declare function observerCount(source: ReadonlySignal<unknown>): number | undefined;
119
+
120
+ /**
121
+ * A simple reactive store built on top of signals.
122
+ * Useful for structured state with multiple fields.
123
+ */
124
+
125
+ type StoreState = Record<string, unknown>;
126
+ declare class Store<T extends StoreState> {
127
+ private readonly _signals;
128
+ constructor(initial: T);
129
+ get<K extends keyof T>(key: K): T[K];
130
+ set<K extends keyof T>(key: K, value: T[K]): void;
131
+ signal<K extends keyof T>(key: K): Signal<T[K]>;
132
+ subscribe<K extends keyof T>(key: K, fn: Subscriber<T[K]>): Unsubscribe;
133
+ getSnapshot(): T;
134
+ }
135
+ declare function createStore<T extends StoreState>(initial: T): Store<T>;
136
+
137
+ /**
138
+ * StreetUI async resources — framework-native asynchronous data.
139
+ *
140
+ * A `resource` wraps a Promise-returning loader and exposes its lifecycle as
141
+ * ordinary StreetUI signals (status / data / error), so it composes with
142
+ * `derived`, `effect`, `when()`, `listOf` and the renderer with no second
143
+ * reactive system.
144
+ *
145
+ * State machine:
146
+ *
147
+ * idle ──(load)──▶ loading ──(resolve)──▶ success
148
+ * │
149
+ * └────(reject)──────▶ error
150
+ *
151
+ * Refetch keeps the previously-loaded `data` visible while `status` is
152
+ * `'loading'` again (see `isRefetching`) — there is no separate `'refetching'`
153
+ * status; it is expressed through `status === 'loading'` with `data` still set.
154
+ *
155
+ * The resource is transport-agnostic: the loader is any function returning a
156
+ * value or a Promise. When it accepts the provided `AbortSignal`, in-flight
157
+ * work is cancelled on `dispose()` or when a newer request supersedes it.
158
+ */
159
+
160
+ type ResourceStatus = 'idle' | 'loading' | 'success' | 'error';
161
+ /** Context handed to the loader; carries an AbortSignal for cancellation. */
162
+ interface ResourceLoaderContext {
163
+ readonly signal: AbortSignal;
164
+ }
165
+ /** Any value-or-Promise producing function. Receives an abort-aware context. */
166
+ type ResourceLoader<T> = (ctx: ResourceLoaderContext) => Promise<T> | T;
167
+ interface ResourceOptions<T = unknown> {
168
+ /** Load immediately on creation. Defaults to `true`. When `false`, stays `idle` until `refetch()`. */
169
+ readonly immediate?: boolean;
170
+ /**
171
+ * Explicit reactive dependencies. When any listed signal changes, the
172
+ * resource refetches. Dependencies are explicit (not auto-tracked from the
173
+ * loader body) so there is no risk of an accidental infinite refetch loop.
174
+ */
175
+ readonly watch?: ReadonlyArray<ReadonlySignal<unknown>>;
176
+ /**
177
+ * Optional teardown registrar (e.g. a route's `ctx.onCleanup`). When given,
178
+ * the resource registers its own `dispose` so it is cleaned up automatically
179
+ * when its owner is removed.
180
+ */
181
+ readonly onCleanup?: (fn: () => void) => void;
182
+ /**
183
+ * Server-provided initial value for hydration. When present the resource
184
+ * starts in `'success'` with this data already visible, and the initial
185
+ * auto-load is skipped (so the client does not refetch data the server
186
+ * already resolved). This is the client half of SSR resource transfer; the
187
+ * server side awaits `refetch()` before serializing. Set `immediate: true`
188
+ * explicitly to force a client refetch anyway.
189
+ */
190
+ readonly initialData?: T;
191
+ /** Server-provided initial error for hydration (mirrors `initialData`). */
192
+ readonly initialError?: unknown;
193
+ /**
194
+ * Explicit initial status override. Rarely needed — inferred as `'success'`
195
+ * from `initialData` or `'error'` from `initialError`.
196
+ */
197
+ readonly initialStatus?: ResourceStatus;
198
+ }
199
+ interface Resource<T> {
200
+ /** Reactive lifecycle status. */
201
+ readonly status: ReadonlySignal<ResourceStatus>;
202
+ /** The last successfully-loaded value, or `undefined` before first success. */
203
+ readonly data: ReadonlySignal<T | undefined>;
204
+ /** The most recent error, or `undefined` when there is none. Typed `unknown` — never `any`. */
205
+ readonly error: ReadonlySignal<unknown>;
206
+ /** Convenience: `status === 'loading'`. */
207
+ readonly loading: ReadonlySignal<boolean>;
208
+ /** Convenience: loading while previously-loaded data is still present (a refetch). */
209
+ readonly isRefetching: ReadonlySignal<boolean>;
210
+ /** Trigger a new request. Resolves when the request settles (or is superseded). */
211
+ refetch(): Promise<void>;
212
+ /** Cancel in-flight work, drop watchers, and ignore any late results. Idempotent. */
213
+ dispose(): void;
214
+ }
215
+ declare function resource<T>(loader: ResourceLoader<T>, options?: ResourceOptions<T>): Resource<T>;
216
+
217
+ export { DerivedSignal, type ReactiveConsumer, type ReactiveSource, type ReadonlySignal, type Resource, type ResourceLoader, type ResourceLoaderContext, type ResourceOptions, type ResourceStatus, Signal, type SignalKind, Store, type StoreState, type Subscriber, type Unsubscribe, batch, createStore, derived, effect, isBatching, observerCount, resource, signal, signalKind };
@@ -0,0 +1,217 @@
1
+ /**
2
+ * StreetUI reactive signals — framework-owned reactivity, no external libraries.
3
+ *
4
+ * Architecture:
5
+ * Signal<T> — writable, holds a value, notifies on change
6
+ * DerivedSignal<T> — read-only, lazily computed from other signals
7
+ * effect() — side-effect that re-runs when dependencies change
8
+ * batch() — run multiple updates before notifying
9
+ */
10
+ type Subscriber<T> = (value: T) => void;
11
+ type Unsubscribe = () => void;
12
+ /**
13
+ * Any reactive source that can have downstream consumers attached.
14
+ * Both Signal and DerivedSignal implement this.
15
+ */
16
+ interface ReactiveSource<T> {
17
+ get(): T;
18
+ peek(): T;
19
+ subscribe(fn: Subscriber<T>): Unsubscribe;
20
+ /** Internal: remove a downstream consumer. */
21
+ _removeConsumer(consumer: ReactiveConsumer): void;
22
+ }
23
+ /**
24
+ * A downstream consumer (DerivedSignal or Effect) that can be invalidated
25
+ * and can register itself as depending on a source.
26
+ */
27
+ interface ReactiveConsumer {
28
+ _invalidate(): void;
29
+ /** Internal: called by a source to register a dependency. */
30
+ _addSource(src: ReactiveSource<unknown>): void;
31
+ }
32
+ interface ReadonlySignal<T> {
33
+ get(): T;
34
+ peek(): T;
35
+ subscribe(fn: Subscriber<T>): Unsubscribe;
36
+ }
37
+ declare class Signal<T> implements ReactiveSource<T>, ReadonlySignal<T> {
38
+ protected _value: T;
39
+ private readonly _subscribers;
40
+ private readonly _consumers;
41
+ constructor(initial: T);
42
+ get(): T;
43
+ peek(): T;
44
+ set(value: T): void;
45
+ update(fn: (current: T) => T): void;
46
+ subscribe(fn: Subscriber<T>): Unsubscribe;
47
+ _removeConsumer(consumer: ReactiveConsumer): void;
48
+ /**
49
+ * Called by the batch machinery after the batch has completed.
50
+ * Notifies subscribers with the final coalesced value.
51
+ */
52
+ _flushBatch(value: unknown): void;
53
+ private _flush;
54
+ /**
55
+ * @internal DevTools inspection only. The number of live observers
56
+ * (direct subscribers plus derived/effect consumers). Read-only; never
57
+ * mutates reactive state.
58
+ */
59
+ _observerCount(): number;
60
+ }
61
+ declare class DerivedSignal<T> implements ReactiveSource<T>, ReactiveConsumer, ReadonlySignal<T> {
62
+ private _value;
63
+ private _dirty;
64
+ private _disposed;
65
+ private readonly _fn;
66
+ private readonly _subscribers;
67
+ /** All upstream sources this derived currently reads from. */
68
+ private readonly _sources;
69
+ /** Downstream consumers that depend on this derived. */
70
+ private readonly _consumers;
71
+ constructor(fn: () => T);
72
+ get(): T;
73
+ peek(): T;
74
+ subscribe(fn: Subscriber<T>): Unsubscribe;
75
+ _addSource(src: ReactiveSource<unknown>): void;
76
+ _removeConsumer(consumer: ReactiveConsumer): void;
77
+ _invalidate(): void;
78
+ private _recompute;
79
+ dispose(): void;
80
+ /**
81
+ * @internal DevTools inspection only. Live observers (subscribers plus
82
+ * downstream consumers). Read-only.
83
+ */
84
+ _observerCount(): number;
85
+ }
86
+ declare function signal<T>(initial: T): Signal<T>;
87
+ declare function derived<T>(fn: () => T): DerivedSignal<T>;
88
+ declare function effect(fn: () => void | (() => void)): Unsubscribe;
89
+ /**
90
+ * Run multiple signal updates as an atomic batch.
91
+ *
92
+ * Within the callback, calls to signal.set() are deferred — each signal
93
+ * accumulates its latest value. When the outermost batch() returns,
94
+ * each modified signal fires its subscribers exactly once with the final
95
+ * value. Nested batch() calls are supported; the flush only runs when the
96
+ * outermost batch exits.
97
+ *
98
+ * Example:
99
+ * batch(() => {
100
+ * count.set(1);
101
+ * count.set(2);
102
+ * count.set(3);
103
+ * });
104
+ * // subscribers see count = 3 exactly once
105
+ */
106
+ declare function batch(fn: () => void): void;
107
+ /** True when inside a batch() call. Useful for advanced scheduling integration. */
108
+ declare function isBatching(): boolean;
109
+ /** Whether a signal is writable (`signal()`) or computed (`derived()`). */
110
+ type SignalKind = 'writable' | 'derived';
111
+ /** Classify a reactive value as writable or derived. */
112
+ declare function signalKind(source: ReadonlySignal<unknown>): SignalKind;
113
+ /**
114
+ * The number of live observers on a signal — direct subscribers plus derived
115
+ * or effect consumers — or `undefined` if the source does not expose the count.
116
+ * Read-only; safe for DevTools. Never mutates reactive state.
117
+ */
118
+ declare function observerCount(source: ReadonlySignal<unknown>): number | undefined;
119
+
120
+ /**
121
+ * A simple reactive store built on top of signals.
122
+ * Useful for structured state with multiple fields.
123
+ */
124
+
125
+ type StoreState = Record<string, unknown>;
126
+ declare class Store<T extends StoreState> {
127
+ private readonly _signals;
128
+ constructor(initial: T);
129
+ get<K extends keyof T>(key: K): T[K];
130
+ set<K extends keyof T>(key: K, value: T[K]): void;
131
+ signal<K extends keyof T>(key: K): Signal<T[K]>;
132
+ subscribe<K extends keyof T>(key: K, fn: Subscriber<T[K]>): Unsubscribe;
133
+ getSnapshot(): T;
134
+ }
135
+ declare function createStore<T extends StoreState>(initial: T): Store<T>;
136
+
137
+ /**
138
+ * StreetUI async resources — framework-native asynchronous data.
139
+ *
140
+ * A `resource` wraps a Promise-returning loader and exposes its lifecycle as
141
+ * ordinary StreetUI signals (status / data / error), so it composes with
142
+ * `derived`, `effect`, `when()`, `listOf` and the renderer with no second
143
+ * reactive system.
144
+ *
145
+ * State machine:
146
+ *
147
+ * idle ──(load)──▶ loading ──(resolve)──▶ success
148
+ * │
149
+ * └────(reject)──────▶ error
150
+ *
151
+ * Refetch keeps the previously-loaded `data` visible while `status` is
152
+ * `'loading'` again (see `isRefetching`) — there is no separate `'refetching'`
153
+ * status; it is expressed through `status === 'loading'` with `data` still set.
154
+ *
155
+ * The resource is transport-agnostic: the loader is any function returning a
156
+ * value or a Promise. When it accepts the provided `AbortSignal`, in-flight
157
+ * work is cancelled on `dispose()` or when a newer request supersedes it.
158
+ */
159
+
160
+ type ResourceStatus = 'idle' | 'loading' | 'success' | 'error';
161
+ /** Context handed to the loader; carries an AbortSignal for cancellation. */
162
+ interface ResourceLoaderContext {
163
+ readonly signal: AbortSignal;
164
+ }
165
+ /** Any value-or-Promise producing function. Receives an abort-aware context. */
166
+ type ResourceLoader<T> = (ctx: ResourceLoaderContext) => Promise<T> | T;
167
+ interface ResourceOptions<T = unknown> {
168
+ /** Load immediately on creation. Defaults to `true`. When `false`, stays `idle` until `refetch()`. */
169
+ readonly immediate?: boolean;
170
+ /**
171
+ * Explicit reactive dependencies. When any listed signal changes, the
172
+ * resource refetches. Dependencies are explicit (not auto-tracked from the
173
+ * loader body) so there is no risk of an accidental infinite refetch loop.
174
+ */
175
+ readonly watch?: ReadonlyArray<ReadonlySignal<unknown>>;
176
+ /**
177
+ * Optional teardown registrar (e.g. a route's `ctx.onCleanup`). When given,
178
+ * the resource registers its own `dispose` so it is cleaned up automatically
179
+ * when its owner is removed.
180
+ */
181
+ readonly onCleanup?: (fn: () => void) => void;
182
+ /**
183
+ * Server-provided initial value for hydration. When present the resource
184
+ * starts in `'success'` with this data already visible, and the initial
185
+ * auto-load is skipped (so the client does not refetch data the server
186
+ * already resolved). This is the client half of SSR resource transfer; the
187
+ * server side awaits `refetch()` before serializing. Set `immediate: true`
188
+ * explicitly to force a client refetch anyway.
189
+ */
190
+ readonly initialData?: T;
191
+ /** Server-provided initial error for hydration (mirrors `initialData`). */
192
+ readonly initialError?: unknown;
193
+ /**
194
+ * Explicit initial status override. Rarely needed — inferred as `'success'`
195
+ * from `initialData` or `'error'` from `initialError`.
196
+ */
197
+ readonly initialStatus?: ResourceStatus;
198
+ }
199
+ interface Resource<T> {
200
+ /** Reactive lifecycle status. */
201
+ readonly status: ReadonlySignal<ResourceStatus>;
202
+ /** The last successfully-loaded value, or `undefined` before first success. */
203
+ readonly data: ReadonlySignal<T | undefined>;
204
+ /** The most recent error, or `undefined` when there is none. Typed `unknown` — never `any`. */
205
+ readonly error: ReadonlySignal<unknown>;
206
+ /** Convenience: `status === 'loading'`. */
207
+ readonly loading: ReadonlySignal<boolean>;
208
+ /** Convenience: loading while previously-loaded data is still present (a refetch). */
209
+ readonly isRefetching: ReadonlySignal<boolean>;
210
+ /** Trigger a new request. Resolves when the request settles (or is superseded). */
211
+ refetch(): Promise<void>;
212
+ /** Cancel in-flight work, drop watchers, and ignore any late results. Idempotent. */
213
+ dispose(): void;
214
+ }
215
+ declare function resource<T>(loader: ResourceLoader<T>, options?: ResourceOptions<T>): Resource<T>;
216
+
217
+ export { DerivedSignal, type ReactiveConsumer, type ReactiveSource, type ReadonlySignal, type Resource, type ResourceLoader, type ResourceLoaderContext, type ResourceOptions, type ResourceStatus, Signal, type SignalKind, Store, type StoreState, type Subscriber, type Unsubscribe, batch, createStore, derived, effect, isBatching, observerCount, resource, signal, signalKind };