@nisli/core 0.43.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/query.js ADDED
@@ -0,0 +1,231 @@
1
+ /**
2
+ * query.ts — Declarative async data loading with cache
3
+ *
4
+ * `query()` absorbs the loading/error/cache/refetch boilerplate that
5
+ * every data-fetching component would otherwise write manually.
6
+ *
7
+ * Inspired by TanStack Query / React Query, but built on signals.
8
+ *
9
+ * ```ts
10
+ * const tasks = query(
11
+ * () => ['tasks', scopeId.value],
12
+ * () => api.getTasks(scopeId.value),
13
+ * );
14
+ * tasks.data // Signal<Task[] | undefined>
15
+ * tasks.loading // Signal<boolean>
16
+ * tasks.error // Signal<Error | null>
17
+ * tasks.refetch() // manual refetch
18
+ * ```
19
+ */
20
+ import { signal, effect, computed } from './signal.js';
21
+ import { hasContext, getCurrentComponent } from './context.js';
22
+ import { inject } from './injector.js';
23
+ /**
24
+ * Global query cache. Managed as an auto-singleton via inject().
25
+ * Components use query() which accesses the client internally.
26
+ */
27
+ export class QueryClient {
28
+ cache = new Map();
29
+ inFlight = new Map();
30
+ /** Check if a cache entry is fresh */
31
+ isFresh(key, staleTime) {
32
+ const entry = this.cache.get(key);
33
+ if (!entry)
34
+ return false;
35
+ return Date.now() - entry.fetchedAt < staleTime;
36
+ }
37
+ /** Get cached data */
38
+ getCached(key) {
39
+ return this.cache.get(key)?.data;
40
+ }
41
+ /** Store data in cache */
42
+ set(key, data, staleTime) {
43
+ this.cache.set(key, { data, fetchedAt: Date.now(), staleTime });
44
+ }
45
+ /** Get in-flight promise for deduplication */
46
+ getInFlight(key) {
47
+ return this.inFlight.get(key);
48
+ }
49
+ /** Register an in-flight request */
50
+ setInFlight(key, promise) {
51
+ // Attach a catch handler to prevent unhandled rejection on the stored promise.
52
+ // Callers who await this promise get the original behavior from their own try/catch.
53
+ const handled = promise.catch(() => { });
54
+ this.inFlight.set(key, promise);
55
+ // Clean up when done
56
+ handled.finally(() => {
57
+ if (this.inFlight.get(key) === promise) {
58
+ this.inFlight.delete(key);
59
+ }
60
+ });
61
+ }
62
+ /**
63
+ * Invalidate cached entries whose key array starts with the given prefix elements.
64
+ * Returns the number of entries invalidated.
65
+ *
66
+ * invalidate(['tasks']) matches ['tasks'], ['tasks', '1'], ['tasks', '2', 'x']
67
+ */
68
+ invalidate(keyPrefix) {
69
+ let count = 0;
70
+ for (const [serialized] of this.cache) {
71
+ try {
72
+ const key = JSON.parse(serialized);
73
+ if (Array.isArray(key) && keyPrefix.every((v, i) => JSON.stringify(v) === JSON.stringify(key[i]))) {
74
+ this.cache.delete(serialized);
75
+ count++;
76
+ }
77
+ }
78
+ catch {
79
+ // Skip invalid keys
80
+ }
81
+ }
82
+ return count;
83
+ }
84
+ /** Prefetch and cache data */
85
+ async prefetch(key, fetcher, staleTime = 0) {
86
+ const serialized = serializeKey(key);
87
+ if (this.isFresh(serialized, staleTime))
88
+ return;
89
+ const data = await fetcher();
90
+ this.set(serialized, data, staleTime);
91
+ }
92
+ /** Clear all cache */
93
+ clear() {
94
+ this.cache.clear();
95
+ this.inFlight.clear();
96
+ }
97
+ }
98
+ // ── Key serialization ───────────────────────────────────────────────
99
+ function serializeKey(key) {
100
+ return JSON.stringify(key);
101
+ }
102
+ // ── query() — the main API ──────────────────────────────────────────
103
+ /**
104
+ * Declarative async data loading with caching and auto-refetch.
105
+ *
106
+ * @param keyFn - Returns a cache key array. Signal reads are auto-tracked for refetch.
107
+ * @param fetcher - Async function that fetches the data.
108
+ * @param options - Optional configuration (staleTime, retry, enabled, etc.)
109
+ */
110
+ export function query(keyFn, fetcher, options = {}) {
111
+ const { staleTime = 0, retry = 0, enabled, initialData, onSuccess, onError, } = options;
112
+ const data = signal(initialData);
113
+ const loading = signal(false);
114
+ const error = signal(null);
115
+ // Get or lazily create the QueryClient
116
+ let client;
117
+ try {
118
+ client = inject(QueryClient);
119
+ }
120
+ catch {
121
+ // If no DI context, create a local client
122
+ client = new QueryClient();
123
+ }
124
+ // Track the current fetch generation to handle race conditions
125
+ let fetchGeneration = 0;
126
+ let disposed = false;
127
+ const doFetch = async () => {
128
+ if (disposed)
129
+ return;
130
+ // Check enabled
131
+ if (enabled && !enabled())
132
+ return;
133
+ const key = keyFn();
134
+ const serialized = serializeKey(key);
135
+ const generation = ++fetchGeneration;
136
+ // Check cache freshness
137
+ if (staleTime > 0 && client.isFresh(serialized, staleTime)) {
138
+ const cached = client.getCached(serialized);
139
+ if (cached !== undefined) {
140
+ data.value = cached;
141
+ return;
142
+ }
143
+ }
144
+ // Check for in-flight deduplication
145
+ const existing = client.getInFlight(serialized);
146
+ if (existing) {
147
+ loading.value = true;
148
+ try {
149
+ const result = await existing;
150
+ if (generation === fetchGeneration && !disposed) {
151
+ data.value = result;
152
+ error.value = null;
153
+ }
154
+ }
155
+ catch (e) {
156
+ if (generation === fetchGeneration && !disposed) {
157
+ error.value = e instanceof Error ? e : new Error(String(e));
158
+ }
159
+ }
160
+ finally {
161
+ if (generation === fetchGeneration && !disposed) {
162
+ loading.value = false;
163
+ }
164
+ }
165
+ return;
166
+ }
167
+ loading.value = true;
168
+ error.value = null;
169
+ let lastError = null;
170
+ const attempts = retry + 1;
171
+ for (let attempt = 0; attempt < attempts; attempt++) {
172
+ if (disposed || generation !== fetchGeneration)
173
+ return;
174
+ const promise = fetcher();
175
+ // Prevent unhandled rejection on the shared in-flight reference
176
+ promise.catch(() => { });
177
+ client.setInFlight(serialized, promise);
178
+ try {
179
+ const result = await promise;
180
+ // Guard against stale responses
181
+ if (generation === fetchGeneration && !disposed) {
182
+ data.value = result;
183
+ error.value = null;
184
+ loading.value = false;
185
+ client.set(serialized, result, staleTime);
186
+ onSuccess?.(result);
187
+ }
188
+ return; // success
189
+ }
190
+ catch (e) {
191
+ lastError = e instanceof Error ? e : new Error(String(e));
192
+ // Continue to next retry attempt
193
+ }
194
+ }
195
+ // All retries exhausted
196
+ if (generation === fetchGeneration && !disposed) {
197
+ error.value = lastError;
198
+ loading.value = false;
199
+ if (lastError)
200
+ onError?.(lastError);
201
+ }
202
+ };
203
+ // Auto-fetch on mount and when dependencies change
204
+ const disposeEffect = effect(() => {
205
+ // Reading keyFn() inside the effect tracks signal dependencies
206
+ const _key = keyFn();
207
+ // Also track enabled()
208
+ if (enabled)
209
+ enabled();
210
+ // Schedule the async fetch — catch to prevent unhandled promise rejection
211
+ doFetch().catch(() => { });
212
+ });
213
+ // Register disposal if in component context
214
+ if (hasContext()) {
215
+ getCurrentComponent().addDisposer(() => {
216
+ disposed = true;
217
+ disposeEffect();
218
+ });
219
+ }
220
+ const refetch = () => {
221
+ fetchGeneration++; // invalidate any in-flight
222
+ doFetch().catch(() => { });
223
+ };
224
+ return {
225
+ data,
226
+ loading: loading,
227
+ error,
228
+ refetch,
229
+ };
230
+ }
231
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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"}
package/dist/ref.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * ref.ts — Element reference primitive for imperative DOM access
3
+ *
4
+ * `ref()` creates a container that receives a DOM element reference
5
+ * after the template is mounted. Use it for focus management, DOM
6
+ * measurement, scroll management, and third-party library init.
7
+ *
8
+ * ```ts
9
+ * const inputRef = ref<HTMLInputElement>();
10
+ *
11
+ * onMount(() => inputRef.current?.focus());
12
+ *
13
+ * return html`<input ${inputRef} @keydown.enter=${onSubmit} />`;
14
+ * ```
15
+ *
16
+ * The template engine detects `Ref` objects in expression positions
17
+ * (not inside attribute values) and assigns the nearest parent element.
18
+ */
19
+ export declare const REF_BRAND: unique symbol;
20
+ export interface Ref<T extends Element = Element> {
21
+ readonly [REF_BRAND]: true;
22
+ /** The DOM element, or null before mount / after unmount */
23
+ current: T | null;
24
+ }
25
+ /**
26
+ * Create a ref container for imperative DOM access.
27
+ *
28
+ * After the template is mounted, `ref.current` holds the element.
29
+ * After dispose, `ref.current` is set back to null.
30
+ */
31
+ export declare function ref<T extends Element = Element>(): Ref<T>;
32
+ /**
33
+ * Check if a value is a Ref.
34
+ * Used by the template engine to detect refs in expression slots.
35
+ */
36
+ export declare function isRef(value: unknown): value is Ref;
package/dist/ref.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * ref.ts — Element reference primitive for imperative DOM access
3
+ *
4
+ * `ref()` creates a container that receives a DOM element reference
5
+ * after the template is mounted. Use it for focus management, DOM
6
+ * measurement, scroll management, and third-party library init.
7
+ *
8
+ * ```ts
9
+ * const inputRef = ref<HTMLInputElement>();
10
+ *
11
+ * onMount(() => inputRef.current?.focus());
12
+ *
13
+ * return html`<input ${inputRef} @keydown.enter=${onSubmit} />`;
14
+ * ```
15
+ *
16
+ * The template engine detects `Ref` objects in expression positions
17
+ * (not inside attribute values) and assigns the nearest parent element.
18
+ */
19
+ // ── Brand for ref detection in templates ─────────────────────────────
20
+ export const REF_BRAND = Symbol.for('backlog.ref');
21
+ // ── Public API ──────────────────────────────────────────────────────
22
+ /**
23
+ * Create a ref container for imperative DOM access.
24
+ *
25
+ * After the template is mounted, `ref.current` holds the element.
26
+ * After dispose, `ref.current` is set back to null.
27
+ */
28
+ export function ref() {
29
+ return {
30
+ [REF_BRAND]: true,
31
+ current: null,
32
+ };
33
+ }
34
+ /**
35
+ * Check if a value is a Ref.
36
+ * Used by the template engine to detect refs in expression slots.
37
+ */
38
+ export function isRef(value) {
39
+ return (value !== null &&
40
+ typeof value === 'object' &&
41
+ REF_BRAND in value);
42
+ }
43
+ //# sourceMappingURL=ref.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ref.js","sourceRoot":"","sources":["../src/ref.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,wEAAwE;AAExE,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;AAUnD,uEAAuE;AAEvE;;;;;GAKG;AACH,MAAM,UAAU,GAAG;IACjB,OAAO;QACL,CAAC,SAAS,CAAC,EAAE,IAAa;QAC1B,OAAO,EAAE,IAAI;KACd,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,KAAK,CAAC,KAAc;IAClC,OAAO,CACL,KAAK,KAAK,IAAI;QACd,OAAO,KAAK,KAAK,QAAQ;QACzB,SAAS,IAAK,KAAiC,CAChD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * signal.ts — Reactive primitives: signal(), computed(), effect()
3
+ *
4
+ * Push-pull hybrid model:
5
+ * - Writes push "dirty" flags up the dependency graph
6
+ * - Reads pull fresh values lazily (computed only recalculates when read)
7
+ * - Multiple synchronous writes coalesce into one microtask flush
8
+ *
9
+ * Dependency tracking is automatic: reading a signal inside a computed
10
+ * or effect registers it as a dependency. Dependencies are re-tracked
11
+ * on every execution (dynamic/conditional tracking).
12
+ */
13
+ /**
14
+ * Register the context hook. Called by component.ts at module init.
15
+ * The hook returns an `addDisposer` function if inside setup context, null otherwise.
16
+ */
17
+ export declare function setContextHook(hook: () => ((fn: () => void) => void) | null): void;
18
+ export declare const SIGNAL_BRAND: unique symbol;
19
+ /** Readable reactive container. */
20
+ export interface ReadonlySignal<T> {
21
+ readonly [SIGNAL_BRAND]: true;
22
+ readonly value: T;
23
+ /** Subscribe to value changes. Returns unsubscribe function. */
24
+ subscribe(fn: (value: T) => void): () => void;
25
+ }
26
+ /** Read-write reactive container. */
27
+ export interface Signal<T> extends ReadonlySignal<T> {
28
+ value: T;
29
+ }
30
+ /**
31
+ * Create a writable reactive signal.
32
+ *
33
+ * ```ts
34
+ * const count = signal(0);
35
+ * count.value; // read (auto-tracks in reactive contexts)
36
+ * count.value = 5; // write (notifies dependents)
37
+ * ```
38
+ */
39
+ export declare function signal<T>(initialValue: T): Signal<T>;
40
+ /**
41
+ * Create a derived, lazy, cached computed signal.
42
+ * Re-evaluates only when dependencies change AND the value is read.
43
+ *
44
+ * ```ts
45
+ * const doubled = computed(() => count.value * 2);
46
+ * doubled.value; // lazy evaluation
47
+ * ```
48
+ */
49
+ export declare function computed<T>(fn: () => T): ReadonlySignal<T>;
50
+ /**
51
+ * Create a side-effect that re-runs when its dependencies change.
52
+ * Returns a dispose function to stop the effect.
53
+ *
54
+ * The effect function may return a cleanup callback that runs
55
+ * before each re-execution and on disposal.
56
+ *
57
+ * ```ts
58
+ * const dispose = effect(() => {
59
+ * console.log('count is', count.value);
60
+ * return () => { // cleanup
61
+ * console.log('cleaning up');
62
+ * };
63
+ * });
64
+ * dispose(); // stop the effect
65
+ * ```
66
+ */
67
+ export declare function effect(fn: () => void | (() => void)): () => void;
68
+ /**
69
+ * Check if a value is a signal (writable or computed).
70
+ * Used by the template engine to detect signals in expression slots.
71
+ */
72
+ export declare function isSignal(value: unknown): value is ReadonlySignal<unknown>;
73
+ /**
74
+ * Run a function with activeObserver set to null, preventing any
75
+ * signal reads inside `fn` from being tracked by an outer effect.
76
+ *
77
+ * Used by component.ts to isolate connectedCallback: child component
78
+ * setup signal reads must not leak into a parent effect's dependency
79
+ * set. See ADR 0008 Gap 1.
80
+ */
81
+ export declare function untrack(fn: () => void): void;
82
+ /**
83
+ * Synchronously execute all pending effects.
84
+ *
85
+ * In normal usage, effects run automatically on the next microtask.
86
+ * Use flush() only when you need effects to have executed before
87
+ * the next line — e.g., imperative DOM measurement after state change.
88
+ *
89
+ * flush() is idempotent: calling it with no pending effects is a no-op.
90
+ */
91
+ export declare function flush(): void;
92
+ /** Backward compat alias for tests. */
93
+ export { flush as flushEffects };