floppy-disk 3.6.0 → 3.7.0-beta.1

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.
@@ -44,17 +44,7 @@ export type MutationState<TData, TVariable, TError> = {
44
44
  error: TError;
45
45
  errorUpdatedAt: number;
46
46
  });
47
- export declare const INITIAL_STATE: {
48
- state: string;
49
- isPending: boolean;
50
- isSuccess: boolean;
51
- isError: boolean;
52
- variable: undefined;
53
- data: undefined;
54
- dataUpdatedAt: undefined;
55
- error: undefined;
56
- errorUpdatedAt: undefined;
57
- };
47
+ export declare const INITIAL_STATE: MutationState<any, any, any>;
58
48
  /**
59
49
  * Configuration options for a mutation.
60
50
  *
@@ -63,15 +63,149 @@ export type QueryState<TData, TError> = {
63
63
  error: TError;
64
64
  errorUpdatedAt: number;
65
65
  });
66
+ type Internal<TData, TError> = {
67
+ isInvalidated?: boolean;
68
+ promise?: Promise<QueryState<TData, TError>> | undefined;
69
+ promiseResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
70
+ retryTimeoutId?: number;
71
+ retryResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
72
+ garbageCollectionTimeoutId?: number;
73
+ rollbackData?: TData | undefined;
74
+ };
75
+ type AdditionalStoreApi<TData, TError> = {
76
+ /**
77
+ * A deterministic hash string derived from the query variable.
78
+ *
79
+ * Used as the unique identifier for this query instance in the internal cache.
80
+ *
81
+ * @remarks
82
+ * - Structurally identical variables will produce the same hash.
83
+ */
84
+ variableHash: string;
85
+ /**
86
+ * Sets initial data for the query if it has not been initialized.
87
+ *
88
+ * @param data - Initial data
89
+ * @param revalidate - Whether to mark the data as invalidated (will trigger revalidation)
90
+ *
91
+ * @returns `true` if the data was set, `false` otherwise
92
+ *
93
+ * @remarks
94
+ * - Only applies when the query is in the `INITIAL` state.
95
+ * - Useful for hydration or preloaded data.
96
+ */
97
+ setInitialData: (data: TData, revalidate?: boolean) => boolean;
98
+ /**
99
+ * Executes the query function.
100
+ *
101
+ * @param options - Execution options
102
+ * @param options.overwriteOngoingExecution - Whether to start a new execution instead of reusing an ongoing one (default: `true`)
103
+ *
104
+ * @returns A promise resolving to the latest query state
105
+ *
106
+ * @remarks
107
+ * - By default, each call **starts a new execution** even if one is already in progress.
108
+ * - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
109
+ * - Handles:
110
+ * - Pending state
111
+ * - Success state
112
+ * - Error state
113
+ * - Retry logic
114
+ */
115
+ execute: (options?: {
116
+ overwriteOngoingExecution?: boolean;
117
+ }) => Promise<QueryState<TData, TError>>;
118
+ /**
119
+ * Re-executes the query if needed based on freshness or invalidation.
120
+ *
121
+ * @param options - Revalidation options
122
+ * @param options.overwriteOngoingExecution - Whether to overwrite an ongoing execution (default: `true`)
123
+ *
124
+ * @returns The current state if still fresh, otherwise a promise of the new state
125
+ *
126
+ * @remarks
127
+ * - Skips execution if data is still fresh (`staleTime`) **AND** the query has not been invalidated.
128
+ * - If execution is not skipped, by default it will start a new execution even if one is already in progress.
129
+ * - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
130
+ */
131
+ revalidate: (options?: {
132
+ overwriteOngoingExecution?: boolean;
133
+ }) => Promise<QueryState<TData, TError>>;
134
+ /**
135
+ * Marks the query as invalidated and optionally triggers re-execution.
136
+ *
137
+ * @param options - Invalidation options
138
+ * @param options.overwriteOngoingExecution - Whether to overwrite an ongoing execution (default: `true`)
139
+ *
140
+ * @returns `true` if execution was triggered, `false` otherwise
141
+ *
142
+ * @remarks
143
+ * - Invalidated queries are treated as stale regardless of `staleTime`.
144
+ * - The next `revalidate` will always execute until a successful result clears the invalidation.
145
+ * - If there are active subscribers: Execution is triggered immediately.
146
+ * - Otherwise: The query remains invalidated and will execute on the next revalidation.
147
+ * - By default, starts a new execution even if one is already in progress.
148
+ * - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
149
+ */
150
+ invalidate: (options?: {
151
+ overwriteOngoingExecution?: boolean;
152
+ }) => boolean;
153
+ /**
154
+ * Resets the query state to its initial state.
155
+ *
156
+ * @remarks
157
+ * - Cancels retry logic and ignores any ongoing execution results.
158
+ */
159
+ reset: () => void;
160
+ /**
161
+ * Deletes the query store for the current variable.
162
+ *
163
+ * @returns `true` if deleted, `false` otherwise
164
+ *
165
+ * @remarks
166
+ * - Cannot delete while there are active subscribers.
167
+ */
168
+ delete: () => boolean;
169
+ /**
170
+ * Performs an optimistic update on the query data.
171
+ *
172
+ * @param data - Optimistic data to set
173
+ *
174
+ * @returns Controls for managing the optimistic update
175
+ *
176
+ * @remarks
177
+ * - Temporarily replaces the current data.
178
+ * - Stores previous data for rollback.
179
+ * - Commonly used with mutations for instant UI updates.
180
+ *
181
+ * @example
182
+ * const { rollback, revalidate } = query.optimisticUpdate(newData);
183
+ */
184
+ optimisticUpdate: (data: TData) => {
185
+ revalidate: () => Promise<QueryState<TData, TError>>;
186
+ rollback: () => TData;
187
+ };
188
+ /**
189
+ * Restores the data before the last optimistic update.
190
+ *
191
+ * @returns The restored data
192
+ *
193
+ * @remarks
194
+ * - Should be used if an optimistic update fails.
195
+ */
196
+ rollbackOptimisticUpdate: () => TData;
197
+ /**
198
+ * Internal data, do not mutate!
199
+ */
200
+ internal: Readonly<Internal<TData, TError>>;
201
+ };
66
202
  /**
67
203
  * Configuration options for a query.
68
204
  *
69
205
  * @remarks
70
206
  * Controls caching, retry behavior, lifecycle, and side effects of an async operation.
71
207
  */
