floppy-disk 3.0.0-alpha.1 → 3.0.0-alpha.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.
@@ -1,4 +1,4 @@
1
- import { type InitStoreOptions, type SetState } from 'floppy-disk';
1
+ import { type InitStoreOptions, type SetState } from 'floppy-disk/vanilla';
2
2
  /**
3
3
  * Represents the state of a mutation.
4
4
  *
@@ -90,8 +90,8 @@ export type MutationOptions<TData, TVariable> = InitStoreOptions<MutationState<T
90
90
  * const result = await useCreateUser.execute({ name: 'John' });
91
91
  */
92
92
  export declare const createMutation: <TData, TVariable = undefined>(mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => Promise<TData>, options?: MutationOptions<TData, TVariable>) => (<TStateSlice = MutationState<TData, TVariable>>(selector?: (state: MutationState<TData, TVariable>) => TStateSlice) => TStateSlice) & {
93
- subscribe: (subscriber: import("floppy-disk").Subscriber<MutationState<TData, TVariable>>) => () => void;
94
- getSubscribers: () => Set<import("floppy-disk").Subscriber<MutationState<TData, TVariable>>>;
93
+ subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<MutationState<TData, TVariable>>) => () => void;
94
+ getSubscribers: () => Set<import("../vanilla.d.mts").Subscriber<MutationState<TData, TVariable>>>;
95
95
  getState: () => MutationState<TData, TVariable>;
96
96
  /**
97
97
  * Manually updates the mutation state.
@@ -1,4 +1,4 @@
1
- import { type InitStoreOptions, type SetState } from 'floppy-disk';
1
+ import { type InitStoreOptions, type SetState } from 'floppy-disk/vanilla';
2
2
  /**
3
3
  * Represents the state of a query.
4
4
  *
@@ -202,45 +202,58 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
202
202
  /**
203
203
  * Executes the query function.
204
204
  *
205
- * @param overwriteOngoingExecution - Whether to start a new execution instead of reusing an ongoing one
205
+ * @param options - Execution options
206
+ * @param options.overwriteOngoingExecution - Whether to start a new execution instead of reusing an ongoing one (default: `true`)
206
207
  *
207
208
  * @returns A promise resolving to the latest query state
208
209
  *
209
210
  * @remarks
210
- * - Deduplicates concurrent executions by default.
211
- * - If `overwriteOngoingExecution` is true, a new execution replaces the current one.
211
+ * - By default, each call starts a new execution even if one is already in progress.
212
+ * - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
212
213
  * - Handles:
213
214
  * - Pending state
214
215
  * - Success state
215
216
  * - Error state
216
217
  * - Retry logic
217
218
  */
218
- execute: (overwriteOngoingExecution?: boolean) => Promise<QueryState<TData>>;
219
+ execute: (options?: {
220
+ overwriteOngoingExecution?: boolean;
221
+ }) => Promise<QueryState<TData>>;
219
222
  /**
220
- * Re-executes the query if the data is stale.
223
+ * Re-executes the query if needed based on freshness or invalidation.
221
224
  *
222
- * @param overwriteOngoingExecution - Whether to overwrite an ongoing execution
225
+ * @param options - Revalidation options
226
+ * @param options.overwriteOngoingExecution - Whether to overwrite an ongoing execution (default: `true`)
223
227
  *
224
228
  * @returns The current state if still fresh, otherwise a promise of the new state
225
229
  *
226
230
  * @remarks
227
- * - Skips execution if data is still fresh (`staleTime`) and the query has not been invalidated.
228
- * - Used for automatic revalidation (e.g. focus or reconnect).
231
+ * - Skips execution if data is still fresh (`staleTime`) **AND** the query has not been invalidated.
232
+ * - If execution is not skipped, by default it will start a new execution even if one is already in progress.
233
+ * - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
229
234
  */
230
- revalidate: (overwriteOngoingExecution?: boolean) => Promise<QueryState<TData>>;
235
+ revalidate: (options?: {
236
+ overwriteOngoingExecution?: boolean;
237
+ }) => Promise<QueryState<TData>>;
231
238
  /**
232
239
  * Marks the query as invalidated and optionally triggers re-execution.
233
240
  *
234
- * @param overwriteOngoingExecution - Whether to overwrite an ongoing execution
241
+ * @param options - Invalidation options
242
+ * @param options.overwriteOngoingExecution - Whether to overwrite an ongoing execution (default: `true`)
235
243
  *
236
244
  * @returns `true` if execution was triggered, `false` otherwise
237
245
  *
238
246
  * @remarks
239
247
  * - Invalidated queries are treated as stale regardless of `staleTime`.
240
248
  * - The next `revalidate` will always execute until a successful result clears the invalidation.
241
- * - If there are active subscribers, execution will be triggered immediately.
249
+ * - If there are active subscribers: Execution is triggered immediately.
250
+ * - Otherwise: The query remains invalidated and will execute on the next revalidation.
251
+ * - By default, starts a new execution even if one is already in progress.
252
+ * - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
242
253
  */
