yuustate 0.0.1 → 3.10.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.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Check if this runs on browser.
3
+ */
4
+ export declare const isClient: boolean;
5
+ /**
6
+ * Empty function.
7
+ */
8
+ export declare const noop: () => void;
9
+ /**
10
+ * If the value is a function, it will invoke the function.\
11
+ * If the value is not a function, it will just return it.
12
+ */
13
+ export declare const getValue: <T, P extends any[]>(valueOrComputeValueFn: T | ((...params: P) => T), ...params: P) => T;
@@ -0,0 +1,7 @@
1
+ export declare const isPlainObject: (value: any) => boolean;
2
+ /**
3
+ * Get stable hash string from any value.
4
+ *
5
+ * Reference: https://github.com/TanStack/query/blob/v5.90.3/packages/query-core/src/utils.ts#L216
6
+ */
7
+ export declare const getHash: (value?: any) => string;
@@ -0,0 +1,121 @@
1
+ export type SetStateInput<TState> = Partial<TState> | ((state: TState) => Partial<TState>);
2
+ /**
3
+ * A subscriber function that is called whenever the state updates.
4
+ *
5
+ * @param state - The latest state
6
+ * @param prevState - The previous state before the update
7
+ * @param changedKeys - The top-level keys that changed (shallow diff)
8
+ *
9
+ * @remarks
10
+ * - Subscribers are only called when at least one field changes.
11
+ * - Change detection is performed per key using `Object.is`.
12
+ * - `changedKeys` only includes top-level keys; nested changes must be inferred by the consumer.
13
+ */
14
+ export type Subscriber<TState> = (state: TState, prevState: TState, changedKeys: Array<keyof TState>) => void;
15
+ /**
16
+ * Core store API for managing state.
17
+ *
18
+ * @remarks
19
+ * - The store performs **shallow change detection per key** before notifying subscribers.
20
+ * - Subscribers are only notified when at least one field changes.
21
+ * - State is treated as **immutable**. Mutating nested values directly will not trigger updates.
22
+ * - Designed to be framework-agnostic (React bindings are built separately).
23
+ * - By default, `setState` is **disabled on the server** to prevent accidental shared state between requests.
24
+ */
25
+ export type StoreApi<TState extends Record<string, any>> = {
26
+ /**
27
+ * Updates the state.
28
+ *
29
+ * @param value - Partial state or updater function
30
+ *
31
+ * @remarks
32
+ * - Accepts either a partial object or a function `(state) => partial`
33
+ * - Updates are shallowly merged into the current state
34
+ */
35
+ setState: (value: SetStateInput<TState>) => void;
36
+ /**
37
+ * Get the current state.
38
+ *
39
+ * @returns The latest state snapshot
40
+ */
41
+ getState: () => TState;
42
+ /**
43
+ * Subscribes to state changes.
44
+ *
45
+ * @param subscriber - Function called when state updates
46
+ *
47
+ * @returns Unsubscribe function
48
+ *
49
+ * @remarks
50
+ * - Subscribers are only called when at least one root key changes
51
+ * - Receives `(state, prevState, changedKeys)`
52
+ */
53
+ subscribe: (subscriber: Subscriber<TState>) => () => void;
54
+ /**
55
+ * Returns the number of active subscribers.
56
+ *
57
+ * @returns Total subscriber count
58
+ *
59
+ * @remarks
60
+ * - Can be used to determine if the store is currently in use
61
+ */
62
+ getSubscriberCount: () => number;
63
+ };
64
+ /**
65
+ * Lifecycle hooks for the store.
66
+ *
67
+ * These hooks allow you to attach side effects based on subscription lifecycle.
68
+ *
69
+ * @remarks
70
+ * Useful for:
71
+ * - Lazy initialization (e.g. start fetching on first subscribe)
72
+ * - Cleanup (e.g. cancel timers, disconnect sockets)
73
+ * - Resource management (e.g. garbage collection)
74
+ */
75
+ export type InitStoreOptions<TState extends Record<string, any>, TStoreProps extends Record<string, any> = object> = {
76
+ onFirstSubscribe?: (state: TState, store: StoreApi<TState> & TStoreProps) => void;
77
+ onSubscribe?: (state: TState, store: StoreApi<TState> & TStoreProps) => void;
78
+ onUnsubscribe?: (state: TState, store: StoreApi<TState> & TStoreProps) => void;
79
+ onLastUnsubscribe?: (state: TState, store: StoreApi<TState> & TStoreProps) => void;
80
+ /**
81
+ * Called whenever the state changes, without counting as a subscriber.
82
+ * Acts like a "spy" on state updates.
83
+ * Useful for devtools, logging, or debugging state changes.
84
+ */
85
+ onStateChange?: (state: TState, prevState: TState, changedKeys: Array<keyof TState>) => void;
86
+ /**
87
+ * By default, calling `setState` on the server is disallowed to prevent shared state across requests.
88
+ * Set this to `true` only if you explicitly intend to mutate state during server execution.
89
+ */
90
+ allowSetStateServerSide?: boolean;
91
+ };
92
+ /**
93
+ * Creates a vanilla store with pub-sub capabilities.
94
+ *
95
+ * The store state must be an **object**.\
96
+ * Updates are applied as shallow merges, so non-object states are not supported.
97
+ *
98
+ * @param initialState - The initial state of the store
99
+ * @param options - Optional lifecycle hooks
100
+ *
101
+ * @returns A store API for managing state and subscriptions
102
+ *
103
+ * @remarks
104
+ * - State updates are **shallowly compared per key** before notifying subscribers.
105
+ * - Subscribers are only notified when at least one updated field changes (using `Object.is` comparison).
106
+ * - Subscribers receive the new state, previous state, and changed top-level keys.
107
+ * - State is expected to be treated as **immutable**.
108
+ * - Mutating nested values directly will not trigger updates.
109
+ * - Lifecycle hooks allow side-effect management tied to subscription count.
110
+ * - By default, `setState` is **not allowed on the server** to prevent accidental shared state between requests.
111
+ * - This helps avoid leaking data between users in server environments.
112
+ * - If you intentionally want to allow this behavior, set `allowSetStateServerSide: true`.
113
+ *
114
+ * @example
115
+ * const store = initStore({ count: 0 });
116
+ *
117
+ * store.subscribe((state) => console.log(state.count));
118
+ * store.setState({ count: 1 }); // triggers subscriber
119
+ * store.setState({ count: 1 }); // no-op (no change)
120
+ */
121
+ export declare const initStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => StoreApi<TState>;
@@ -0,0 +1,3 @@
1
+ export * from "./vanilla/basic.mjs";
2
+ export * from "./vanilla/hash.mjs";
3
+ export * from "./vanilla/store.mjs";
@@ -0,0 +1,90 @@
1
+ const isClient = typeof window !== "undefined" && !("Deno" in window);
2
+ const noop = () => {
3
+ };
4
+ const getValue = (valueOrComputeValueFn, ...params) => {
5
+ if (typeof valueOrComputeValueFn === "function") {
6
+ return valueOrComputeValueFn(...params);
7
+ }
8
+ return valueOrComputeValueFn;
9
+ };
10
+
11
+ const hasObjectPrototype = (value) => {
12
+ return Object.prototype.toString.call(value) === "[object Object]";
13
+ };
14
+ const isPlainObject = (value) => {
15
+ if (!hasObjectPrototype(value)) return false;
16
+ const ctor = value.constructor;
17
+ if (typeof ctor === "undefined") return true;
18
+ const prot = ctor.prototype;
19
+ if (!hasObjectPrototype(prot)) return false;
20
+ if (!prot.hasOwnProperty("isPrototypeOf")) return false;
21
+ if (Object.getPrototypeOf(value) !== Object.prototype) return false;
22
+ return true;
23
+ };
24
+ const getHash = (value) => JSON.stringify(
25
+ value,
26
+ (_, val) => isPlainObject(val) ? Object.keys(val).sort().reduce((result, key) => {
27
+ result[key] = val[key];
28
+ return result;
29
+ }, {}) : val
30
+ );
31
+
32
+ const initStore = (initialState, options = {}) => {
33
+ const {
34
+ onFirstSubscribe = noop,
35
+ onSubscribe = noop,
36
+ onUnsubscribe = noop,
37
+ onLastUnsubscribe = noop,
38
+ onStateChange = noop,
39
+ allowSetStateServerSide = false
40
+ } = options;
41
+ const subscribers = /* @__PURE__ */ new Set();
42
+ const getSubscriberCount = () => subscribers.size;
43
+ const subscribe = (subscriber) => {
44
+ subscribers.add(subscriber);
45
+ if (subscribers.size === 1) onFirstSubscribe(state, storeApi);
46
+ onSubscribe(state, storeApi);
47
+ return () => {
48
+ subscribers.delete(subscriber);
49
+ onUnsubscribe(state, storeApi);
50
+ if (subscribers.size === 0) onLastUnsubscribe(state, storeApi);
51
+ };
52
+ };
53
+ let state = initialState;
54
+ const getState = () => state;
55
+ const setState = (value) => {
56
+ if (!isClient && !allowSetStateServerSide) {
57
+ console.error(
58
+ "setState on the server is not allowed by default. Set `allowSetStateServerSide: true` to allow it."
59
+ );
60
+ return;
61
+ }
62
+ const prevState = state;
63
+ const newValue = getValue(value, state);
64
+ const changedKeys = [];
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
+ }
72
+ if (!Object.is(prevState[key], newValue[key])) {
73
+ changedKeys.push(key);
74
+ }
75
+ }
76
+ if (changedKeys.length === 0) return;
77
+ state = { ...prevState, ...newValue };
78
+ onStateChange(state, prevState, changedKeys);
79
+ [...subscribers].forEach((subscriber) => subscriber(state, prevState, changedKeys));
80
+ };
81
+ const storeApi = {
82
+ getState,
83
+ setState,
84
+ subscribe,
85
+ getSubscriberCount
86
+ };
87
+ return storeApi;
88
+ };
89
+
90
+ export { getHash, getValue, initStore, isClient, isPlainObject, noop };
package/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "yuustate/vanilla";
package/index.js ADDED
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ var vanilla = require('yuustate/vanilla');
4
+
5
+
6
+
7
+ Object.keys(vanilla).forEach(function (k) {
8
+ if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
9
+ enumerable: true,
10
+ get: function () { return vanilla[k]; }
11
+ });
12
+ });
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "yuustate",
3
3
  "description": "Lightweight unified state management for sync and async data.",