72
- export type QueryOptions<TData, TVariable extends StoreKey, TError = Error> = InitStoreOptions<QueryState<TData, TError>, {
73
- variableHash: string;
74
- }> & {
208
+ export type QueryOptions<TData, TVariable extends StoreKey, TError = Error> = InitStoreOptions<QueryState<TData, TError>, AdditionalStoreApi<TData, TError>> & {
75
209
  /**
76
210
  * Time (in milliseconds) that data is considered fresh.
77
211
  *
@@ -219,19 +353,19 @@ export declare const createQuery: <TData, TVariable extends StoreKey = never, TE
219
353
  initialData?: never;
220
354
  initialDataIsStale?: never;
221
355
  })) => QueryState<TData, TError>) & {
222
- variableHash: string;
356
+ setState: (value: SetStateInput<QueryState<TData, TError>>) => void;
357
+ getState: () => QueryState<TData, TError>;
358
+ subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<QueryState<TData, TError>>) => () => void;
359
+ getSubscriberCount: () => number;
223
360
  /**
224
- * Internal data, do not mutate!
361
+ * A deterministic hash string derived from the query variable.
362
+ *
363
+ * Used as the unique identifier for this query instance in the internal cache.
364
+ *
365
+ * @remarks
366
+ * - Structurally identical variables will produce the same hash.
225
367
  */
226
- metadata: {
227
- isInvalidated?: boolean;
228
- promise?: Promise<QueryState<TData, TError>> | undefined;
229
- promiseResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
230
- retryTimeoutId?: number;
231
- retryResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
232
- garbageCollectionTimeoutId?: number;
233
- rollbackData?: TData | undefined;
234
- };
368
+ variableHash: string;
235
369
  /**
236
370
  * Sets initial data for the query if it has not been initialized.
237
371
  *
@@ -344,10 +478,10 @@ export declare const createQuery: <TData, TVariable extends StoreKey = never, TE
344
478
  * - Should be used if an optimistic update fails.
345
479
  */
346
480
  rollbackOptimisticUpdate: () => TData;
347
- subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<QueryState<TData, TError>>) => () => void;
348
- getSubscriberCount: () => number;
349
- getState: () => QueryState<TData, TError>;
350
- setState: (value: SetStateInput<QueryState<TData, TError>>) => void;
481
+ /**
482
+ * Internal data, do not mutate!
483
+ */
484
+ internal: Readonly<Internal<TData, TError>>;
351
485
  }) & {
352
486
  /**
353
487
  * Executes all query instances.
@@ -382,3 +516,4 @@ export declare const createQuery: <TData, TVariable extends StoreKey = never, TE
382
516
  */
383
517
  resetAll: () => void;
384
518
  };
519
+ export {};
@@ -1,8 +1,37 @@
1
- import { type InitStoreOptions } from "../vanilla.mjs";
1
+ import { type InitStoreOptions, type StoreApi } from "../vanilla.mjs";
2
2
  type GoodInputForHash = string | number | boolean | null | Date;
3
3
  export type StoreKey = GoodInputForHash | {
4
4
  [key: string | number]: StoreKey | StoreKey[];
5
5
  };
6
+ type AdditionalStoreApi<TKey> = {
7
+ /**
8
+ * The original key used to identify this store instance.\
9
+ * This value is not hashed and is preserved as-is.
10
+ */
11
+ key: TKey;
12
+ /**
13
+ * A deterministic hash string derived from {@link key}.
14
+ *
15
+ * Used internally as the unique identifier for caching and retrieving store instances.
16
+ *
17
+ * @remarks
18
+ * - Guarantees that structurally identical keys produce the same hash.
19
+ */
20
+ keyHash: string;
21
+ /**
22
+ * Deletes this store instance from the internal cache.
23
+ *
24
+ * @returns `true` if the store was successfully deleted, otherwise `false`.
25
+ *
26
+ * @remarks
27
+ * - If there are active subscribers, the deletion is ignored and `false` is returned.
28
+ * - When deletion succeeds:
29
+ * - The store is removed from the cache.
30
+ * - Its state is reset to the initial state.
31
+ * - Intended for manual cleanup of unused or ephemeral stores.
32
+ */
33
+ delete: () => boolean;
34
+ };
6
35
  /**
7
36
  * Creates a factory for multiple stores identified by a key.
8
37
  *
@@ -36,23 +65,12 @@ export type StoreKey = GoodInputForHash | {
36
65
  *
37
66
  * @see https://floppy-disk.vercel.app/docs/stores
38
67
  */
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?: {
68
+ export declare const createStores: <TState extends Record<string, any>, TKey extends StoreKey>(initialState: TState, options?: InitStoreOptions<TState, AdditionalStoreApi<TKey>>) => (key?: TKey) => ((options?: {
43
69
  /**
44
70
  * Initial state used on first render (and will also update the store state right after that)
45
71
  *
46
72
  * If provided, `initialState` will be applied **once per store instance**
47
73
  */
48
74
  initialState?: Partial<TState>;
49
- }) => TState) & {
50
- delete: () => boolean;
51
- setState: (value: import("../vanilla.d.mts").SetStateInput<TState>) => void;
52
- getState: () => TState;
53
- subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<TState>) => () => void;
54
- getSubscriberCount: () => number;
55
- key: TKey;
56
- keyHash: string;
57
- };
75
+ }) => TState) & StoreApi<TState> & AdditionalStoreApi<TKey>;
58
76
  export {};
package/esm/react.mjs CHANGED
@@ -102,11 +102,7 @@ const createStores = (initialState, options) => {
102
102
  store.key = key;
103
103
  store.keyHash = keyHash;
104
104
  stores.set(keyHash, store);
105
- }
106
- const useStore = (options2) => useStoreState(store, options2);
107
- return Object.assign(useStore, {
108
- ...store,
109
- delete: () => {
105
+ store.delete = () => {
110
106
  if (store.getSubscriberCount() > 0) {
111
107
  console.warn(
112
108
  "Cannot delete store while it still has active subscribers. Unsubscribe all listeners before deleting the store."
@@ -115,8 +111,10 @@ const createStores = (initialState, options) => {
115
111
  }
116
112
  store.setState(initialState);
117
113
  return stores.delete(keyHash);
118
- }
119
- });
114
+ };
115
+ }
116
+ const useStore = (options2) => useStoreState(store, options2);
117
+ return Object.assign(useStore, store);
120
118
  };
121
119
  return getStore;
122
120
  };
