spark-html-query 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/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # ⚡ spark-html-query
2
+
3
+ Declarative async data for [spark-html](https://www.npmjs.com/package/spark-html)
4
+ — a **self-fetching reactive store**. One dependency (`spark-html`), built
5
+ entirely on its `store()`.
6
+
7
+ A `query` runs an async function and exposes the result as reactive store state.
8
+ Any component reads it with the same `useStore` it already knows, and re-renders
9
+ as the request settles — no `onMount`, no manual `loading` flags, no `fetch`
10
+ boilerplate.
11
+
12
+ ```js
13
+ import { query } from 'spark-html-query';
14
+
15
+ query('user', () => fetch('/api/user').then((r) => r.json()));
16
+ ```
17
+
18
+ ```html
19
+ <!-- any component -->
20
+ <script>const user = useStore('user');</script>
21
+
22
+ <p :hidden="!user.loading">Loading…</p>
23
+ <p :hidden="!user.error">Failed: {user.error.message}</p>
24
+ <h1 :hidden="user.loading">{user.data?.name}</h1>
25
+ <button onclick="{user.refetch}">Reload</button>
26
+ ```
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ npm install spark-html-query
32
+ ```
33
+
34
+ ## State
35
+
36
+ `useStore(name)` returns a reactive object:
37
+
38
+ | Key | Meaning |
39
+ |-----|---------|
40
+ | `data` | The latest resolved value (or `initialData` / `null` before the first). |
41
+ | `error` | The last rejection, or `null`. |
42
+ | `loading` | `true` until the first successful result (no `data` yet). |
43
+ | `fetching` | `true` during **any** in-flight fetch, including a refetch over existing data. |
44
+ | `refetch()` | Re-run the fetcher. A newer call supersedes an older in-flight one. |
45
+ | `mutate(next)` | Set `data` directly without fetching (optimistic update). Value or `(prev) => next`. |
46
+ | `stop()` | Stop the `refetchInterval` poller, if any. |
47
+
48
+ ## Options
49
+
50
+ ```js
51
+ query('feed', fetchFeed, {
52
+ initialData: [], // seed data; skips the initial `loading` state
53
+ refetchInterval: 30000, // poll every 30s
54
+ lazy: true, // with initialData: wait for the first refetch()
55
+ });
56
+ ```
57
+
58
+ ## Pairs with `derived`
59
+
60
+ Shape a query into exactly what a component needs, memoized — the view updates
61
+ as the request settles:
62
+
63
+ ```js
64
+ import { query } from 'spark-html-query';
65
+ import { derived } from 'spark-html';
66
+
67
+ query('todos', fetchTodos);
68
+ derived('todoStats', ['todos'], (q) => ({
69
+ total: q.data?.length ?? 0,
70
+ done: q.data?.filter((t) => t.done).length ?? 0,
71
+ loading: q.loading,
72
+ }));
73
+ ```
74
+
75
+ > `loading` vs `fetching`: show a **skeleton** on `loading` (first load, no data
76
+ > yet) and a subtle **spinner** on `fetching` (background refresh that keeps the
77
+ > stale data visible). That's the stale-while-revalidate pattern, declaratively.
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "spark-html-query",
3
+ "version": "0.1.0",
4
+ "description": "Declarative async data for spark-html — a self-fetching reactive store with loading/error/refetch. One dependency.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "homepage": "https://wilkinnovo.github.io/spark",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.d.ts",
12
+ "default": "./src/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "test": "node test/query.js"
17
+ },
18
+ "files": [
19
+ "src"
20
+ ],
21
+ "peerDependencies": {
22
+ "spark-html": ">=0.21.0"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/wilkinnovo/spark.git",
27
+ "directory": "packages/spark-html-query"
28
+ },
29
+ "keywords": [
30
+ "spark-html",
31
+ "query",
32
+ "fetch",
33
+ "async",
34
+ "data",
35
+ "store",
36
+ "swr"
37
+ ],
38
+ "license": "MIT"
39
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * spark-html-query — declarative async data for spark-html.
3
+ *
4
+ * `query(name, fetcher, options?)` creates a self-fetching store. Read it with
5
+ * `useStore(name)` from any component; it re-renders as the request settles.
6
+ */
7
+
8
+ export interface QueryState<T> {
9
+ /** The latest resolved value, or `initialData` / `null` before the first one. */
10
+ data: T | null;
11
+ /** The last rejection, or `null`. */
12
+ error: unknown;
13
+ /** True until the first successful result (no `data` yet). */
14
+ loading: boolean;
15
+ /** True during ANY in-flight fetch, including a refetch over existing data. */
16
+ fetching: boolean;
17
+ /** Re-run the fetcher. A newer call supersedes an older in-flight one. */
18
+ refetch: () => Promise<void>;
19
+ /** Set `data` directly without fetching (optimistic update). Accepts a value or updater. */
20
+ mutate: (next: T | ((prev: T | null) => T)) => void;
21
+ /** Stop the `refetchInterval` poller, if any. */
22
+ stop: () => void;
23
+ }
24
+
25
+ export interface QueryOptions<T> {
26
+ /** Seed `data` before the first fetch (skips the initial `loading` state). */
27
+ initialData?: T | null;
28
+ /** Poll the fetcher on this interval (ms). */
29
+ refetchInterval?: number;
30
+ /** With `initialData`, don't fetch on creation — wait for the first `refetch()`. */
31
+ lazy?: boolean;
32
+ }
33
+
34
+ /**
35
+ * Create a named, self-fetching reactive store.
36
+ *
37
+ * ```ts
38
+ * query('user', () => fetch('/api/user').then((r) => r.json()));
39
+ * // component: const user = useStore('user'); → {user.data?.name}
40
+ * ```
41
+ */
42
+ export function query<T = unknown>(
43
+ name: string,
44
+ fetcher: () => Promise<T> | T,
45
+ options?: QueryOptions<T>,
46
+ ): QueryState<T>;
47
+
48
+ declare const _default: { query: typeof query };
49
+ export default _default;
package/src/index.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * spark-html-query — declarative async data for spark-html.
3
+ *
4
+ * A `query` is a named store that fetches itself. It runs an async function and
5
+ * exposes the result as reactive store state — `{ data, error, loading,
6
+ * fetching, refetch, mutate, stop }` — so any component reads it with the same
7
+ * `useStore` it already knows, and re-renders as the request settles. Built
8
+ * entirely on `spark-html`'s `store()`: zero extra runtime, one dependency.
9
+ *
10
+ * import { query } from 'spark-html-query';
11
+ *
12
+ * query('user', () => fetch('/api/user').then((r) => r.json()), {
13
+ * refetchInterval: 30000, // optional: poll every 30s
14
+ * initialData: null, // optional: seed before the first fetch
15
+ * });
16
+ *
17
+ * // any component:
18
+ * // <script>const user = useStore('user');</script>
19
+ * // <p :hidden="!user.loading">Loading…</p>
20
+ * // <p :hidden="!user.error">Failed: {user.error.message}</p>
21
+ * // <h1 :hidden="user.loading">{user.data?.name}</h1>
22
+ * // <button onclick="{user.refetch}">Reload</button>
23
+ *
24
+ * Pairs with `derived()` — derive a shaped view from a query store, and the view
25
+ * updates as the request settles, memoized.
26
+ */
27
+ import { store } from 'spark-html';
28
+
29
+ export function query(name, fetcher, options = {}) {
30
+ const s = store(name, {
31
+ data: options.initialData ?? null,
32
+ error: null,
33
+ loading: options.initialData == null, // loading until the first result
34
+ fetching: false, // true during ANY fetch (incl. refetch)
35
+ });
36
+ // Tag for tooling (spark-html-devtools) — non-enumerable, never in state dumps.
37
+ try {
38
+ Object.defineProperty(s, Symbol.for('spark.storeKind'), { value: 'query', configurable: true });
39
+ } catch { /* ignore */ }
40
+
41
+ let runId = 0;
42
+ async function run() {
43
+ const id = ++runId;
44
+ s.fetching = true;
45
+ if (s.data == null) s.loading = true;
46
+ try {
47
+ const data = await fetcher();
48
+ if (id !== runId) return; // superseded by a newer refetch — drop
49
+ s.data = data;
50
+ s.error = null;
51
+ } catch (e) {
52
+ if (id !== runId) return;
53
+ s.error = e;
54
+ } finally {
55
+ if (id === runId) { s.loading = false; s.fetching = false; }
56
+ }
57
+ }
58
+
59
+ // Imperatively set data without a fetch (optimistic updates / cache writes).
60
+ function mutate(next) {
61
+ s.data = typeof next === 'function' ? next(s.data) : next;
62
+ s.error = null;
63
+ }
64
+
65
+ s.refetch = run;
66
+ s.mutate = mutate;
67
+
68
+ let timer = null;
69
+ if (options.refetchInterval > 0 && typeof setInterval === 'function') {
70
+ timer = setInterval(run, options.refetchInterval);
71
+ }
72
+ s.stop = () => { if (timer) { clearInterval(timer); timer = null; } };
73
+
74
+ // Kick off the first fetch (unless seeded with initialData and told to wait).
75
+ if (!(options.initialData != null && options.lazy)) run();
76
+
77
+ return s;
78
+ }
79
+
80
+ export default { query };