4
4
  "private": false,
5
- "version": "0.0.1",
5
+ "version": "3.10.0",
6
6
  "keywords": [
7
7
  "utilities",
8
8
  "store",
@@ -22,9 +22,56 @@
22
22
  },
23
23
  "homepage": "https://github.com/afiiif/yuustate#readme",
24
24
  "sideEffects": false,
25
+ "files": [
26
+ "**"
27
+ ],
28
+ "main": "./index.js",
29
+ "types": "./index.d.ts",
30
+ "typesVersions": {
31
+ ">=4.5": {
32
+ "esm/*": [
33
+ "esm/*"
34
+ ],
35
+ "*": [
36
+ "*"
37
+ ]
38
+ },
39
+ "*": {
40
+ "esm/*": [
41
+ "ts_version_4.5_and_above_is_required.d.ts"
42
+ ],
43
+ "*": [
44
+ "ts_version_4.5_and_above_is_required.d.ts"
45
+ ]
46
+ }
47
+ },
48
+ "exports": {
49
+ "./package.json": "./package.json",
50
+ ".": {
51
+ "import": {
52
+ "types": "./esm/index.d.mts",
53
+ "default": "./esm/index.mjs"
54
+ },
55
+ "default": {
56
+ "types": "./index.d.ts",
57
+ "default": "./index.js"
58
+ }
59
+ },
60
+ "./*": {
61
+ "import": {
62
+ "types": "./esm/*.d.mts",
63
+ "default": "./esm/*.mjs"
64
+ },
65
+ "default": {
66
+ "types": "./*.d.ts",
67
+ "default": "./*.js"
68
+ }
69
+ }
70
+ },
71
+ "packageManager": "pnpm@10.32.1",
25
72
  "peerDependencies": {
26
- "@types/react": ">=17.0",
27
- "react": ">=17.0"
73
+ "@types/react": ">=18.0",
74
+ "react": ">=18.0"
28
75
  },