@@ -156,8 +154,8 @@ const createQuery = (queryFn, options = {}) => {
156
154
  onFirstSubscribe: (state, store) => {
157
155
  var _a;
158
156
  (_a = options.onFirstSubscribe) == null ? void 0 : _a.call(options, state, store);
159
- const { metadata, revalidate: revalidate2 } = internals.get(store);
160
- clearTimeout(metadata.garbageCollectionTimeoutId);
157
+ const { internal, revalidate: revalidate2 } = store;
158
+ clearTimeout(internal.garbageCollectionTimeoutId);
161
159
  if (isClient) {
162
160
  if (revalidateOnFocus) {
163
161
  focusListeners.add(revalidate2);
@@ -178,15 +176,15 @@ const createQuery = (queryFn, options = {}) => {
178
176
  onLastUnsubscribe: (state, store) => {
179
177
  var _a;
180
178
  (_a = options.onLastUnsubscribe) == null ? void 0 : _a.call(options, state, store);
181
- const { metadata, revalidate: revalidate2 } = internals.get(store);
182
- clearTimeout(metadata.retryTimeoutId);
183
- if (metadata.retryResolver) {
179
+ const { internal, revalidate: revalidate2 } = store;
180
+ clearTimeout(internal.retryTimeoutId);
181
+ if (internal.retryResolver) {
184
182
  store.setState({ willRetryAt: void 0 });
185
- metadata.retryResolver(store.getState());
186
- metadata.retryResolver = void 0;
183
+ internal.retryResolver(store.getState());
184
+ internal.retryResolver = void 0;
187
185
  }
188
- metadata.garbageCollectionTimeoutId = setTimeout(() => {
189
- if (metadata.promiseResolver || metadata.retryResolver) {
186
+ internal.garbageCollectionTimeoutId = setTimeout(() => {
187
+ if (internal.promiseResolver || internal.retryResolver) {
190
188
  store.setState(initialState);
191
189
  } else {
192
190
  stores.delete(variableHash);
@@ -210,14 +208,13 @@ const createQuery = (queryFn, options = {}) => {
210
208
  }
211
209
  }
212
210
  });
213
- const internals = /* @__PURE__ */ new WeakMap();
214
- const configureInternals = (store, variable) => ({
215
- metadata: {},
211
+ const getApis = (store, variable) => ({
212
+ internal: {},
216
213
  setInitialData: (data, revalidate2 = false) => {
217
214
  const state = store.getState();
218
215
  if (state.state === "INITIAL" && state.data === void 0) {
219
- const { metadata } = internals.get(store);
220
- if (revalidate2) metadata.isInvalidated = true;
216
+ const { internal } = store;
217
+ if (revalidate2) internal.isInvalidated = true;
221
218
  store.setState({
222
219
  state: "SUCCESS",
223
220
  isSuccess: true,
@@ -235,28 +232,28 @@ const createQuery = (queryFn, options = {}) => {
235
232
  return revalidate(store, variable, overwriteOngoingExecution);
236
233
  },
237
234
  invalidate: (options2) => {
238
- const { metadata } = internals.get(store);
239
- metadata.isInvalidated = true;
235
+ const { internal } = store;
236
+ internal.isInvalidated = true;
240
237
  if (store.getSubscriberCount() > 0) {
241
- internals.get(store).execute(options2);
238
+ store.execute(options2);
242
239
  return true;
243
240
  }
244
241
  return false;
245
242
  },
246
243
  reset: () => {
247
244
  var _a, _b;
248
- const { metadata } = internals.get(store);
249
- clearTimeout(metadata.retryTimeoutId);
250
- if (metadata.retryResolver || metadata.promiseResolver) {
245
+ const { internal } = store;
246
+ clearTimeout(internal.retryTimeoutId);
247
+ if (internal.retryResolver || internal.promiseResolver) {
251
248
  console.debug(
252
249
  "Ongoing query execution was ignored due to reset(). The result will not update the store state."
253
250
  );
254
- (_a = metadata.promiseResolver) == null ? void 0 : _a.call(metadata, initialState);
255
- (_b = metadata.retryResolver) == null ? void 0 : _b.call(metadata, initialState);
256
- metadata.promiseResolver = void 0;
257
- metadata.retryResolver = void 0;
251
+ (_a = internal.promiseResolver) == null ? void 0 : _a.call(internal, initialState);
252
+ (_b = internal.retryResolver) == null ? void 0 : _b.call(internal, initialState);
253
+ internal.promiseResolver = void 0;
254
+ internal.retryResolver = void 0;
258
255
  }
259
- metadata.promise = void 0;
256
+ internal.promise = void 0;
260
257
  store.setState(initialState);
261
258
  },
262
259
  delete: () => {
@@ -266,35 +263,36 @@ const createQuery = (queryFn, options = {}) => {
266
263
  );
267
264
  return false;
268
265
  }
269
- internals.get(store).reset();
266
+ store.reset();
270
267
  return stores.delete(store.variableHash);
271
268
  },
272
269
  optimisticUpdate: (optimisticData) => {
273
- const { metadata, revalidate: revalidate2, rollbackOptimisticUpdate } = internals.get(store);
274
- metadata.rollbackData = store.getState().data;
270
+ const { internal, revalidate: revalidate2, rollbackOptimisticUpdate } = store;
271
+ internal.rollbackData = store.getState().data;
275
272
  store.setState({ data: optimisticData });
276
273
  return { revalidate: revalidate2, rollback: rollbackOptimisticUpdate };
277
274
  },
278
275
  rollbackOptimisticUpdate: () => {
279
- const { metadata } = internals.get(store);
280
- store.setState({ data: metadata.rollbackData });
281
- return metadata.rollbackData;
276
+ const { internal } = store;
277
+ store.setState({ data: internal.rollbackData });
278
+ return internal.rollbackData;
282
279
  }
283
280
  });
284
281
  const execute = async (store, variable, overwriteOngoingExecution = false) => {
285
- const { metadata } = internals.get(store);
286
- if (!overwriteOngoingExecution && metadata.promise) return metadata.promise;
287
- clearTimeout(metadata.retryTimeoutId);
282
+ const { internal: _internal } = store;
283
+ const internal = _internal;
284
+ if (!overwriteOngoingExecution && internal.promise) return internal.promise;
285
+ clearTimeout(internal.retryTimeoutId);
288
286
  const createPromise = () => {
289
287
  const promise = new Promise((resolve) => {
290
- metadata.promiseResolver = resolve;
288
+ internal.promiseResolver = resolve;
291
289
  const stateBeforeExecute = store.getState();
292
290
  store.setState({
293
291
  isPending: true,
294
292
  isRevalidating: stateBeforeExecute.state === "SUCCESS",
295
293
  willRetryAt: void 0,
296
- isRetrying: !!metadata.retryResolver,
297
- retryCount: metadata.retryResolver ? stateBeforeExecute.retryCount + 1 : 0
294
+ isRetrying: !!internal.retryResolver,
295
+ retryCount: internal.retryResolver ? stateBeforeExecute.retryCount + 1 : 0
298
296
  });
299
297
  queryFn(variable, stateBeforeExecute, store.variableHash).then((data) => {
300
298
  var _a;
@@ -303,8 +301,8 @@ const createQuery = (queryFn, options = {}) => {
303
301
  "Query function returned undefined. Successful responses must not be undefined."
304
302
  );
305
303
  }
306
- if (!metadata.promiseResolver) return;
307
- if (promise !== metadata.promise) return resolve(metadata.promise);
304
+ if (!internal.promiseResolver) return;
305
+ if (promise !== internal.promise) return resolve(internal.promise);
308
306
  const now = Date.now();
309
307
  store.setState({
310
308
  isPending: false,
@@ -320,17 +318,17 @@ const createQuery = (queryFn, options = {}) => {
320
318
  error: void 0,
321
319
  errorUpdatedAt: void 0
322
320
  });
323
- metadata.isInvalidated = false;
324
- metadata.rollbackData = data;
321
+ internal.isInvalidated = false;
322
+ internal.rollbackData = data;
325
323
  resolve(store.getState());
326
- (_a = metadata.retryResolver) == null ? void 0 : _a.call(metadata, store.getState());
327
- metadata.retryResolver = void 0;
324
+ (_a = internal.retryResolver) == null ? void 0 : _a.call(internal, store.getState());
325
+ internal.retryResolver = void 0;
328
326
  onSuccess(data, variable, stateBeforeExecute);
329
327
  onSettled(variable, stateBeforeExecute);
330
328
  }).catch((error) => {
331
329
  var _a;
332
- if (!metadata.promiseResolver && !metadata.retryResolver) return;
333
- if (promise !== metadata.promise) return resolve(metadata.promise);
330
+ if (!internal.promiseResolver && !internal.retryResolver) return;
331
+ if (promise !== internal.promise) return resolve(internal.promise);
334
332
  const nextState = {
335
333
  ...store.getState(),
336
334
  isPending: false,
@@ -340,8 +338,8 @@ const createQuery = (queryFn, options = {}) => {
340
338
  const [shouldRetry, retryDelay] = shouldRetryFn(error, nextState);
341
339
  const hasSubscriber = store.getSubscriberCount() > 0;
342
340
  if (shouldRetry && hasSubscriber) {
343
- metadata.retryResolver = resolve;
344
- metadata.retryTimeoutId = setTimeout(createPromise, retryDelay);
341
+ internal.retryResolver = resolve;
342
+ internal.retryTimeoutId = setTimeout(createPromise, retryDelay);
345
343
  store.setState({
346
344
  isPending: false,
347
345
  isRevalidating: false,
@@ -366,31 +364,31 @@ const createQuery = (queryFn, options = {}) => {
366
364
  });
367
365
  const state = store.getState();
368
366
  resolve(state);
369
- (_a = metadata.retryResolver) == null ? void 0 : _a.call(metadata, state);
370
- metadata.retryResolver = void 0;
367
+ (_a = internal.retryResolver) == null ? void 0 : _a.call(internal, state);
368
+ internal.retryResolver = void 0;
371
369
  if (onError) onError(error, variable, stateBeforeExecute);
372
370
  else console.error(state);
373
371
  onSettled(variable, stateBeforeExecute);
374
372
  }
375
373
  }).finally(() => {
376
- if (metadata.promise === promise) {
377
- metadata.promise = void 0;
378
- metadata.promiseResolver = void 0;
374
+ if (internal.promise === promise) {
375
+ internal.promise = void 0;
376
+ internal.promiseResolver = void 0;
379
377
  }
380
378
  });
381
379
  });
382
- metadata.promise = promise;
380
+ internal.promise = promise;
383
381
  return promise;
384
382
  };
385
383
  return createPromise();
386
384
  };
387
385
  const revalidate = async (store, variable, overwriteOngoingExecution) => {
388
- const { metadata } = internals.get(store);
389
- if (!overwriteOngoingExecution && metadata.promise) return metadata.promise;
386
+ const { internal } = store;
387
+ if (!overwriteOngoingExecution && internal.promise) return internal.promise;
390
388
  const state = store.getState();
391
389
  if (state.dataUpdatedAt) {
392
390
  const isFresh = state.dataUpdatedAt + staleTime > Date.now();
393
- if (isFresh && !metadata.isInvalidated) return state;
391
+ if (isFresh && !internal.isInvalidated) return state;
394
392
  }
395
393
  return execute(store, variable, overwriteOngoingExecution);
396
394
  };
@@ -407,7 +405,16 @@ const createQuery = (queryFn, options = {}) => {
407
405
  );
408
406
  store.variableHash = variableHash;
409
407
  stores.set(variableHash, store);
410
- internals.set(store, configureInternals(store, variable));
408
+ const apis = getApis(store, variable);
409
+ store.setInitialData = apis.setInitialData;
410
+ store.execute = apis.execute;
411
+ store.revalidate = apis.revalidate;
412
+ store.invalidate = apis.invalidate;
413
+ store.reset = apis.reset;
414
+ store.delete = apis.delete;
415
+ store.optimisticUpdate = apis.optimisticUpdate;
416
+ store.rollbackOptimisticUpdate = apis.rollbackOptimisticUpdate;
417
+ store.internal = apis.internal;
411
418
  }
412
419
  const useStore = (options2 = {}) => {
413
420
  const {
@@ -475,15 +482,11 @@ const createQuery = (queryFn, options = {}) => {
475
482
  return trackedState;
476
483
  };
477
484
  return Object.assign(useStore, {
478
- subscribe: store.subscribe,
479
- getSubscriberCount: store.getSubscriberCount,
480
- getState: store.getState,
485
+ ...store,
481
486
  setState: (value) => {
482
487
  console.debug("Manual setState (not via provided actions) on query store");
483
488
  store.setState(value);
484
- },
485
- ...internals.get(store),
486
- variableHash
489
+ }
487
490
  });
488
491
  };
489
492
  return Object.assign(getStore, {
@@ -494,7 +497,7 @@ const createQuery = (queryFn, options = {}) => {
494
497
  * - Useful for bulk refetching.
495
498
  */
496
499
  executeAll: (options2) => {
497
- stores.forEach((store) => internals.get(store).execute(options2));
500
+ stores.forEach((store) => store.execute(options2));
498
501
  },
499
502
  /**
500
503
  * Revalidates all query instances.
@@ -503,7 +506,7 @@ const createQuery = (queryFn, options = {}) => {
503
506
  * - Only re-fetches stale queries.
504
507
  */
505
508
  revalidateAll: (options2) => {
506
- stores.forEach((store) => internals.get(store).revalidate(options2));
509
+ stores.forEach((store) => store.revalidate(options2));
507
510
  },
508
511
  /**
509
512
  * Invalidates all query instances.
@@ -513,13 +516,13 @@ const createQuery = (queryFn, options = {}) => {
513
516
  * - Invalidated queries bypass `staleTime` until successfully executed again.
514
517
  */
515
518
  invalidateAll: (options2) => {
516
- stores.forEach((store) => internals.get(store).invalidate(options2));
519
+ stores.forEach((store) => store.invalidate(options2));
517
520
  },
518
521
  /**
519
522
  * Resets all query instances.
520
523
  */
521
524
  resetAll: () => {
522
- stores.forEach((store) => internals.get(store).reset());
525
+ stores.forEach((store) => store.reset());
523
526
  }
524
527
  });
525
528
  };
package/esm/vanilla.mjs CHANGED
@@ -62,7 +62,13 @@ const initStore = (initialState, options = {}) => {
62
62
  const prevState = state;
63
63
  const newValue = getValue(value, state);
64
64
  const changedKeys = [];
65
- for (const key in newValue) {
65
+ for (const key of Object.keys(newValue)) {
66
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
67
+ console.warn(
68
+ `Ignored unsafe key "${String(key)}" in setState(). This key is reserved and may indicate a prototype pollution attempt or malformed payload.`
69
+ );
70
+ continue;
71
+ }
66
72
  if (!Object.is(prevState[key], newValue[key])) {
67
73
  changedKeys.push(key);
68
74
  }
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.6.0",
5
+ "version": "3.7.0-beta.1",
6
6
  "keywords": [
7
7
  "utilities",
8
8
  "store",
@@ -68,6 +68,7 @@
68
68
  }
69
69
  }
70
70
  },
71
+ "packageManager": "pnpm@10.32.1",
71
72
  "peerDependencies": {
72
73
  "@types/react": ">=17.0",
73
74
  "react": ">=17.0"
@@ -80,4 +81,4 @@
80
81
  "optional": true
81
82
  }
82
83
  }
83
- }
84
+ }
@@ -44,17 +44,7 @@ export type MutationState<TData, TVariable, TError> = {
44
44
  error: TError;
45
45
  errorUpdatedAt: number;
46
46
  });
47
- export declare const INITIAL_STATE: {
48
- state: string;
49
- isPending: boolean;
50
- isSuccess: boolean;
51
- isError: boolean;
52
- variable: undefined;
53
- data: undefined;
54
- dataUpdatedAt: undefined;
55
- error: undefined;
56
- errorUpdatedAt: undefined;
57
- };
47
+ export declare const INITIAL_STATE: MutationState<any, any, any>;
58
48
  /**
59
49
  * Configuration options for a mutation.
60
50
  *
@@ -63,15 +63,149 @@ export type QueryState<TData, TError> = {
63
63
  error: TError;
64
64
  errorUpdatedAt: number;
65
65
  });
66
+ type Internal<TData, TError> = {
67
+ isInvalidated?: boolean;
68
+ promise?: Promise<QueryState<TData, TError>> | undefined;
69
+ promiseResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
70
+ retryTimeoutId?: number;
71
+ retryResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
72
+ garbageCollectionTimeoutId?: number;
73
+ rollbackData?: TData | undefined;
74
+ };
75
+ type AdditionalStoreApi<TData, TError> = {
76
+ /**
77
+ * A deterministic hash string derived from the query variable.
78
+ *
79
+ * Used as the unique identifier for this query instance in the internal cache.
80
+ *
81
+ * @remarks
82
+ * - Structurally identical variables will produce the same hash.
83
+ */
84
+ variableHash: string;
85
+ /**
86
+ * Sets initial data for the query if it has not been initialized.
87
+ *
88
+ * @param data - Initial data
89
+ * @param revalidate - Whether to mark the data as invalidated (will trigger revalidation)
90
+ *
91
+ * @returns `true` if the data was set, `false` otherwise
92
+ *
93
+ * @remarks
94
+ * - Only applies when the query is in the `INITIAL` state.
95
+ * - Useful for hydration or preloaded data.
96
+ */
97
+ setInitialData: (data: TData, revalidate?: boolean) => boolean;
98
+ /**
99
+ * Executes the query function.
100
+ *
101
+ * @param options - Execution options
102
+ * @param options.overwriteOngoingExecution - Whether to start a new execution instead of reusing an ongoing one (default: `true`)
103
+ *
104
+ * @returns A promise resolving to the latest query state
105
+ *
106
+ * @remarks
107
+ * - By default, each call **starts a new execution** even if one is already in progress.
108
+ * - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
109
+ * - Handles:
110
+ * - Pending state
111
+ * - Success state
112
+ * - Error state
113
+ * - Retry logic
114
+ */
115
+ execute: (options?: {
116
+ overwriteOngoingExecution?: boolean;
117
+ }) => Promise<QueryState<TData, TError>>;
118
+ /**
119
+ * Re-executes the query if needed based on freshness or invalidation.
120
+ *
121
+ * @param options - Revalidation options
122
+ * @param options.overwriteOngoingExecution - Whether to overwrite an ongoing execution (default: `true`)
123
+ *
124
+ * @returns The current state if still fresh, otherwise a promise of the new state
125
+ *
126
+ * @remarks
127
+ * - Skips execution if data is still fresh (`staleTime`) **AND** the query has not been invalidated.
128
+ * - If execution is not skipped, by default it will start a new execution even if one is already in progress.
129
+ * - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
130
+ */
131
+ revalidate: (options?: {
132
+ overwriteOngoingExecution?: boolean;
133
+ }) => Promise<QueryState<TData, TError>>;
134
+ /**
135
+ * Marks the query as invalidated and optionally triggers re-execution.
136
+ *
137
+ * @param options - Invalidation options
138
+ * @param options.overwriteOngoingExecution - Whether to overwrite an ongoing execution (default: `true`)
139
+ *
140
+ * @returns `true` if execution was triggered, `false` otherwise
141
+ *
142
+ * @remarks
143
+ * - Invalidated queries are treated as stale regardless of `staleTime`.
144
+ * - The next `revalidate` will always execute until a successful result clears the invalidation.
145
+ * - If there are active subscribers: Execution is triggered immediately.
146
+ * - Otherwise: The query remains invalidated and will execute on the next revalidation.
147
+ * - By default, starts a new execution even if one is already in progress.
148
+ * - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
149
+ */
150
+ invalidate: (options?: {
151
+ overwriteOngoingExecution?: boolean;
152
+ }) => boolean;
153
+ /**
154
+ * Resets the query state to its initial state.
155
+ *
156
+ * @remarks
157
+ * - Cancels retry logic and ignores any ongoing execution results.
158
+ */
159
+ reset: () => void;
160
+ /**
161
+ * Deletes the query store for the current variable.
162
+ *
163
+ * @returns `true` if deleted, `false` otherwise
164
+ *
165
+ * @remarks
166
+ * - Cannot delete while there are active subscribers.
167
+ */
168
+ delete: () => boolean;
169
+ /**
170
+ * Performs an optimistic update on the query data.
171
+ *
172
+ * @param data - Optimistic data to set
173
+ *
174
+ * @returns Controls for managing the optimistic update
175
+ *
176
+ * @remarks
177
+ * - Temporarily replaces the current data.
178
+ * - Stores previous data for rollback.
179
+ * - Commonly used with mutations for instant UI updates.
180
+ *
181
+ * @example
182
+ * const { rollback, revalidate } = query.optimisticUpdate(newData);
183
+ */
184
+ optimisticUpdate: (data: TData) => {
185
+ revalidate: () => Promise<QueryState<TData, TError>>;
186
+ rollback: () => TData;
187
+ };
188
+ /**
189
+ * Restores the data before the last optimistic update.
190
+ *
191
+ * @returns The restored data
192
+ *
193
+ * @remarks
194
+ * - Should be used if an optimistic update fails.
195
+ */
196
+ rollbackOptimisticUpdate: () => TData;
197
+ /**
198
+ * Internal data, do not mutate!
199
+ */
200
+ internal: Readonly<Internal<TData, TError>>;
201
+ };
66
202
  /**
67
203
  * Configuration options for a query.
68
204
  *
69
205
  * @remarks
70
206
  * Controls caching, retry behavior, lifecycle, and side effects of an async operation.
71
207
  */
72
- export type QueryOptions<TData, TVariable extends StoreKey, TError = Error> = InitStoreOptions<QueryState<TData, TError>, {
73
- variableHash: string;
74
- }> & {
208
+ export type QueryOptions<TData, TVariable extends StoreKey, TError = Error> = InitStoreOptions<QueryState<TData, TError>, AdditionalStoreApi<TData, TError>> & {
75
209
  /**
76
210
  * Time (in milliseconds) that data is considered fresh.
77
211
  *
@@ -219,19 +353,19 @@ export declare const createQuery: <TData, TVariable extends StoreKey = never, TE
219
353
  initialData?: never;
220
354
  initialDataIsStale?: never;
221
355
  })) => QueryState<TData, TError>) & {
222
- variableHash: string;
356
+ setState: (value: SetStateInput<QueryState<TData, TError>>) => void;
357
+ getState: () => QueryState<TData, TError>;
358
+ subscribe: (subscriber: import("../vanilla.ts").Subscriber<QueryState<TData, TError>>) => () => void;
359
+ getSubscriberCount: () => number;
223
360
  /**
224
- * Internal data, do not mutate!
361
+ * A deterministic hash string derived from the query variable.
362
+ *
363
+ * Used as the unique identifier for this query instance in the internal cache.
364
+ *
365
+ * @remarks
366
+ * - Structurally identical variables will produce the same hash.
225
367
  */
226
- metadata: {
227
- isInvalidated?: boolean;
228
- promise?: Promise<QueryState<TData, TError>> | undefined;
229
- promiseResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
230
- retryTimeoutId?: number;
231
- retryResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
232
- garbageCollectionTimeoutId?: number;
233
- rollbackData?: TData | undefined;
234
- };
368
+ variableHash: string;
235
369
  /**
236
370
  * Sets initial data for the query if it has not been initialized.
237
371
  *
@@ -344,10 +478,10 @@ export declare const createQuery: <TData, TVariable extends StoreKey = never, TE
344
478
  * - Should be used if an optimistic update fails.
345
479
  */
346
480
  rollbackOptimisticUpdate: () => TData;
347
- subscribe: (subscriber: import("../vanilla.ts").Subscriber<QueryState<TData, TError>>) => () => void;
348
- getSubscriberCount: () => number;
349
- getState: () => QueryState<TData, TError>;
350
- setState: (value: SetStateInput<QueryState<TData, TError>>) => void;
481
+ /**
482
+ * Internal data, do not mutate!
483
+ */
484
+ internal: Readonly<Internal<TData, TError>>;
351
485
  }) & {
352
486
  /**
353
487
  * Executes all query instances.
@@ -382,3 +516,4 @@ export declare const createQuery: <TData, TVariable extends StoreKey = never, TE
382
516
  */
383
517
  resetAll: () => void;
384
518
  };
519
+ export {};
@@ -1,8 +1,37 @@
1
- import { type InitStoreOptions } from "../vanilla.ts";
1
+ import { type InitStoreOptions, type StoreApi } from "../vanilla.ts";
2
2
  type GoodInputForHash = string | number | boolean | null | Date;
3
3
  export type StoreKey = GoodInputForHash | {
4
4
  [key: string | number]: StoreKey | StoreKey[];
5
5
  };
6
+ type AdditionalStoreApi<TKey> = {
7
+ /**
8
+ * The original key used to identify this store instance.\
9
+ * This value is not hashed and is preserved as-is.
10
+ */
11
+ key: TKey;
12
+ /**
13
+ * A deterministic hash string derived from {@link key}.
14
+ *
15
+ * Used internally as the unique identifier for caching and retrieving store instances.
16
+ *
17
+ * @remarks
18
+ * - Guarantees that structurally identical keys produce the same hash.
19
+ */
20
+ keyHash: string;
21
+ /**
22
+ * Deletes this store instance from the internal cache.
23
+ *
24
+ * @returns `true` if the store was successfully deleted, otherwise `false`.
25
+ *
26
+ * @remarks
27
+ * - If there are active subscribers, the deletion is ignored and `false` is returned.
28
+ * - When deletion succeeds:
29
+ * - The store is removed from the cache.
30
+ * - Its state is reset to the initial state.
31
+ * - Intended for manual cleanup of unused or ephemeral stores.
32
+ */
33
+ delete: () => boolean;
34
+ };
6
35
  /**
7
36
  * Creates a factory for multiple stores identified by a key.
8
37
  *
@@ -36,23 +65,12 @@ export type StoreKey = GoodInputForHash | {
36
65
  *
37
66
  * @see https://floppy-disk.vercel.app/docs/stores
38
67
  */
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?: {
68
+ export declare const createStores: <TState extends Record<string, any>, TKey extends StoreKey>(initialState: TState, options?: InitStoreOptions<TState, AdditionalStoreApi<TKey>>) => (key?: TKey) => ((options?: {
43
69
  /**
44
70
  * Initial state used on first render (and will also update the store state right after that)
45
71
  *
46
72
  * If provided, `initialState` will be applied **once per store instance**
47
73
  */
48
74
  initialState?: Partial<TState>;
49
- }) => TState) & {
50
- delete: () => boolean;
51
- setState: (value: import("../vanilla.ts").SetStateInput<TState>) => void;
52
- getState: () => TState;
53
- subscribe: (subscriber: import("../vanilla.ts").Subscriber<TState>) => () => void;
54
- getSubscriberCount: () => number;
55
- key: TKey;
56
- keyHash: string;
57
- };
75
+ }) => TState) & StoreApi<TState> & AdditionalStoreApi<TKey>;
58
76
  export {};
package/react.js CHANGED
@@ -104,11 +104,7 @@ const createStores = (initialState, options) => {
104
104
  store.key = key;
105
105
  store.keyHash = keyHash;
106
106
  stores.set(keyHash, store);
107
- }
108
- const useStore = (options2) => useStoreState(store, options2);
109
- return Object.assign(useStore, {
110
- ...store,
111
- delete: () => {
107
+ store.delete = () => {
112
108
  if (store.getSubscriberCount() > 0) {
113
109
  console.warn(
114
110
  "Cannot delete store while it still has active subscribers. Unsubscribe all listeners before deleting the store."
@@ -117,8 +113,10 @@ const createStores = (initialState, options) => {
117
113
  }
118
114
  store.setState(initialState);
119
115
  return stores.delete(keyHash);
120
- }
121
- });
116
+ };
117
+ }
118
+ const useStore = (options2) => useStoreState(store, options2);
119
+ return Object.assign(useStore, store);
122
120
  };
123
121
  return getStore;
124
122
  };
@@ -158,8 +156,8 @@ const createQuery = (queryFn, options = {}) => {
158
156
  onFirstSubscribe: (state, store) => {
159
157
  var _a;
160
158
  (_a = options.onFirstSubscribe) == null ? void 0 : _a.call(options, state, store);
161
- const { metadata, revalidate: revalidate2 } = internals.get(store);
162
- clearTimeout(metadata.garbageCollectionTimeoutId);
159
+ const { internal, revalidate: revalidate2 } = store;
160
+ clearTimeout(internal.garbageCollectionTimeoutId);
163
161
  if (vanilla.isClient) {
164
162
  if (revalidateOnFocus) {
165
163
  focusListeners.add(revalidate2);
@@ -180,15 +178,15 @@ const createQuery = (queryFn, options = {}) => {
180
178
  onLastUnsubscribe: (state, store) => {
181
179
  var _a;
182
180
  (_a = options.onLastUnsubscribe) == null ? void 0 : _a.call(options, state, store);
183
- const { metadata, revalidate: revalidate2 } = internals.get(store);
184
- clearTimeout(metadata.retryTimeoutId);
185
- if (metadata.retryResolver) {
181
+ const { internal, revalidate: revalidate2 } = store;
182
+ clearTimeout(internal.retryTimeoutId);
183
+ if (internal.retryResolver) {
186
184
  store.setState({ willRetryAt: void 0 });
187
- metadata.retryResolver(store.getState());
188
- metadata.retryResolver = void 0;
185
+ internal.retryResolver(store.getState());
186
+ internal.retryResolver = void 0;
189
187
  }
190
- metadata.garbageCollectionTimeoutId = setTimeout(() => {
191
- if (metadata.promiseResolver || metadata.retryResolver) {
188
+ internal.garbageCollectionTimeoutId = setTimeout(() => {
189
+ if (internal.promiseResolver || internal.retryResolver) {
192
190
  store.setState(initialState);
193
191
  } else {
194
192
  stores.delete(variableHash);
@@ -212,14 +210,13 @@ const createQuery = (queryFn, options = {}) => {
212
210
  }
213
211
  }
214
212
  });
215
- const internals = /* @__PURE__ */ new WeakMap();
216
- const configureInternals = (store, variable) => ({
217
- metadata: {},
213
+ const getApis = (store, variable) => ({
214
+ internal: {},
218
215
  setInitialData: (data, revalidate2 = false) => {
219
216
  const state = store.getState();
220
217
  if (state.state === "INITIAL" && state.data === void 0) {
221
- const { metadata } = internals.get(store);
222
- if (revalidate2) metadata.isInvalidated = true;
218
+ const { internal } = store;
219
+ if (revalidate2) internal.isInvalidated = true;
223
220
  store.setState({
224
221
  state: "SUCCESS",
225
222
  isSuccess: true,
@@ -237,28 +234,28 @@ const createQuery = (queryFn, options = {}) => {
237
234
  return revalidate(store, variable, overwriteOngoingExecution);
238
235
  },
239
236
  invalidate: (options2) => {
240
- const { metadata } = internals.get(store);
241
- metadata.isInvalidated = true;
237
+ const { internal } = store;
238
+ internal.isInvalidated = true;
242
239
  if (store.getSubscriberCount() > 0) {
243
- internals.get(store).execute(options2);
240
+ store.execute(options2);
244
241
  return true;
245
242
  }
246
243
  return false;
247
244
  },
248
245
  reset: () => {
249
246
  var _a, _b;
250
- const { metadata } = internals.get(store);
251
- clearTimeout(metadata.retryTimeoutId);
252
- if (metadata.retryResolver || metadata.promiseResolver) {
247
+ const { internal } = store;
248
+ clearTimeout(internal.retryTimeoutId);
249
+ if (internal.retryResolver || internal.promiseResolver) {
253
250
  console.debug(
254
251
  "Ongoing query execution was ignored due to reset(). The result will not update the store state."
255
252
  );
256
- (_a = metadata.promiseResolver) == null ? void 0 : _a.call(metadata, initialState);
257
- (_b = metadata.retryResolver) == null ? void 0 : _b.call(metadata, initialState);
258
- metadata.promiseResolver = void 0;
259
- metadata.retryResolver = void 0;
253
+ (_a = internal.promiseResolver) == null ? void 0 : _a.call(internal, initialState);
254
+ (_b = internal.retryResolver) == null ? void 0 : _b.call(internal, initialState);
255
+ internal.promiseResolver = void 0;
256
+ internal.retryResolver = void 0;
260
257
  }
261
- metadata.promise = void 0;
258
+ internal.promise = void 0;
262
259
  store.setState(initialState);
263
260
  },
264
261
  delete: () => {
@@ -268,35 +265,36 @@ const createQuery = (queryFn, options = {}) => {
268
265
  );
269
266
  return false;
270
267
  }
271
- internals.get(store).reset();
268
+ store.reset();
272
269
  return stores.delete(store.variableHash);
273
270
  },
274
271
  optimisticUpdate: (optimisticData) => {
275
- const { metadata, revalidate: revalidate2, rollbackOptimisticUpdate } = internals.get(store);
276
- metadata.rollbackData = store.getState().data;
272
+ const { internal, revalidate: revalidate2, rollbackOptimisticUpdate } = store;
273
+ internal.rollbackData = store.getState().data;
277
274
  store.setState({ data: optimisticData });
278
275
  return { revalidate: revalidate2, rollback: rollbackOptimisticUpdate };
279
276
  },
280
277
  rollbackOptimisticUpdate: () => {
281
- const { metadata } = internals.get(store);
282
- store.setState({ data: metadata.rollbackData });
283
- return metadata.rollbackData;
278
+ const { internal } = store;
279
+ store.setState({ data: internal.rollbackData });
280
+ return internal.rollbackData;
284
281
  }
285
282
  });
286
283
  const execute = async (store, variable, overwriteOngoingExecution = false) => {
287
- const { metadata } = internals.get(store);
288
- if (!overwriteOngoingExecution && metadata.promise) return metadata.promise;
289
- clearTimeout(metadata.retryTimeoutId);
284
+ const { internal: _internal } = store;
285
+ const internal = _internal;
286
+ if (!overwriteOngoingExecution && internal.promise) return internal.promise;
287
+ clearTimeout(internal.retryTimeoutId);
290
288
  const createPromise = () => {
291
289
  const promise = new Promise((resolve) => {
292
- metadata.promiseResolver = resolve;
290
+ internal.promiseResolver = resolve;
293
291
  const stateBeforeExecute = store.getState();
294
292
  store.setState({
295
293
  isPending: true,
296
294
  isRevalidating: stateBeforeExecute.state === "SUCCESS",
297
295
  willRetryAt: void 0,
298
- isRetrying: !!metadata.retryResolver,
299
- retryCount: metadata.retryResolver ? stateBeforeExecute.retryCount + 1 : 0
296
+ isRetrying: !!internal.retryResolver,
297
+ retryCount: internal.retryResolver ? stateBeforeExecute.retryCount + 1 : 0
300
298
  });
301
299
  queryFn(variable, stateBeforeExecute, store.variableHash).then((data) => {
302
300
  var _a;
@@ -305,8 +303,8 @@ const createQuery = (queryFn, options = {}) => {
305
303
  "Query function returned undefined. Successful responses must not be undefined."
306
304
  );
307
305
  }
308
- if (!metadata.promiseResolver) return;
309
- if (promise !== metadata.promise) return resolve(metadata.promise);
306
+ if (!internal.promiseResolver) return;
307
+ if (promise !== internal.promise) return resolve(internal.promise);
310
308
  const now = Date.now();
311
309
  store.setState({
312
310
  isPending: false,
@@ -322,17 +320,17 @@ const createQuery = (queryFn, options = {}) => {
322
320
  error: void 0,
323
321
  errorUpdatedAt: void 0
324
322
  });
325
- metadata.isInvalidated = false;
326
- metadata.rollbackData = data;
323
+ internal.isInvalidated = false;
324
+ internal.rollbackData = data;
327
325
  resolve(store.getState());
328
- (_a = metadata.retryResolver) == null ? void 0 : _a.call(metadata, store.getState());
329
- metadata.retryResolver = void 0;
326
+ (_a = internal.retryResolver) == null ? void 0 : _a.call(internal, store.getState());
327
+ internal.retryResolver = void 0;
330
328
  onSuccess(data, variable, stateBeforeExecute);
331
329
  onSettled(variable, stateBeforeExecute);
332
330
  }).catch((error) => {
333
331
  var _a;
334
- if (!metadata.promiseResolver && !metadata.retryResolver) return;
335
- if (promise !== metadata.promise) return resolve(metadata.promise);
332
+ if (!internal.promiseResolver && !internal.retryResolver) return;
333
+ if (promise !== internal.promise) return resolve(internal.promise);
336
334
  const nextState = {
337
335
  ...store.getState(),
338
336
  isPending: false,
@@ -342,8 +340,8 @@ const createQuery = (queryFn, options = {}) => {
342
340
  const [shouldRetry, retryDelay] = shouldRetryFn(error, nextState);
343
341
  const hasSubscriber = store.getSubscriberCount() > 0;
344
342
  if (shouldRetry && hasSubscriber) {
345
- metadata.retryResolver = resolve;
346
- metadata.retryTimeoutId = setTimeout(createPromise, retryDelay);
343
+ internal.retryResolver = resolve;
344
+ internal.retryTimeoutId = setTimeout(createPromise, retryDelay);
347
345
  store.setState({
348
346
  isPending: false,
349
347
  isRevalidating: false,
@@ -368,31 +366,31 @@ const createQuery = (queryFn, options = {}) => {
368
366
  });
369
367
  const state = store.getState();
370
368
  resolve(state);
371
- (_a = metadata.retryResolver) == null ? void 0 : _a.call(metadata, state);
372
- metadata.retryResolver = void 0;
369
+ (_a = internal.retryResolver) == null ? void 0 : _a.call(internal, state);
370
+ internal.retryResolver = void 0;
373
371
  if (onError) onError(error, variable, stateBeforeExecute);
374
372
  else console.error(state);
375
373
  onSettled(variable, stateBeforeExecute);
376
374
  }
377
375
  }).finally(() => {
378
- if (metadata.promise === promise) {
379
- metadata.promise = void 0;
380
- metadata.promiseResolver = void 0;
376
+ if (internal.promise === promise) {
377
+ internal.promise = void 0;
378
+ internal.promiseResolver = void 0;
381
379
  }
382
380
  });
383
381
  });
384
- metadata.promise = promise;
382
+ internal.promise = promise;
385
383
  return promise;
386
384
  };
387
385
  return createPromise();
388
386
  };
389
387
  const revalidate = async (store, variable, overwriteOngoingExecution) => {
390
- const { metadata } = internals.get(store);
391
- if (!overwriteOngoingExecution && metadata.promise) return metadata.promise;
388
+ const { internal } = store;
389
+ if (!overwriteOngoingExecution && internal.promise) return internal.promise;
392
390
  const state = store.getState();
393
391
  if (state.dataUpdatedAt) {
394
392
  const isFresh = state.dataUpdatedAt + staleTime > Date.now();
395
- if (isFresh && !metadata.isInvalidated) return state;
393
+ if (isFresh && !internal.isInvalidated) return state;
396
394
  }
397
395
  return execute(store, variable, overwriteOngoingExecution);
398
396
  };
@@ -409,7 +407,16 @@ const createQuery = (queryFn, options = {}) => {
409
407
  );
410
408
  store.variableHash = variableHash;
411
409
  stores.set(variableHash, store);
412
- internals.set(store, configureInternals(store, variable));
410
+ const apis = getApis(store, variable);
411
+ store.setInitialData = apis.setInitialData;
412
+ store.execute = apis.execute;
413
+ store.revalidate = apis.revalidate;
414
+ store.invalidate = apis.invalidate;
415
+ store.reset = apis.reset;
416
+ store.delete = apis.delete;
417
+ store.optimisticUpdate = apis.optimisticUpdate;
418
+ store.rollbackOptimisticUpdate = apis.rollbackOptimisticUpdate;
419
+ store.internal = apis.internal;
413
420
  }
414
421
  const useStore = (options2 = {}) => {
415
422
  const {
@@ -477,15 +484,11 @@ const createQuery = (queryFn, options = {}) => {
477
484
  return trackedState;
478
485
  };
479
486
  return Object.assign(useStore, {
480
- subscribe: store.subscribe,
481
- getSubscriberCount: store.getSubscriberCount,
482
- getState: store.getState,
487
+ ...store,
483
488
  setState: (value) => {
484
489
  console.debug("Manual setState (not via provided actions) on query store");
485
490
  store.setState(value);
486
- },
487
- ...internals.get(store),
488
- variableHash
491
+ }
489
492
  });
490
493
  };
491
494
  return Object.assign(getStore, {
@@ -496,7 +499,7 @@ const createQuery = (queryFn, options = {}) => {
496
499
  * - Useful for bulk refetching.
497
500
  */
498
501
  executeAll: (options2) => {
499
- stores.forEach((store) => internals.get(store).execute(options2));
502
+ stores.forEach((store) => store.execute(options2));
500
503
  },
501
504
  /**
502
505
  * Revalidates all query instances.
@@ -505,7 +508,7 @@ const createQuery = (queryFn, options = {}) => {
505
508
  * - Only re-fetches stale queries.
506
509
  */
507
510
  revalidateAll: (options2) => {
508
- stores.forEach((store) => internals.get(store).revalidate(options2));
511
+ stores.forEach((store) => store.revalidate(options2));
509
512
  },
510
513
  /**
511
514
  * Invalidates all query instances.
@@ -515,13 +518,13 @@ const createQuery = (queryFn, options = {}) => {
515
518
  * - Invalidated queries bypass `staleTime` until successfully executed again.
516
519
  */
517
520
  invalidateAll: (options2) => {
518
- stores.forEach((store) => internals.get(store).invalidate(options2));
521
+ stores.forEach((store) => store.invalidate(options2));
519
522
  },
520
523
  /**
521
524
  * Resets all query instances.
522
525
  */
523
526
  resetAll: () => {
524
- stores.forEach((store) => internals.get(store).reset());
527
+ stores.forEach((store) => store.reset());
525
528
  }
526
529
  });
527
530
  };
package/vanilla.js CHANGED
@@ -64,7 +64,13 @@ const initStore = (initialState, options = {}) => {
64
64
  const prevState = state;
65
65
  const newValue = getValue(value, state);
66
66
  const changedKeys = [];
67
- for (const key in newValue) {
67
+ for (const key of Object.keys(newValue)) {
68
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
69
+ console.warn(
70
+ `Ignored unsafe key "${String(key)}" in setState(). This key is reserved and may indicate a prototype pollution attempt or malformed payload.`
71
+ );
72
+ continue;
73
+ }
68
74
  if (!Object.is(prevState[key], newValue[key])) {
69
75
  changedKeys.push(key);
70
76
  }