@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 CHANGED
@@ -1,83 +1,318 @@
1
1
  # @marianmeres/fetch-store
2
2
 
3
- [Store](https://github.com/marianmeres/store) utility for collecting results and meta info
4
- of any type of async work. Typically used for http requests.
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
- $ npm i @marianmeres/fetch-store
10
+ deno add jsr:@marianmeres/fetch-store
10
11
  ```
11
12
 
12
- ## Basic example
13
+ ```shell
14
+ npm i @marianmeres/fetch-store
15
+ ```
16
+
17
+ ## Basic Example
13
18
 
14
- ```javascript
15
- // foo-store.js
16
- const foo = createFetchStore(async () => {
17
- const r = await fetch('foo.json');
18
- if (!r.ok) throw new Error('Not OK!');
19
- return await r.json();
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
- ```sveltehtml
24
- <!--FooComponent.svelte-->
36
+ ### Svelte Integration
37
+
38
+ ```svelte
39
+ <!-- UserProfile.svelte -->
25
40
  <script>
26
- onMount(foo.fetch)
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 $foo.isFetching}
48
+ {#if $userStore.isFetching}
30
49
  <Spinner />
31
- {:else if $foo.lastFetchError}
32
- <Error error={$foo.lastFetchError} />
50
+ {:else if $userStore.lastFetchError}
51
+ <Error error={$userStore.lastFetchError} />
33
52
  {:else}
34
- <Foo foo={$foo.data} />
53
+ <UserCard user={$userStore.data} />
35
54
  {/if}
36
55
  ```
37
56
 
38
- ## Api
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 store = createFetchStore<T>(
42
- // the async worker
43
- fetchWorker: (...args) => Promise<any>,
44
- // optional, initial `data` value of the store
45
- initial?: T = null,
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
- // subscription
54
- store.subscribe((v) => {
55
- // main result
56
- // v.data: T;
57
-
58
- // meta
59
- // v.isFetching: boolean;
60
- // v.lastFetchStart: Date;
61
- // v.lastFetchEnd: Date;
62
- // v.lastFetchError: Date;
63
- // v.successCounter: number;
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
- // instance api
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
- // do the async work
69
- store.fetch(...args): Promise<T>;
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
- // do the async work, but do not update meta
72
- store.fetchSilent(...args): Promise<T>;
172
+ ```typescript
173
+ // Before (v2.x)
174
+ createFetchStore(fetchWorker, initial, dataFactory, options)
73
175
 
74
- // do the async work only once (if within threshold since last)
75
- store.fetchOnce(args: any[], thresholdMs: number): Promise<void>;
176
+ // After (v3.x)
177
+ createFetchStore(fetchWorker, initial, options)
178
+ // dataFactory is now in options: { dataFactory: (data, old) => transformedData }
179
+ ```
76
180
 
77
- // to reset internal meta store
78
- store.reset();
79
- store.resetError();
181
+ **Migration:** Move your `dataFactory` from the third parameter into the options object:
80
182
 
81
- // for manual hackings
82
- store.getInternalDataStore();
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
- import { CreateStoreOptions, StoreLike, StoreReadable } from '@marianmeres/store';
2
- import { DataFactory, FetchStoreValue } from './index.js';
3
- export interface FetchStreamStoreMeta {
4
- isFetching: boolean;
5
- lastFetchStart: Date | null;
6
- lastFetchEnd: Date | null;
7
- lastFetchError: Error | null;
8
- }
9
- interface FetchStreamStoreOptions<T> extends CreateStoreOptions<T> {
10
- onReset: () => void;
11
- }
12
- type FetchStreamEventName = 'data' | 'error' | 'end';
13
- type FetchStreamEventEmitFn = (eventName: FetchStreamEventName, eventData: any) => () => void;
14
- export interface FetchStreamStore<T, V> extends StoreReadable<T> {
15
- fetchStream: (args?: any[], recursiveDelayMs?: number | (() => number)) => () => void;
16
- fetchStreamWorker: (emit: FetchStreamEventEmitFn, ...args: any[]) => any;
17
- reset: () => void;
18
- resetError: () => void;
19
- getInternalDataStore: () => StoreLike<V>;
20
- }
21
- export declare const createFetchStreamStore: <T>(fetchStreamWorker: (...args: any[]) => () => void, initial?: T | null, dataFactory?: DataFactory<T> | null, options?: Partial<FetchStreamStoreOptions<T>>) => {
22
- subscribe: (cb: import("@marianmeres/store").Subscribe<FetchStoreValue<T>>) => import("@marianmeres/store").Unsubscribe;
23
- get: () => FetchStoreValue<T>;
24
- fetchStream: (fetchArgs?: any[], recursiveDelayMs?: number | (() => number)) => () => void;
25
- reset: () => void;
26
- resetError: () => void;
27
- getInternalDataStore: () => StoreLike<T>;
28
- fetchStreamWorker: (...args: any[]) => () => void;
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
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./fetch-store.js";
3
+ export * from "./fetch-stream-store.js";
package/dist/mod.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./fetch-store.js";
3
+ export * from "./fetch-stream-store.js";
@@ -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": "2.12.0",
4
- "description": "",
3
+ "version": "3.0.3",
5
4
  "type": "module",
6
- "main": "./dist/index.cjs",
7
- "module": "./dist/index.js",
8
- "browser": "./dist/index.umd.js",
9
- "exports": {
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": "^1.1.0",
60
- "@marianmeres/store": "^1.6.0"
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}}});