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 +77 -0
- package/package.json +39 -0
- package/src/index.d.ts +49 -0
- package/src/index.js +80 -0
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 };
|