29
76
  "peerDependenciesMeta": {
30
77
  "@types/react": {
@@ -0,0 +1,143 @@
1
+ import { type InitStoreOptions, type SetStateInput } from "yuustate/vanilla";
2
+ /**
3
+ * Represents the state of a mutation.
4
+ *
5
+ * @remarks
6
+ * A mutation does not cache results and only tracks the latest execution.
7
+ *
8
+ * The state transitions are:
9
+ * - `INITIAL` → before any execution
10
+ * - `SUCCESS` → when mutation resolves successfully
11
+ * - `ERROR` → when mutation fails
12
+ *
13
+ * Unlike queries:
14
+ * - No retry mechanism
15
+ * - No caching across executions
16
+ */
17
+ export type MutationState<TData, TVariable, TError> = {
18
+ isPending: boolean;
19
+ } & ({
20
+ state: "INITIAL";
21
+ isSuccess: false;
22
+ isError: false;
23
+ variable: undefined;
24
+ data: undefined;
25
+ dataUpdatedAt: undefined;
26
+ error: undefined;
27
+ errorUpdatedAt: undefined;
28
+ } | {
29
+ state: "SUCCESS";
30
+ isSuccess: true;
31
+ isError: false;
32
+ variable: TVariable;
33
+ data: TData;
34
+ dataUpdatedAt: number;
35
+ error: undefined;
36
+ errorUpdatedAt: undefined;
37
+ } | {
38
+ state: "ERROR";
39
+ isSuccess: false;
40
+ isError: true;
41
+ variable: TVariable;
42
+ data: undefined;
43
+ dataUpdatedAt: undefined;
44
+ error: TError;
45
+ errorUpdatedAt: number;
46
+ });
47
+ export declare const INITIAL_STATE: MutationState<any, any, any>;
48
+ /**
49
+ * Configuration options for a mutation.
50
+ *
51
+ * @remarks
52
+ * Lifecycle callbacks are triggered for each execution.
53
+ */
54
+ export type MutationOptions<TData, TVariable, TError = Error> = InitStoreOptions<MutationState<TData, TVariable, TError>> & {
55
+ /**
56
+ * Called when the mutation succeeds.\
57
+ * If multiple concurrent executions happened, only the latest execution triggers this callback.
58
+ */
59
+ onSuccess?: (data: TData, variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => void;
60
+ /**
61
+ * Called when the mutation fails.\
62
+ * If multiple concurrent executions happened, only the latest execution triggers this callback.
63
+ */
64
+ onError?: (error: TError, variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => void;
65
+ /**
66
+ * Called after the mutation settles (either success or error).\
67
+ * If multiple concurrent executions happened, only the latest execution triggers this callback.
68
+ */
69
+ onSettled?: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => void;
70
+ };
71
+ /**
72
+ * Creates a mutation store for handling async operations that modify data.
73
+ *
74
+ * @param mutationFn - Async function that performs the mutation
75
+ * @param options - Optional configuration and lifecycle callbacks
76
+ *
77
+ * @returns A function that acts as both:
78
+ * - A React hook for subscribing to mutation state
79
+ * - A mutation controller (execute, reset, etc.)
80
+ *
81
+ * @remarks
82
+ * - Mutations are **not cached** and only track the latest execution.
83
+ * - Designed for operations that change data (e.g. create, update, delete).
84
+ * - No retry mechanism is provided by default.
85
+ * - The mutation always resolves (never throws): the result contains either `data` or `error`.
86
+ * - If multiple executions triggered at the same time:
87
+ * - Only the latest execution is allowed to update the state.
88
+ * - Results from previous executions are ignored if a newer one exists.
89
+ *
90
+ * @example
91
+ * const useCreateUser = createMutation(async (input) => {
92
+ * return api.createUser(input);
93
+ * });
94
+ *
95
+ * const { isPending } = useCreateUser();
96
+ * const result = await useCreateUser.execute({ name: 'John' });
97
+ *
98
+ * @see https://yuustate.vercel.app/docs/mutation
99
+ */
100
+ 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>) & {
101
+ subscribe: (subscriber: import("../vanilla.ts").Subscriber<MutationState<TData, TVariable, TError>>) => () => void;
102
+ getSubscriberCount: () => number;
103
+ getState: () => MutationState<TData, TVariable, TError>;
104
+ /**
105
+ * Manually updates the mutation state.
106
+ *
107
+ * @remarks
108
+ * - Intended for advanced use cases.
109
+ * - Prefer using provided mutation actions (`execute`, `reset`) instead.
110
+ */
111
+ setState: (value: SetStateInput<MutationState<TData, TVariable, TError>>) => void;
112
+ /**
113
+ * Executes the mutation.
114
+ *
115
+ * @param variable - Input passed to the mutation function
116
+ *
117
+ * @returns A promise that always resolves with:
118
+ * - `{ data, variable }` on success
119
+ * - `{ error, variable }` on failure
120
+ *
121
+ * @remarks
122
+ * - The promise never rejects to simplify async handling.
123
+ * - If a mutation is already in progress, a warning is logged.
124
+ * - When a new execution starts, all previous pending executions will resolve with the result of the latest execution.
125
+ */
126
+ execute: TVariable extends undefined ? () => Promise<{
127
+ variable: undefined;
128
+ data?: TData;
129
+ error?: TError;
130
+ }> : (variable: TVariable) => Promise<{
131
+ variable: TVariable;
132
+ data?: TData;
133
+ error?: TError;
134
+ }>;
135
+ /**
136
+ * Resets the mutation state back to its initial state.
137
+ *
138
+ * @remarks
139
+ * - Does not cancel any ongoing execution.
140
+ * - If an execution is still pending, its result may override the reset state.
141
+ */
142
+ reset: () => void;
143
+ };