@marianmeres/fetch-store 3.1.0 → 3.2.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/AGENTS.md +79 -0
- package/API.md +243 -0
- package/CHANGELOG.md +120 -0
- package/README.md +26 -3
- package/dist/fetch-store.d.ts +3 -3
- package/dist/fetch-store.js +3 -1
- package/dist/fetch-stream-store.d.ts +4 -4
- package/dist/fetch-stream-store.js +28 -15
- package/dist/mod.d.ts +3 -3
- package/dist/mod.js +1 -1
- package/dist/types.d.ts +3 -1
- package/dist/types.js +5 -0
- package/package.json +70 -19
package/AGENTS.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @marianmeres/fetch-store — Agent Guide
|
|
2
|
+
|
|
3
|
+
> Reactive store for async fetch operations. Tracks loading/error/success state, supports caching, polling, abort, deduplication, and streaming (SSE/WebSocket). Svelte-compatible but framework-agnostic.
|
|
4
|
+
|
|
5
|
+
## Quick Reference
|
|
6
|
+
|
|
7
|
+
- **Stack**: TypeScript (erasable-only), Node LTS (`.node-version`), pnpm, `node:test`, Biome
|
|
8
|
+
- **Runtime**: universal — web standards only (browser, Node, any ES2022 runtime); developed
|
|
9
|
+
and tested on Node LTS
|
|
10
|
+
- **Install**: `pnpm install` | **Test**: `pnpm test` | **Check all**: `pnpm run ci` |
|
|
11
|
+
**Build**: `pnpm run build` | **Release**: `pnpm run rp` (patch) / `pnpm run rpm` (minor),
|
|
12
|
+
human only
|
|
13
|
+
- **Registries**: npm (built `dist/`, from `package.json`) and JSR (the `.ts` source, from
|
|
14
|
+
`jsr.json`). `package.json` holds the version; `jsr.json` has none.
|
|
15
|
+
- **Dependencies**: [@marianmeres/store](https://www.npmjs.com/package/@marianmeres/store)
|
|
16
|
+
(which brings [@marianmeres/pubsub](https://www.npmjs.com/package/@marianmeres/pubsub)):
|
|
17
|
+
an npm dependency in `package.json` and a `jsr:` import in `jsr.json`; keep both ranges
|
|
18
|
+
in step
|
|
19
|
+
|
|
20
|
+
## Project Structure
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
src/
|
|
24
|
+
├── mod.ts — Export barrel (types + both stores)
|
|
25
|
+
├── types.ts — All public type definitions
|
|
26
|
+
├── fetch-store.ts — createFetchStore (request-style: fetch, fetchOnce, polling)
|
|
27
|
+
└── fetch-stream-store.ts — createFetchStreamStore (push-style: SSE/WebSocket)
|
|
28
|
+
tests/
|
|
29
|
+
├── fetch-store.test.ts — Combined node:test suite for both stores
|
|
30
|
+
└── consumer/ — A downstream project's type check of the built package
|
|
31
|
+
dist/ — `pnpm run build` output; gitignored; the only code npm gets
|
|
32
|
+
package.json — Manifest of record: version, exports, scripts
|
|
33
|
+
tsconfig.json — Typecheck view (src + tests, noEmit)
|
|
34
|
+
tsconfig.build.json — Build: src -> dist (ESM + .d.ts)
|
|
35
|
+
jsr.json — JSR config: exports, store import, publish set (no version)
|
|
36
|
+
biome.json — Format + lint
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Critical Conventions
|
|
40
|
+
|
|
41
|
+
1. **Two stores, separate files** — `createFetchStore` (request/response) and `createFetchStreamStore` (push-based events). Do not merge; they have distinct lifecycles.
|
|
42
|
+
2. **Types live in `types.ts`** — Both stores re-export their public types from this single file. When adding a type, add it there and re-export in the implementation file for BC.
|
|
43
|
+
3. **Generation token pattern** — `fetch-store.ts` uses `_generation` to invalidate late-resolving fetches after `reset()`. Any new async path that writes to stores must honor it.
|
|
44
|
+
4. **Silent ≠ invisible** — `fetchSilent` skips `isFetching` but still updates `lastFetchStart`, `lastFetchEnd`, `successCounter`, so `fetchOnce`-style caching works.
|
|
45
|
+
5. **Two separate inflight promises** — `_inflightPromise` and `_inflightSilentPromise` are tracked independently; `fetchOnce` joins whichever is active.
|
|
46
|
+
6. **Tabs, 4-wide, 90-col** — See [biome.json](./biome.json). `pnpm run format` before
|
|
47
|
+
committing; `pnpm run lint` is the read-only check CI runs. No lint warnings: a
|
|
48
|
+
deliberate exception carries a `biome-ignore` with its reason.
|
|
49
|
+
7. **Published types must stand alone** — `pnpm run typecheck:consumer` builds `dist/` and
|
|
50
|
+
type-checks [tests/consumer/index.ts](./tests/consumer/index.ts) against it by package
|
|
51
|
+
name, with a plain `lib`, no `@types/node` and `skipLibCheck: false`. Our own typecheck
|
|
52
|
+
loads `@types/node`, which hides missing lib types (e.g. `Symbol.dispose`, needed by
|
|
53
|
+
store's `Unsubscribe` and declared via `/// <reference lib>` in `src/types.ts`).
|
|
54
|
+
8. **Never throw from worker cleanup** — stream worker's returned cancel function is wrapped in try/catch.
|
|
55
|
+
9. **Runtime policy** — Node at the keyboard, npm as the registry. No `Deno.*`, `Bun.*`,
|
|
56
|
+
`jsr:` or `npm:` specifiers; erasable-only TS (`import type`, `.ts` import extensions,
|
|
57
|
+
no `enum` / `namespace` / parameter properties). Node never checks types:
|
|
58
|
+
`pnpm run typecheck` is the only type gate. Deno exists only for JSR: `test:deno` runs
|
|
59
|
+
the same suite against the JSR dependency graph, and `publish:jsr` runs it first.
|
|
60
|
+
Nothing is built or tested for Bun.
|
|
61
|
+
|
|
62
|
+
## Before Making Changes
|
|
63
|
+
|
|
64
|
+
- [ ] Read [docs/architecture.md](./docs/architecture.md) to understand the state-machine semantics
|
|
65
|
+
- [ ] Check [docs/conventions.md](./docs/conventions.md) for meta-update patterns
|
|
66
|
+
- [ ] Touching the public API or publish shape: also `pnpm run test:deno` and
|
|
67
|
+
`pnpm run jsr:check`
|
|
68
|
+
- [ ] Run `pnpm run ci` (typecheck + lint + tests + consumer typecheck) — the suite covers every known edge case including late-resolve/generation bugs
|
|
69
|
+
- [ ] Update [CHANGELOG.md](./CHANGELOG.md) for any behavior change
|
|
70
|
+
|
|
71
|
+
## Documentation Index
|
|
72
|
+
|
|
73
|
+
- [Architecture](./docs/architecture.md) — State model, data flow, generation token
|
|
74
|
+
- [Conventions](./docs/conventions.md) — Code style, meta update rules, testing patterns
|
|
75
|
+
- [Tasks](./docs/tasks.md) — Add a method, fix a meta bug, release
|
|
76
|
+
- Domains:
|
|
77
|
+
- [FetchStore](./docs/domains/fetch-store.md) — request-style store
|
|
78
|
+
- [FetchStreamStore](./docs/domains/fetch-stream-store.md) — push-style store
|
|
79
|
+
- Human docs: [README.md](./README.md), [API.md](./API.md), [CHANGELOG.md](./CHANGELOG.md)
|
package/API.md
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# API
|
|
2
|
+
|
|
3
|
+
## Functions
|
|
4
|
+
|
|
5
|
+
### `createFetchStore(fetchWorker, initial?, options?)`
|
|
6
|
+
|
|
7
|
+
Creates a reactive store for async fetch operations.
|
|
8
|
+
|
|
9
|
+
**Type signature:**
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
createFetchStore<T, A extends unknown[] = unknown[]>(
|
|
13
|
+
fetchWorker: (...args: A) => Promise<T>,
|
|
14
|
+
initial?: T | null,
|
|
15
|
+
options?: Partial<FetchStoreOptions<T>>
|
|
16
|
+
): FetchStore<T, A>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Parameters:**
|
|
20
|
+
|
|
21
|
+
- `fetchWorker` (`(...args: A) => Promise<T>`) — Async function performing the fetch. When `abortable: true`, receives `AbortSignal` as the last argument.
|
|
22
|
+
- `initial` (`T | null`, optional) — Initial data value. Default: `null`.
|
|
23
|
+
- `options` (`Partial<FetchStoreOptions<T>>`, optional) — See [FetchStoreOptions](#fetchstoreoptions).
|
|
24
|
+
|
|
25
|
+
**Returns:** [`FetchStore<T, A>`](#fetchstore)
|
|
26
|
+
|
|
27
|
+
**Example:**
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
const userStore = createFetchStore(
|
|
31
|
+
async (id: string) => (await fetch(`/api/users/${id}`)).json() as Promise<User>,
|
|
32
|
+
null,
|
|
33
|
+
{ fetchOnceDefaultThresholdMs: 60_000 }
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
await userStore.fetch("123");
|
|
37
|
+
const { data } = userStore.get();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
### `createFetchStreamStore(fetchStreamWorker, initial?, options?)`
|
|
43
|
+
|
|
44
|
+
Creates a reactive store for streaming data sources (SSE, WebSocket, push-based).
|
|
45
|
+
|
|
46
|
+
**Type signature:**
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
createFetchStreamStore<T, A extends unknown[] = unknown[]>(
|
|
50
|
+
fetchStreamWorker: (
|
|
51
|
+
emit: FetchStreamEventEmitFn<T>,
|
|
52
|
+
...args: A
|
|
53
|
+
) => (() => void) | void,
|
|
54
|
+
initial?: T | null,
|
|
55
|
+
options?: Partial<FetchStreamStoreOptions<T>>
|
|
56
|
+
): FetchStreamStore<T, A>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Parameters:**
|
|
60
|
+
|
|
61
|
+
- `fetchStreamWorker` — Worker function. Receives an `emit` callback as first arg and user args after. May return a cleanup function.
|
|
62
|
+
- `initial` (`T | null`, optional) — Initial data value. Default: `null`.
|
|
63
|
+
- `options` (`Partial<FetchStreamStoreOptions<T>>`, optional) — See [FetchStreamStoreOptions](#fetchstreamstoreoptions).
|
|
64
|
+
|
|
65
|
+
**Returns:** [`FetchStreamStore<T, A>`](#fetchstreamstore)
|
|
66
|
+
|
|
67
|
+
**Example:**
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const store = createFetchStreamStore<Message>((emit, url: string) => {
|
|
71
|
+
const es = new EventSource(url);
|
|
72
|
+
es.onmessage = (e) => emit("data", JSON.parse(e.data));
|
|
73
|
+
es.onerror = (e) => emit("error", e);
|
|
74
|
+
return () => es.close();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const stop = store.fetchStream(["/api/events"]);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Interfaces
|
|
83
|
+
|
|
84
|
+
### `FetchStore`
|
|
85
|
+
|
|
86
|
+
Returned by `createFetchStore`.
|
|
87
|
+
|
|
88
|
+
| Method | Signature | Description |
|
|
89
|
+
| ---------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
90
|
+
| `subscribe` | `(fn: (value: FetchStoreValue<T>) => void) => () => void` | Svelte-compatible subscription. Fires immediately with the current value, then on every change. Returns an unsubscribe function. |
|
|
91
|
+
| `get` | `() => FetchStoreValue<T>` | Synchronous snapshot of current value. |
|
|
92
|
+
| `fetch` | `(...args: A) => Promise<T \| null>` | Run the worker. Sets `isFetching: true`. Returns data or `null` on error/abort. |
|
|
93
|
+
| `fetchSilent` | `(...args: A) => Promise<T \| null>` | Like `fetch` but never sets `isFetching`. Still advances `successCounter`, timestamps. |
|
|
94
|
+
| `fetchOnce` | `(args?: A \| unknown, thresholdMs?: number) => Promise<T \| null>` | Fetch only if no successful fetch yet **or** last fetch older than `thresholdMs`. Joins any in-flight request. |
|
|
95
|
+
| `fetchOnceSilent` | `(args?: A \| unknown, thresholdMs?: number) => Promise<T \| null>` | Silent version of `fetchOnce`. |
|
|
96
|
+
| `fetchRecursive` | `(args?: A \| unknown, delayMs?: number \| (() => number), options?: { silent?: boolean }) => () => void` | Polling. Each iteration waits for the previous to finish. Returns cancel fn. Default uses `fetchSilent`; set `silent: false` to flip `isFetching`. `delayMs` as function allows dynamic intervals — returning `0` stops polling. |
|
|
97
|
+
| `reset` | `() => void` | Clear data + meta to initial state. Aborts in-flight (when abortable). Invalidates late-resolving fetches via an internal generation token. |
|
|
98
|
+
| `resetError` | `() => void` | Clear `lastFetchError` **and** `lastFetchSilentError`. Other state untouched. |
|
|
99
|
+
| `touch` | `(data?: T) => void` | Advance `lastFetchStart`/`lastFetchEnd` to now (tricks `fetchOnce`). Optionally replaces data. Increments `successCounter`. |
|
|
100
|
+
| `abort` | `() => void` | Abort any in-flight requests (requires `abortable: true`). |
|
|
101
|
+
| `fetchWorker` | `(...args: A) => Promise<T>` | The original worker, exposed for composition. |
|
|
102
|
+
| `getInternalDataStore` | `() => StoreLike<T \| null>` | Access the underlying data store. Bypasses `dataFactory` and meta — use with care. |
|
|
103
|
+
|
|
104
|
+
`fetchArgs` can be passed either as a tuple (`["x"]`) or as a single non-array value (`"x"`); the store normalizes it.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### `FetchStreamStore`
|
|
109
|
+
|
|
110
|
+
Returned by `createFetchStreamStore`.
|
|
111
|
+
|
|
112
|
+
| Method | Signature | Description |
|
|
113
|
+
| ---------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
114
|
+
| `subscribe` | `(fn: (value: FetchStreamStoreValue<T>) => void) => () => void` | Svelte-compatible subscription. |
|
|
115
|
+
| `get` | `() => FetchStreamStoreValue<T>` | Synchronous snapshot. |
|
|
116
|
+
| `fetchStream` | `(args?: A \| unknown, recursiveDelayMs?: number \| (() => number)) => () => void` | Start the stream. Returns cancel fn, which calls the worker's cleanup and closes the run (`isFetching: false`, `lastFetchEnd`) if it was still active. If `recursiveDelayMs > 0`, the worker is restarted `recursiveDelayMs` after every `emit("end")`. Starting a new stream while one is running cancels the previous one. |
|
|
117
|
+
| `reset` | `() => void` | Stop the running stream (calls its cleanup), clear data + meta. |
|
|
118
|
+
| `resetError` | `() => void` | Clear `lastFetchError` only. |
|
|
119
|
+
| `fetchStreamWorker` | raw worker | Original worker, exposed for composition. |
|
|
120
|
+
| `getInternalDataStore` | `() => StoreLike<T \| null>` | Underlying data store. |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Types
|
|
125
|
+
|
|
126
|
+
### `FetchStoreValue<T>`
|
|
127
|
+
|
|
128
|
+
The value returned by `get()` and passed to subscribers.
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
interface FetchStoreValue<T> {
|
|
132
|
+
data: T | null;
|
|
133
|
+
isFetching: boolean;
|
|
134
|
+
lastFetchStart: Date | null;
|
|
135
|
+
lastFetchEnd: Date | null;
|
|
136
|
+
lastFetchError: Error | null;
|
|
137
|
+
lastFetchSilentError: Error | null;
|
|
138
|
+
successCounter: number;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
| Field | Description |
|
|
143
|
+
| --------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
144
|
+
| `data` | Fetched data; `null` before first successful fetch or after `reset()`. |
|
|
145
|
+
| `isFetching` | `true` during a `fetch()` call. Silent fetches do not flip this. |
|
|
146
|
+
| `lastFetchStart` / `lastFetchEnd` | Timestamps of the most recent fetch (silent or not). |
|
|
147
|
+
| `lastFetchError` | Error from the last non-silent fetch. Cleared at the start of the next non-silent `fetch()`. |
|
|
148
|
+
| `lastFetchSilentError` | Error from the last silent fetch. Cleared on next silent success. |
|
|
149
|
+
| `successCounter` | Incremented on every successful fetch, silent or not (and by `touch()`). |
|
|
150
|
+
|
|
151
|
+
### `FetchStreamStoreValue<T>`
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
interface FetchStreamStoreValue<T> {
|
|
155
|
+
data: T | null;
|
|
156
|
+
isFetching: boolean;
|
|
157
|
+
lastFetchStart: Date | null;
|
|
158
|
+
lastFetchEnd: Date | null;
|
|
159
|
+
lastFetchError: Error | null;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Set to `isFetching: true` when `fetchStream()` starts; flipped back to `false` on `emit("end")` or when the returned cancel fn is called.
|
|
164
|
+
|
|
165
|
+
### `FetchStoreOptions`
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
interface FetchStoreOptions<T> extends CreateStoreOptions<T> {
|
|
169
|
+
fetchOnceDefaultThresholdMs: number;
|
|
170
|
+
onReset: () => void;
|
|
171
|
+
dedupeInflight: boolean;
|
|
172
|
+
abortable: boolean;
|
|
173
|
+
dataFactory: DataFactory<T>;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
| Option | Default | Description |
|
|
178
|
+
| ----------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
179
|
+
| `fetchOnceDefaultThresholdMs` | `300_000` (5 min) | TTL for `fetchOnce` / `fetchOnceSilent`. Use `0` at call site to skip threshold and rely only on `successCounter`. |
|
|
180
|
+
| `dedupeInflight` | `false` | Concurrent `fetch()` (or `fetchSilent()`) calls return the same promise. |
|
|
181
|
+
| `abortable` | `false` | Worker receives `AbortSignal` as last arg. A new `fetch()` aborts the previous in-flight one. |
|
|
182
|
+
| `dataFactory` | — | `(data, old?) => T` — transform/merge each incoming payload. |
|
|
183
|
+
| `onReset` | — | Called after `reset()` wipes state. |
|
|
184
|
+
|
|
185
|
+
### `FetchStreamStoreOptions`
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
interface FetchStreamStoreOptions<T> extends CreateStoreOptions<T> {
|
|
189
|
+
onReset: () => void;
|
|
190
|
+
dataFactory: DataFactory<T>;
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### `DataFactory<T>`
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
type DataFactory<T> = (data: T, old?: T) => T;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Applied to every data write. Use for merge strategies (shallow merge, deep merge, array append, etc.).
|
|
201
|
+
|
|
202
|
+
### `FetchStreamEventName`
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
type FetchStreamEventName = "data" | "error" | "end";
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### `FetchStreamEventEmitFn<T>`
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
type FetchStreamEventEmitFn<T = unknown> = (
|
|
212
|
+
eventName: FetchStreamEventName,
|
|
213
|
+
eventData?: T | Error
|
|
214
|
+
) => void;
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Passed to the stream worker as its first argument. Semantics:
|
|
218
|
+
|
|
219
|
+
- `emit("data", value)` — writes data; clears any prior `lastFetchError`.
|
|
220
|
+
- `emit("error", err)` — sets `lastFetchError`; does not touch data.
|
|
221
|
+
- `emit("end")` — sets `isFetching: false`, `lastFetchEnd`; **does not** clear errors.
|
|
222
|
+
|
|
223
|
+
Emits after `cancel()` / `reset()` are silently ignored, including any emit from the worker's cleanup function. The worker does not need to emit `"end"` on cancel: `cancel()` closes the run itself.
|
|
224
|
+
|
|
225
|
+
### `FetchMetaBase` / `FetchStoreMeta` / `FetchStreamStoreMeta`
|
|
226
|
+
|
|
227
|
+
Base shape shared by both stores; the concrete `*StoreValue<T>` is `{ data } & *StoreMeta`.
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
interface FetchMetaBase {
|
|
231
|
+
isFetching: boolean;
|
|
232
|
+
lastFetchStart: Date | null;
|
|
233
|
+
lastFetchEnd: Date | null;
|
|
234
|
+
lastFetchError: Error | null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
interface FetchStoreMeta extends FetchMetaBase {
|
|
238
|
+
successCounter: number;
|
|
239
|
+
lastFetchSilentError: Error | null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface FetchStreamStoreMeta extends FetchMetaBase {}
|
|
243
|
+
```
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Bug fixes
|
|
6
|
+
|
|
7
|
+
- **Type-checks in projects without the disposable lib.** `subscribe()` returns
|
|
8
|
+
`@marianmeres/store`'s `Unsubscribe`, which declares `[Symbol.dispose]`. A consumer
|
|
9
|
+
whose `lib` predates `esnext.disposable`, with `skipLibCheck: false` and no `@types/node`
|
|
10
|
+
(which declares `Symbol.dispose` itself), got `TS2550: Property 'dispose' does not exist
|
|
11
|
+
on type 'SymbolConstructor'` from store's declarations. `dist/types.d.ts` now carries
|
|
12
|
+
`/// <reference lib="esnext.disposable" />`, so the types bring the lib they need
|
|
13
|
+
(`using sub = store.subscribe(...)` type-checks too). Runtime is unchanged:
|
|
14
|
+
`Symbol.dispose` was already polyfilled by `@marianmeres/pubsub`. (`src/types.ts`; guarded
|
|
15
|
+
by `pnpm run typecheck:consumer`.)
|
|
16
|
+
|
|
17
|
+
### Tooling
|
|
18
|
+
|
|
19
|
+
- **Moved off Deno.** Node LTS is the dev runtime and the CI gate (GitHub Actions); pnpm,
|
|
20
|
+
`node:test`, Biome and `tsc` replace `deno test`, `deno fmt` / `deno lint` and the
|
|
21
|
+
`scripts/build-npm.ts` build. `package.json` is the manifest of record; `deno.json` and
|
|
22
|
+
`deno.lock` are gone. The source changes are type-level and formatting only.
|
|
23
|
+
- **Still on npm and JSR.** npm gets `dist/` built by `tsc`; JSR keeps getting the `.ts`
|
|
24
|
+
source from `jsr.json`, which has no version of its own (`publish:jsr` passes
|
|
25
|
+
`--set-version` from `package.json`). `pnpm run test:deno` runs the same suite on Deno
|
|
26
|
+
against the JSR dependency graph before every JSR publish.
|
|
27
|
+
- **npm package shape.** `exports` map (`types` + `default`) next to `main` / `types`;
|
|
28
|
+
`sideEffects: false`; `API.md`, `AGENTS.md` and `CHANGELOG.md` are now packed. The
|
|
29
|
+
built JavaScript is identical to 3.1.1 apart from line wrapping. The `.d.ts` files
|
|
30
|
+
re-export `./types.ts` (was `./types.js`), which TypeScript 5.9 and 7 resolve to the
|
|
31
|
+
sibling `.d.ts` under both `bundler` and `nodenext` resolution.
|
|
32
|
+
- **`@marianmeres/pubsub` is no longer a direct dependency.** Nothing imports it; it still
|
|
33
|
+
arrives through `@marianmeres/store`.
|
|
34
|
+
|
|
35
|
+
## 3.1.1
|
|
36
|
+
|
|
37
|
+
### Bug fixes
|
|
38
|
+
|
|
39
|
+
- **Stopping a running stream no longer leaves `isFetching: true`** ([#1](https://github.com/marianmeres/fetch-store/issues/1)). A 3.1.0 regression: since emits after cancel are ignored, the worker's `"end"` could never arrive once `stop()` was called, whether it came from the cleanup function (the pattern the docs showed) or asynchronously afterwards. `isFetching` stayed `true` and `lastFetchEnd` stayed `null` until the next `fetchStream()` or `reset()`. The cancel function now closes the run itself (`isFetching: false`, `lastFetchEnd: new Date()`) when it was still active; errors are preserved, as with `"end"`. Restarting a stream and `reset()` skip this write, so subscribers see no intermediate `isFetching: false`. Calling `emit("end")` from cleanup is no longer needed, but it is harmless. (`src/fetch-stream-store.ts`: internal `stop(closeRun)` behind the public cancel fn; tests `A8`.)
|
|
40
|
+
|
|
41
|
+
## 3.1.0
|
|
42
|
+
|
|
43
|
+
A thorough re-analysis surfaced several real bugs and design gaps. This release
|
|
44
|
+
fixes all of them and tightens a few type signatures. Most fixes are behaviorally
|
|
45
|
+
backward compatible; the BC-visible ones are called out under "Breaking changes"
|
|
46
|
+
below.
|
|
47
|
+
|
|
48
|
+
### Bug fixes
|
|
49
|
+
|
|
50
|
+
- **Aborted fetch no longer leaves `isFetching: true` forever.** When a `fetch()` was aborted (manual `abort()` or `reset()`), the `AbortError` branch returned early without resetting meta. `isFetching` got stuck until another fetch started. Now the abort branch clears `isFetching` and sets `lastFetchEnd` (when we are still the current request). (`src/fetch-store.ts` — abort-branch meta update; test `A1`.)
|
|
51
|
+
|
|
52
|
+
- **`fetchSilent` now advances `lastFetchStart`, `lastFetchEnd`, and `successCounter`.** Previously silent variants updated only `lastFetchSilentError`. That broke `fetchOnceSilent` as a caching primitive: both the cold-store gate (`!successCounter`) and the threshold gate (`lastFetchStart`) never advanced, so every `fetchOnceSilent` call unconditionally re-fetched. The "silent" semantic (no loading-spinner flicker) is preserved — only `isFetching` stays unchanged. (`src/fetch-store.ts` — `doFetchSilent` meta update; tests `A2-*`.)
|
|
53
|
+
|
|
54
|
+
- **`FetchStreamStore.reset()` now stops the running stream.** Previously `reset()` only cleared state; the worker kept running, emitting into a freshly-reset store (zombie stream / leak). The store now tracks the current stream's cancel function and invokes it before wiping state. (`src/fetch-stream-store.ts` — `_currentCancel` tracked at closure scope; test `A3`.)
|
|
55
|
+
|
|
56
|
+
- **`emit("end")` no longer wipes a preceding `emit("error")`.** The emit handler unconditionally cleared `lastFetchError` at the top, so the common `emit("error", e); emit("end")` pattern lost the error by the time subscribers saw the terminal state. Error-clearing is now scoped to `"data"` events only (data implies recovery). (`src/fetch-stream-store.ts` — error-clear moved into the data branch; test `A4`.)
|
|
57
|
+
|
|
58
|
+
- **Synchronous throw in a stream worker no longer leaves `isFetching: true`.** `_metaStore.set({ isFetching: true })` ran before `try { worker(...) }`; a sync throw was caught, but `isFetching` was never reset and the returned cancel function was a no-op. The catch branch now sets `isFetching: false`, `lastFetchEnd`, and `lastFetchError`. (`src/fetch-stream-store.ts` — `catch` branch meta update; test `A5`.)
|
|
59
|
+
|
|
60
|
+
- **Non-abortable in-flight fetches can no longer overwrite `reset()`.** When `abortable: false` and a fetch was mid-`await`, calling `reset()` wiped state, but the awaited response resumed and wrote stale data + meta on top. A generation token is now bumped on `reset()`; any in-flight fetch whose generation no longer matches discards its writes. (`src/fetch-store.ts` — `_generation` token; test `A6`.)
|
|
61
|
+
|
|
62
|
+
- **`fetchOnce` joins in-flight requests instead of returning stale data.** When `isFetching` was `true`, both gates failed and the function returned `_dataStore.get()` immediately (usually `null` or stale). Now `fetchOnce` always awaits the current in-flight promise — regardless of the `dedupeInflight` option. (`src/fetch-store.ts` — explicit in-flight join in `_fetchOnce`; test `A7`.)
|
|
63
|
+
|
|
64
|
+
- **Concurrent `fetchOnce` calls dedupe internally.** Two `fetchOnce()` calls on a cold store used to both see `successCounter: 0` and both fire. The in-flight join above covers this: the second call now joins the first's in-flight promise. (`src/fetch-store.ts`; test `C3`.)
|
|
65
|
+
|
|
66
|
+
- **Stream worker emits after cancel are silently ignored.** A worker that does not honor its cleanup function synchronously could still write data/errors to the store after the user called `stop()`. The emit callback now short-circuits on the cancel flag. (`src/fetch-stream-store.ts` — `_aborted` guard inside the emit callback; test `C6`.)
|
|
67
|
+
|
|
68
|
+
- **README code samples now match the 3.x signature.** The `Abortable` and `Deduplication` sections showed 4-arg calls (`createFetchStore(worker, null, null, { ... })`) left over from the 2.x → 3.x migration. Fixed to 3 args. (`README.md`.)
|
|
69
|
+
|
|
70
|
+
### Design improvements
|
|
71
|
+
|
|
72
|
+
- **Worker-argument types now propagate through the store.** `createFetchStore` and `createFetchStreamStore` gained a second generic `A extends unknown[]` inferred from the worker. `store.fetch(...)` / `store.fetchStream(...)` now type-check arguments against the worker's signature. Defaults preserve the old `unknown[]` behavior for untyped workers. (`src/types.ts` — `FetchStore<T, A>` and `FetchStreamStore<T, A>`.)
|
|
73
|
+
|
|
74
|
+
- **`FetchStreamEventEmitFn<T>` is generic.** The public type is now parameterized over `T` with a default of `unknown`, matching the implementation. Workers typed against this interface get type safety on `emit("data", ...)` payloads. (`src/types.ts`.)
|
|
75
|
+
|
|
76
|
+
- **`fetchRecursive` accepts `{ silent?: boolean }`.** New third argument lets polling flip `isFetching` each iteration when needed. Default remains `silent: true` to preserve the old behavior.
|
|
77
|
+
|
|
78
|
+
- **`resetError()` clears both error fields.** Previously only cleared `lastFetchError`; now also clears `lastFetchSilentError`.
|
|
79
|
+
|
|
80
|
+
- **Stream store `fetchStream()` cancels a previously running stream.** Starting a new stream while one is already active now calls the previous stream's cleanup function before the new worker runs — avoids parallel zombie streams if a consumer forgets to call the returned cancel function.
|
|
81
|
+
|
|
82
|
+
### Breaking changes
|
|
83
|
+
|
|
84
|
+
The changes below are technically BC-visible. None are expected to affect most real-world usage — the old behaviors were bugs — but each is documented in case you were depending on the exact old semantics.
|
|
85
|
+
|
|
86
|
+
1. **`fetchSilent` now bumps `successCounter` and updates `lastFetchStart` / `lastFetchEnd`.** If you were subscribing to `successCounter` as a "visible-fetch counter", silent fetches (including those inside `fetchRecursive`) now increment it. Use a separate counter in your own code if you need the old semantics.
|
|
87
|
+
|
|
88
|
+
2. **`fetchOnceSilent` now caches on its threshold.** Previously every call re-fetched (because silent didn't advance `lastFetchStart` / `successCounter`). If you were using `fetchOnceSilent` in place of `fetchSilent` and relying on the broken no-cache behavior, switch to `fetchSilent` directly or pass `thresholdMs: 0`.
|
|
89
|
+
|
|
90
|
+
3. **`fetchOnce` waits for an in-flight fetch instead of returning stale data immediately.** If you were calling `fetchOnce()` during a pending `fetch()` and reading the result synchronously, you may observe a longer wait now (the correct data arrives instead of `null` / stale).
|
|
91
|
+
|
|
92
|
+
4. **`emit("end")` no longer clears `lastFetchError`.** If you were relying on `end` as a "reset the error" signal, either emit a `"data"` first or call `resetError()` explicitly.
|
|
93
|
+
|
|
94
|
+
5. **`FetchStreamStore.reset()` now stops the running stream.** If you were calling `reset()` purely to wipe displayed state while keeping the underlying stream alive, that no longer works. Start a fresh `fetchStream()` after `reset()`.
|
|
95
|
+
|
|
96
|
+
6. **`FetchStore<T>` is now `FetchStore<T, A>` (with default).** The second generic is optional (`A extends unknown[] = unknown[]`) so existing `FetchStore<User>` annotations keep working. Code that explicitly asserted the type of `fetch` as `(...args: unknown[]) => Promise<T | null>` may now see a narrower signature.
|
|
97
|
+
|
|
98
|
+
7. **`FetchStreamEventEmitFn` became `FetchStreamEventEmitFn<T>` (with default).** `type FetchStreamEventEmitFn = ...` style references still compile (default `T = unknown`).
|
|
99
|
+
|
|
100
|
+
8. **`resetError()` now also clears `lastFetchSilentError`.** If you were calling `resetError()` and reading `lastFetchSilentError` afterwards, expect `null`.
|
|
101
|
+
|
|
102
|
+
### Non-changes worth noting
|
|
103
|
+
|
|
104
|
+
- The public `createFetchStore(worker, initial?, options?)` / `createFetchStreamStore(worker, initial?, options?)` signatures are **unchanged** from 3.0.x. No third positional argument was added or removed.
|
|
105
|
+
- `getInternalDataStore()` still returns a fully writable `StoreLike<T | null>` — consumers using it as an escape hatch keep working exactly as before.
|
|
106
|
+
- Svelte store contract (synchronous notify on subscribe, strict-equality short-circuit) is unchanged.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 3.0.4
|
|
111
|
+
|
|
112
|
+
- Docs updates.
|
|
113
|
+
|
|
114
|
+
## 3.0.3
|
|
115
|
+
|
|
116
|
+
- Deps bump.
|
|
117
|
+
|
|
118
|
+
## 3.0.0
|
|
119
|
+
|
|
120
|
+
Major release. See earlier README "Breaking Changes" section (now in git history) for the 2.x → 3.x migration — simplified generic types, moved `dataFactory` into `options`, and changed `DataFactory<T>` to receive strongly-typed `T` instead of `unknown`.
|
package/README.md
CHANGED
|
@@ -9,13 +9,16 @@ Reactive store for async fetch operations with loading/error/success state track
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
|
|
12
|
+
npm install @marianmeres/fetch-store
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
|
|
16
|
+
deno add jsr:@marianmeres/fetch-store
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
Plain ESM with type declarations, web standards only: runs in browsers, Node and any
|
|
20
|
+
ES2022 runtime.
|
|
21
|
+
|
|
19
22
|
## Usage
|
|
20
23
|
|
|
21
24
|
```typescript
|
|
@@ -99,10 +102,12 @@ const feed = createFetchStreamStore<Message>((emit, url: string) => {
|
|
|
99
102
|
const es = new EventSource(url);
|
|
100
103
|
es.onmessage = (e) => emit("data", JSON.parse(e.data));
|
|
101
104
|
es.onerror = (e) => emit("error", e);
|
|
102
|
-
return () =>
|
|
105
|
+
return () => es.close();
|
|
103
106
|
});
|
|
104
107
|
|
|
105
108
|
const stop = feed.fetchStream(["/api/events"]);
|
|
109
|
+
// ...later: closes the connection, sets isFetching: false
|
|
110
|
+
stop();
|
|
106
111
|
```
|
|
107
112
|
|
|
108
113
|
## Features
|
|
@@ -120,6 +125,24 @@ const stop = feed.fetchStream(["/api/events"]);
|
|
|
120
125
|
|
|
121
126
|
See [API.md](API.md) for the complete API reference.
|
|
122
127
|
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
Node LTS (`.node-version`), pnpm, Biome, `node:test`. The source is erasable-only
|
|
131
|
+
TypeScript that Node runs directly; `dist/` is built only for publishing to npm. JSR is
|
|
132
|
+
served the `.ts` source (`jsr.json`).
|
|
133
|
+
|
|
134
|
+
| Command | Does |
|
|
135
|
+
| ----------------------------------- | ------------------------------------------------------------ |
|
|
136
|
+
| `pnpm install` | installs the dev tooling |
|
|
137
|
+
| `pnpm test` / `pnpm run test:watch` | `node --test` over `tests/**/*.test.ts` |
|
|
138
|
+
| `pnpm run test:deno` | the same suite on Deno, as JSR consumers run it |
|
|
139
|
+
| `pnpm run typecheck` | `tsc --noEmit` over `src` and `tests` |
|
|
140
|
+
| `pnpm run typecheck:consumer` | builds `dist/`, type-checks it as a downstream project would |
|
|
141
|
+
| `pnpm run lint` / `pnpm run format` | `biome check .` read-only / with `--write` |
|
|
142
|
+
| `pnpm run ci` | typecheck + lint + test + consumer typecheck, what CI runs |
|
|
143
|
+
| `pnpm run build` | `tsc -p tsconfig.build.json` → `dist/` |
|
|
144
|
+
| `pnpm run rp` / `pnpm run rpm` | patch / minor release, then publish to npm and JSR |
|
|
145
|
+
|
|
123
146
|
## Changelog
|
|
124
147
|
|
|
125
148
|
See [CHANGELOG.md](CHANGELOG.md).
|
package/dist/fetch-store.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export
|
|
2
|
-
export {
|
|
3
|
-
import type { FetchStore, FetchStoreOptions } from "./types.
|
|
1
|
+
export { createFetchStreamStore } from "./fetch-stream-store.ts";
|
|
2
|
+
export type { DataFactory, FetchStore, FetchStoreMeta, FetchStoreOptions, FetchStoreValue, } from "./types.ts";
|
|
3
|
+
import type { FetchStore, FetchStoreOptions } from "./types.ts";
|
|
4
4
|
/**
|
|
5
5
|
* Creates a reactive store for managing async fetch operations with built-in
|
|
6
6
|
* state tracking for loading, errors, and success counts.
|
package/dist/fetch-store.js
CHANGED
|
@@ -104,7 +104,9 @@ export const createFetchStore = (fetchWorker, initial = null, options = {}) => {
|
|
|
104
104
|
return null;
|
|
105
105
|
// Only clear isFetching if we're still the current request
|
|
106
106
|
// (otherwise a newer fetch owns the meta).
|
|
107
|
-
if (!abortable ||
|
|
107
|
+
if (!abortable ||
|
|
108
|
+
myController === _abortController ||
|
|
109
|
+
_abortController === null) {
|
|
108
110
|
_metaStore.set({
|
|
109
111
|
..._metaStore.get(),
|
|
110
112
|
isFetching: false,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { FetchStreamEventEmitFn, FetchStreamEventName, FetchStreamStore, FetchStreamStoreMeta, FetchStreamStoreOptions, FetchStreamStoreValue, } from "./types.
|
|
2
|
-
import type { FetchStreamEventName, FetchStreamStore, FetchStreamStoreOptions } from "./types.
|
|
1
|
+
export type { FetchStreamEventEmitFn, FetchStreamEventName, FetchStreamStore, FetchStreamStoreMeta, FetchStreamStoreOptions, FetchStreamStoreValue, } from "./types.ts";
|
|
2
|
+
import type { FetchStreamEventName, FetchStreamStore, FetchStreamStoreOptions } from "./types.ts";
|
|
3
3
|
/**
|
|
4
4
|
* Creates a reactive store for handling streaming data sources like SSE, WebSocket,
|
|
5
5
|
* or any push-based data pattern.
|
|
@@ -19,11 +19,11 @@ import type { FetchStreamEventName, FetchStreamStore, FetchStreamStoreOptions }
|
|
|
19
19
|
* const eventSource = new EventSource(url);
|
|
20
20
|
* eventSource.onmessage = (e) => emit("data", JSON.parse(e.data));
|
|
21
21
|
* eventSource.onerror = (e) => emit("error", e);
|
|
22
|
-
* return () =>
|
|
22
|
+
* return () => eventSource.close();
|
|
23
23
|
* });
|
|
24
24
|
*
|
|
25
25
|
* const stop = sseStore.fetchStream(["/api/events"]);
|
|
26
|
-
* // Later: stop()
|
|
26
|
+
* // Later: stop() closes the connection and the run (isFetching: false, lastFetchEnd)
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
29
|
export declare const createFetchStreamStore: <T, A extends unknown[] = unknown[]>(fetchStreamWorker: (emit: (eventName: FetchStreamEventName, eventData?: T | Error) => void, ...args: A) => (() => void) | void, initial?: T | null, options?: Partial<FetchStreamStoreOptions<T>>) => FetchStreamStore<T, A>;
|
|
@@ -19,11 +19,11 @@ const DEFAULT_OPTIONS = {};
|
|
|
19
19
|
* const eventSource = new EventSource(url);
|
|
20
20
|
* eventSource.onmessage = (e) => emit("data", JSON.parse(e.data));
|
|
21
21
|
* eventSource.onerror = (e) => emit("error", e);
|
|
22
|
-
* return () =>
|
|
22
|
+
* return () => eventSource.close();
|
|
23
23
|
* });
|
|
24
24
|
*
|
|
25
25
|
* const stop = sseStore.fetchStream(["/api/events"]);
|
|
26
|
-
* // Later: stop()
|
|
26
|
+
* // Later: stop() closes the connection and the run (isFetching: false, lastFetchEnd)
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
29
|
export const createFetchStreamStore = (fetchStreamWorker, initial = null, options = {}) => {
|
|
@@ -40,13 +40,17 @@ export const createFetchStreamStore = (fetchStreamWorker, initial = null, option
|
|
|
40
40
|
const _dataStore = createStore(initial);
|
|
41
41
|
const _metaStore = createStore(_createMetaObj());
|
|
42
42
|
const { subscribe, get } = createDerivedStore([_dataStore, _metaStore], ([data, meta]) => ({ data, ...meta }));
|
|
43
|
-
// Tracks the
|
|
44
|
-
|
|
43
|
+
// Tracks the stop fn of the currently running stream so reset() and a new
|
|
44
|
+
// fetchStream() can stop it.
|
|
45
|
+
let _currentStop = null;
|
|
45
46
|
const fetchStream = (fetchArgs = [], recursiveDelayMs = 0) => {
|
|
46
47
|
let _timer;
|
|
47
48
|
let _aborted = false;
|
|
48
49
|
let _abortFn;
|
|
49
|
-
|
|
50
|
+
// `closeRun` is false when the caller is about to overwrite meta anyway
|
|
51
|
+
// (reset, or a new fetchStream superseding this one), so subscribers see no
|
|
52
|
+
// intermediate `isFetching: false` flicker.
|
|
53
|
+
const stop = (closeRun) => {
|
|
50
54
|
if (_aborted)
|
|
51
55
|
return;
|
|
52
56
|
_aborted = true;
|
|
@@ -60,9 +64,18 @@ export const createFetchStreamStore = (fetchStreamWorker, initial = null, option
|
|
|
60
64
|
// cleanup should not throw, but guard anyway
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
|
-
if (
|
|
64
|
-
|
|
67
|
+
if (_currentStop === stop)
|
|
68
|
+
_currentStop = null;
|
|
69
|
+
// Every emit from here on is ignored (including an "end" from the cleanup
|
|
70
|
+
// above), so the store closes the run itself. Nothing to close when idle
|
|
71
|
+
// between recursive runs.
|
|
72
|
+
const meta = _metaStore.get();
|
|
73
|
+
if (closeRun && meta.isFetching) {
|
|
74
|
+
_metaStore.set({ ...meta, isFetching: false, lastFetchEnd: new Date() });
|
|
75
|
+
}
|
|
65
76
|
};
|
|
77
|
+
// Public cancel fn takes no args (safe to pass directly as an event handler).
|
|
78
|
+
const cancel = () => stop(true);
|
|
66
79
|
const normalizedArgs = (Array.isArray(fetchArgs) ? fetchArgs : [fetchArgs]);
|
|
67
80
|
const runOnce = () => {
|
|
68
81
|
if (_aborted)
|
|
@@ -131,19 +144,19 @@ export const createFetchStreamStore = (fetchStreamWorker, initial = null, option
|
|
|
131
144
|
});
|
|
132
145
|
}
|
|
133
146
|
};
|
|
134
|
-
// If a previous stream was still running,
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
147
|
+
// If a previous stream was still running, stop it before starting a new one.
|
|
148
|
+
if (_currentStop)
|
|
149
|
+
_currentStop(false);
|
|
150
|
+
_currentStop = stop;
|
|
138
151
|
runOnce();
|
|
139
152
|
return cancel;
|
|
140
153
|
};
|
|
141
154
|
const reset = () => {
|
|
142
155
|
// Stop any running stream (calls the worker's cleanup function).
|
|
143
|
-
if (
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
156
|
+
if (_currentStop) {
|
|
157
|
+
const s = _currentStop;
|
|
158
|
+
_currentStop = null;
|
|
159
|
+
s(false);
|
|
147
160
|
}
|
|
148
161
|
_dataStore.set(initial);
|
|
149
162
|
_metaStore.set(_createMetaObj());
|
package/dist/mod.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "./
|
|
2
|
-
export * from "./fetch-store.
|
|
3
|
-
export * from "./
|
|
1
|
+
export * from "./fetch-store.ts";
|
|
2
|
+
export * from "./fetch-stream-store.ts";
|
|
3
|
+
export * from "./types.ts";
|
package/dist/mod.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference lib="esnext.disposable" preserve="true" />
|
|
1
2
|
import type { CreateStoreOptions, StoreLike, StoreReadable } from "@marianmeres/store";
|
|
2
3
|
/**
|
|
3
4
|
* Factory function that transforms fetched data, useful for merge strategies.
|
|
@@ -171,7 +172,8 @@ export interface FetchStreamStore<T, A extends unknown[] = unknown[]> extends St
|
|
|
171
172
|
* Starts the stream and optionally restarts it after completion.
|
|
172
173
|
* @param args - Arguments passed to the stream worker
|
|
173
174
|
* @param recursiveDelayMs - Delay before restarting stream after "end" event (0 = no restart)
|
|
174
|
-
* @returns Cancel function
|
|
175
|
+
* @returns Cancel function: calls the worker's cleanup, ignores any later emits,
|
|
176
|
+
* and closes the run (`isFetching: false`, `lastFetchEnd`) if it was still active.
|
|
175
177
|
*/
|
|
176
178
|
fetchStream: (args?: A | unknown, recursiveDelayMs?: number | (() => number)) => () => void;
|
|
177
179
|
/**
|
package/dist/types.js
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
+
// The stores' subscribe() returns @marianmeres/store's `Unsubscribe`, which implements
|
|
2
|
+
// `[Symbol.dispose]`. That needs the disposable lib, which a consumer on an older `lib`
|
|
3
|
+
// (and without @types/node) does not load; `preserve` keeps the directive in dist/types.d.ts.
|
|
4
|
+
// At runtime Symbol.dispose is there: @marianmeres/pubsub, loaded by store, polyfills it.
|
|
5
|
+
/// <reference lib="esnext.disposable" preserve="true" />
|
|
1
6
|
export {};
|
package/package.json
CHANGED
|
@@ -1,20 +1,71 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
2
|
+
"name": "@marianmeres/fetch-store",
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "Reactive store for async fetch operations with loading/error/success state tracking, caching, polling, request deduplication, abort support, and streaming (SSE, WebSocket). Svelte-compatible but framework-agnostic.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Marian Meres",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"main": "./dist/mod.js",
|
|
13
|
+
"types": "./dist/mod.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/mod.d.ts",
|
|
17
|
+
"default": "./dist/mod.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"API.md",
|
|
23
|
+
"AGENTS.md",
|
|
24
|
+
"CHANGELOG.md"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/marianmeres/fetch-store.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/marianmeres/fetch-store/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/marianmeres/fetch-store#readme",
|
|
34
|
+
"keywords": [
|
|
35
|
+
"fetch",
|
|
36
|
+
"store",
|
|
37
|
+
"svelte-store",
|
|
38
|
+
"reactive",
|
|
39
|
+
"async",
|
|
40
|
+
"polling",
|
|
41
|
+
"abort",
|
|
42
|
+
"sse",
|
|
43
|
+
"websocket"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@marianmeres/store": "^3.0.1"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@biomejs/biome": "^2.6.6",
|
|
50
|
+
"@types/node": "^26.6.6",
|
|
51
|
+
"typescript": "^7.0.2"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "pnpm run clean && tsc -p tsconfig.build.json",
|
|
55
|
+
"clean": "rm -rf dist",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"typecheck:consumer": "pnpm run build && tsc -p tests/consumer/tsconfig.json",
|
|
58
|
+
"lint": "biome check .",
|
|
59
|
+
"format": "biome check --write .",
|
|
60
|
+
"test": "node --test \"tests/**/*.test.ts\"",
|
|
61
|
+
"test:watch": "node --test --watch \"tests/**/*.test.ts\"",
|
|
62
|
+
"test:deno": "deno test --no-check --no-lock --config jsr.json tests/",
|
|
63
|
+
"jsr:check": "deno publish --dry-run --allow-dirty --no-check --set-version \"$npm_package_version\"",
|
|
64
|
+
"ci": "pnpm run typecheck && pnpm run lint && pnpm test && pnpm run typecheck:consumer",
|
|
65
|
+
"release": "npx --yes @marianmeres/release@^2.2.0",
|
|
66
|
+
"publish:jsr": "pnpm run test:deno && deno publish --no-check --set-version \"$npm_package_version\"",
|
|
67
|
+
"publish:all": "pnpm publish && pnpm run publish:jsr",
|
|
68
|
+
"rp": "pnpm run release -y && pnpm run publish:all",
|
|
69
|
+
"rpm": "pnpm run release minor -y && pnpm run publish:all"
|
|
70
|
+
}
|
|
71
|
+
}
|