243
- invalidate: (overwriteOngoingExecution?: boolean) => boolean;
254
+ invalidate: (options?: {
255
+ overwriteOngoingExecution?: boolean;
256
+ }) => boolean;
244
257
  /**
245
258
  * Resets the query state to its initial state.
246
259
  *
@@ -285,8 +298,8 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
285
298
  * - Should be used if an optimistic update fails.
286
299
  */
287
300
  rollbackOptimisticUpdate: () => TData;
288
- subscribe: (subscriber: import("floppy-disk").Subscriber<QueryState<TData>>) => () => void;
289
- getSubscribers: () => Set<import("floppy-disk").Subscriber<QueryState<TData>>>;
301
+ subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<QueryState<TData>>) => () => void;
302
+ getSubscribers: () => Set<import("../vanilla.d.mts").Subscriber<QueryState<TData>>>;
290
303
  getState: () => QueryState<TData>;
291
304
  setState: (value: SetState<QueryState<TData>>) => void;
292
305
  }) & {
@@ -296,14 +309,18 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
296
309
  * @remarks
297
310
  * - Useful for bulk refetching.
298
311
  */
299
- executeAll: (overwriteOngoingExecution?: boolean) => void;
312
+ executeAll: (options?: {
313
+ overwriteOngoingExecution?: boolean;
314
+ }) => void;
300
315
  /**
301
316
  * Revalidates all query instances.
302
317
  *
303
318
  * @remarks
304
319
  * - Only re-fetches stale queries.
305
320
  */
306
- revalidateAll: (overwriteOngoingExecution?: boolean) => void;
321
+ revalidateAll: (options?: {
322
+ overwriteOngoingExecution?: boolean;
323
+ }) => void;
307
324
  /**
308
325
  * Invalidates all query instances.
309
326
  *
@@ -311,7 +328,9 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
311
328
  * - Marks all queries as invalidated and triggers revalidation if active.
312
329
  * - Invalidated queries bypass `staleTime` until successfully executed again.
313
330
  */
314
- invalidateAll: (overwriteOngoingExecution?: boolean) => void;
331
+ invalidateAll: (options?: {
332
+ overwriteOngoingExecution?: boolean;
333
+ }) => void;
315
334
  /**
316
335
  * Resets all query instances.
317
336
  */
@@ -1,4 +1,4 @@
1
- import { type InitStoreOptions } from 'floppy-disk';
1
+ import { type InitStoreOptions } from 'floppy-disk/vanilla';
2
2
  /**
3
3
  * Creates a single store with a bound React hook.
4
4
  *
@@ -22,4 +22,4 @@ import { type InitStoreOptions } from 'floppy-disk';
22
22
  *
23
23
  * useCounter.setState({ count: 1 });
24
24
  */
25
- export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (<TStateSlice = TState>(selector?: (state: TState) => TStateSlice) => TStateSlice) & import("floppy-disk").StoreApi<TState>;
25
+ export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (<TStateSlice = TState>(selector?: (state: TState) => TStateSlice) => TStateSlice) & import("../vanilla.d.mts").StoreApi<TState>;
@@ -1,4 +1,4 @@
1
- import { type InitStoreOptions } from 'floppy-disk';
1
+ import { type InitStoreOptions } from 'floppy-disk/vanilla';
2
2
  /**
3
3
  * Creates a factory for multiple stores identified by a key.
4
4
  *
@@ -25,8 +25,8 @@ import { type InitStoreOptions } from 'floppy-disk';
25
25
  */
26
26
  export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => (<TStateSlice = TState>(selector?: (state: TState) => TStateSlice) => TStateSlice) & {
27
27
  delete: () => boolean;
28
- setState: (value: import("floppy-disk").SetState<TState>) => void;
28
+ setState: (value: import("../vanilla.d.mts").SetState<TState>) => void;
29
29
  getState: () => TState;
30
- subscribe: (subscriber: import("floppy-disk").Subscriber<TState>) => () => void;
31
- getSubscribers: () => Set<import("floppy-disk").Subscriber<TState>>;
30
+ subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<TState>) => () => void;
31
+ getSubscribers: () => Set<import("../vanilla.d.mts").Subscriber<TState>>;
32
32
  };
@@ -1,4 +1,4 @@
1
- import { type StoreApi } from 'floppy-disk';
1
+ import { type StoreApi } from 'floppy-disk/vanilla';
2
2
  export declare const useStoreUpdateNotifier: <TState extends Record<string, any>, TStateSlice = TState>(store: StoreApi<TState>, selector: (state: TState) => TStateSlice) => void;
