@marianmeres/fetch-store 2.12.0 → 3.0.3
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 +286 -51
- package/dist/fetch-store.d.ts +33 -0
- package/dist/fetch-store.js +280 -0
- package/dist/fetch-stream-store.d.ts +28 -30
- package/dist/fetch-stream-store.js +129 -0
- package/dist/mod.d.ts +3 -0
- package/dist/mod.js +3 -0
- package/dist/types.d.ts +208 -0
- package/dist/types.js +1 -0
- package/package.json +7 -49
- package/dist/index.cjs +0 -1
- package/dist/index.d.ts +0 -31
- package/dist/index.js +0 -1
- package/dist/index.umd.js +0 -1
package/README.md
CHANGED
|
@@ -1,83 +1,318 @@
|
|
|
1
1
|
# @marianmeres/fetch-store
|
|
2
2
|
|
|
3
|
-
[
|
|
4
|
-
|
|
3
|
+
A reactive [store](https://github.com/marianmeres/store) utility for managing async fetch
|
|
4
|
+
operations with built-in state tracking for loading, errors, and success counts. Designed
|
|
5
|
+
for Svelte-compatible reactivity but works with any framework.
|
|
5
6
|
|
|
6
7
|
## Install
|
|
7
8
|
|
|
8
9
|
```shell
|
|
9
|
-
|
|
10
|
+
deno add jsr:@marianmeres/fetch-store
|
|
10
11
|
```
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
```shell
|
|
14
|
+
npm i @marianmeres/fetch-store
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Basic Example
|
|
13
18
|
|
|
14
|
-
```
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
```typescript
|
|
20
|
+
// user-store.ts
|
|
21
|
+
import { createFetchStore } from "@marianmeres/fetch-store";
|
|
22
|
+
|
|
23
|
+
const userStore = createFetchStore(async (userId: string) => {
|
|
24
|
+
const response = await fetch(`/api/users/${userId}`);
|
|
25
|
+
if (!response.ok) throw new Error("Failed to fetch user");
|
|
26
|
+
return await response.json();
|
|
20
27
|
});
|
|
28
|
+
|
|
29
|
+
// Fetch user data
|
|
30
|
+
await userStore.fetch("123");
|
|
31
|
+
|
|
32
|
+
// Access current state
|
|
33
|
+
const { data, isFetching, lastFetchError } = userStore.get();
|
|
21
34
|
```
|
|
22
35
|
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
### Svelte Integration
|
|
37
|
+
|
|
38
|
+
```svelte
|
|
39
|
+
<!-- UserProfile.svelte -->
|
|
25
40
|
<script>
|
|
26
|
-
onMount
|
|
41
|
+
import { onMount } from 'svelte';
|
|
42
|
+
import { userStore } from './user-store.ts';
|
|
43
|
+
|
|
44
|
+
export let userId;
|
|
45
|
+
onMount(() => userStore.fetch(userId));
|
|
27
46
|
</script>
|
|
28
47
|
|
|
29
|
-
{#if $
|
|
48
|
+
{#if $userStore.isFetching}
|
|
30
49
|
<Spinner />
|
|
31
|
-
{:else if $
|
|
32
|
-
<Error error={$
|
|
50
|
+
{:else if $userStore.lastFetchError}
|
|
51
|
+
<Error error={$userStore.lastFetchError} />
|
|
33
52
|
{:else}
|
|
34
|
-
<
|
|
53
|
+
<UserCard user={$userStore.data} />
|
|
35
54
|
{/if}
|
|
36
55
|
```
|
|
37
56
|
|
|
38
|
-
##
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- **Reactive state management** - Subscribe to loading, error, and data states
|
|
60
|
+
- **Silent fetching** - Update data without triggering loading indicators
|
|
61
|
+
- **Fetch-once with caching** - Prevent redundant fetches within a time threshold
|
|
62
|
+
- **Polling support** - Built-in recursive fetching for real-time updates
|
|
63
|
+
- **Request deduplication** - Optionally return in-flight promises for concurrent calls
|
|
64
|
+
- **Abort support** - Cancel in-flight requests with AbortController integration
|
|
65
|
+
- **Stream support** - Handle SSE, WebSocket, or push-based data sources
|
|
66
|
+
|
|
67
|
+
## Abortable Requests
|
|
68
|
+
|
|
69
|
+
Enable automatic request cancellation when a new fetch starts:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { createFetchStore } from "@marianmeres/fetch-store";
|
|
73
|
+
|
|
74
|
+
const searchStore = createFetchStore(
|
|
75
|
+
async (query: string, signal: AbortSignal) => {
|
|
76
|
+
const response = await fetch(`/api/search?q=${query}`, { signal });
|
|
77
|
+
if (!response.ok) throw new Error("Search failed");
|
|
78
|
+
return await response.json();
|
|
79
|
+
},
|
|
80
|
+
null,
|
|
81
|
+
null,
|
|
82
|
+
{ abortable: true }
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Rapid calls - each new fetch aborts the previous one
|
|
86
|
+
searchStore.fetch("h"); // Aborted
|
|
87
|
+
searchStore.fetch("he"); // Aborted
|
|
88
|
+
searchStore.fetch("hel"); // Aborted
|
|
89
|
+
searchStore.fetch("hello"); // This one completes
|
|
90
|
+
|
|
91
|
+
// Manual abort
|
|
92
|
+
searchStore.abort();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Request Deduplication
|
|
96
|
+
|
|
97
|
+
Prevent duplicate requests when multiple calls happen while one is in-flight:
|
|
39
98
|
|
|
40
99
|
```typescript
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// optional, used to modify data returned by fetchWorker... (usefull for various
|
|
47
|
-
// data update strategies, like merge/deepmerge/set etc)
|
|
48
|
-
dataFactory?: (raw: any, old?: any) => T = null,
|
|
49
|
-
// options (see source)
|
|
50
|
-
options? = null
|
|
100
|
+
const userStore = createFetchStore(
|
|
101
|
+
async (userId) => fetch(`/api/users/${userId}`).then(r => r.json()),
|
|
102
|
+
null,
|
|
103
|
+
null,
|
|
104
|
+
{ dedupeInflight: true }
|
|
51
105
|
);
|
|
52
106
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
107
|
+
// These all return the same promise (only one HTTP request is made)
|
|
108
|
+
const p1 = userStore.fetch("123");
|
|
109
|
+
const p2 = userStore.fetch("123");
|
|
110
|
+
const p3 = userStore.fetch("123");
|
|
111
|
+
|
|
112
|
+
p1 === p2 && p2 === p3; // true
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Streaming Data
|
|
116
|
+
|
|
117
|
+
For streaming data sources, use `createFetchStreamStore`:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { createFetchStreamStore } from "@marianmeres/fetch-store";
|
|
121
|
+
|
|
122
|
+
const sseStore = createFetchStreamStore((emit, url) => {
|
|
123
|
+
const eventSource = new EventSource(url);
|
|
124
|
+
|
|
125
|
+
eventSource.onmessage = (event) => {
|
|
126
|
+
emit("data", JSON.parse(event.data));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
eventSource.onerror = (error) => {
|
|
130
|
+
emit("error", error);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Return cleanup function
|
|
134
|
+
return () => {
|
|
135
|
+
eventSource.close();
|
|
136
|
+
emit("end");
|
|
137
|
+
};
|
|
64
138
|
});
|
|
65
139
|
|
|
66
|
-
//
|
|
140
|
+
// Start streaming (with optional auto-restart after 5s delay)
|
|
141
|
+
const stop = sseStore.fetchStream(["/api/events"], 5000);
|
|
142
|
+
|
|
143
|
+
// Later: stop the stream
|
|
144
|
+
stop();
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Breaking Changes (v3.0.0)
|
|
148
|
+
|
|
149
|
+
This major release includes the following breaking changes:
|
|
150
|
+
|
|
151
|
+
### Simplified Generic Types
|
|
152
|
+
|
|
153
|
+
The `FetchStore<T>` and `FetchStreamStore<T>` interfaces have been simplified from a
|
|
154
|
+
two-generic pattern to a single generic:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// Before (v2.x)
|
|
158
|
+
interface FetchStore<T, V = T extends FetchStoreValue<infer D> ? D : T> { ... }
|
|
159
|
+
|
|
160
|
+
// After (v3.x)
|
|
161
|
+
interface FetchStore<T> extends StoreReadable<FetchStoreValue<T>> { ... }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Migration:** If you were using explicit generic parameters, simply use the data type
|
|
165
|
+
directly: `FetchStore<User>` instead of `FetchStore<FetchStoreValue<User>, User>`.
|
|
166
|
+
|
|
167
|
+
### Simplified API Signature
|
|
67
168
|
|
|
68
|
-
|
|
69
|
-
|
|
169
|
+
The `createFetchStore` and `createFetchStreamStore` functions now have a simpler signature.
|
|
170
|
+
The `dataFactory` parameter has been moved into the `options` object:
|
|
70
171
|
|
|
71
|
-
|
|
72
|
-
|
|
172
|
+
```typescript
|
|
173
|
+
// Before (v2.x)
|
|
174
|
+
createFetchStore(fetchWorker, initial, dataFactory, options)
|
|
73
175
|
|
|
74
|
-
//
|
|
75
|
-
|
|
176
|
+
// After (v3.x)
|
|
177
|
+
createFetchStore(fetchWorker, initial, options)
|
|
178
|
+
// dataFactory is now in options: { dataFactory: (data, old) => transformedData }
|
|
179
|
+
```
|
|
76
180
|
|
|
77
|
-
|
|
78
|
-
store.reset();
|
|
79
|
-
store.resetError();
|
|
181
|
+
**Migration:** Move your `dataFactory` from the third parameter into the options object:
|
|
80
182
|
|
|
81
|
-
|
|
82
|
-
|
|
183
|
+
```typescript
|
|
184
|
+
// Before
|
|
185
|
+
const store = createFetchStore(
|
|
186
|
+
async () => fetchData(),
|
|
187
|
+
null,
|
|
188
|
+
(data, old) => ({ ...old, ...data }),
|
|
189
|
+
{ dedupeInflight: true }
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// After
|
|
193
|
+
const store = createFetchStore(
|
|
194
|
+
async () => fetchData(),
|
|
195
|
+
null,
|
|
196
|
+
{
|
|
197
|
+
dataFactory: (data, old) => ({ ...old, ...data }),
|
|
198
|
+
dedupeInflight: true
|
|
199
|
+
}
|
|
200
|
+
);
|
|
83
201
|
```
|
|
202
|
+
|
|
203
|
+
### DataFactory Type Change
|
|
204
|
+
|
|
205
|
+
The `DataFactory<T>` type now receives properly typed data instead of `unknown`:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// Before (v2.x)
|
|
209
|
+
type DataFactory<T> = (raw: any, old?: T) => T;
|
|
210
|
+
|
|
211
|
+
// After (v3.x)
|
|
212
|
+
type DataFactory<T> = (data: T, old?: T) => T;
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The `fetchWorker` must now return `Promise<T>` directly, making the types consistent
|
|
216
|
+
throughout the API.
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
## API Reference
|
|
220
|
+
|
|
221
|
+
### `createFetchStore<T>(fetchWorker, initial?, options?)`
|
|
222
|
+
|
|
223
|
+
Creates a reactive store for async fetch operations.
|
|
224
|
+
|
|
225
|
+
**Parameters:**
|
|
226
|
+
|
|
227
|
+
| Parameter | Type | Default | Description |
|
|
228
|
+
|-----------|------|---------|-------------|
|
|
229
|
+
| `fetchWorker` | `(...args) => Promise<T>` | required | Async function performing the fetch. Must return data of type T. When `abortable: true`, receives `AbortSignal` as the last argument. |
|
|
230
|
+
| `initial` | `T \| null` | `null` | Initial data value |
|
|
231
|
+
| `options` | `FetchStoreOptions<T>` | `{}` | Configuration options |
|
|
232
|
+
|
|
233
|
+
**Options:**
|
|
234
|
+
|
|
235
|
+
| Option | Type | Default | Description |
|
|
236
|
+
|--------|------|---------|-------------|
|
|
237
|
+
| `fetchOnceDefaultThresholdMs` | `number` | `300000` | Default threshold (5 min) for `fetchOnce` before allowing re-fetch |
|
|
238
|
+
| `dedupeInflight` | `boolean` | `false` | If true, concurrent `fetch()` calls return the same promise |
|
|
239
|
+
| `abortable` | `boolean` | `false` | If true, creates AbortController for each fetch, aborting previous requests |
|
|
240
|
+
| `dataFactory` | `(data: T, old?: T) => T` | - | Transform fetched data (useful for merge strategies) |
|
|
241
|
+
| `onReset` | `() => void` | - | Callback invoked when `reset()` is called |
|
|
242
|
+
|
|
243
|
+
**Returns:** `FetchStore<T>`
|
|
244
|
+
|
|
245
|
+
### FetchStore Methods
|
|
246
|
+
|
|
247
|
+
| Method | Signature | Description |
|
|
248
|
+
|--------|-----------|-------------|
|
|
249
|
+
| `subscribe` | `(fn: (value) => void) => () => void` | Subscribe to store changes (Svelte-compatible) |
|
|
250
|
+
| `get` | `() => FetchStoreValue<T>` | Get current store value |
|
|
251
|
+
| `fetch` | `(...args) => Promise<T \| null>` | Execute fetch, sets `isFetching` to true |
|
|
252
|
+
| `fetchSilent` | `(...args) => Promise<T \| null>` | Execute fetch without updating `isFetching` |
|
|
253
|
+
| `fetchOnce` | `(args?, thresholdMs?) => Promise<T \| null>` | Fetch only if not already fetched or threshold passed |
|
|
254
|
+
| `fetchOnceSilent` | `(args?, thresholdMs?) => Promise<T \| null>` | Silent version of `fetchOnce` |
|
|
255
|
+
| `fetchRecursive` | `(args?, delayMs?) => () => void` | Start polling, returns cancel function |
|
|
256
|
+
| `reset` | `() => void` | Reset store to initial state |
|
|
257
|
+
| `resetError` | `() => void` | Clear `lastFetchError` only |
|
|
258
|
+
| `touch` | `(data?: T) => void` | Update timestamps (tricks `fetchOnce`), optionally set data |
|
|
259
|
+
| `abort` | `() => void` | Abort in-flight requests (requires `abortable: true`) |
|
|
260
|
+
| `getInternalDataStore` | `() => StoreLike<T>` | Access internal data store |
|
|
261
|
+
|
|
262
|
+
### FetchStoreValue Properties
|
|
263
|
+
|
|
264
|
+
The store value contains both data and metadata:
|
|
265
|
+
|
|
266
|
+
| Property | Type | Description |
|
|
267
|
+
|----------|------|-------------|
|
|
268
|
+
| `data` | `T \| null` | The fetched data (null before first fetch or after reset) |
|
|
269
|
+
| `isFetching` | `boolean` | Whether a fetch is in progress |
|
|
270
|
+
| `lastFetchStart` | `Date \| null` | When the last fetch started |
|
|
271
|
+
| `lastFetchEnd` | `Date \| null` | When the last fetch completed |
|
|
272
|
+
| `lastFetchError` | `Error \| null` | Error from the last fetch, if any |
|
|
273
|
+
| `lastFetchSilentError` | `Error \| null` | Error from the last silent fetch, if any |
|
|
274
|
+
| `successCounter` | `number` | Number of successful fetches |
|
|
275
|
+
|
|
276
|
+
### `createFetchStreamStore<T>(fetchStreamWorker, initial?, options?)`
|
|
277
|
+
|
|
278
|
+
Creates a reactive store for streaming data sources.
|
|
279
|
+
|
|
280
|
+
**Parameters:**
|
|
281
|
+
|
|
282
|
+
| Parameter | Type | Default | Description |
|
|
283
|
+
|-----------|------|---------|-------------|
|
|
284
|
+
| `fetchStreamWorker` | `(emit, ...args) => (() => void) \| void` | required | Worker receiving emit callback for events. Emit receives data of type T. Should return cleanup function. |
|
|
285
|
+
| `initial` | `T \| null` | `null` | Initial data value |
|
|
286
|
+
| `options` | `FetchStreamStoreOptions<T>` | `{}` | Configuration options |
|
|
287
|
+
|
|
288
|
+
**Options:**
|
|
289
|
+
|
|
290
|
+
| Option | Type | Default | Description |
|
|
291
|
+
|--------|------|---------|-------------|
|
|
292
|
+
| `dataFactory` | `(data: T, old?: T) => T` | - | Transform received data (useful for merge strategies) |
|
|
293
|
+
| `onReset` | `() => void` | - | Callback invoked when `reset()` is called |
|
|
294
|
+
|
|
295
|
+
**Emit Events:**
|
|
296
|
+
- `emit("data", value)` - Push new data
|
|
297
|
+
- `emit("error", error)` - Report an error
|
|
298
|
+
- `emit("end")` - Signal stream completion
|
|
299
|
+
|
|
300
|
+
**Returns:** `FetchStreamStore<T>`
|
|
301
|
+
|
|
302
|
+
### FetchStreamStore Methods
|
|
303
|
+
|
|
304
|
+
| Method | Signature | Description |
|
|
305
|
+
|--------|-----------|-------------|
|
|
306
|
+
| `subscribe` | `(fn: (value) => void) => () => void` | Subscribe to store changes |
|
|
307
|
+
| `get` | `() => FetchStreamStoreValue<T>` | Get current store value |
|
|
308
|
+
| `fetchStream` | `(args?, recursiveDelayMs?) => () => void` | Start stream, returns stop function. If `recursiveDelayMs > 0`, restarts after "end" event. |
|
|
309
|
+
| `reset` | `() => void` | Reset store to initial state |
|
|
310
|
+
| `resetError` | `() => void` | Clear `lastFetchError` only |
|
|
311
|
+
| `getInternalDataStore` | `() => StoreLike<T>` | Access internal data store |
|
|
312
|
+
|
|
313
|
+
## Package Identity
|
|
314
|
+
|
|
315
|
+
- **Name:** @marianmeres/fetch-store
|
|
316
|
+
- **Author:** Marian Meres
|
|
317
|
+
- **Repository:** https://github.com/marianmeres/fetch-store
|
|
318
|
+
- **License:** MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type { DataFactory, FetchStore, FetchStoreMeta, FetchStoreOptions, FetchStoreValue, } from "./types.js";
|
|
2
|
+
export { createFetchStreamStore } from "./fetch-stream-store.js";
|
|
3
|
+
import type { FetchStore, FetchStoreOptions } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Creates a reactive store for managing async fetch operations with built-in
|
|
6
|
+
* state tracking for loading, errors, and success counts.
|
|
7
|
+
*
|
|
8
|
+
* @template T - The type of data the store will hold
|
|
9
|
+
* @param fetchWorker - Async function that performs the actual fetch operation.
|
|
10
|
+
* Must return data of type T. When `abortable: true`, receives AbortSignal as the last argument.
|
|
11
|
+
* @param initial - Initial data value (default: null)
|
|
12
|
+
* @param options - Configuration options including optional dataFactory for merge strategies
|
|
13
|
+
* @returns A FetchStore instance with reactive subscription and fetch methods
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const userStore = createFetchStore(
|
|
18
|
+
* async (userId: string) => {
|
|
19
|
+
* const res = await fetch(`/api/users/${userId}`);
|
|
20
|
+
* return res.json() as Promise<User>;
|
|
21
|
+
* },
|
|
22
|
+
* null,
|
|
23
|
+
* { fetchOnceDefaultThresholdMs: 60000 }
|
|
24
|
+
* );
|
|
25
|
+
*
|
|
26
|
+
* userStore.subscribe(({ data, isFetching, lastFetchError }) => {
|
|
27
|
+
* console.log({ data, isFetching, lastFetchError });
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* await userStore.fetch("123");
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare const createFetchStore: <T>(fetchWorker: (...args: unknown[]) => Promise<T>, initial?: T | null, options?: Partial<FetchStoreOptions<T>>) => FetchStore<T>;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { createDerivedStore, createStore } from "@marianmeres/store";
|
|
2
|
+
// Re-export stream store for backwards compatibility
|
|
3
|
+
export { createFetchStreamStore } from "./fetch-stream-store.js";
|
|
4
|
+
const DEFAULT_OPTIONS = {
|
|
5
|
+
fetchOnceDefaultThresholdMs: 300_000, // 5 minutes
|
|
6
|
+
dedupeInflight: false,
|
|
7
|
+
abortable: false,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Creates a reactive store for managing async fetch operations with built-in
|
|
11
|
+
* state tracking for loading, errors, and success counts.
|
|
12
|
+
*
|
|
13
|
+
* @template T - The type of data the store will hold
|
|
14
|
+
* @param fetchWorker - Async function that performs the actual fetch operation.
|
|
15
|
+
* Must return data of type T. When `abortable: true`, receives AbortSignal as the last argument.
|
|
16
|
+
* @param initial - Initial data value (default: null)
|
|
17
|
+
* @param options - Configuration options including optional dataFactory for merge strategies
|
|
18
|
+
* @returns A FetchStore instance with reactive subscription and fetch methods
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const userStore = createFetchStore(
|
|
23
|
+
* async (userId: string) => {
|
|
24
|
+
* const res = await fetch(`/api/users/${userId}`);
|
|
25
|
+
* return res.json() as Promise<User>;
|
|
26
|
+
* },
|
|
27
|
+
* null,
|
|
28
|
+
* { fetchOnceDefaultThresholdMs: 60000 }
|
|
29
|
+
* );
|
|
30
|
+
*
|
|
31
|
+
* userStore.subscribe(({ data, isFetching, lastFetchError }) => {
|
|
32
|
+
* console.log({ data, isFetching, lastFetchError });
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* await userStore.fetch("123");
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export const createFetchStore = (fetchWorker, initial = null, options = {}) => {
|
|
39
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
40
|
+
const { fetchOnceDefaultThresholdMs, dedupeInflight, abortable } = mergedOptions;
|
|
41
|
+
const dataFactory = options.dataFactory;
|
|
42
|
+
// Use factory for data transformation strategies (merge/deepmerge/set/...)
|
|
43
|
+
const _createData = (data, old) => typeof dataFactory === "function" ? dataFactory(data, old ?? undefined) : data;
|
|
44
|
+
const _createMetaObj = () => ({
|
|
45
|
+
isFetching: false,
|
|
46
|
+
lastFetchStart: null,
|
|
47
|
+
lastFetchEnd: null,
|
|
48
|
+
lastFetchError: null,
|
|
49
|
+
successCounter: 0,
|
|
50
|
+
lastFetchSilentError: null,
|
|
51
|
+
});
|
|
52
|
+
const _dataStore = createStore(initial);
|
|
53
|
+
const _metaStore = createStore(_createMetaObj());
|
|
54
|
+
const { subscribe, get } = createDerivedStore([_dataStore, _metaStore], ([data, meta]) => ({ data, ...meta }));
|
|
55
|
+
// In-flight promise for deduplication
|
|
56
|
+
let _inflightPromise = null;
|
|
57
|
+
let _inflightSilentPromise = null;
|
|
58
|
+
// AbortController for cancellation
|
|
59
|
+
let _abortController = null;
|
|
60
|
+
let _abortControllerSilent = null;
|
|
61
|
+
const abort = () => {
|
|
62
|
+
if (_abortController) {
|
|
63
|
+
_abortController.abort();
|
|
64
|
+
_abortController = null;
|
|
65
|
+
}
|
|
66
|
+
if (_abortControllerSilent) {
|
|
67
|
+
_abortControllerSilent.abort();
|
|
68
|
+
_abortControllerSilent = null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const fetch = (...rest) => {
|
|
72
|
+
// If deduplication is enabled and there's an in-flight request, return it
|
|
73
|
+
if (dedupeInflight && _inflightPromise) {
|
|
74
|
+
return _inflightPromise;
|
|
75
|
+
}
|
|
76
|
+
// If abortable, abort any previous request and create new controller
|
|
77
|
+
if (abortable) {
|
|
78
|
+
if (_abortController) {
|
|
79
|
+
_abortController.abort();
|
|
80
|
+
}
|
|
81
|
+
_abortController = new AbortController();
|
|
82
|
+
}
|
|
83
|
+
const currentController = _abortController;
|
|
84
|
+
const doFetch = async () => {
|
|
85
|
+
_metaStore.set({
|
|
86
|
+
..._metaStore.get(),
|
|
87
|
+
isFetching: true,
|
|
88
|
+
lastFetchStart: new Date(),
|
|
89
|
+
lastFetchEnd: null,
|
|
90
|
+
lastFetchError: null,
|
|
91
|
+
});
|
|
92
|
+
let error = null;
|
|
93
|
+
let newSuccessCounter = _metaStore.get().successCounter;
|
|
94
|
+
try {
|
|
95
|
+
// Pass signal as the last argument if abortable
|
|
96
|
+
const args = abortable && currentController
|
|
97
|
+
? [...rest, currentController.signal]
|
|
98
|
+
: rest;
|
|
99
|
+
_dataStore.set(_createData(await fetchWorker(...args), _dataStore.get()));
|
|
100
|
+
newSuccessCounter++;
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
// Don't treat abort as an error that should be stored
|
|
104
|
+
if (e instanceof DOMException && e.name === "AbortError") {
|
|
105
|
+
// Request was aborted, don't update meta
|
|
106
|
+
return _dataStore.get();
|
|
107
|
+
}
|
|
108
|
+
error = e instanceof Error ? e : new Error(String(e));
|
|
109
|
+
}
|
|
110
|
+
_metaStore.set({
|
|
111
|
+
..._metaStore.get(),
|
|
112
|
+
isFetching: false,
|
|
113
|
+
lastFetchEnd: new Date(),
|
|
114
|
+
lastFetchError: error,
|
|
115
|
+
successCounter: newSuccessCounter,
|
|
116
|
+
});
|
|
117
|
+
// Return fetched (or last) data (for non-subscribe consumption)
|
|
118
|
+
return _metaStore.get().lastFetchError ? null : _dataStore.get();
|
|
119
|
+
};
|
|
120
|
+
if (dedupeInflight) {
|
|
121
|
+
_inflightPromise = doFetch().finally(() => {
|
|
122
|
+
_inflightPromise = null;
|
|
123
|
+
if (currentController === _abortController) {
|
|
124
|
+
_abortController = null;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return _inflightPromise;
|
|
128
|
+
}
|
|
129
|
+
return doFetch().finally(() => {
|
|
130
|
+
if (currentController === _abortController) {
|
|
131
|
+
_abortController = null;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
// Similar to fetch, but does not touch meta.isFetching, allowing data updates
|
|
136
|
+
// without triggering loading spinners
|
|
137
|
+
const fetchSilent = (...rest) => {
|
|
138
|
+
// If deduplication is enabled and there's an in-flight request, return it
|
|
139
|
+
if (dedupeInflight && _inflightSilentPromise) {
|
|
140
|
+
return _inflightSilentPromise;
|
|
141
|
+
}
|
|
142
|
+
// If abortable, abort any previous silent request and create new controller
|
|
143
|
+
if (abortable) {
|
|
144
|
+
if (_abortControllerSilent) {
|
|
145
|
+
_abortControllerSilent.abort();
|
|
146
|
+
}
|
|
147
|
+
_abortControllerSilent = new AbortController();
|
|
148
|
+
}
|
|
149
|
+
const currentController = _abortControllerSilent;
|
|
150
|
+
const doFetchSilent = async () => {
|
|
151
|
+
const currentMeta = _metaStore.get();
|
|
152
|
+
if (currentMeta.lastFetchSilentError) {
|
|
153
|
+
_metaStore.set({ ...currentMeta, lastFetchSilentError: null });
|
|
154
|
+
}
|
|
155
|
+
let error = null;
|
|
156
|
+
try {
|
|
157
|
+
// Pass signal as the last argument if abortable
|
|
158
|
+
const args = abortable && currentController
|
|
159
|
+
? [...rest, currentController.signal]
|
|
160
|
+
: rest;
|
|
161
|
+
_dataStore.set(_createData(await fetchWorker(...args), _dataStore.get()));
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
// Don't treat abort as an error that should be stored
|
|
165
|
+
if (e instanceof DOMException && e.name === "AbortError") {
|
|
166
|
+
return _dataStore.get();
|
|
167
|
+
}
|
|
168
|
+
error = e instanceof Error ? e : new Error(String(e));
|
|
169
|
+
}
|
|
170
|
+
if (error) {
|
|
171
|
+
_metaStore.set({ ..._metaStore.get(), lastFetchSilentError: error });
|
|
172
|
+
}
|
|
173
|
+
// Return fetched (or last) data (for non-subscribe consumption)
|
|
174
|
+
return _metaStore.get().lastFetchSilentError ? null : _dataStore.get();
|
|
175
|
+
};
|
|
176
|
+
if (dedupeInflight) {
|
|
177
|
+
_inflightSilentPromise = doFetchSilent().finally(() => {
|
|
178
|
+
_inflightSilentPromise = null;
|
|
179
|
+
if (currentController === _abortControllerSilent) {
|
|
180
|
+
_abortControllerSilent = null;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return _inflightSilentPromise;
|
|
184
|
+
}
|
|
185
|
+
return doFetchSilent().finally(() => {
|
|
186
|
+
if (currentController === _abortControllerSilent) {
|
|
187
|
+
_abortControllerSilent = null;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
// Sets internal timestamps to now, tricking fetchOnce into thinking data was just fetched.
|
|
192
|
+
// Useful for external cache invalidation.
|
|
193
|
+
const touch = (data) => {
|
|
194
|
+
if (data !== undefined) {
|
|
195
|
+
_dataStore.set(data);
|
|
196
|
+
}
|
|
197
|
+
const currentMeta = _metaStore.get();
|
|
198
|
+
const now = new Date();
|
|
199
|
+
_metaStore.set({
|
|
200
|
+
...currentMeta,
|
|
201
|
+
lastFetchStart: now,
|
|
202
|
+
lastFetchEnd: now,
|
|
203
|
+
successCounter: currentMeta.successCounter + 1,
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
const _fetchOnce = async (isSilent, fetchArgs = [], thresholdMs = fetchOnceDefaultThresholdMs) => {
|
|
207
|
+
const { successCounter, isFetching, lastFetchStart } = _metaStore.get();
|
|
208
|
+
const args = Array.isArray(fetchArgs) ? fetchArgs : [fetchArgs];
|
|
209
|
+
if (!successCounter && !isFetching) {
|
|
210
|
+
return isSilent ? await fetchSilent(...args) : await fetch(...args);
|
|
211
|
+
}
|
|
212
|
+
// Expired threshold?
|
|
213
|
+
if (thresholdMs &&
|
|
214
|
+
!isFetching &&
|
|
215
|
+
lastFetchStart &&
|
|
216
|
+
Date.now() - lastFetchStart.valueOf() > thresholdMs) {
|
|
217
|
+
return isSilent ? await fetchSilent(...args) : await fetch(...args);
|
|
218
|
+
}
|
|
219
|
+
return _dataStore.get();
|
|
220
|
+
};
|
|
221
|
+
// Use falsy threshold (0) to skip threshold check
|
|
222
|
+
const fetchOnce = async (fetchArgs = [], thresholdMs = fetchOnceDefaultThresholdMs) => {
|
|
223
|
+
return await _fetchOnce(false, fetchArgs, thresholdMs);
|
|
224
|
+
};
|
|
225
|
+
const fetchOnceSilent = async (fetchArgs = [], thresholdMs = fetchOnceDefaultThresholdMs) => {
|
|
226
|
+
return await _fetchOnce(true, fetchArgs, thresholdMs);
|
|
227
|
+
};
|
|
228
|
+
// Recursive polling (whether it's "long" or "short" polling depends on the server)
|
|
229
|
+
const fetchRecursive = (fetchArgs = [], delayMs = 500) => {
|
|
230
|
+
let _timer;
|
|
231
|
+
let _aborted = false;
|
|
232
|
+
const _fetchRecursive = (args = [], delay = 500) => {
|
|
233
|
+
const normalizedArgs = Array.isArray(args) ? args : [args];
|
|
234
|
+
const resolvedDelay = typeof delay === "function" ? delay() : delay;
|
|
235
|
+
fetchSilent(...normalizedArgs).then(() => {
|
|
236
|
+
if (_timer)
|
|
237
|
+
clearTimeout(_timer);
|
|
238
|
+
if (resolvedDelay > 0 && !_aborted) {
|
|
239
|
+
_timer = setTimeout(() => !_aborted && _fetchRecursive(normalizedArgs, delay), resolvedDelay);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
// Return a "cancel" (or "stop") control fn
|
|
243
|
+
return () => {
|
|
244
|
+
if (_timer)
|
|
245
|
+
clearTimeout(_timer);
|
|
246
|
+
_aborted = true;
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
return _fetchRecursive(fetchArgs, delayMs);
|
|
250
|
+
};
|
|
251
|
+
// Resets the store to initial state; useful for cache invalidation
|
|
252
|
+
const reset = () => {
|
|
253
|
+
abort();
|
|
254
|
+
_dataStore.set(initial);
|
|
255
|
+
_metaStore.set(_createMetaObj());
|
|
256
|
+
_inflightPromise = null;
|
|
257
|
+
_inflightSilentPromise = null;
|
|
258
|
+
if (typeof mergedOptions.onReset === "function")
|
|
259
|
+
mergedOptions.onReset();
|
|
260
|
+
};
|
|
261
|
+
const resetError = () => {
|
|
262
|
+
_metaStore.update((old) => ({ ...old, lastFetchError: null }));
|
|
263
|
+
};
|
|
264
|
+
return {
|
|
265
|
+
subscribe,
|
|
266
|
+
get,
|
|
267
|
+
fetch,
|
|
268
|
+
fetchSilent,
|
|
269
|
+
fetchOnce,
|
|
270
|
+
fetchOnceSilent,
|
|
271
|
+
fetchRecursive,
|
|
272
|
+
reset,
|
|
273
|
+
resetError,
|
|
274
|
+
getInternalDataStore: () => _dataStore,
|
|
275
|
+
touch,
|
|
276
|
+
abort,
|
|
277
|
+
// Expose raw worker as well
|
|
278
|
+
fetchWorker,
|
|
279
|
+
};
|
|
280
|
+
};
|
|
@@ -1,30 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
};
|
|
30
|
-
export {};
|
|
1
|
+
export type { FetchStreamEventEmitFn, FetchStreamEventName, FetchStreamStore, FetchStreamStoreMeta, FetchStreamStoreOptions, FetchStreamStoreValue, } from "./types.js";
|
|
2
|
+
import type { FetchStreamEventName, FetchStreamStore, FetchStreamStoreOptions } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a reactive store for handling streaming data sources like SSE, WebSocket,
|
|
5
|
+
* or any push-based data pattern.
|
|
6
|
+
*
|
|
7
|
+
* @template T - The type of data the store will hold
|
|
8
|
+
* @param fetchStreamWorker - Worker function that receives an emit callback for sending events.
|
|
9
|
+
* The emit callback receives data of type T. Should return a cleanup/abort function, or void
|
|
10
|
+
* if no cleanup is needed.
|
|
11
|
+
* @param initial - Initial data value (default: null)
|
|
12
|
+
* @param options - Configuration options including optional dataFactory for merge strategies
|
|
13
|
+
* @returns A FetchStreamStore instance with reactive subscription and stream control
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const sseStore = createFetchStreamStore<Message>((emit, url) => {
|
|
18
|
+
* const eventSource = new EventSource(url);
|
|
19
|
+
* eventSource.onmessage = (e) => emit("data", JSON.parse(e.data));
|
|
20
|
+
* eventSource.onerror = (e) => emit("error", e);
|
|
21
|
+
* return () => eventSource.close();
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* const stop = sseStore.fetchStream(["/api/events"]);
|
|
25
|
+
* // Later: stop() to close the connection
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare const createFetchStreamStore: <T>(fetchStreamWorker: (emit: (eventName: FetchStreamEventName, eventData?: T | Error) => void, ...args: unknown[]) => (() => void) | void, initial?: T | null, options?: Partial<FetchStreamStoreOptions<T>>) => FetchStreamStore<T>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createDerivedStore, createStore } from "@marianmeres/store";
|
|
2
|
+
const DEFAULT_OPTIONS = {};
|
|
3
|
+
/**
|
|
4
|
+
* Creates a reactive store for handling streaming data sources like SSE, WebSocket,
|
|
5
|
+
* or any push-based data pattern.
|
|
6
|
+
*
|
|
7
|
+
* @template T - The type of data the store will hold
|
|
8
|
+
* @param fetchStreamWorker - Worker function that receives an emit callback for sending events.
|
|
9
|
+
* The emit callback receives data of type T. Should return a cleanup/abort function, or void
|
|
10
|
+
* if no cleanup is needed.
|
|
11
|
+
* @param initial - Initial data value (default: null)
|
|
12
|
+
* @param options - Configuration options including optional dataFactory for merge strategies
|
|
13
|
+
* @returns A FetchStreamStore instance with reactive subscription and stream control
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const sseStore = createFetchStreamStore<Message>((emit, url) => {
|
|
18
|
+
* const eventSource = new EventSource(url);
|
|
19
|
+
* eventSource.onmessage = (e) => emit("data", JSON.parse(e.data));
|
|
20
|
+
* eventSource.onerror = (e) => emit("error", e);
|
|
21
|
+
* return () => eventSource.close();
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* const stop = sseStore.fetchStream(["/api/events"]);
|
|
25
|
+
* // Later: stop() to close the connection
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export const createFetchStreamStore = (fetchStreamWorker, initial = null, options = {}) => {
|
|
29
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
30
|
+
const dataFactory = options.dataFactory;
|
|
31
|
+
// Use factory for data transformation strategies (merge/deepmerge/set/...)
|
|
32
|
+
const _createData = (data, old) => typeof dataFactory === "function" ? dataFactory(data, old ?? undefined) : data;
|
|
33
|
+
const _createMetaObj = () => ({
|
|
34
|
+
isFetching: false,
|
|
35
|
+
lastFetchStart: null,
|
|
36
|
+
lastFetchEnd: null,
|
|
37
|
+
lastFetchError: null,
|
|
38
|
+
});
|
|
39
|
+
const _dataStore = createStore(initial);
|
|
40
|
+
const _metaStore = createStore(_createMetaObj());
|
|
41
|
+
const { subscribe, get } = createDerivedStore([_dataStore, _metaStore], ([data, meta]) => ({ data, ...meta }));
|
|
42
|
+
const fetchStream = (fetchArgs = [], recursiveDelayMs = 0) => {
|
|
43
|
+
let _timer;
|
|
44
|
+
let _aborted = false;
|
|
45
|
+
let _abortFn;
|
|
46
|
+
// Must be hoisted so recursive calls can be cancelled properly
|
|
47
|
+
let _abort = () => {
|
|
48
|
+
if (typeof _abortFn === "function") {
|
|
49
|
+
_abortFn();
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.warn("`abort` is a noop (the fetchStreamWorker did not return a function).");
|
|
53
|
+
}
|
|
54
|
+
if (_timer)
|
|
55
|
+
clearTimeout(_timer);
|
|
56
|
+
_aborted = true;
|
|
57
|
+
};
|
|
58
|
+
// Inner worker (maybe recursive)
|
|
59
|
+
const _fetchStream = (args = [], delayMs = 0) => {
|
|
60
|
+
const normalizedArgs = Array.isArray(args) ? args : [args];
|
|
61
|
+
const delay = typeof delayMs === "function" ? delayMs() : delayMs;
|
|
62
|
+
_metaStore.set({
|
|
63
|
+
..._metaStore.get(),
|
|
64
|
+
isFetching: true,
|
|
65
|
+
lastFetchStart: new Date(),
|
|
66
|
+
lastFetchEnd: null,
|
|
67
|
+
lastFetchError: null,
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
_abortFn = fetchStreamWorker((eventName, eventData) => {
|
|
71
|
+
if (_metaStore.get().lastFetchError) {
|
|
72
|
+
_metaStore.set({ ..._metaStore.get(), lastFetchError: null });
|
|
73
|
+
}
|
|
74
|
+
if (eventName === "data" && eventData !== undefined && !(eventData instanceof Error)) {
|
|
75
|
+
_dataStore.set(_createData(eventData, _dataStore.get()));
|
|
76
|
+
}
|
|
77
|
+
else if (eventName === "error") {
|
|
78
|
+
const error = eventData instanceof Error
|
|
79
|
+
? eventData
|
|
80
|
+
: new Error(String(eventData));
|
|
81
|
+
_metaStore.set({ ..._metaStore.get(), lastFetchError: error });
|
|
82
|
+
}
|
|
83
|
+
else if (eventName === "end") {
|
|
84
|
+
_metaStore.set({
|
|
85
|
+
..._metaStore.get(),
|
|
86
|
+
isFetching: false,
|
|
87
|
+
lastFetchEnd: new Date(),
|
|
88
|
+
});
|
|
89
|
+
// Maybe recursive?
|
|
90
|
+
if (delay > 0 && !_aborted) {
|
|
91
|
+
if (_timer)
|
|
92
|
+
clearTimeout(_timer);
|
|
93
|
+
_timer = setTimeout(() => {
|
|
94
|
+
if (!_aborted) {
|
|
95
|
+
_abort = _fetchStream(normalizedArgs, delayMs);
|
|
96
|
+
}
|
|
97
|
+
}, delay);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}, ...normalizedArgs);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
104
|
+
_metaStore.set({ ..._metaStore.get(), lastFetchError: error });
|
|
105
|
+
}
|
|
106
|
+
return _abort;
|
|
107
|
+
};
|
|
108
|
+
return _fetchStream(fetchArgs, recursiveDelayMs);
|
|
109
|
+
};
|
|
110
|
+
const reset = () => {
|
|
111
|
+
_dataStore.set(initial);
|
|
112
|
+
_metaStore.set(_createMetaObj());
|
|
113
|
+
if (typeof mergedOptions.onReset === "function")
|
|
114
|
+
mergedOptions.onReset();
|
|
115
|
+
};
|
|
116
|
+
const resetError = () => {
|
|
117
|
+
_metaStore.update((old) => ({ ...old, lastFetchError: null }));
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
subscribe,
|
|
121
|
+
get,
|
|
122
|
+
fetchStream,
|
|
123
|
+
reset,
|
|
124
|
+
resetError,
|
|
125
|
+
getInternalDataStore: () => _dataStore,
|
|
126
|
+
// Expose raw worker as well
|
|
127
|
+
fetchStreamWorker,
|
|
128
|
+
};
|
|
129
|
+
};
|
package/dist/mod.d.ts
ADDED
package/dist/mod.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { CreateStoreOptions, StoreLike, StoreReadable } from "@marianmeres/store";
|
|
2
|
+
/**
|
|
3
|
+
* Factory function that transforms fetched data, useful for merge strategies.
|
|
4
|
+
* Receives the new data and optionally the previous value.
|
|
5
|
+
*
|
|
6
|
+
* @template T - The data type
|
|
7
|
+
* @param data - New data from the fetch worker
|
|
8
|
+
* @param old - Previous data value (optional, for merge strategies)
|
|
9
|
+
* @returns Transformed data of type T
|
|
10
|
+
*/
|
|
11
|
+
export type DataFactory<T> = (data: T, old?: T) => T;
|
|
12
|
+
/**
|
|
13
|
+
* Base metadata interface shared by all fetch stores.
|
|
14
|
+
* Contains common fields for tracking fetch operation state.
|
|
15
|
+
*/
|
|
16
|
+
export interface FetchMetaBase {
|
|
17
|
+
/** Whether a fetch operation is currently in progress */
|
|
18
|
+
isFetching: boolean;
|
|
19
|
+
/** Timestamp when the last fetch operation started */
|
|
20
|
+
lastFetchStart: Date | null;
|
|
21
|
+
/** Timestamp when the last fetch operation completed */
|
|
22
|
+
lastFetchEnd: Date | null;
|
|
23
|
+
/** Error from the last fetch operation, if any */
|
|
24
|
+
lastFetchError: Error | null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Extended metadata for FetchStore including success tracking and silent fetch errors.
|
|
28
|
+
*/
|
|
29
|
+
export interface FetchStoreMeta extends FetchMetaBase {
|
|
30
|
+
/** Count of successful fetch operations (incremented on each success) */
|
|
31
|
+
successCounter: number;
|
|
32
|
+
/** Error from the last silent fetch operation, if any */
|
|
33
|
+
lastFetchSilentError: Error | null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Metadata for FetchStreamStore (uses base meta fields only).
|
|
37
|
+
*/
|
|
38
|
+
export interface FetchStreamStoreMeta extends FetchMetaBase {
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Combined value type for FetchStore containing both data and metadata.
|
|
42
|
+
* This is the shape of the value returned by `get()` and passed to subscribers.
|
|
43
|
+
*
|
|
44
|
+
* @template T - The data type stored in the fetch store
|
|
45
|
+
*/
|
|
46
|
+
export interface FetchStoreValue<T> extends FetchStoreMeta {
|
|
47
|
+
/** The fetched data (null before first successful fetch or after reset) */
|
|
48
|
+
data: T | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Combined value type for FetchStreamStore containing both data and metadata.
|
|
52
|
+
* This is the shape of the value returned by `get()` and passed to subscribers.
|
|
53
|
+
*
|
|
54
|
+
* @template T - The data type stored in the stream store
|
|
55
|
+
*/
|
|
56
|
+
export interface FetchStreamStoreValue<T> extends FetchStreamStoreMeta {
|
|
57
|
+
/** The streamed data (null before first data or after reset) */
|
|
58
|
+
data: T | null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Main FetchStore interface providing reactive state management for async fetch operations.
|
|
62
|
+
* Extends StoreReadable for Svelte-compatible subscribe functionality.
|
|
63
|
+
*
|
|
64
|
+
* @template T - The data type stored in the fetch store
|
|
65
|
+
*/
|
|
66
|
+
export interface FetchStore<T> extends StoreReadable<FetchStoreValue<T>> {
|
|
67
|
+
/**
|
|
68
|
+
* Executes the fetch worker and updates store state.
|
|
69
|
+
* Sets `isFetching` to true during the operation.
|
|
70
|
+
* @param args - Arguments passed to the fetch worker
|
|
71
|
+
* @returns Promise resolving to fetched data or null on error
|
|
72
|
+
*/
|
|
73
|
+
fetch: (...args: unknown[]) => Promise<T | null>;
|
|
74
|
+
/**
|
|
75
|
+
* Like `fetch`, but does not set `isFetching` to true.
|
|
76
|
+
* Useful for background refreshes without showing loading spinners.
|
|
77
|
+
* @param args - Arguments passed to the fetch worker
|
|
78
|
+
* @returns Promise resolving to fetched data or null on error
|
|
79
|
+
*/
|
|
80
|
+
fetchSilent: (...args: unknown[]) => Promise<T | null>;
|
|
81
|
+
/**
|
|
82
|
+
* Fetches data only if not already fetched or if the threshold has passed.
|
|
83
|
+
* Useful for caching scenarios where you want to avoid redundant fetches.
|
|
84
|
+
* @param args - Arguments passed to the fetch worker
|
|
85
|
+
* @param thresholdMs - Time in ms before re-fetching is allowed (default from options)
|
|
86
|
+
* @returns Promise resolving to fetched data or null on error
|
|
87
|
+
*/
|
|
88
|
+
fetchOnce: (args?: unknown[], thresholdMs?: number) => Promise<T | null>;
|
|
89
|
+
/**
|
|
90
|
+
* Like `fetchOnce`, but uses silent fetching (no `isFetching` state change).
|
|
91
|
+
* @param args - Arguments passed to the fetch worker
|
|
92
|
+
* @param thresholdMs - Time in ms before re-fetching is allowed (default from options)
|
|
93
|
+
* @returns Promise resolving to fetched data or null on error
|
|
94
|
+
*/
|
|
95
|
+
fetchOnceSilent: (args?: unknown[], thresholdMs?: number) => Promise<T | null>;
|
|
96
|
+
/**
|
|
97
|
+
* Starts recursive polling using silent fetch.
|
|
98
|
+
* Each fetch waits for the previous one to complete before scheduling the next.
|
|
99
|
+
* @param args - Arguments passed to the fetch worker
|
|
100
|
+
* @param delayMs - Delay between fetches in ms, or function returning delay
|
|
101
|
+
* @returns Cancel function to stop the polling
|
|
102
|
+
*/
|
|
103
|
+
fetchRecursive: (args?: unknown[], delayMs?: number | (() => number)) => () => void;
|
|
104
|
+
/**
|
|
105
|
+
* Resets the store to its initial state, clearing all data and metadata.
|
|
106
|
+
* Also aborts any in-flight requests if abortable is enabled.
|
|
107
|
+
*/
|
|
108
|
+
reset: () => void;
|
|
109
|
+
/**
|
|
110
|
+
* Clears only the `lastFetchError` field without resetting other state.
|
|
111
|
+
*/
|
|
112
|
+
resetError: () => void;
|
|
113
|
+
/**
|
|
114
|
+
* Returns the internal data store for direct manipulation.
|
|
115
|
+
* Use with caution as it bypasses the fetch store's state management.
|
|
116
|
+
*/
|
|
117
|
+
getInternalDataStore: () => StoreLike<T | null>;
|
|
118
|
+
/**
|
|
119
|
+
* The raw fetch worker function passed to createFetchStore.
|
|
120
|
+
*/
|
|
121
|
+
fetchWorker: (...args: unknown[]) => Promise<unknown>;
|
|
122
|
+
/**
|
|
123
|
+
* Updates internal timestamps to trick `fetchOnce` into thinking data was just fetched.
|
|
124
|
+
* Optionally sets new data. Useful for external cache invalidation.
|
|
125
|
+
* @param data - Optional new data to set
|
|
126
|
+
*/
|
|
127
|
+
touch: (data?: T) => void;
|
|
128
|
+
/**
|
|
129
|
+
* Aborts any in-flight fetch requests.
|
|
130
|
+
* Only effective when `abortable: true` option is set.
|
|
131
|
+
*/
|
|
132
|
+
abort: () => void;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Event names that can be emitted by the stream worker.
|
|
136
|
+
* - `data`: New data chunk received
|
|
137
|
+
* - `error`: An error occurred
|
|
138
|
+
* - `end`: Stream has completed
|
|
139
|
+
*/
|
|
140
|
+
export type FetchStreamEventName = "data" | "error" | "end";
|
|
141
|
+
/**
|
|
142
|
+
* Emit function passed to the stream worker for sending events.
|
|
143
|
+
* @param eventName - The type of event being emitted
|
|
144
|
+
* @param eventData - The event payload (data chunk or error)
|
|
145
|
+
*/
|
|
146
|
+
export type FetchStreamEventEmitFn = (eventName: FetchStreamEventName, eventData?: unknown) => void;
|
|
147
|
+
/**
|
|
148
|
+
* FetchStreamStore interface for handling streaming data sources.
|
|
149
|
+
* Useful for SSE, WebSocket, or any streaming data pattern.
|
|
150
|
+
*
|
|
151
|
+
* @template T - The data type stored in the stream store
|
|
152
|
+
*/
|
|
153
|
+
export interface FetchStreamStore<T> extends StoreReadable<FetchStreamStoreValue<T>> {
|
|
154
|
+
/**
|
|
155
|
+
* Starts the stream and optionally restarts it after completion.
|
|
156
|
+
* @param args - Arguments passed to the stream worker
|
|
157
|
+
* @param recursiveDelayMs - Delay before restarting stream after "end" event (0 = no restart)
|
|
158
|
+
* @returns Cancel function to stop the stream
|
|
159
|
+
*/
|
|
160
|
+
fetchStream: (args?: unknown[], recursiveDelayMs?: number | (() => number)) => () => void;
|
|
161
|
+
/**
|
|
162
|
+
* The raw stream worker function passed to createFetchStreamStore.
|
|
163
|
+
*/
|
|
164
|
+
fetchStreamWorker: (emit: FetchStreamEventEmitFn, ...args: unknown[]) => unknown;
|
|
165
|
+
/**
|
|
166
|
+
* Resets the store to its initial state, clearing all data and metadata.
|
|
167
|
+
*/
|
|
168
|
+
reset: () => void;
|
|
169
|
+
/**
|
|
170
|
+
* Clears only the `lastFetchError` field without resetting other state.
|
|
171
|
+
*/
|
|
172
|
+
resetError: () => void;
|
|
173
|
+
/**
|
|
174
|
+
* Returns the internal data store for direct manipulation.
|
|
175
|
+
* Use with caution as it bypasses the stream store's state management.
|
|
176
|
+
*/
|
|
177
|
+
getInternalDataStore: () => StoreLike<T | null>;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Configuration options for createFetchStore.
|
|
181
|
+
* Extends CreateStoreOptions from @marianmeres/store.
|
|
182
|
+
*
|
|
183
|
+
* @template T - The data type stored in the fetch store
|
|
184
|
+
*/
|
|
185
|
+
export interface FetchStoreOptions<T> extends CreateStoreOptions<T> {
|
|
186
|
+
/** Default threshold in ms for fetchOnce before allowing re-fetch (default: 300000 = 5 min) */
|
|
187
|
+
fetchOnceDefaultThresholdMs: number;
|
|
188
|
+
/** Callback invoked when reset() is called */
|
|
189
|
+
onReset: () => void;
|
|
190
|
+
/** If true, subsequent fetch() calls while one is in-flight return the existing promise */
|
|
191
|
+
dedupeInflight: boolean;
|
|
192
|
+
/** If true, creates AbortController for each fetch, passing signal to worker and aborting previous requests */
|
|
193
|
+
abortable: boolean;
|
|
194
|
+
/** Optional factory function to transform fetched data (useful for merge strategies) */
|
|
195
|
+
dataFactory: DataFactory<T>;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Configuration options for createFetchStreamStore.
|
|
199
|
+
* Extends CreateStoreOptions from @marianmeres/store.
|
|
200
|
+
*
|
|
201
|
+
* @template T - The data type stored in the stream store
|
|
202
|
+
*/
|
|
203
|
+
export interface FetchStreamStoreOptions<T> extends CreateStoreOptions<T> {
|
|
204
|
+
/** Callback invoked when reset() is called */
|
|
205
|
+
onReset: () => void;
|
|
206
|
+
/** Optional factory function to transform streamed data (useful for merge strategies) */
|
|
207
|
+
dataFactory: DataFactory<T>;
|
|
208
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,62 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/fetch-store",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "3.0.3",
|
|
5
4
|
"type": "module",
|
|
6
|
-
"main": "
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"require": "./dist/index.cjs",
|
|
11
|
-
"import": "./dist/index.js"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"dist"
|
|
15
|
-
],
|
|
16
|
-
"types": "./dist/index.d.ts",
|
|
17
|
-
"scripts": {
|
|
18
|
-
"clean": "rimraf dist/*",
|
|
19
|
-
"prettier": "prettier --write \"{src,tests}/**/*.{js,ts,json}\"",
|
|
20
|
-
"release": "release -v minor",
|
|
21
|
-
"release:patch": "release -v patch",
|
|
22
|
-
"test": "node --loader ts-node/esm tests/index.ts",
|
|
23
|
-
"test:watch": "nodemon --delay 0.5 -q -w \"src/**/*.*\" -w \"tests/**/*.*\" -x \"node --loader ts-node/esm tests/index.ts\" --",
|
|
24
|
-
"build": "npm run clean && rollup -c",
|
|
25
|
-
"dev": "npm run clean && rollup -c -w"
|
|
26
|
-
},
|
|
5
|
+
"main": "dist/mod.js",
|
|
6
|
+
"types": "dist/mod.d.ts",
|
|
7
|
+
"author": "Marian Meres",
|
|
8
|
+
"license": "MIT",
|
|
27
9
|
"repository": {
|
|
28
10
|
"type": "git",
|
|
29
11
|
"url": "git+https://github.com/marianmeres/fetch-store.git"
|
|
30
12
|
},
|
|
31
|
-
"keywords": [
|
|
32
|
-
"interval"
|
|
33
|
-
],
|
|
34
|
-
"author": "Marian Meres <marian@meres.sk>",
|
|
35
|
-
"license": "MIT",
|
|
36
13
|
"bugs": {
|
|
37
14
|
"url": "https://github.com/marianmeres/fetch-store/issues"
|
|
38
15
|
},
|
|
39
|
-
"homepage": "https://github.com/marianmeres/fetch-store#readme",
|
|
40
|
-
"devDependencies": {
|
|
41
|
-
"@marianmeres/clog": "^1.0.0",
|
|
42
|
-
"@marianmeres/release": "^1.0.0",
|
|
43
|
-
"@marianmeres/test-runner": "^2.0.15",
|
|
44
|
-
"@rollup/plugin-commonjs": "^24.0.1",
|
|
45
|
-
"@rollup/plugin-node-resolve": "^15.0.1",
|
|
46
|
-
"@rollup/plugin-terser": "^0.4.0",
|
|
47
|
-
"@rollup/plugin-typescript": "^11.0.0",
|
|
48
|
-
"@types/node": "^18.15.1",
|
|
49
|
-
"mkdirp": "^2.1.5",
|
|
50
|
-
"nodemon": "^3.0.2",
|
|
51
|
-
"prettier": "^2.8.4",
|
|
52
|
-
"rimraf": "^4.4.0",
|
|
53
|
-
"rollup": "^3.19.1",
|
|
54
|
-
"ts-node": "^10.9.1",
|
|
55
|
-
"tslib": "^2.5.0",
|
|
56
|
-
"typescript": "^4.9.5"
|
|
57
|
-
},
|
|
58
16
|
"dependencies": {
|
|
59
|
-
"@marianmeres/pubsub": "^
|
|
60
|
-
"@marianmeres/store": "^
|
|
17
|
+
"@marianmeres/pubsub": "^2.4.1",
|
|
18
|
+
"@marianmeres/store": "^2.3.0"
|
|
61
19
|
}
|
|
62
20
|
}
|
package/dist/index.cjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";const t=t=>"function"==typeof t,e=(e,r="")=>{if(!t(e))throw new TypeError(`${r} Expecting function arg`.trim())},r=e=>t(e.subscribe),n=(r,n=null)=>{const s=e=>t(null==n?void 0:n.persist)&&n.persist(e);let c=(()=>{const t=new Map,e=e=>(t.has(e)||t.set(e,new Set),t.get(e)),r=(t,r)=>{if(!t)throw new TypeError("Expecting valid event name.");if("function"!=typeof r)throw new TypeError("Expecting valid callback function.");return e(t).add(r),()=>e(t).delete(r)};return{publish:(t,r)=>{e(t).forEach(t=>t(r))},subscribe:r,subscribeOnce:(t,e)=>{const n=r(t,t=>{e(t),n()});return n},unsubscribe:(t,r)=>{e(t).delete(r)},unsubscribeAll:e=>t.delete(e)}})(),l=r;s(l);const a=()=>l,u=t=>{l!==t&&(l=t,s(l),c.publish("change",l))};return{set:u,get:a,update:t=>{e(t,"[update]"),u(t(a()))},subscribe:t=>(e(t,"[subscribe]"),t(l),c.subscribe("change",t))}},s=(s,c,l=null)=>{const a=e=>t(null==l?void 0:l.persist)&&l.persist(e),u=n(null==l?void 0:l.initialValue),o=[];if(s.forEach(t=>{if(!r(t))throw new TypeError("Expecting array of StoreLike objects");t.subscribe(t=>o.push(t))()}),!t(c))throw new TypeError("Expecting second argument to be the derivative function");if(!c.length||c.length>2)throw new TypeError("Expecting the derivative function to have exactly 1 or 2 arguments");let i=0,h=[];const F=t=>{e(t,"[derived.subscribe]"),i++||s.forEach((t,e)=>{h.push(t.subscribe(t=>{o[e]=t,1===c.length?(u.set(c(o)),a(u.get())):c(o,t=>{u.set(t),a(u.get())})}))});const r=u.subscribe(t);return()=>{--i||(h.forEach(t=>t()),h=[]),r()}};return{get:()=>{let t;return F(e=>t=e)(),t},subscribe:F}},c=t=>"function"==typeof t,l={},a={fetchOnceDefaultThresholdMs:3e5},u=t=>"function"==typeof t;exports.createFetchStore=(t,e=null,r=null,c={})=>{const{fetchOnceDefaultThresholdMs:l}={...a,...c||{}},o=(t,e)=>u(r)?r?.(t,e):t,i=n(o(e),c),h=n({isFetching:!1,lastFetchStart:null,lastFetchEnd:null,lastFetchError:null,successCounter:0,lastFetchSilentError:null}),{subscribe:F,get:g}=s([i,h],([t,e])=>({data:t,...e})),E=async(...e)=>{let r=h.get();r.isFetching=!0,r.lastFetchStart=new Date,r.lastFetchEnd=null,r.lastFetchError=null,h.set({...r});try{i.set(o(await t(...e),i.get())),r.successCounter++}catch(t){r.lastFetchError=t}finally{r.isFetching=!1,r.lastFetchEnd=new Date}return h.set({...r}),h.get().lastFetchError?null:i.get()},f=async(...e)=>{let r=h.get(),n=0;r.lastFetchSilentError&&(h.set({...r,lastFetchSilentError:null}),n++);try{i.set(o(await t(...e),i.get()))}catch(t){r.lastFetchSilentError=t,n++}return n&&h.set({...r}),h.get().lastFetchSilentError?null:i.get()},d=async(t,e=[],r=l)=>{const{successCounter:n,isFetching:s,lastFetchStart:c}=h.get();return Array.isArray(e)||(e=[e]),n||s?r&&!s&&c&&Date.now()-new Date(c).valueOf()>r?t?await f(...e):await E(...e):i.get():t?await f(...e):await E(...e)},b=(t=[],e=500)=>{let r,n=!1;return((t=[],e=500)=>{Array.isArray(t)||(t=[t]);const s=u(e)?e():e;return f(...t).then(()=>{r&&clearTimeout(r),s>0&&!n&&(r=setTimeout(()=>!n&&b(t,e),s))}),()=>{r&&clearTimeout(r),n=!0}})(t,e)};return{subscribe:F,get:g,fetch:E,fetchSilent:f,fetchOnce:async(t=[],e=l)=>await d(!1,t,e),fetchOnceSilent:async(t=[],e=l)=>await d(!0,t,e),fetchRecursive:b,reset:()=>{i.set(o(e)),h.set({isFetching:!1,lastFetchStart:null,lastFetchEnd:null,lastFetchError:null,successCounter:0,lastFetchSilentError:null}),"function"==typeof c.onReset&&c.onReset()},resetError:()=>h.update(t=>({...t,lastFetchError:null})),getInternalDataStore:()=>i,touch:t=>{t&&i.set(t);let e=h.get();const r=new Date;h.set({...e,lastFetchStart:r,lastFetchEnd:r,successCounter:e.successCounter+1})},fetchWorker:t}},exports.createFetchStreamStore=(t,e=null,r=null,a={})=>{a={...l,...a||{}};const u=(t,e)=>c(r)?r?.(t,e):t,o=n(u(e),a),i=n({isFetching:!1,lastFetchStart:null,lastFetchEnd:null,lastFetchError:null}),{subscribe:h,get:F}=s([o,i],([t,e])=>({data:t,...e}));return{subscribe:h,get:F,fetchStream:(e=[],r=0)=>{let n,s,l=!1,a=()=>{"function"==typeof s?s():console.warn("`abort` is a noop (the fetchStreamWorker did not return a function)."),n&&clearTimeout(n),l=!0};const h=(e=[],r=0)=>{Array.isArray(e)||(e=[e]);const F=c(r)?r():r;i.update(t=>({...t,isFetching:!0,lastFetchStart:new Date,lastFetchEnd:null,lastFetchError:null}));try{s=t((t,s)=>{i.get().lastFetchError&&i.update(t=>({...t,lastFetchError:null})),"data"===t?o.set(u(s,o.get())):"error"===t?i.update(t=>({...t,lastFetchError:s})):"end"===t&&(i.update(t=>({...t,isFetching:!1,lastFetchEnd:new Date})),F>0&&!l&&(n&&clearTimeout(n),n=setTimeout(()=>{l||(a=h(e,r))},F)))},...e)}catch(t){i.update(e=>({...e,lastFetchError:t}))}return a};return h(e,r)},reset:()=>{o.set(u(e)),i.set({isFetching:!1,lastFetchStart:null,lastFetchEnd:null,lastFetchError:null}),"function"==typeof a.onReset&&a.onReset()},resetError:()=>i.update(t=>({...t,lastFetchError:null})),getInternalDataStore:()=>o,fetchStreamWorker:t}};
|
package/dist/index.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { CreateStoreOptions, StoreLike, StoreReadable } from '@marianmeres/store';
|
|
2
|
-
export { createFetchStreamStore } from './fetch-stream-store.js';
|
|
3
|
-
export interface FetchStoreMeta {
|
|
4
|
-
isFetching: boolean;
|
|
5
|
-
lastFetchStart: Date | null;
|
|
6
|
-
lastFetchEnd: Date | null;
|
|
7
|
-
lastFetchError: Error | null;
|
|
8
|
-
successCounter: number;
|
|
9
|
-
lastFetchSilentError: Error | null;
|
|
10
|
-
}
|
|
11
|
-
export interface FetchStoreValue<T> extends FetchStoreMeta {
|
|
12
|
-
data: T;
|
|
13
|
-
}
|
|
14
|
-
export interface FetchStore<T, V> extends StoreReadable<T> {
|
|
15
|
-
fetch: (...args: any[]) => Promise<V | null>;
|
|
16
|
-
fetchSilent: (...args: any[]) => Promise<V | null>;
|
|
17
|
-
fetchOnce: (args?: any[], thresholdMs?: number) => Promise<V | null>;
|
|
18
|
-
fetchOnceSilent: (args?: any[], thresholdMs?: number) => Promise<V | null>;
|
|
19
|
-
fetchRecursive: (args?: any[], delayMs?: number) => () => void;
|
|
20
|
-
reset: () => void;
|
|
21
|
-
resetError: () => void;
|
|
22
|
-
getInternalDataStore: () => StoreLike<V>;
|
|
23
|
-
fetchWorker: (...args: any[]) => Promise<any>;
|
|
24
|
-
touch: (data?: V) => void;
|
|
25
|
-
}
|
|
26
|
-
interface FetchStoreOptions<T> extends CreateStoreOptions<T> {
|
|
27
|
-
fetchOnceDefaultThresholdMs: number;
|
|
28
|
-
onReset: () => void;
|
|
29
|
-
}
|
|
30
|
-
export type DataFactory<T> = (raw: any, old?: any) => T;
|
|
31
|
-
export declare const createFetchStore: <T>(fetchWorker: (...args: any[]) => Promise<any>, initial?: T | null, dataFactory?: DataFactory<T> | null, options?: Partial<FetchStoreOptions<T>>) => FetchStore<FetchStoreValue<T>, T>;
|
package/dist/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const t=t=>"function"==typeof t,e=(e,r="")=>{if(!t(e))throw new TypeError(`${r} Expecting function arg`.trim())},r=e=>t(e.subscribe),n=(r,n=null)=>{const s=e=>t(null==n?void 0:n.persist)&&n.persist(e);let c=(()=>{const t=new Map,e=e=>(t.has(e)||t.set(e,new Set),t.get(e)),r=(t,r)=>{if(!t)throw new TypeError("Expecting valid event name.");if("function"!=typeof r)throw new TypeError("Expecting valid callback function.");return e(t).add(r),()=>e(t).delete(r)};return{publish:(t,r)=>{e(t).forEach(t=>t(r))},subscribe:r,subscribeOnce:(t,e)=>{const n=r(t,t=>{e(t),n()});return n},unsubscribe:(t,r)=>{e(t).delete(r)},unsubscribeAll:e=>t.delete(e)}})(),l=r;s(l);const a=()=>l,u=t=>{l!==t&&(l=t,s(l),c.publish("change",l))};return{set:u,get:a,update:t=>{e(t,"[update]"),u(t(a()))},subscribe:t=>(e(t,"[subscribe]"),t(l),c.subscribe("change",t))}},s=(s,c,l=null)=>{const a=e=>t(null==l?void 0:l.persist)&&l.persist(e),u=n(null==l?void 0:l.initialValue),o=[];if(s.forEach(t=>{if(!r(t))throw new TypeError("Expecting array of StoreLike objects");t.subscribe(t=>o.push(t))()}),!t(c))throw new TypeError("Expecting second argument to be the derivative function");if(!c.length||c.length>2)throw new TypeError("Expecting the derivative function to have exactly 1 or 2 arguments");let i=0,h=[];const g=t=>{e(t,"[derived.subscribe]"),i++||s.forEach((t,e)=>{h.push(t.subscribe(t=>{o[e]=t,1===c.length?(u.set(c(o)),a(u.get())):c(o,t=>{u.set(t),a(u.get())})}))});const r=u.subscribe(t);return()=>{--i||(h.forEach(t=>t()),h=[]),r()}};return{get:()=>{let t;return g(e=>t=e)(),t},subscribe:g}},c=t=>"function"==typeof t,l={},a=(t,e=null,r=null,a={})=>{a={...l,...a||{}};const u=(t,e)=>c(r)?r?.(t,e):t,o=n(u(e),a),i=n({isFetching:!1,lastFetchStart:null,lastFetchEnd:null,lastFetchError:null}),{subscribe:h,get:g}=s([o,i],([t,e])=>({data:t,...e}));return{subscribe:h,get:g,fetchStream:(e=[],r=0)=>{let n,s,l=!1,a=()=>{"function"==typeof s?s():console.warn("`abort` is a noop (the fetchStreamWorker did not return a function)."),n&&clearTimeout(n),l=!0};const h=(e=[],r=0)=>{Array.isArray(e)||(e=[e]);const g=c(r)?r():r;i.update(t=>({...t,isFetching:!0,lastFetchStart:new Date,lastFetchEnd:null,lastFetchError:null}));try{s=t((t,s)=>{i.get().lastFetchError&&i.update(t=>({...t,lastFetchError:null})),"data"===t?o.set(u(s,o.get())):"error"===t?i.update(t=>({...t,lastFetchError:s})):"end"===t&&(i.update(t=>({...t,isFetching:!1,lastFetchEnd:new Date})),g>0&&!l&&(n&&clearTimeout(n),n=setTimeout(()=>{l||(a=h(e,r))},g)))},...e)}catch(t){i.update(e=>({...e,lastFetchError:t}))}return a};return h(e,r)},reset:()=>{o.set(u(e)),i.set({isFetching:!1,lastFetchStart:null,lastFetchEnd:null,lastFetchError:null}),"function"==typeof a.onReset&&a.onReset()},resetError:()=>i.update(t=>({...t,lastFetchError:null})),getInternalDataStore:()=>o,fetchStreamWorker:t}},u={fetchOnceDefaultThresholdMs:3e5},o=t=>"function"==typeof t,i=(t,e=null,r=null,c={})=>{const{fetchOnceDefaultThresholdMs:l}={...u,...c||{}},a=(t,e)=>o(r)?r?.(t,e):t,i=n(a(e),c),h=n({isFetching:!1,lastFetchStart:null,lastFetchEnd:null,lastFetchError:null,successCounter:0,lastFetchSilentError:null}),{subscribe:g,get:E}=s([i,h],([t,e])=>({data:t,...e})),F=async(...e)=>{let r=h.get();r.isFetching=!0,r.lastFetchStart=new Date,r.lastFetchEnd=null,r.lastFetchError=null,h.set({...r});try{i.set(a(await t(...e),i.get())),r.successCounter++}catch(t){r.lastFetchError=t}finally{r.isFetching=!1,r.lastFetchEnd=new Date}return h.set({...r}),h.get().lastFetchError?null:i.get()},f=async(...e)=>{let r=h.get(),n=0;r.lastFetchSilentError&&(h.set({...r,lastFetchSilentError:null}),n++);try{i.set(a(await t(...e),i.get()))}catch(t){r.lastFetchSilentError=t,n++}return n&&h.set({...r}),h.get().lastFetchSilentError?null:i.get()},d=async(t,e=[],r=l)=>{const{successCounter:n,isFetching:s,lastFetchStart:c}=h.get();return Array.isArray(e)||(e=[e]),n||s?r&&!s&&c&&Date.now()-new Date(c).valueOf()>r?t?await f(...e):await F(...e):i.get():t?await f(...e):await F(...e)},b=(t=[],e=500)=>{let r,n=!1;return((t=[],e=500)=>{Array.isArray(t)||(t=[t]);const s=o(e)?e():e;return f(...t).then(()=>{r&&clearTimeout(r),s>0&&!n&&(r=setTimeout(()=>!n&&b(t,e),s))}),()=>{r&&clearTimeout(r),n=!0}})(t,e)};return{subscribe:g,get:E,fetch:F,fetchSilent:f,fetchOnce:async(t=[],e=l)=>await d(!1,t,e),fetchOnceSilent:async(t=[],e=l)=>await d(!0,t,e),fetchRecursive:b,reset:()=>{i.set(a(e)),h.set({isFetching:!1,lastFetchStart:null,lastFetchEnd:null,lastFetchError:null,successCounter:0,lastFetchSilentError:null}),"function"==typeof c.onReset&&c.onReset()},resetError:()=>h.update(t=>({...t,lastFetchError:null})),getInternalDataStore:()=>i,touch:t=>{t&&i.set(t);let e=h.get();const r=new Date;h.set({...e,lastFetchStart:r,lastFetchEnd:r,successCounter:e.successCounter+1})},fetchWorker:t}};export{i as createFetchStore,a as createFetchStreamStore};
|
package/dist/index.umd.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self)["fetch-store"]={})}(this,function(t){"use strict";const e=t=>"function"==typeof t,r=(t,r="")=>{if(!e(t))throw new TypeError(`${r} Expecting function arg`.trim())},n=t=>e(t.subscribe),s=(t,n=null)=>{const s=t=>e(null==n?void 0:n.persist)&&n.persist(t);let c=(()=>{const t=new Map,e=e=>(t.has(e)||t.set(e,new Set),t.get(e)),r=(t,r)=>{if(!t)throw new TypeError("Expecting valid event name.");if("function"!=typeof r)throw new TypeError("Expecting valid callback function.");return e(t).add(r),()=>e(t).delete(r)};return{publish:(t,r)=>{e(t).forEach(t=>t(r))},subscribe:r,subscribeOnce:(t,e)=>{const n=r(t,t=>{e(t),n()});return n},unsubscribe:(t,r)=>{e(t).delete(r)},unsubscribeAll:e=>t.delete(e)}})(),l=t;s(l);const a=()=>l,o=t=>{l!==t&&(l=t,s(l),c.publish("change",l))};return{set:o,get:a,update:t=>{r(t,"[update]"),o(t(a()))},subscribe:t=>(r(t,"[subscribe]"),t(l),c.subscribe("change",t))}},c=(t,c,l=null)=>{const a=t=>e(null==l?void 0:l.persist)&&l.persist(t),o=s(null==l?void 0:l.initialValue),u=[];if(t.forEach(t=>{if(!n(t))throw new TypeError("Expecting array of StoreLike objects");t.subscribe(t=>u.push(t))()}),!e(c))throw new TypeError("Expecting second argument to be the derivative function");if(!c.length||c.length>2)throw new TypeError("Expecting the derivative function to have exactly 1 or 2 arguments");let i=0,h=[];const f=e=>{r(e,"[derived.subscribe]"),i++||t.forEach((t,e)=>{h.push(t.subscribe(t=>{u[e]=t,1===c.length?(o.set(c(u)),a(o.get())):c(u,t=>{o.set(t),a(o.get())})}))});const n=o.subscribe(e);return()=>{--i||(h.forEach(t=>t()),h=[]),n()}};return{get:()=>{let t;return f(e=>t=e)(),t},subscribe:f}},l=t=>"function"==typeof t,a={},o={fetchOnceDefaultThresholdMs:3e5},u=t=>"function"==typeof t;t.createFetchStore=(t,e=null,r=null,n={})=>{const{fetchOnceDefaultThresholdMs:l}={...o,...n||{}},a=(t,e)=>u(r)?r?.(t,e):t,i=s(a(e),n),h=s({isFetching:!1,lastFetchStart:null,lastFetchEnd:null,lastFetchError:null,successCounter:0,lastFetchSilentError:null}),{subscribe:f,get:d}=c([i,h],([t,e])=>({data:t,...e})),g=async(...e)=>{let r=h.get();r.isFetching=!0,r.lastFetchStart=new Date,r.lastFetchEnd=null,r.lastFetchError=null,h.set({...r});try{i.set(a(await t(...e),i.get())),r.successCounter++}catch(t){r.lastFetchError=t}finally{r.isFetching=!1,r.lastFetchEnd=new Date}return h.set({...r}),h.get().lastFetchError?null:i.get()},F=async(...e)=>{let r=h.get(),n=0;r.lastFetchSilentError&&(h.set({...r,lastFetchSilentError:null}),n++);try{i.set(a(await t(...e),i.get()))}catch(t){r.lastFetchSilentError=t,n++}return n&&h.set({...r}),h.get().lastFetchSilentError?null:i.get()},E=async(t,e=[],r=l)=>{const{successCounter:n,isFetching:s,lastFetchStart:c}=h.get();return Array.isArray(e)||(e=[e]),n||s?r&&!s&&c&&Date.now()-new Date(c).valueOf()>r?t?await F(...e):await g(...e):i.get():t?await F(...e):await g(...e)},p=(t=[],e=500)=>{let r,n=!1;return((t=[],e=500)=>{Array.isArray(t)||(t=[t]);const s=u(e)?e():e;return F(...t).then(()=>{r&&clearTimeout(r),s>0&&!n&&(r=setTimeout(()=>!n&&p(t,e),s))}),()=>{r&&clearTimeout(r),n=!0}})(t,e)};return{subscribe:f,get:d,fetch:g,fetchSilent:F,fetchOnce:async(t=[],e=l)=>await E(!1,t,e),fetchOnceSilent:async(t=[],e=l)=>await E(!0,t,e),fetchRecursive:p,reset:()=>{i.set(a(e)),h.set({isFetching:!1,lastFetchStart:null,lastFetchEnd:null,lastFetchError:null,successCounter:0,lastFetchSilentError:null}),"function"==typeof n.onReset&&n.onReset()},resetError:()=>h.update(t=>({...t,lastFetchError:null})),getInternalDataStore:()=>i,touch:t=>{t&&i.set(t);let e=h.get();const r=new Date;h.set({...e,lastFetchStart:r,lastFetchEnd:r,successCounter:e.successCounter+1})},fetchWorker:t}},t.createFetchStreamStore=(t,e=null,r=null,n={})=>{n={...a,...n||{}};const o=(t,e)=>l(r)?r?.(t,e):t,u=s(o(e),n),i=s({isFetching:!1,lastFetchStart:null,lastFetchEnd:null,lastFetchError:null}),{subscribe:h,get:f}=c([u,i],([t,e])=>({data:t,...e}));return{subscribe:h,get:f,fetchStream:(e=[],r=0)=>{let n,s,c=!1,a=()=>{"function"==typeof s?s():console.warn("`abort` is a noop (the fetchStreamWorker did not return a function)."),n&&clearTimeout(n),c=!0};const h=(e=[],r=0)=>{Array.isArray(e)||(e=[e]);const f=l(r)?r():r;i.update(t=>({...t,isFetching:!0,lastFetchStart:new Date,lastFetchEnd:null,lastFetchError:null}));try{s=t((t,s)=>{i.get().lastFetchError&&i.update(t=>({...t,lastFetchError:null})),"data"===t?u.set(o(s,u.get())):"error"===t?i.update(t=>({...t,lastFetchError:s})):"end"===t&&(i.update(t=>({...t,isFetching:!1,lastFetchEnd:new Date})),f>0&&!c&&(n&&clearTimeout(n),n=setTimeout(()=>{c||(a=h(e,r))},f)))},...e)}catch(t){i.update(e=>({...e,lastFetchError:t}))}return a};return h(e,r)},reset:()=>{u.set(o(e)),i.set({isFetching:!1,lastFetchStart:null,lastFetchEnd:null,lastFetchError:null}),"function"==typeof n.onReset&&n.onReset()},resetError:()=>i.update(t=>({...t,lastFetchError:null})),getInternalDataStore:()=>u,fetchStreamWorker:t}}});
|