floppy-disk 3.2.0 → 3.2.2

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.
Files changed (2) hide show
  1. package/README.md +412 -126
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A unified state model for **sync & async** data.
4
4
 
5
- If you know [Zustand](https://zustand.docs.pmnd.rs) & [TanStack-Query](https://tanstack.com/query), you already know FloppyDisk.\
5
+ If you know [Zustand](https://zustand.docs.pmnd.rs) & [TanStack-Query](https://tanstack.com/query), you already know FloppyDisk(.ts).\
6
6
  It keeps what works, removes unnecessary complexity, and unifies everything into a simpler API.\
7
7
  No relearning—just a better experience.
8
8
 
@@ -16,79 +16,125 @@ Demo: https://afiiif.github.io/floppy-disk/
16
16
  npm install floppy-disk
17
17
  ```
18
18
 
19
- ## Global Store
19
+ ## In short, it is:
20
20
 
21
- Here's how to create and use a store:
21
+ - **Like Zustand, but has additional capability:**
22
+ - No selector: auto optimize re-render.
23
+ - Store events: `onSubscribe`, `onUnSubscribe`, etc.
24
+ - Easier to set initial state from server
25
+ - Smaller bundle
26
+ - **Like TanStack Query, but:**
27
+ - DX is very similar to Zustand → One mental model for sync & async
28
+ - Extremely less bundle size → With almost the same capabilities
29
+
30
+ # Store (Global State)
31
+
32
+ A store is a global state container that can be used both **inside and outside** React.\
33
+ With FloppyDisk, creating a store is simple:
22
34
 
23
35
  ```tsx
24
36
  import { createStore } from "floppy-disk/react";
25
37
 
26
- const useDigimon = createStore({
27
- age: 7,
28
- level: "Rookie",
38
+ const useLawn = createStore({
39
+ plants: 3,
40
+ zombies: 1,
29
41
  });
30
42
  ```
31
43
 
32
- You can use the store both inside and outside of React components.
44
+ Use it inside a component:
33
45
 
34
46
  ```tsx
35
- function MyDigimon() {
36
- const { age } = useDigimon();
37
- return <div>Digimon age: {age}</div>;
38
- // This component will only re-render when `age` changes.
39
- // Changes to `level` will NOT trigger a re-render.
47
+ function MyPlants() {
48
+ const { plants } = useLawn(); // No selectors needed.
49
+
50
+ return <div>Plants: {plants}</div>; // Only re-render when plants state changes
40
51
  }
52
+ ```
41
53
 
42
- function Control() {
43
- return (
44
- <>
45
- <button
46
- onClick={() => {
47
- // You can setState directly
48
- useDigimon.setState((prev) => ({ age: prev.age + 1 }));
49
- }}
50
- >
51
- Increase digimon's age
52
- </button>
54
+ Update the state **anywhere**:
53
55
 
54
- <button onClick={evolve}>Evolve</button>
55
- </>
56
- );
57
- }
56
+ ```tsx
57
+ const addPlant = () => {
58
+ useLawn.setState(prev => ({ plants: prev.plants + 1 }));
59
+ };
60
+ ```
58
61
 
59
- // You can create a custom actions
60
- const evolve = () => {
61
- const { level } = useDigimon.getState();
62
+ ## Updating State
62
63
 
63
- const order = ["In-Training", "Rookie", "Champion", "Ultimate"];
64
- const nextLevel = order[order.indexOf(level) + 1];
64
+ You can update state using `setState`:
65
65
 
66
- if (!nextLevel) return console.warn("Already at ultimate level");
66
+ ```tsx
67
+ const useLawn = createStore({ plants: 3, zombies: 1 });
68
+ // Current state: { plants: 3, zombies: 1 }
67
69
 
68
- useDigimon.setState({ level: nextLevel });
69
- };
70
+ useLawn.setState({ plants: 5, zombies: 5 });
71
+ // Current state: { plants: 5, zombies: 5 }
72
+
73
+ useLawn.setState({ plants: 7 }); // 👈 Partial update
74
+ // Current state: { plants: 7, zombies: 5 }
75
+
76
+ useLawn.setState(prev => ({ plants: prev.plant + 2 })); // 👈 Using function
77
+ // Current state: { plants: 9, zombies: 5 }
70
78
  ```
71
79
 
72
- ### Store Subscription
80
+ ## Reading State Outside React
73
81
 
74
- At its core, FloppyDisk is a **pub-sub store**.
82
+ Stores are not limited to React. You can access state **anywhere**:
75
83
 
76
- You can subscribe manually:
84
+ ```tsx
85
+ const state = useLawn.getState();
86
+ console.log(state.plants);
87
+ ```
88
+
89
+ ## Subscribing to Changes
90
+
91
+ You can subscribe to state changes:
77
92
 
78
93
  ```tsx
79
- const unsubscribe = useMyStore.subscribe((state, prev) => {
80
- console.log("New state:", state);
94
+ const unsubscribeLawn = useLawn.subscribe((currentState, prevState) => {
95
+ console.log("State changed:", currentState);
81
96
  });
82
97
 
83
98
  // Later
84
- unsubscribe();
99
+ unsubscribeLawn(); // when you no longer need it
100
+ ```
101
+
102
+ ## Transient Updates (No Re-render)
103
+
104
+ Sometimes you want to listen to changes **without triggering re-renders**.
105
+ You can do this by simply subscribing to the store:
106
+
107
+ ```tsx
108
+ function MyComponent() {
109
+
110
+ useEffect(() => useLawn.subscribe((currentState, prevState) => {
111
+ if (currentState.zombies !== prevState.zombies) {
112
+ console.log("Zombie updated");
113
+ // Do something ...
114
+ }
115
+ }), []);
116
+
117
+ ...
118
+ }
85
119
  ```
86
120
 
87
- FloppyDisk provides lifecycle hooks tied to subscription count.
121
+ # Store Events
122
+
123
+ FloppyDisk provides lifecycle events to help you understand when **subscribers are added or removed**, and react accordingly.
124
+
125
+ Each store exposes the following events:
126
+
127
+ - `onFirstSubscribe` → triggered right after the first subscriber is added
128
+ - `onSubscribe` → triggered after any subscriber is added (including the first)
129
+ - `onUnsubscribe` → triggered right after a subscriber is removed
130
+ - `onLastUnsubscribe` → triggered after the last subscriber is removed
88
131
 
89
132
  ```tsx
90
- const useTowerDefense = createStore(
91
- { archers: 3, mages: 1, barracks: 2, artillery: 1 },
133
+ const useLawn = createStore(
134
+ {
135
+ plants: 3,
136
+ zombies: 1,
137
+ },
92
138
  {
93
139
  onFirstSubscribe: () => {
94
140
  console.log("First subscriber! We’re officially popular 🎉");
@@ -106,46 +152,47 @@ const useTowerDefense = createStore(
106
152
  );
107
153
  ```
108
154
 
109
- ### Differences from Zustand
155
+ ## Use Cases
110
156
 
111
- If you're coming from Zustand, this should feel very familiar.\
112
- Key differences:
157
+ These events let you control resource lifecycle based on usage.\
158
+ You know exactly:
159
+ - when something starts being used
160
+ - when it's no longer needed
113
161
 
114
- 1. **No Selectors Needed**\
115
- You don't need selectors when using hooks.
116
- FloppyDisk automatically tracks which parts of the state are used and optimizes re-renders accordingly.
117
- 2. **Object-Only Store Initialization**\
118
- In FloppyDisk, stores **must** be initialized with an object. Primitive values or function initializers are not allowed.
119
162
 
120
- Zustand examples:
163
+ **Perfect for:**
164
+ - opening / closing connections
165
+ - starting / stopping polling
166
+ - initializing expensive resources
167
+ - adding / removing window event listeners
121
168
 
122
- ```tsx
123
- const useDate = create(new Date(2021, 01, 11));
124
-
125
- const useCounter = create((set) => ({
126
- value: 1,
127
- increment: () => set((prev) => ({ value: prev.value + 1 })),
128
- }));
129
- ```
169
+ ## State Changes Event
130
170
 
131
- FloppyDisk equivalents:
171
+ Sometimes you want to observe state changes **without becoming a subscriber**.
132
172
 
133
- ```tsx
134
- const useDate = createStore({ value: new Date(2021, 01, 11) });
173
+ In addition to lifecycle events, FloppyDisk provides `onStateChange` event.
174
+ It listens to changes, but does NOT count as a subscriber.
175
+ It Acts like a "**spy**" on state updates.
135
176
 
136
- const useCounter = createStore({ value: 1 });
137
- const increment = () => useCounter.setState((prev) => ({ value: prev.value + 1 }));
138
- // Unlike Zustand, defining actions inside the store is **discouraged** in FloppyDisk.
139
- // This improves tree-shakeability and keeps your store minimal.
177
+ Useful for devtools, logging, or debugging state changes.
140
178
 
141
- // However, it's still possible to mix actions with the state if you understand how closures work:
142
- const useCounterAlt = createStore({
143
- value: 1,
144
- increment: () => useCounterAlt.setState((prev) => ({ value: prev.value + 1 })),
145
- });
179
+ ```tsx
180
+ const useLawn = createStore(
181
+ {
182
+ plants: 3,
183
+ zombies: 1,
184
+ },
185
+ {
186
+ onStateChange: (currentState, prevState) => {
187
+ if (currentState.zombies === 0 && prevState.zombies > 30) {
188
+ toast("🏆 Achievement unlocked! Clear more than 30 zombies at once!");
189
+ }
190
+ }
191
+ },
192
+ );
146
193
  ```
147
194
 
148
- ## Async State (Query & Mutation)
195
+ # Query & Mutation Store for Async State
149
196
 
150
197
  FloppyDisk also provides a powerful async state layer, inspired by [TanStack-Query](https://tanstack.com/query) but with a simpler API.
151
198
 
@@ -158,7 +205,7 @@ Instead, we use:
158
205
  - **execute** → run the async operation (same as "fetch" in TanStack-Query)
159
206
  - **revalidate** → re-run while keeping existing data (same as "refetch" in TanStack-Query)
160
207
 
161
- ### Query vs Mutation
208
+ ## Query vs Mutation
162
209
 
163
210
  <details>
164
211
 
@@ -210,64 +257,56 @@ If you need retry mechanism, then you can always add it manually.
210
257
 
211
258
  </details>
212
259
 
213
- ### Single Query
260
+ ## Single Query
214
261
 
215
262
  Create a query using `createQuery`:
216
263
 
217
264
  ```tsx
218
265
  import { createQuery } from "floppy-disk/react";
219
266
 
220
- const myCoolQuery = createQuery(
267
+ const myQuery = createQuery(
221
268
  myAsyncFn,
222
269
  // { staleTime: 5000, revalidateOnFocus: false } <-- optional options
223
270
  );
271
+ ```
224
272
 
225
- const useMyCoolQuery = myCoolQuery();
273
+ Use it inside your component:
226
274
 
227
- // Use it inside your component:
275
+ ```tsx
276
+ const useMyQuery = myQuery();
228
277
 
229
278
  function MyComponent() {
230
- const query = useMyCoolQuery();
231
- if (query.state === "INITIAL") return <div>Loading...</div>;
232
- if (query.error) return <div>Error: {query.error.message}</div>;
233
- return <div>{JSON.stringify(query.data)}</div>;
279
+ const { data, error } = useMyQuery();
280
+
281
+ if (!data && !error) return <div>Loading...</div>;
282
+ if (error) return <div>Error: {error.message}</div>;
283
+
284
+ return <div>{data.foo} {data.bar}</div>;
234
285
  }
235
286
  ```
236
287
 
237
- ### Query State: Two Independent Dimensions
288
+ ## Query State: Two Independent Dimensions
238
289
 
239
290
  FloppyDisk tracks two things separately:
240
291
 
241
- - Is it running? → `isPending`\
242
- (value: `boolean`)
243
- - What's the result? → `state`\
244
- (value: `INITIAL | 'SUCCESS' | 'ERROR' | 'SUCCESS_BUT_REVALIDATION_ERROR'`)
292
+ - Is it running? → `isPending`
293
+ - What's the result? → `state`
245
294
 
246
295
  They are **independent**.
247
296
 
248
- ### Automatic Re-render Optimization
249
-
250
- Just like the global store, FloppyDisk tracks usage automatically:
251
-
252
- ```tsx
253
- const { data } = useMyQuery();
254
- // ^Only data changes will trigger a re-render
255
-
256
- const value = useMyQuery().data?.foo.bar.baz;
257
- // ^Only data.foo.bar.baz changes will trigger a re-render
258
- ```
259
-
260
- ### Keyed Query (Dynamic Params)
297
+ ## Keyed Query (Dynamic Params)
261
298
 
262
299
  You can create parameterized queries:
263
300
 
264
301
  ```tsx
265
- import { getUserById, type GetUserByIdResponse } from "../utils";
302
+ import { createQuery } from "floppy-disk/react";
266
303
 
267
- type MyQueryParam = { id: string };
304
+ import { getZombieById, type GetZombieByIdResponse } from "../utils"; // Your own module
268
305
 
269
- const userQuery = createQuery<GetUserByIdResponse, MyQueryParam>(
270
- getUserById,
306
+ type ZombieQueryParam = { id: string };
307
+
308
+ const zombieQuery = createQuery<GetZombieByIdResponse, ZombieQueryParam>(
309
+ getZombieById,
271
310
  // { staleTime: 5000, revalidateOnFocus: false } <-- optional options
272
311
  );
273
312
  ```
@@ -275,18 +314,44 @@ const userQuery = createQuery<GetUserByIdResponse, MyQueryParam>(
275
314
  Use it with parameters:
276
315
 
277
316
  ```tsx
278
- function UserDetail({ id }) {
279
- const useUserQuery = userQuery({ id: 1 });
280
- const query = useUserQuery();
281
- if (query.state === "INITIAL") return <div>Loading...</div>;
282
- if (query.error) return <div>Error: {query.error.message}</div>;
283
- return <div>{JSON.stringify(query.data)}</div>;
317
+ function ZombieDetail({ id }) {
318
+ const useZombieQuery = zombieQuery({ id });
319
+ const { data, error } = useZombieQuery();
320
+
321
+ if (!data && !error) return <div>Loading...</div>;
322
+ if (error) return <div>Error: {error.message}</div>;
323
+
324
+ return <div>Name: {data.name}, hp: {data.hp}</div>;
284
325
  }
285
326
  ```
286
327
 
287
328
  Each unique parameter creates its own cache entry.
288
329
 
289
- ### Infinite Query
330
+ ## Store Inheritance
331
+
332
+ Queries in FloppyDisk are built on top of the core store.
333
+ This means every query inherits the same capabilities, such as `subscribe`, `getState`, and store events.
334
+ It also gets **automatic reactivity** out of the box, so components rerender only when the state they use actually changes.
335
+
336
+ ```tsx
337
+ const { data } = useMyQuery();
338
+ // ^Only data changes will trigger a re-render
339
+
340
+ const value = useMyQuery().data?.foo.bar.baz;
341
+ // ^Only data.foo.bar.baz changes will trigger a re-render
342
+ ```
343
+
344
+ Get state outside React:
345
+
346
+ ```tsx
347
+ const myPlantQuery = createQuery<MyPlantResponse>(getMyPlant); // Query without paramerer
348
+ const zombieQuery = createQuery<GetZombieByIdResponse, { id: string }>(getZombieById); // Parameterized query
349
+
350
+ const getMyPlantQueryData = () => myPlantQuery().getState().data;
351
+ const getUserQueryData = ({ id }) => zombieQuery({ id }).getState().data;
352
+ ```
353
+
354
+ ## Infinite Query
290
355
 
291
356
  FloppyDisk does **not provide** a dedicated "infinite query" API.\
292
357
  Instead, it embraces a simpler and more flexible approach:
@@ -304,15 +369,15 @@ No special abstraction needed.
304
369
  Here is the example on how to implement infinite query properly:
305
370
 
306
371
  ```tsx
307
- type GetPostParams = {
372
+ type GetPlantParams = {
308
373
  cursor?: string; // For pagination
309
374
  };
310
- type GetPostsResponse = {
311
- posts: Post[];
375
+ type GetPlantsResponse = {
376
+ plants: Plant[];
312
377
  meta: { nextCursor: string };
313
378
  };
314
379
 
315
- const postsQuery = createQuery<GetPostsResponse, GetPostParams>(getPosts, {
380
+ const plantsQuery = createQuery<GetPlantsResponse, GetPlantParams>(getPlants, {
316
381
  staleTime: Infinity,
317
382
  revalidateOnFocus: false,
318
383
  revalidateOnReconnect: false,
@@ -323,16 +388,16 @@ function Main() {
323
388
  }
324
389
 
325
390
  function Page({ cursor }: { cursor?: string }) {
326
- const usePostsQuery = postsQuery({ cursor });
327
- const { state, data, error } = usePostsQuery();
391
+ const usePlantsQuery = plantsQuery({ cursor });
392
+ const { state, data, error } = usePlantsQuery();
328
393
 
329
- if (state === "INITIAL") return <div>Loading...</div>;
394
+ if (!data && !error) return <div>Loading...</div>;
330
395
  if (error) return <div>Error</div>;
331
396
 
332
397
  return (
333
398
  <>
334
- {data.posts.map((post) => (
335
- <PostCard key={post.id} post={post} />
399
+ {data.plants.map((plant) => (
400
+ <PlantCard key={plant.id} plant={plant} />
336
401
  ))}
337
402
  {data.meta.nextCursor && <LoadMore nextCursor={data.meta.nextCursor} />}
338
403
  </>
@@ -341,7 +406,7 @@ function Page({ cursor }: { cursor?: string }) {
341
406
 
342
407
  function LoadMore({ nextCursor }: { nextCursor?: string }) {
343
408
  const [isNextPageRequested, setIsNextPageRequested] = useState(() => {
344
- const stateOfNextPageQuery = postsQuery({ cursor: nextCursor }).getState();
409
+ const stateOfNextPageQuery = plantsQuery({ cursor: nextCursor }).getState();
345
410
  return stateOfNextPageQuery.isPending || stateOfNextPageQuery.isSuccess;
346
411
  });
347
412
 
@@ -349,7 +414,7 @@ function LoadMore({ nextCursor }: { nextCursor?: string }) {
349
414
  return <Page cursor={nextCursor} />;
350
415
  }
351
416
 
352
- return <BottomObserver onReachBottom={() => setIsNextPageRequested(true)} />;
417
+ return <DomObserver onReachBottom={() => setIsNextPageRequested(true)} />;
353
418
  }
354
419
  ```
355
420
 
@@ -366,11 +431,104 @@ If revalidation is triggered:
366
431
  This leads to a **confusing and unstable user experience**.\
367
432
  Revalidating dozens of previously viewed pages rarely provides value to the user.
368
433
 
369
- ## SSR Guidance
434
+ ## Mutation
435
+
436
+ Mutations are used to perform write operations—such as creating, updating, or deleting data.
437
+
438
+ FloppyDisk provides two ways to use mutations:
439
+ - Global mutation → shared state across components
440
+ - Local mutation → isolated per component
441
+
442
+ ### Global Mutation
443
+
444
+ Create a global mutation using `createMutation`:
445
+
446
+ ```tsx
447
+ import { createMutation } from "floppy-disk/react";
448
+
449
+ const useUpdatePlant = createMutation(updatePlant, {
450
+ onSuccess: (data) => {
451
+ console.log("Global success:", data);
452
+ },
453
+ });
454
+ ```
455
+
456
+ Use it inside any component:
457
+
458
+ ```tsx
459
+ function UpdateButton() {
460
+ const { isPending } = useUpdatePlant();
461
+
462
+ return (
463
+ <button
464
+ disabled={isPending}
465
+ onClick={() => {
466
+ useUpdatePlant.execute({ id: 1, name: "Sunflower", hp: 300 });
467
+ }}
468
+ >
469
+ Update User
470
+ </button>
471
+ );
472
+ }
473
+ ```
474
+
475
+ Characteristics:
476
+ - Shared across all components
477
+ - Single source of truth for mutation state
478
+ - Can be triggered from anywhere using `.execute()`
479
+ - Useful for global actions (e.g. forms, shared actions)
480
+
481
+ ### Local Mutation
482
+
483
+ Create a local mutation using `useMutation`:
484
+
485
+ ```tsx
486
+ import { useMutation } from "floppy-disk/react";
487
+
488
+ function UpdateForm() {
489
+ const [result, { execute }] = useMutation(updateZombie, {
490
+ onSuccess: (data) => {
491
+ console.log("Local success:", data);
492
+ },
493
+ });
494
+
495
+ return (
496
+ <div>
497
+ <button
498
+ disabled={result.isPending}
499
+ onClick={() => {
500
+ execute({ id: 27, name: "Gargantuar", hp: 3000 });
501
+ }}
502
+ >
503
+ Submit
504
+ </button>
505
+
506
+ {result.isError && <div>Error occurred</div>}
507
+ </div>
508
+ );
509
+ }
510
+ ```
511
+
512
+ Characteristics:
513
+ - Isolated per component instance
514
+ - Each usage has its own state
515
+ - No shared side effects
516
+ - Ideal for component-scoped interactions
517
+
518
+ ### Execution
519
+
520
+ Both global and local mutations:
521
+
522
+ - Execute via `execute(input)`
523
+ - Return a Promise that **never throw**.\
524
+ It returns `{ variable: TVariable; data?: TData; error?: TError }` instead.
525
+ - Update state automatically (`isPending`, `isSuccess`, `isError`, etc.)
526
+
527
+ # SSR Guidance
370
528
 
371
529
  Examples for using stores and queries in SSR with isolated data (no shared state between users).
372
530
 
373
- ### Initialize Store State from Server
531
+ ## Initialize Store State from Server
374
532
 
375
533
  ```tsx
376
534
  const useCountStore = createStore({ count: 0 });
@@ -384,7 +542,7 @@ function Page({ initialCount }) {
384
542
  }
385
543
  ```
386
544
 
387
- ### Initialize Query Data from Server
545
+ ## Initialize Query Data from Server
388
546
 
389
547
  ```tsx
390
548
  async function MyServerComponent() {
@@ -404,3 +562,131 @@ function MyClientComponent({ initialData }) {
404
562
  return <>count is {data.count}</>; // Output: count is 3
405
563
  }
406
564
  ```
565
+
566
+ # Query State Machine
567
+
568
+ This is how the query state transition flow looks like:
569
+
570
+ ```
571
+ initial failed, won't retry
572
+ ┌────────────────────────────┐ ┌────────────────────────────┐
573
+ │ isPending: false │ ■│ isPending: false │
574
+ │ isRevalidating: false │ │ isRevalidating: false │
575
+ │ │ ┌──────────────────▶ │ │
576
+ │ state: "INITIAL" │ │ ■│ state: "ERROR" │
577
+ │ isSuccess: false │ │ │ isSuccess: false │
578
+ │ data: undefined │ │ │ data: undefined │
579
+ │ dataUpdatedAt: undefined │ │ │ dataUpdatedAt: undefined │
580
+ │ dataStaleAt: undefined │ │ │ dataStaleAt: undefined │
581
+ │ isError: false │ │ ■│ isError: true │
582
+ │ error: undefined │ │ ■│ error: <TError> │
583
+ │ errorUpdatedAt: undefined │ │ ■│ errorUpdatedAt: <number> │
584
+ │ │ │ │ │
585
+ │ willRetryAt: undefined │ │ │ willRetryAt: undefined │
586
+ │ isRetrying: false │ │ •│ isRetrying: false │
587
+ │ retryCount: 0 │ │ •│ retryCount: 0 (reset) │
588
+ └─────────────┬──────────────┘ │ └────────────────────────────┘
589
+ │ │
590
+ │ execute │
591
+ ▼ │ waiting retry delay
592
+ ┌────────────────────────────┐ (N) ┌────────────────────────────┐
593
+ ■│ isPending: true [ƒ]│ │ ■│ isPending: false │
594
+ │ isRevalidating: false │ fail │ │ isRevalidating: false │
595
+ │ ├──────────▶ Should retry? ────(Y)────▶ │ │
596
+ │ state: "INITIAL" │ ▲ │ state: "INITIAL" │
597
+ │ isSuccess: false │ │ │ isSuccess: false │
598
+ │ data: undefined │ │ │ data: undefined │
599
+ │ dataUpdatedAt: undefined │ │ │ dataUpdatedAt: undefined │
600
+ │ dataStaleAt: undefined │ │ │ dataStaleAt: undefined │
601
+ │ isError: false │ │ │ isError: false │
602
+ │ error: undefined │ │ │ error: undefined │
603
+ │ errorUpdatedAt: undefined │ │ │ errorUpdatedAt: undefined │
604
+ │ │ │ │ │
605
+ │ willRetryAt: undefined │ │ ■│ willRetryAt: <number> │
606
+ │ isRetrying: false │ │ │ isRetrying: false │
607
+ │ retryCount: 0 │ │ │ retryCount: <number> │
608
+ └─────────────┬──────────────┘ │ └─────────────┬──────────────┘
609
+ │ │ │
610
+ │ success │ │ retrying
611
+ ▼ │ ▼
612
+ ┌────────────────────────────┐ │ ┌────────────────────────────┐
613
+ ■│ isPending: false │ │ ■│ isPending: true [ƒ]│
614
+ │ isRevalidating: false │ │ fail │ isRevalidating: false │
615
+ │ │ └────────────────────┤ │
616
+ ■│ state: "SUCCESS" │ │ state: "INITIAL" │
617
+ ■│ isSuccess: true │ │ isSuccess: false │
618
+ ■│ data: <TData> │ │ data: undefined │
619
+ ■│ dataUpdatedAt: <number> │ │ dataUpdatedAt: undefined │
620
+ ■│ dataStaleAt: <number> │ │ dataStaleAt: undefined │
621
+ │ isError: false │ │ isError: false │
622
+ │ error: undefined │ │ error: undefined │
623
+ │ errorUpdatedAt: undefined │ success │ errorUpdatedAt: undefined │
624
+ │ │ ◀─────────────────────────────────────┤ │
625
+ │ willRetryAt: undefined │ ■│ willRetryAt: undefined │
626
+ •│ isRetrying: false │ ■│ isRetrying: true │
627
+ •│ retryCount: 0 (reset) │ ■│ retryCount: <number> (+1) │
628
+ └────────────────────────────┘ └────────────────────────────┘
629
+ ```
630
+
631
+ And then after success:
632
+
633
+ ```
634
+ success failed, won't retry
635
+ ┌────────────────────────────┐ ┌─────────────────────────────────────────┐
636
+ │ isPending: false │ ■│ isPending: false │
637
+ │ isRevalidating: false │ ■│ isRevalidating: false │
638
+ │ │ ┌──────────────────▶ │ │
639
+ │ state: "SUCCESS" │ │ ■│ state: "SUCCESS_BUT_REVALIDATION_ERROR" │
640
+ │ isSuccess: true │ │ │ isSuccess: true │
641
+ │ data: <TData> │ │ │ data: <TData> │
642
+ │ dataUpdatedAt: <number> │ │ │ dataUpdatedAt: <number> │
643
+ │ dataStaleAt: <number> │ │ │ dataStaleAt: <number> │
644
+ │ isError: false │ │ │ isError: false │
645
+ │ error: undefined │ │ ■│ error: <TError> │
646
+ │ errorUpdatedAt: undefined │ │ ■│ errorUpdatedAt: <number> │
647
+ │ │ │ │ │
648
+ │ willRetryAt: undefined │ │ │ willRetryAt: undefined │
649
+ │ isRetrying: false │ │ •│ isRetrying: false │
650
+ │ retryCount: 0 │ │ •│ retryCount: 0 (reset) │
651
+ └─────────────┬──────────────┘ │ └─────────────────────────────────────────┘
652
+ │ │
653
+ │ revalidate │
654
+ ▼ │ waiting retry delay
655
+ ┌────────────────────────────┐ (N) ┌────────────────────────────┐
656
+ ■│ isPending: true [ƒ]│ │ ■│ isPending: false │
657
+ ■│ isRevalidating: true │ fail │ ■│ isRevalidating: false │
658
+ │ ├──────────▶ Should retry? ────(Y)────▶ │ │
659
+ │ state: "SUCCESS" │ ▲ │ state: "SUCCESS" │
660
+ │ isSuccess: true │ │ │ isSuccess: true │
661
+ │ data: <TData> │ │ │ data: <TData> │
662
+ │ dataUpdatedAt: <number> │ │ │ dataUpdatedAt: <number> │
663
+ │ dataStaleAt: <number> │ │ │ dataStaleAt: <number> │
664
+ │ isError: false │ │ │ isError: false │
665
+ │ error: undefined │ │ │ error: undefined │
666
+ │ errorUpdatedAt: undefined │ │ │ errorUpdatedAt: undefined │
667
+ │ │ │ │ │
668
+ │ willRetryAt: undefined │ │ ■│ willRetryAt: <number> │
669
+ │ isRetrying: false │ │ │ isRetrying: false │
670
+ │ retryCount: 0 │ │ │ retryCount: <number> │
671
+ └─────────────┬──────────────┘ │ └─────────────┬──────────────┘
672
+ │ │ │
673
+ │ success │ │ retrying
674
+ ▼ │ ▼
675
+ ┌────────────────────────────┐ │ ┌────────────────────────────┐
676
+ ■│ isPending: false │ │ ■│ isPending: true [ƒ]│
677
+ ■│ isRevalidating: false │ │ fail ■│ isRevalidating: true │
678
+ │ │ └────────────────────┤ │
679
+ │ state: "SUCCESS" │ │ state: "SUCCESS" │
680
+ │ isSuccess: true │ │ isSuccess: true │
681
+ ■│ data: <TData> │ │ data: <TData> │
682
+ ■│ dataUpdatedAt: <number> │ │ dataUpdatedAt: <number> │
683
+ ■│ dataStaleAt: <number> │ │ dataStaleAt: <number> │
684
+ │ isError: false │ │ isError: false │
685
+ │ error: undefined │ │ error: undefined │
686
+ │ errorUpdatedAt: undefined │ success │ errorUpdatedAt: undefined │
687
+ │ │ ◀─────────────────────────────────────┤ │
688
+ │ willRetryAt: undefined │ ■│ willRetryAt: undefined │
689
+ •│ isRetrying: false │ ■│ isRetrying: true │
690
+ •│ retryCount: 0 (reset) │ ■│ retryCount: <number> (+1) │
691
+ └────────────────────────────┘ └────────────────────────────┘
692
+ ```
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "floppy-disk",
3
3
  "description": "Lightweight unified state management for sync and async data.",
4
4
  "private": false,
5
- "version": "3.2.0",
5
+ "version": "3.2.2",
6
6
  "keywords": [
7
7
  "utilities",
8
8
  "store",