3
3
  /**
4
4
  * React hook for subscribing to a store with optional state selection.
package/esm/react.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useLayoutEffect, useEffect, useState, useRef } from 'react';
2
- import { isClient, identity, shallow, initStore, getHash, noop } from 'floppy-disk';
2
+ import { isClient, identity, shallow, initStore, getHash, noop } from 'floppy-disk/vanilla';
3
3
 
4
4
  const useIsomorphicLayoutEffect = isClient ? useLayoutEffect : useEffect;
5
5
 
@@ -157,17 +157,17 @@ const createQuery = (queryFn, options = {}) => {
157
157
  }
158
158
  return false;
159
159
  },
160
- execute: (overwriteOngoingExecution = false) => {
160
+ execute: ({ overwriteOngoingExecution = true } = {}) => {
161
161
  return execute(store, variable, overwriteOngoingExecution);
162
162
  },
163
- revalidate: (overwriteOngoingExecution = false) => {
163
+ revalidate: ({ overwriteOngoingExecution = true } = {}) => {
164
164
  return revalidate(store, variable, overwriteOngoingExecution);
165
165
  },
166
- invalidate: (overwriteOngoingExecution = false) => {
166
+ invalidate: (options2) => {
167
167
  const { metadata } = internals.get(store);
168
168
  metadata.isInvalidated = true;
169
169
  if (store.getSubscribers().size > 0) {
170
- internals.get(store).execute(overwriteOngoingExecution);
170
+ internals.get(store).execute(options2);
171
171
  return true;
172
172
  }
173
173
  return false;
@@ -326,7 +326,7 @@ const createQuery = (queryFn, options = {}) => {
326
326
  const useStore = (options2 = {}, selector = identity) => {
327
327
  useStoreUpdateNotifier(store, selector);
328
328
  useIsomorphicLayoutEffect(() => {
329
- if (options2.enabled !== false) revalidate(store, variable);
329
+ if (options2.enabled !== false) revalidate(store, variable, false);
330
330
  }, [store, options2.enabled]);
331
331
  const storeState = store.getState();
332
332
  let storeStateToBeUsed = storeState;
@@ -356,8 +356,8 @@ const createQuery = (queryFn, options = {}) => {
356
356
  * @remarks
357
357
  * - Useful for bulk refetching.
358
358
  */
