floppy-disk 3.3.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,11 +2,10 @@
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(.ts).\
6
- It keeps what works, removes unnecessary complexity, and unifies everything into a simpler API.\
7
- No relearning—just a better experience.
5
+ Built on the patterns you know. Refined into something simpler.\
6
+ Inspired by [Zustand](https://zustand.docs.pmnd.rs) & [TanStack-Query](https://tanstack.com/query).
8
7
 
9
- _Smaller bundle. Zero dependencies._
8
+ _Fine-grained reactivity, minimal boilerplate, zero dependencies._
10
9
 
11
10
  Demo: https://afiiif.github.io/floppy-disk/
12
11
 
@@ -16,18 +15,25 @@ Demo: https://afiiif.github.io/floppy-disk/
16
15
  npm install floppy-disk
17
16
  ```
18
17
 
19
- ## In short, it is:
18
+ **Read the docs https://floppy-disk.vercel.app**
20
19
 
21
- - **Like Zustand, but has additional capabilities:**
22
- - No selectors: automatically optimizes re-renders
23
- - Store events: `onFirstSubscribe`, `onSubscribe`, `onUnsubscribe`, `onLastUnsubscribe`
24
- - Easier to set initial state on SSR/SSG
25
- - Smaller bundle
26
- - **Like TanStack Query, but:**
27
- - DX is very similar to Zustand → One mental model for sync & async
28
- - Much smaller bundle than TanStack Query → With nearly the same capabilities
20
+ <br>
29
21
 
30
- **Docs: https://floppy-disk.vercel.app**
22
+ ---
23
+
24
+ **Like Zustand, but has additional capabilities:**
25
+ - No selectors: automatically optimizes re-renders
26
+ - Store events: `onFirstSubscribe`, `onSubscribe`, `onUnsubscribe`, `onLastUnsubscribe`
27
+ - Easier to set initial state on SSR/SSG
28
+ - Smaller bundle
29
+
30
+ **Like TanStack Query, but:**
31
+ - DX is very similar to Zustand → One mental model for sync & async
32
+ - Much smaller bundle than TanStack Query → With nearly the same capabilities
33
+
34
+ ---
35
+
36
+ <br>
31
37
 
32
38
  ## Store (Global State)
33
39
 
@@ -105,7 +105,7 @@ export type MutationOptions<TData, TVariable, TError = Error> = InitStoreOptions
105
105
  * const { isPending } = useCreateUser();
106
106
  * const result = await useCreateUser.execute({ name: 'John' });
107
107
  *
108
- * @see https://floppy-disk.vercel.app/docs/async/mutation
108
+ * @see https://floppy-disk.vercel.app/docs/mutation
109
109
  */
110
110
  export declare const createMutation: <TData, TVariable = undefined, TError = Error>(mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => Promise<TData>, options?: MutationOptions<TData, TVariable, TError>) => (() => MutationState<TData, TVariable, TError>) & {
111
111
  subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<MutationState<TData, TVariable, TError>>) => () => void;
@@ -1,4 +1,5 @@
1
1
  import { type InitStoreOptions, type SetState } from "../vanilla.mjs";
2
+ import type { StoreKey } from "./create-stores.mjs";
2
3
  /**
3
4
  * Represents the state of a query.
4
5
  *
@@ -68,7 +69,9 @@ export type QueryState<TData, TError> = {
68
69
  * @remarks
69
70
  * Controls caching, retry behavior, lifecycle, and side effects of an async operation.
70
71
  */
71
- export type QueryOptions<TData, TVariable extends Record<string, any>, TError = Error> = InitStoreOptions<QueryState<TData, TError>> & {
72
+ export type QueryOptions<TData, TVariable extends StoreKey, TError = Error> = InitStoreOptions<QueryState<TData, TError>, {
73
+ variableHash: string;
74
+ }> & {
72
75
  /**
73
76
  * Time (in milliseconds) that data is considered fresh.
74
77
  *
@@ -162,9 +165,9 @@ export type QueryOptions<TData, TVariable extends Record<string, any>, TError =
162
165
  * // ...
163
166
  * }
164
167
  *
165
- * @see https://floppy-disk.vercel.app/docs/async/query
168
+ * @see https://floppy-disk.vercel.app/docs/query
166
169
  */
167
- export declare const createQuery: <TData, TVariable extends Record<string, any> = never, TError = Error>(queryFn: (variable: TVariable, currentState: QueryState<TData, TError>, variableHash: string) => Promise<TData>, options?: QueryOptions<TData, TVariable, TError>) => ((variable?: TVariable) => ((options?: {
170
+ export declare const createQuery: <TData, TVariable extends StoreKey = never, TError = Error>(queryFn: (variable: TVariable, currentState: QueryState<TData, TError>, variableHash: string) => Promise<TData>, options?: QueryOptions<TData, TVariable, TError>) => ((variable?: TVariable) => ((options?: {
168
171
  /**
169
172
  * Whether the query should be ravalidated automatically on mount.
170
173
  *
@@ -216,11 +219,11 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
216
219
  initialData?: never;
217
220
  initialDataIsStale?: never;
218
221
  })) => QueryState<TData, TError>) & {
222
+ variableHash: string;
219
223
  /**
220
224
  * Internal data, do not mutate!
221
225
  */
222
226
  metadata: {
223
- variableHash: string;
224
227
  isInvalidated?: boolean;
225
228
  promise?: Promise<QueryState<TData, TError>> | undefined;
226
229
  promiseResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
@@ -25,7 +25,7 @@ import { type InitStoreOptions } from "../vanilla.mjs";
25
25
  *
26
26
  * useMyStore.setState({ foo: 2 }); // only components using foo will re-render
27
27
  *
28
- * @see https://floppy-disk.vercel.app/docs/sync/store
28
+ * @see https://floppy-disk.vercel.app/docs/store
29
29
  */
30
30
  export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => ((options?: {
31
31
  /**
@@ -1,4 +1,8 @@
1
1
  import { type InitStoreOptions } from "../vanilla.mjs";
2
+ type GoodInputForHash = string | number | boolean | null | Date;
3
+ export type StoreKey = GoodInputForHash | {
4
+ [key: string | number]: StoreKey | StoreKey[];
5
+ };
2
6
  /**
3
7
  * Creates a factory for multiple stores identified by a key.
4
8
  *
@@ -30,9 +34,12 @@ import { type InitStoreOptions } from "../vanilla.mjs";
30
34
  * return <div>{state.name}</div>;
31
35
  * }
32
36
  *
33
- * @see https://floppy-disk.vercel.app/docs/sync/stores
37
+ * @see https://floppy-disk.vercel.app/docs/stores
34
38
  */
35
- export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => ((options?: {
39
+ export declare const createStores: <TState extends Record<string, any>, TKey extends StoreKey>(initialState: TState, options?: InitStoreOptions<TState, {
40
+ key: TKey;
41
+ keyHash: string;
42
+ }>) => (key?: TKey) => ((options?: {
36
43
  /**
37
44
  * Initial state used on first render (and will also update the store state right after that)
38
45
  *
@@ -48,3 +55,4 @@ export declare const createStores: <TState extends Record<string, any>, TKey ext
48
55
  key: TKey;
49
56
  keyHash: string;
50
57
  };
58
+ export {};
@@ -21,7 +21,7 @@ import { type MutationOptions, type MutationState } from "./create-mutation.mjs"
21
21
  * - Only the latest execution is allowed to update the state.
22
22
  * - Results from previous executions are ignored if a newer one exists.
23
23
  *
24
- * @see https://floppy-disk.vercel.app/docs/async/mutation
24
+ * @see https://floppy-disk.vercel.app/docs/mutation
25
25
  */
26
26
  export declare const useMutation: <TData, TVariable = undefined, TError = Error>(
27
27
  /**
package/esm/react.mjs CHANGED
@@ -94,7 +94,11 @@ const createStores = (initialState, options) => {
94
94
  if (stores.has(keyHash)) {
95
95
  store = stores.get(keyHash);
96
96
  } else {
97
- store = initStore(initialState, options);
97
+ store = initStore(
98
+ initialState,
99
+ options
100
+ // Intentionally using as any: don't want to add generic on `initStore`
101
+ );
98
102
  store.key = key;
99
103
  store.keyHash = keyHash;
100
104
  stores.set(keyHash, store);
@@ -207,8 +211,8 @@ const createQuery = (queryFn, options = {}) => {
207
211
  }
208
212
  });
209
213
  const internals = /* @__PURE__ */ new WeakMap();
210
- const configureInternals = (store, variable, variableHash) => ({
211
- metadata: { variableHash },
214
+ const configureInternals = (store, variable) => ({
215
+ metadata: {},
212
216
  setInitialData: (data, revalidate2 = false) => {
213
217
  const state = store.getState();
214
218
  if (state.state === "INITIAL" && state.data === void 0) {
@@ -263,7 +267,7 @@ const createQuery = (queryFn, options = {}) => {
263
267
  return false;
264
268
  }
265
269
  internals.get(store).reset();
266
- return stores.delete(variableHash);
270
+ return stores.delete(store.variableHash);
267
271
  },
268
272
  optimisticUpdate: (optimisticData) => {
269
273
  const { metadata, revalidate: revalidate2, rollbackOptimisticUpdate } = internals.get(store);
@@ -292,7 +296,7 @@ const createQuery = (queryFn, options = {}) => {
292
296
  isRetrying: !!metadata.retryResolver,
293
297
  retryCount: metadata.retryResolver ? stateBeforeExecute.retryCount + 1 : 0
294
298
  });
295
- queryFn(variable, stateBeforeExecute, metadata.variableHash).then((data) => {
299
+ queryFn(variable, stateBeforeExecute, store.variableHash).then((data) => {
296
300
  var _a;
297
301
  if (data === void 0) {
298
302
  console.error(
@@ -396,9 +400,14 @@ const createQuery = (queryFn, options = {}) => {
396
400
  if (stores.has(variableHash)) {
397
401
  store = stores.get(variableHash);
398
402
  } else {
399
- store = initStore(initialState, configureStoreEvents(variableHash));
403
+ store = initStore(
404
+ initialState,
405
+ configureStoreEvents(variableHash)
406
+ // Intentionally using as any: don't want to add generic on `initStore`
407
+ );
408
+ store.variableHash = variableHash;
400
409
  stores.set(variableHash, store);
401
- internals.set(store, configureInternals(store, variable, variableHash));
410
+ internals.set(store, configureInternals(store, variable));
402
411
  }
403
412
  const useStore = (options2 = {}) => {
404
413
  const {
@@ -473,7 +482,8 @@ const createQuery = (queryFn, options = {}) => {
473
482
  console.debug("Manual setState (not via provided actions) on query store");
474
483
  store.setState(value);
475
484
  },
476
- ...internals.get(store)
485
+ ...internals.get(store),
486
+ variableHash
477
487
  });
478
488
  };
479
489
  return Object.assign(getStore, {
@@ -46,11 +46,11 @@ export type StoreApi<TState extends Record<string, any>> = {
46
46
  * - Cleanup (e.g. cancel timers, disconnect sockets)
47
47
  * - Resource management (e.g. garbage collection)
48
48
  */
49
- export type InitStoreOptions<TState extends Record<string, any>> = {
50
- onFirstSubscribe?: (state: TState, store: StoreApi<TState>) => void;
51
- onSubscribe?: (state: TState, store: StoreApi<TState>) => void;
52
- onUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
53
- onLastUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
49
+ export type InitStoreOptions<TState extends Record<string, any>, TStoreProps extends Record<string, any> = object> = {
50
+ onFirstSubscribe?: (state: TState, store: StoreApi<TState> & TStoreProps) => void;
51
+ onSubscribe?: (state: TState, store: StoreApi<TState> & TStoreProps) => void;
52
+ onUnsubscribe?: (state: TState, store: StoreApi<TState> & TStoreProps) => void;
53
+ onLastUnsubscribe?: (state: TState, store: StoreApi<TState> & TStoreProps) => void;
54
54
  /**
55
55
  * Called whenever the state changes, without counting as a subscriber.
56
56
  * Acts like a "spy" on state updates.
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.3.0",
5
+ "version": "3.5.0",
6
6
  "keywords": [
7
7
  "utilities",
8
8
  "store",
@@ -105,7 +105,7 @@ export type MutationOptions<TData, TVariable, TError = Error> = InitStoreOptions
105
105
  * const { isPending } = useCreateUser();
106
106
  * const result = await useCreateUser.execute({ name: 'John' });
107
107
  *
108
- * @see https://floppy-disk.vercel.app/docs/async/mutation
108
+ * @see https://floppy-disk.vercel.app/docs/mutation
109
109
  */
110
110
  export declare const createMutation: <TData, TVariable = undefined, TError = Error>(mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => Promise<TData>, options?: MutationOptions<TData, TVariable, TError>) => (() => MutationState<TData, TVariable, TError>) & {
111
111
  subscribe: (subscriber: import("../vanilla.ts").Subscriber<MutationState<TData, TVariable, TError>>) => () => void;
@@ -1,4 +1,5 @@
1
1
  import { type InitStoreOptions, type SetState } from "../vanilla.ts";
2
+ import type { StoreKey } from "./create-stores.ts";
2
3
  /**
3
4
  * Represents the state of a query.
4
5
  *
@@ -68,7 +69,9 @@ export type QueryState<TData, TError> = {
68
69
  * @remarks
69
70
  * Controls caching, retry behavior, lifecycle, and side effects of an async operation.
70
71
  */
71
- export type QueryOptions<TData, TVariable extends Record<string, any>, TError = Error> = InitStoreOptions<QueryState<TData, TError>> & {
72
+ export type QueryOptions<TData, TVariable extends StoreKey, TError = Error> = InitStoreOptions<QueryState<TData, TError>, {
73
+ variableHash: string;
74
+ }> & {
72
75
  /**
73
76
  * Time (in milliseconds) that data is considered fresh.
74
77
  *
@@ -162,9 +165,9 @@ export type QueryOptions<TData, TVariable extends Record<string, any>, TError =
162
165
  * // ...
163
166
  * }
164
167
  *
165
- * @see https://floppy-disk.vercel.app/docs/async/query
168
+ * @see https://floppy-disk.vercel.app/docs/query
166
169
  */
167
- export declare const createQuery: <TData, TVariable extends Record<string, any> = never, TError = Error>(queryFn: (variable: TVariable, currentState: QueryState<TData, TError>, variableHash: string) => Promise<TData>, options?: QueryOptions<TData, TVariable, TError>) => ((variable?: TVariable) => ((options?: {
170
+ export declare const createQuery: <TData, TVariable extends StoreKey = never, TError = Error>(queryFn: (variable: TVariable, currentState: QueryState<TData, TError>, variableHash: string) => Promise<TData>, options?: QueryOptions<TData, TVariable, TError>) => ((variable?: TVariable) => ((options?: {
168
171
  /**
169
172
  * Whether the query should be ravalidated automatically on mount.
170
173
  *
@@ -216,11 +219,11 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
216
219
  initialData?: never;
217
220
  initialDataIsStale?: never;
218
221
  })) => QueryState<TData, TError>) & {
222
+ variableHash: string;
219
223
  /**
220
224
  * Internal data, do not mutate!
221
225
  */
222
226
  metadata: {
223
- variableHash: string;
224
227
  isInvalidated?: boolean;
225
228
  promise?: Promise<QueryState<TData, TError>> | undefined;
226
229
  promiseResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
@@ -25,7 +25,7 @@ import { type InitStoreOptions } from "../vanilla.ts";
25
25
  *
26
26
  * useMyStore.setState({ foo: 2 }); // only components using foo will re-render
27
27
  *
28
- * @see https://floppy-disk.vercel.app/docs/sync/store
28
+ * @see https://floppy-disk.vercel.app/docs/store
29
29
  */
30
30
  export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => ((options?: {
31
31
  /**
@@ -1,4 +1,8 @@
1
1
  import { type InitStoreOptions } from "../vanilla.ts";
2
+ type GoodInputForHash = string | number | boolean | null | Date;
3
+ export type StoreKey = GoodInputForHash | {
4
+ [key: string | number]: StoreKey | StoreKey[];
5
+ };
2
6
  /**
3
7
  * Creates a factory for multiple stores identified by a key.
4
8
  *
@@ -30,9 +34,12 @@ import { type InitStoreOptions } from "../vanilla.ts";
30
34
  * return <div>{state.name}</div>;
31
35
  * }
32
36
  *
33
- * @see https://floppy-disk.vercel.app/docs/sync/stores
37
+ * @see https://floppy-disk.vercel.app/docs/stores
34
38
  */
35
- export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => ((options?: {
39
+ export declare const createStores: <TState extends Record<string, any>, TKey extends StoreKey>(initialState: TState, options?: InitStoreOptions<TState, {
40
+ key: TKey;
41
+ keyHash: string;
42
+ }>) => (key?: TKey) => ((options?: {
36
43
  /**
37
44
  * Initial state used on first render (and will also update the store state right after that)
38
45
  *
@@ -48,3 +55,4 @@ export declare const createStores: <TState extends Record<string, any>, TKey ext
48
55
  key: TKey;
49
56
  keyHash: string;
50
57
  };
58
+ export {};
@@ -21,7 +21,7 @@ import { type MutationOptions, type MutationState } from "./create-mutation.ts";
21
21
  * - Only the latest execution is allowed to update the state.
22
22
  * - Results from previous executions are ignored if a newer one exists.
23
23
  *
24
- * @see https://floppy-disk.vercel.app/docs/async/mutation
24
+ * @see https://floppy-disk.vercel.app/docs/mutation
25
25
  */
26
26
  export declare const useMutation: <TData, TVariable = undefined, TError = Error>(
27
27
  /**
package/react.js CHANGED
@@ -96,7 +96,11 @@ const createStores = (initialState, options) => {
96
96
  if (stores.has(keyHash)) {
97
97
  store = stores.get(keyHash);
98
98
  } else {
99
- store = vanilla.initStore(initialState, options);
99
+ store = vanilla.initStore(
100
+ initialState,
101
+ options
102
+ // Intentionally using as any: don't want to add generic on `initStore`
103
+ );
100
104
  store.key = key;
101
105
  store.keyHash = keyHash;
102
106
  stores.set(keyHash, store);
@@ -209,8 +213,8 @@ const createQuery = (queryFn, options = {}) => {
209
213
  }
210
214
  });
211
215
  const internals = /* @__PURE__ */ new WeakMap();
212
- const configureInternals = (store, variable, variableHash) => ({
213
- metadata: { variableHash },
216
+ const configureInternals = (store, variable) => ({
217
+ metadata: {},
214
218
  setInitialData: (data, revalidate2 = false) => {
215
219
  const state = store.getState();
216
220
  if (state.state === "INITIAL" && state.data === void 0) {
@@ -265,7 +269,7 @@ const createQuery = (queryFn, options = {}) => {
265
269
  return false;
266
270
  }
267
271
  internals.get(store).reset();
268
- return stores.delete(variableHash);
272
+ return stores.delete(store.variableHash);
269
273
  },
270
274
  optimisticUpdate: (optimisticData) => {
271
275
  const { metadata, revalidate: revalidate2, rollbackOptimisticUpdate } = internals.get(store);
@@ -294,7 +298,7 @@ const createQuery = (queryFn, options = {}) => {
294
298
  isRetrying: !!metadata.retryResolver,
295
299
  retryCount: metadata.retryResolver ? stateBeforeExecute.retryCount + 1 : 0
296
300
  });
297
- queryFn(variable, stateBeforeExecute, metadata.variableHash).then((data) => {
301
+ queryFn(variable, stateBeforeExecute, store.variableHash).then((data) => {
298
302
  var _a;
299
303
  if (data === void 0) {
300
304
  console.error(
@@ -398,9 +402,14 @@ const createQuery = (queryFn, options = {}) => {
398
402
  if (stores.has(variableHash)) {
399
403
  store = stores.get(variableHash);
400
404
  } else {
401
- store = vanilla.initStore(initialState, configureStoreEvents(variableHash));
405
+ store = vanilla.initStore(
406
+ initialState,
407
+ configureStoreEvents(variableHash)
408
+ // Intentionally using as any: don't want to add generic on `initStore`
409
+ );
410
+ store.variableHash = variableHash;
402
411
  stores.set(variableHash, store);
403
- internals.set(store, configureInternals(store, variable, variableHash));
412
+ internals.set(store, configureInternals(store, variable));
404
413
  }
405
414
  const useStore = (options2 = {}) => {
406
415
  const {
@@ -475,7 +484,8 @@ const createQuery = (queryFn, options = {}) => {
475
484
  console.debug("Manual setState (not via provided actions) on query store");
476
485
  store.setState(value);
477
486
  },
478
- ...internals.get(store)
487
+ ...internals.get(store),
488
+ variableHash
479
489
  });
480
490
  };
481
491
  return Object.assign(getStore, {
@@ -46,11 +46,11 @@ export type StoreApi<TState extends Record<string, any>> = {
46
46
  * - Cleanup (e.g. cancel timers, disconnect sockets)
47
47
  * - Resource management (e.g. garbage collection)
48
48
  */
49
- export type InitStoreOptions<TState extends Record<string, any>> = {
50
- onFirstSubscribe?: (state: TState, store: StoreApi<TState>) => void;
51
- onSubscribe?: (state: TState, store: StoreApi<TState>) => void;
52
- onUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
53
- onLastUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
49
+ export type InitStoreOptions<TState extends Record<string, any>, TStoreProps extends Record<string, any> = object> = {
50
+ onFirstSubscribe?: (state: TState, store: StoreApi<TState> & TStoreProps) => void;
51
+ onSubscribe?: (state: TState, store: StoreApi<TState> & TStoreProps) => void;
52
+ onUnsubscribe?: (state: TState, store: StoreApi<TState> & TStoreProps) => void;
53
+ onLastUnsubscribe?: (state: TState, store: StoreApi<TState> & TStoreProps) => void;
54
54
  /**
55
55
  * Called whenever the state changes, without counting as a subscriber.
56
56
  * Acts like a "spy" on state updates.