359
- executeAll: (overwriteOngoingExecution) => {
360
- stores.forEach((store) => internals.get(store).execute(overwriteOngoingExecution));
359
+ executeAll: (options2) => {
360
+ stores.forEach((store) => internals.get(store).execute(options2));
361
361
  },
362
362
  /**
363
363
  * Revalidates all query instances.
@@ -365,8 +365,8 @@ const createQuery = (queryFn, options = {}) => {
365
365
  * @remarks
366
366
  * - Only re-fetches stale queries.
367
367
  */
368
- revalidateAll: (overwriteOngoingExecution) => {
369
- stores.forEach((store) => internals.get(store).revalidate(overwriteOngoingExecution));
368
+ revalidateAll: (options2) => {
369
+ stores.forEach((store) => internals.get(store).revalidate(options2));
370
370
  },
371
371
  /**
372
372
  * Invalidates all query instances.
@@ -375,8 +375,8 @@ const createQuery = (queryFn, options = {}) => {
375
375
  * - Marks all queries as invalidated and triggers revalidation if active.
376
376
  * - Invalidated queries bypass `staleTime` until successfully executed again.
377
377
  */
378
- invalidateAll: (overwriteOngoingExecution) => {
379
- stores.forEach((store) => internals.get(store).invalidate(overwriteOngoingExecution));
378
+ invalidateAll: (options2) => {
379
+ stores.forEach((store) => internals.get(store).invalidate(options2));
380
380
  },
381
381
  /**
382
382
  * Resets all query instances.
@@ -13,17 +13,18 @@ export type SetState<TState> = Partial<TState> | ((state: TState) => Partial<TSt
13
13
  * @param prevState - The previous state before the update
14
14
  *
15
15
  * @remarks
16
- * - Subscribers are always called on every `setState`, regardless of equality.
17
- * - It is the subscriber's responsibility to perform comparisons if needed.
16
+ * - Subscribers are only called when the state actually changes.
17
+ * - Change detection is performed per key using `Object.is`.
18
18
  */
19
19
  export type Subscriber<TState> = (state: TState, prevState: TState) => void;
20
20
  /**
21
21
  * Core store API for managing state.
22
22
  *
23
23
  * @remarks
24
- * - The store is intentionally simple and always notifies subscribers on updates.
25
- * - No internal equality checks are performed.
24
+ * - The store performs **shallow change detection per key** before notifying subscribers.
25
+ * - Subscribers are only notified when at least one field changes.
26
26
  * - Designed to be framework-agnostic (React bindings are built separately).
27
+ * - By default, `setState` is **disabled on the server** to prevent accidental shared state between requests.
27
28
  */
28
29
  export type StoreApi<TState extends Record<string, any>> = {
29
30
  setState: (value: SetState<TState>) => void;
@@ -47,6 +48,7 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
47
48
  onSubscribe?: (state: TState, store: StoreApi<TState>) => void;
48
49
  onUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
49
50
  onLastUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
51
+ allowSetStateServerSide?: boolean;
50
52
  };
51
53
  /**
52
54
  * Creates a vanilla store with pub-sub capabilities.
@@ -64,6 +66,9 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
64
66
  * - Subscribers are only notified when at least one updated field changes (using `Object.is` comparison).
65
67
  * - Subscribers receive both the new state and the previous state.
66
68
  * - Lifecycle hooks allow side-effect management tied to subscription count.
69
+ * - By default, `setState` is **disabled on the server** to prevent accidental shared state between requests.
70
+ * - This avoids leaking data between users in server environments.
71
+ * - You can override this by setting `allowSetStateServerSide: true`.
67
72
  *
68
73
  * @example
69
74
  * const store = initStore({ count: 0 });
package/esm/vanilla.mjs CHANGED
@@ -70,7 +70,8 @@ const initStore = (initialState, options = {}) => {
70
70
  onFirstSubscribe = noop,
71
71
  onSubscribe = noop,
72
72
  onUnsubscribe = noop,
73
- onLastUnsubscribe = noop
73
+ onLastUnsubscribe = noop,
74
+ allowSetStateServerSide = false
74
75
  } = options;
75
76
  const subscribers = /* @__PURE__ */ new Set();
76
77
  const getSubscribers = () => subscribers;
@@ -87,6 +88,12 @@ const initStore = (initialState, options = {}) => {
87
88
  let state = initialState;
88
89
  const getState = () => state;
89
90
  const setState = (value) => {
91
+ if (!isClient && !allowSetStateServerSide) {
92
+ console.error(
93
+ "setState on the server is not allowed by default. Set `allowSetStateServerSide: true` to allow it."
94
+ );
95
+ return;
96
+ }
90
97
  const prevState = state;
91
98
  const newValue = getValue(value, state);
92
99
  for (const key in newValue) {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "floppy-disk",
3
3
  "description": "Lightweight, simple, and powerful state management library",
4
4
  "private": false,
5
- "version": "3.0.0-alpha.1",
5
+ "version": "3.0.0-alpha.3",
6
6
  "publishConfig": {
7
7
  "tag": "alpha"
8
8
  },
@@ -1,4 +1,4 @@
1
- import { type InitStoreOptions, type SetState } from 'floppy-disk';
1
+ import { type InitStoreOptions, type SetState } from 'floppy-disk/vanilla';
2
2
  /**
3
3
  * Represents the state of a mutation.
4
4
  *
@@ -90,8 +90,8 @@ export type MutationOptions<TData, TVariable> = InitStoreOptions<MutationState<T
90
90
  * const result = await useCreateUser.execute({ name: 'John' });
91
91
  */
92
92
  export declare const createMutation: <TData, TVariable = undefined>(mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => Promise<TData>, options?: MutationOptions<TData, TVariable>) => (<TStateSlice = MutationState<TData, TVariable>>(selector?: (state: MutationState<TData, TVariable>) => TStateSlice) => TStateSlice) & {
93
- subscribe: (subscriber: import("floppy-disk").Subscriber<MutationState<TData, TVariable>>) => () => void;
94
- getSubscribers: () => Set<import("floppy-disk").Subscriber<MutationState<TData, TVariable>>>;
93
+ subscribe: (subscriber: import("../vanilla.ts").Subscriber<MutationState<TData, TVariable>>) => () => void;
94
+ getSubscribers: () => Set<import("../vanilla.ts").Subscriber<MutationState<TData, TVariable>>>;
95
95
  getState: () => MutationState<TData, TVariable>;
96
96
  /**
97
97
  * Manually updates the mutation state.
@@ -1,4 +1,4 @@
1
- import { type InitStoreOptions, type SetState } from 'floppy-disk';
1
+ import { type InitStoreOptions, type SetState } from 'floppy-disk/vanilla';
2
2
  /**
3
3
  * Represents the state of a query.
4
4
  *
@@ -202,45 +202,58 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
202
202
  /**
203
203
  * Executes the query function.
204
204
  *
205
- * @param overwriteOngoingExecution - Whether to start a new execution instead of reusing an ongoing one
205
+ * @param options - Execution options
206
+ * @param options.overwriteOngoingExecution - Whether to start a new execution instead of reusing an ongoing one (default: `true`)
206
207
  *
207
208
  * @returns A promise resolving to the latest query state
208
209
  *
209
210
  * @remarks
210
- * - Deduplicates concurrent executions by default.
211
- * - If `overwriteOngoingExecution` is true, a new execution replaces the current one.
211
+ * - By default, each call starts a new execution even if one is already in progress.
212
+ * - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
212
213
  * - Handles:
213
214
  * - Pending state
214
215
  * - Success state
215
216
  * - Error state
216
217
  * - Retry logic
217
218
  */
218
- execute: (overwriteOngoingExecution?: boolean) => Promise<QueryState<TData>>;
219
+ execute: (options?: {
220
+ overwriteOngoingExecution?: boolean;
221
+ }) => Promise<QueryState<TData>>;
219
222
  /**
220
- * Re-executes the query if the data is stale.
223
+ * Re-executes the query if needed based on freshness or invalidation.
221
224
  *
222
- * @param overwriteOngoingExecution - Whether to overwrite an ongoing execution
225
+ * @param options - Revalidation options
226
+ * @param options.overwriteOngoingExecution - Whether to overwrite an ongoing execution (default: `true`)
223
227
  *
224
228
  * @returns The current state if still fresh, otherwise a promise of the new state
225
229
  *
226
230
  * @remarks
227
- * - Skips execution if data is still fresh (`staleTime`) and the query has not been invalidated.
228
- * - Used for automatic revalidation (e.g. focus or reconnect).
231
+ * - Skips execution if data is still fresh (`staleTime`) **AND** the query has not been invalidated.
232
+ * - If execution is not skipped, by default it will start a new execution even if one is already in progress.
233
+ * - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
229
234
  */
230
- revalidate: (overwriteOngoingExecution?: boolean) => Promise<QueryState<TData>>;
235
+ revalidate: (options?: {
236
+ overwriteOngoingExecution?: boolean;
237
+ }) => Promise<QueryState<TData>>;
231
238
  /**
232
239
  * Marks the query as invalidated and optionally triggers re-execution.
233
240
  *
234
- * @param overwriteOngoingExecution - Whether to overwrite an ongoing execution
241
+ * @param options - Invalidation options
242
+ * @param options.overwriteOngoingExecution - Whether to overwrite an ongoing execution (default: `true`)
235
243
  *
236
244
  * @returns `true` if execution was triggered, `false` otherwise
237
245
  *
238
246
  * @remarks
239
247
  * - Invalidated queries are treated as stale regardless of `staleTime`.
240
248
  * - The next `revalidate` will always execute until a successful result clears the invalidation.
241
- * - If there are active subscribers, execution will be triggered immediately.
249
+ * - If there are active subscribers: Execution is triggered immediately.
250
+ * - Otherwise: The query remains invalidated and will execute on the next revalidation.
251
+ * - By default, starts a new execution even if one is already in progress.
252
+ * - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
242
253
  */
243
- invalidate: (overwriteOngoingExecution?: boolean) => boolean;
254
+ invalidate: (options?: {
255
+ overwriteOngoingExecution?: boolean;
256
+ }) => boolean;
244
257
  /**
245
258
  * Resets the query state to its initial state.
246
259
  *
@@ -285,8 +298,8 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
285
298
  * - Should be used if an optimistic update fails.
286
299
  */
287
300
  rollbackOptimisticUpdate: () => TData;
288
- subscribe: (subscriber: import("floppy-disk").Subscriber<QueryState<TData>>) => () => void;
289
- getSubscribers: () => Set<import("floppy-disk").Subscriber<QueryState<TData>>>;
301
+ subscribe: (subscriber: import("../vanilla.ts").Subscriber<QueryState<TData>>) => () => void;
302
+ getSubscribers: () => Set<import("../vanilla.ts").Subscriber<QueryState<TData>>>;
290
303
  getState: () => QueryState<TData>;
291
304
  setState: (value: SetState<QueryState<TData>>) => void;
292
305
  }) & {
@@ -296,14 +309,18 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
296
309
  * @remarks
297
310
  * - Useful for bulk refetching.
298
311
  */
299
- executeAll: (overwriteOngoingExecution?: boolean) => void;
312
+ executeAll: (options?: {
313
+ overwriteOngoingExecution?: boolean;
314
+ }) => void;
300
315
  /**
301
316
  * Revalidates all query instances.
302
317
  *
303
318
  * @remarks
304
319
  * - Only re-fetches stale queries.
305
320
  */
306
- revalidateAll: (overwriteOngoingExecution?: boolean) => void;
321
+ revalidateAll: (options?: {
322
+ overwriteOngoingExecution?: boolean;
323
+ }) => void;
307
324
  /**
308
325
  * Invalidates all query instances.
309
326
  *
@@ -311,7 +328,9 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
311
328
  * - Marks all queries as invalidated and triggers revalidation if active.
312
329
  * - Invalidated queries bypass `staleTime` until successfully executed again.
313
330
  */
314
- invalidateAll: (overwriteOngoingExecution?: boolean) => void;
331
+ invalidateAll: (options?: {
332
+ overwriteOngoingExecution?: boolean;
333
+ }) => void;
315
334
  /**
316
335
  * Resets all query instances.
317
336
  */
@@ -1,4 +1,4 @@
1
- import { type InitStoreOptions } from 'floppy-disk';
1
+ import { type InitStoreOptions } from 'floppy-disk/vanilla';
2
2
  /**
3
3
  * Creates a single store with a bound React hook.
4
4
  *
@@ -22,4 +22,4 @@ import { type InitStoreOptions } from 'floppy-disk';
22
22
  *
23
23
  * useCounter.setState({ count: 1 });
24
24
  */
25
- export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (<TStateSlice = TState>(selector?: (state: TState) => TStateSlice) => TStateSlice) & import("floppy-disk").StoreApi<TState>;
25
+ export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (<TStateSlice = TState>(selector?: (state: TState) => TStateSlice) => TStateSlice) & import("../vanilla.ts").StoreApi<TState>;
@@ -1,4 +1,4 @@
1
- import { type InitStoreOptions } from 'floppy-disk';
1
+ import { type InitStoreOptions } from 'floppy-disk/vanilla';
2
2
  /**
3
3
  * Creates a factory for multiple stores identified by a key.
4
4
  *
@@ -25,8 +25,8 @@ import { type InitStoreOptions } from 'floppy-disk';
25
25
  */
26
26
  export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => (<TStateSlice = TState>(selector?: (state: TState) => TStateSlice) => TStateSlice) & {
27
27
  delete: () => boolean;
28
- setState: (value: import("floppy-disk").SetState<TState>) => void;
28
+ setState: (value: import("../vanilla.ts").SetState<TState>) => void;
29
29
  getState: () => TState;
30
- subscribe: (subscriber: import("floppy-disk").Subscriber<TState>) => () => void;
31
- getSubscribers: () => Set<import("floppy-disk").Subscriber<TState>>;
30
+ subscribe: (subscriber: import("../vanilla.ts").Subscriber<TState>) => () => void;
31
+ getSubscribers: () => Set<import("../vanilla.ts").Subscriber<TState>>;
32
32
  };
@@ -1,4 +1,4 @@
1
- import { type StoreApi } from 'floppy-disk';
1
+ import { type StoreApi } from 'floppy-disk/vanilla';
2
2
  export declare const useStoreUpdateNotifier: <TState extends Record<string, any>, TStateSlice = TState>(store: StoreApi<TState>, selector: (state: TState) => TStateSlice) => void;
3
3
  /**
4
4
  * React hook for subscribing to a store with optional state selection.
package/react.js CHANGED
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  var react = require('react');
4
- var floppyDisk = require('floppy-disk');
4
+ var vanilla = require('floppy-disk/vanilla');
5
5
 
6
- const useIsomorphicLayoutEffect = floppyDisk.isClient ? react.useLayoutEffect : react.useEffect;
6
+ const useIsomorphicLayoutEffect = vanilla.isClient ? react.useLayoutEffect : react.useEffect;
7
7
 
8
8
  const useStoreUpdateNotifier = (store, selector) => {
9
9
  const [, reRender] = react.useState({});
@@ -11,21 +11,21 @@ const useStoreUpdateNotifier = (store, selector) => {
11
11
  selectorRef.current = selector;
12
12
  useIsomorphicLayoutEffect(
13
13
  () => store.subscribe((state, prevState) => {
14
- if (selectorRef.current === floppyDisk.identity) return reRender({});
14
+ if (selectorRef.current === vanilla.identity) return reRender({});
15
15
  const prevSlice = selectorRef.current(prevState);
16
16
  const nextSlice = selectorRef.current(state);
17
- if (!floppyDisk.shallow(prevSlice, nextSlice)) reRender({});
17
+ if (!vanilla.shallow(prevSlice, nextSlice)) reRender({});
18
18
  }),
19
19
  [store]
20
20
  );
21
21
  };
22
- const useStoreState = (store, selector = floppyDisk.identity) => {
22
+ const useStoreState = (store, selector = vanilla.identity) => {
23
23
  useStoreUpdateNotifier(store, selector);
24
24
  return selector(store.getState());
25
25
  };
26
26
 
27
27
  const createStore = (initialState, options) => {
28
- const store = floppyDisk.initStore(initialState, options);
28
+ const store = vanilla.initStore(initialState, options);
29
29
  const useStore = (selector) => useStoreState(store, selector);
30
30
  return Object.assign(useStore, store);
31
31
  };
@@ -33,12 +33,12 @@ const createStore = (initialState, options) => {
33
33
  const createStores = (initialState, options) => {
34
34
  const stores = /* @__PURE__ */ new Map();
35
35
  const getStore = (key = {}) => {
36
- const keyHash = floppyDisk.getHash(key);
36
+ const keyHash = vanilla.getHash(key);
37
37
  let store;
38
38
  if (stores.has(keyHash)) {
39
39
  store = stores.get(keyHash);
40
40
  } else {
41
- store = floppyDisk.initStore(initialState, options);
41
+ store = vanilla.initStore(initialState, options);
42
42
  stores.set(keyHash, store);
43
43
  }
44
44
  const useStore = (selector) => {
@@ -82,9 +82,9 @@ const createQuery = (queryFn, options = {}) => {
82
82
  // 5 minutes
83
83
  revalidateOnFocus = true,
84
84
  revalidateOnReconnect = true,
85
- onSuccess = floppyDisk.noop,
85
+ onSuccess = vanilla.noop,
86
86
  onError,
87
- onSettled = floppyDisk.noop,
87
+ onSettled = vanilla.noop,
88
88
  shouldRetry: shouldRetryFn = (_, s) => s.retryCount === 0 ? [true, 1500] : [false]
89
89
  } = options;
90
90
  const initialState = INITIAL_STATE$1;
@@ -96,7 +96,7 @@ const createQuery = (queryFn, options = {}) => {
96
96
  (_a = options.onFirstSubscribe) == null ? void 0 : _a.call(options, state, store);
97
97
  const { metadata, revalidate: revalidate2 } = internals.get(store);
98
98
  clearTimeout(metadata.garbageCollectionTimeoutId);
99
- if (floppyDisk.isClient) {
99
+ if (vanilla.isClient) {
100
100
  if (revalidateOnFocus) {
101
101
  focusListeners.add(revalidate2);
102
102
  if (!focusListenersAdded) {
@@ -123,7 +123,7 @@ const createQuery = (queryFn, options = {}) => {
123
123
  metadata.garbageCollectionTimeoutId = setTimeout(() => {
124
124
  store.setState(initialState);
125
125
  }, gcTime);
126
- if (floppyDisk.isClient) {
126
+ if (vanilla.isClient) {
127
127
  if (revalidateOnFocus) {
128
128
  focusListeners.delete(revalidate2);
129
129
  if (focusListeners.size === 0) {
@@ -159,17 +159,17 @@ const createQuery = (queryFn, options = {}) => {
159
159
  }
160
160
  return false;
161
161
  },
162
- execute: (overwriteOngoingExecution = false) => {
162
+ execute: ({ overwriteOngoingExecution = true } = {}) => {
163
163
  return execute(store, variable, overwriteOngoingExecution);
164
164
  },
165
- revalidate: (overwriteOngoingExecution = false) => {
165
+ revalidate: ({ overwriteOngoingExecution = true } = {}) => {
166
166
  return revalidate(store, variable, overwriteOngoingExecution);
167
167
  },
168
- invalidate: (overwriteOngoingExecution = false) => {
168
+ invalidate: (options2) => {
169
169
  const { metadata } = internals.get(store);
170
170
  metadata.isInvalidated = true;
171
171
  if (store.getSubscribers().size > 0) {
172
- internals.get(store).execute(overwriteOngoingExecution);
172
+ internals.get(store).execute(options2);
173
173
  return true;
174
174
  }
175
175
  return false;
@@ -316,19 +316,19 @@ const createQuery = (queryFn, options = {}) => {
316
316
  return execute(store, variable, overwriteOngoingExecution);
317
317
  };
318
318
  const getStore = (variable = {}) => {
319
- const variableHash = floppyDisk.getHash(variable);
319
+ const variableHash = vanilla.getHash(variable);
320
320
  let store;
321
321
  if (stores.has(variableHash)) {
322
322
  store = stores.get(variableHash);
323
323
  } else {
324
- store = floppyDisk.initStore(initialState, configureStoreEvents());
324
+ store = vanilla.initStore(initialState, configureStoreEvents());
325
325
  stores.set(variableHash, store);
326
326
  internals.set(store, configureInternals(store, variable, variableHash));
327
327
  }
328
- const useStore = (options2 = {}, selector = floppyDisk.identity) => {
328
+ const useStore = (options2 = {}, selector = vanilla.identity) => {
329
329
  useStoreUpdateNotifier(store, selector);
330
330
  useIsomorphicLayoutEffect(() => {
331
- if (options2.enabled !== false) revalidate(store, variable);
331
+ if (options2.enabled !== false) revalidate(store, variable, false);
332
332
  }, [store, options2.enabled]);
333
333
  const storeState = store.getState();
334
334
  let storeStateToBeUsed = storeState;
@@ -358,8 +358,8 @@ const createQuery = (queryFn, options = {}) => {
358
358
  * @remarks
359
359
  * - Useful for bulk refetching.
360
360
  */
361
- executeAll: (overwriteOngoingExecution) => {
362
- stores.forEach((store) => internals.get(store).execute(overwriteOngoingExecution));
361
+ executeAll: (options2) => {
362
+ stores.forEach((store) => internals.get(store).execute(options2));
363
363
  },
364
364
  /**
365
365
  * Revalidates all query instances.
@@ -367,8 +367,8 @@ const createQuery = (queryFn, options = {}) => {
367
367
  * @remarks
368
368
  * - Only re-fetches stale queries.
369
369
  */
370
- revalidateAll: (overwriteOngoingExecution) => {
371
- stores.forEach((store) => internals.get(store).revalidate(overwriteOngoingExecution));
370
+ revalidateAll: (options2) => {
371
+ stores.forEach((store) => internals.get(store).revalidate(options2));
372
372
  },
373
373
  /**
374
374
  * Invalidates all query instances.
@@ -377,8 +377,8 @@ const createQuery = (queryFn, options = {}) => {
377
377
  * - Marks all queries as invalidated and triggers revalidation if active.
378
378
  * - Invalidated queries bypass `staleTime` until successfully executed again.
379
379
  */
380
- invalidateAll: (overwriteOngoingExecution) => {
381
- stores.forEach((store) => internals.get(store).invalidate(overwriteOngoingExecution));
380
+ invalidateAll: (options2) => {
381
+ stores.forEach((store) => internals.get(store).invalidate(options2));
382
382
  },
383
383
  /**
384
384
  * Resets all query instances.
@@ -407,9 +407,9 @@ const INITIAL_STATE = {
407
407
  errorUpdatedAt: void 0
408
408
  };
409
409
  const createMutation = (mutationFn, options = {}) => {
410
- const { onSuccess = floppyDisk.noop, onError, onSettled = floppyDisk.noop } = options;
410
+ const { onSuccess = vanilla.noop, onError, onSettled = vanilla.noop } = options;
411
411
  const initialState = INITIAL_STATE;
412
- const store = floppyDisk.initStore(initialState, options);
412
+ const store = vanilla.initStore(initialState, options);
413
413
  const useStore = (selector) => useStoreState(store, selector);
414
414
  const execute = (variable) => {
415
415
  const stateBeforeExecute = store.getState();
@@ -13,17 +13,18 @@ export type SetState<TState> = Partial<TState> | ((state: TState) => Partial<TSt
13
13
  * @param prevState - The previous state before the update
14
14
  *
15
15
  * @remarks
16
- * - Subscribers are always called on every `setState`, regardless of equality.
17
- * - It is the subscriber's responsibility to perform comparisons if needed.
16
+ * - Subscribers are only called when the state actually changes.
17
+ * - Change detection is performed per key using `Object.is`.
18
18
  */
19
19
  export type Subscriber<TState> = (state: TState, prevState: TState) => void;
20
20
  /**
21
21
  * Core store API for managing state.
22
22
  *
23
23
  * @remarks
24
- * - The store is intentionally simple and always notifies subscribers on updates.
25
- * - No internal equality checks are performed.
24
+ * - The store performs **shallow change detection per key** before notifying subscribers.
25
+ * - Subscribers are only notified when at least one field changes.
26
26
  * - Designed to be framework-agnostic (React bindings are built separately).
27
+ * - By default, `setState` is **disabled on the server** to prevent accidental shared state between requests.
27
28
  */
28
29
  export type StoreApi<TState extends Record<string, any>> = {
29
30
  setState: (value: SetState<TState>) => void;
@@ -47,6 +48,7 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
47
48
  onSubscribe?: (state: TState, store: StoreApi<TState>) => void;
48
49
  onUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
49
50
  onLastUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
51
+ allowSetStateServerSide?: boolean;
50
52
  };
51
53
  /**
52
54
  * Creates a vanilla store with pub-sub capabilities.
@@ -64,6 +66,9 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
64
66
  * - Subscribers are only notified when at least one updated field changes (using `Object.is` comparison).
65
67
  * - Subscribers receive both the new state and the previous state.
66
68
  * - Lifecycle hooks allow side-effect management tied to subscription count.
69
+ * - By default, `setState` is **disabled on the server** to prevent accidental shared state between requests.
70
+ * - This avoids leaking data between users in server environments.
71
+ * - You can override this by setting `allowSetStateServerSide: true`.
67
72
  *
68
73
  * @example
69
74
  * const store = initStore({ count: 0 });
package/vanilla.js CHANGED
@@ -72,7 +72,8 @@ const initStore = (initialState, options = {}) => {
72
72
  onFirstSubscribe = noop,
73
73
  onSubscribe = noop,
74
74
  onUnsubscribe = noop,
75
- onLastUnsubscribe = noop
75
+ onLastUnsubscribe = noop,
76
+ allowSetStateServerSide = false
76
77
  } = options;
77
78
  const subscribers = /* @__PURE__ */ new Set();
78
79
  const getSubscribers = () => subscribers;
@@ -89,6 +90,12 @@ const initStore = (initialState, options = {}) => {
89
90
  let state = initialState;
90
91
  const getState = () => state;
91
92
  const setState = (value) => {
93
+ if (!isClient && !allowSetStateServerSide) {
94
+ console.error(
95
+ "setState on the server is not allowed by default. Set `allowSetStateServerSide: true` to allow it."
96
+ );
97
+ return;
98
+ }
92
99
  const prevState = state;
93
100
  const newValue = getValue(value, state);
94
101
  for (const key in newValue) {