yuustate 0.0.1 → 3.9.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,343 @@
1
+ import { type InitStoreOptions, type SetStateInput } from "yuustate/vanilla";
2
+ import type { StoreKey } from "./create-stores.mjs";
3
+ type StreamDataState<TData, TError> = {
4
+ state: "INITIAL";
5
+ isSuccess: false;
6
+ isError: false;
7
+ data: undefined;
8
+ dataUpdatedAt: undefined;
9
+ error: undefined;
10
+ errorUpdatedAt: undefined;
11
+ } | {
12
+ state: "SUCCESS";
13
+ isSuccess: true;
14
+ isError: false;
15
+ data: TData;
16
+ dataUpdatedAt: number;
17
+ error: undefined;
18
+ errorUpdatedAt: undefined;
19
+ } | {
20
+ state: "ERROR";
21
+ isSuccess: false;
22
+ isError: true;
23
+ data: undefined;
24
+ dataUpdatedAt: undefined;
25
+ error: TError;
26
+ errorUpdatedAt: number;
27
+ } | {
28
+ state: "SUCCESS_BUT_THEN_ERROR";
29
+ isSuccess: true;
30
+ isError: true;
31
+ data: TData;
32
+ dataUpdatedAt: number;
33
+ error: TError;
34
+ errorUpdatedAt: number;
35
+ };
36
+ /**
37
+ * Represents the full state of a stream.
38
+ *
39
+ * @remarks
40
+ * A stream consists of two independent concerns:
41
+ *
42
+ * 1. **Connection state** — lifecycle of the underlying connection
43
+ * 2. **Data state** — lifecycle of emitted data
44
+ *
45
+ * These two are combined into a single state object.
46
+ *
47
+ * ---
48
+ *
49
+ * ## Connection lifecycle
50
+ *
51
+ * - `INITIAL` → no connection has been established
52
+ * - `CONNECTING` → connection is being established
53
+ * - `CONNECTED` → connection is active
54
+ * - `DISCONNECTED` → connection was previously established but is now closed
55
+ *
56
+ * Timestamps:
57
+ * - `connectingAt` → when connection attempt started
58
+ * - `connectedAt` → when connection was established
59
+ * - `disconnectedAt` → when connection was closed
60
+ *
61
+ * ---
62
+ *
63
+ * ## Data lifecycle
64
+ *
65
+ * - `INITIAL` → no data has been received
66
+ * - `SUCCESS` → data has been received successfully
67
+ * - `ERROR` → error occurred before any data
68
+ * - `SUCCESS_BUT_THEN_ERROR` → data exists, but a later error occurred
69
+ *
70
+ * ---
71
+ *
72
+ * ## Notes
73
+ *
74
+ * - Connection state and data state evolve independently.
75
+ * - A stream may be:
76
+ * - connected but have no data yet
77
+ * - disconnected but still retain previous data
78
+ * - Errors do not necessarily reset data.
79
+ */
80
+ export type StreamState<TData, TError> = ({
81
+ connectionState: "INITIAL";
82
+ connectingAt: undefined;
83
+ connectedAt: undefined;
84
+ disconnectedAt: undefined;
85
+ } & Extract<StreamDataState<TData, TError>, {
86
+ state: "INITIAL";
87
+ }>) | ({
88
+ connectionState: "CONNECTING";
89
+ connectingAt: number;
90
+ connectedAt: undefined;
91
+ disconnectedAt: undefined;
92
+ } & StreamDataState<TData, TError>) | ({
93
+ connectionState: "CONNECTED";
94
+ connectingAt: number;
95
+ connectedAt: number;
96
+ disconnectedAt: undefined;
97
+ } & StreamDataState<TData, TError>) | ({
98
+ connectionState: "DISCONNECTED";
99
+ connectingAt: number;
100
+ connectedAt: number | undefined;
101
+ disconnectedAt: number;
102
+ } & StreamDataState<TData, TError>);
103
+ type DisconnectTrigger = "last-unsubscribe" | "document-hidden" | "offline";
104
+ type ReconnectTrigger = "first-subscribe" | "document-visible" | "online";
105
+ type AdditionalStoreApi<TConnection> = {
106
+ variableHash: string;
107
+ /**
108
+ * Connection controls for the stream.
109
+ *
110
+ * @remarks
111
+ * Provides imperative control over the underlying connection.
112
+ */
113
+ connection: {
114
+ /**
115
+ * Returns the current connection instance.
116
+ *
117
+ * @returns The active connection or `undefined` if not connected
118
+ */
119
+ get: () => Readonly<TConnection> | undefined;
120
+ /**
121
+ * Forces a reconnection.
122
+ *
123
+ * @remarks
124
+ * - Cancels any scheduled disconnect
125
+ * - Starts a new connection if not already connecting
126
+ */
127
+ reconnect: () => void;
128
+ /**
129
+ * Immediately disconnects the current connection.
130
+ *
131
+ * @remarks
132
+ * - Ignores disconnect delay rules
133
+ * - Updates connection state to `DISCONNECTED`
134
+ */
135
+ disconnect: () => void;
136
+ };
137
+ /**
138
+ * Data controls for the stream.
139
+ */
140
+ data: {
141
+ /**
142
+ * Resets the data state back to `INITIAL`.
143
+ *
144
+ * @remarks
145
+ * - Does not affect connection state
146
+ * - Useful for clearing stale or invalid data
147
+ */
148
+ reset: () => void;
149
+ };
150
+ /**
151
+ * Deletes the stream instance.
152
+ *
153
+ * @returns `true` if deleted, `false` otherwise
154
+ *
155
+ * @remarks
156
+ * - Cannot delete while there are active subscribers
157
+ * - Clears connection, state, and cached instance
158
+ */
159
+ delete: () => boolean;
160
+ };
161
+ /**
162
+ * Configuration options for a stream.
163
+ *
164
+ * @remarks
165
+ * Controls connection lifecycle, reconnection behavior, and data retention.
166
+ */
167
+ export type StreamOptions<TConnection, TData, TError = Error> = InitStoreOptions<StreamState<TData, TError>, AdditionalStoreApi<TConnection>> & {
168
+ /**
169
+ * Connection-related behavior.
170
+ */
171
+ connection?: {
172
+ /**
173
+ * Determines when a connection should be disconnected.
174
+ *
175
+ * @param trigger - The reason for the disconnect attempt
176
+ * @param state - Current stream state
177
+ *
178
+ * @returns
179
+ * - `number` → delay (ms) before disconnecting
180
+ * - `false` → prevent disconnection
181
+ *
182
+ * @default Disconnect after 5 seconds for any triggers
183
+ *
184
+ * @remarks
185
+ * Triggers:
186
+ * - `"last-unsubscribe"` → no active subscribers
187
+ * - `"document-hidden"` → tab becomes hidden
188
+ * - `"offline"` → network goes offline
189
+ */
190
+ disconnectOn?: (trigger: DisconnectTrigger, state: StreamState<TData, TError>) => false | number;
191
+ /**
192
+ * Determines whether a connection should reconnect.
193
+ *
194
+ * @param trigger - The reason for the reconnect attempt
195
+ * @param state - Current stream state
196
+ *
197
+ * @returns `true` to reconnect, otherwise `false`
198
+ *
199
+ * @default No reconnection if already connected
200
+ *
201
+ * @remarks
202
+ * Triggers:
203
+ * - `"first-subscribe"` → first subscriber appears
204
+ * - `"document-visible"` → tab becomes visible
205
+ * - `"online"` → network reconnects
206
+ */
207
+ reconnectOn?: (trigger: ReconnectTrigger, state: StreamState<TData, TError>) => boolean;
208
+ };
209
+ /**
210
+ * Data-related behavior.
211
+ */
212
+ data?: {
213
+ /**
214
+ * Time (in milliseconds) before unused stream data is garbage collected.
215
+ *
216
+ * Starts counting after disconnection.
217
+ *
218
+ * @default 5 minutes
219
+ */
220
+ gcTime?: number;
221
+ };
222
+ };
223
+ /**
224
+ * Creates a stream factory for managing real-time connections.
225
+ *
226
+ * @param connect - Function to establish a connection
227
+ * @param disconnect - Function to close a connection
228
+ * @param options - Optional configuration for lifecycle and behavior
229
+ *
230
+ * @returns A function to retrieve or create a stream instance by variable
231
+ *
232
+ * @remarks
233
+ * - Designed for **long-lived, push-based async sources**, such as: WebSocket, SSE, realtime databases
234
+ * - A stream consists of two independent concerns:
235
+ * 1. **Connection state** — lifecycle of the underlying connection
236
+ * 2. **Data state** — lifecycle of emitted data
237
+ * - Connection lifecycle:
238
+ * - Connection is established when needed (e.g. first subscriber)
239
+ * - Connection may be disconnected based on triggers: no subscribers, tab hidden, offline
240
+ * - Streams are **keyed by variable** (via deterministic hashing).
241
+ * - Each unique variable maps to its own stream instance.
242
+ *
243
+ * ---
244
+ *
245
+ * ## Execution model
246
+ *
247
+ * - Streams are **lazy**: No connection until there is a subscriber
248
+ * - Streams are **shared**: Multiple subscribers reuse the same connection
249
+ * - Streams are **stateful**: Data persists across reconnects (unless reset or GC)
250
+ *
251
+ * ---
252
+ *
253
+ * @example
254
+ * const chatStream = createStream(
255
+ * (roomId, emit) => {
256
+ * const ws = new WebSocket(`/chat/${roomId}`);
257
+ *
258
+ * ws.onopen = () => emit.connected();
259
+ * ws.onmessage = (e) => {
260
+ * const msg = JSON.parse(e.data);
261
+ * emit.data((prev) => [...(prev ?? []), msg]);
262
+ * };
263
+ * ws.onerror = (err) => emit.error(err);
264
+ *
265
+ * return ws;
266
+ * },
267
+ * (ws) => ws.close()
268
+ * );
269
+ *
270
+ * function Chat({ roomId }) {
271
+ * const useChat = chatStream(roomId);
272
+ * const state = useChat();
273
+ *
274
+ * return <div>{state.data?.length}</div>;
275
+ * }
276
+ */
277
+ export declare const createStream: <TConnection, TData, TVariable extends StoreKey, TError = Error>(connect: (variable: TVariable, emit: {
278
+ connected: () => void;
279
+ data: (reducer: (data: TData | undefined) => TData) => void;
280
+ error: (error: TError) => void;
281
+ }) => TConnection, disconnect: (connection: TConnection) => void, options?: StreamOptions<TConnection, TData, TError>) => (variable?: TVariable) => ((options?: {
282
+ initialData?: TData;
283
+ }) => StreamState<TData, TError>) & {
284
+ setState: (value: SetStateInput<StreamState<TData, TError>>) => void;
285
+ getState: () => StreamState<TData, TError>;
286
+ subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<StreamState<TData, TError>>) => () => void;
287
+ getSubscriberCount: () => number;
288
+ variableHash: string;
289
+ /**
290
+ * Connection controls for the stream.
291
+ *
292
+ * @remarks
293
+ * Provides imperative control over the underlying connection.
294
+ */
295
+ connection: {
296
+ /**
297
+ * Returns the current connection instance.
298
+ *
299
+ * @returns The active connection or `undefined` if not connected
300
+ */
301
+ get: () => Readonly<TConnection> | undefined;
302
+ /**
303
+ * Forces a reconnection.
304
+ *
305
+ * @remarks
306
+ * - Cancels any scheduled disconnect
307
+ * - Starts a new connection if not already connecting
308
+ */
309
+ reconnect: () => void;
310
+ /**
311
+ * Immediately disconnects the current connection.
312
+ *
313
+ * @remarks
314
+ * - Ignores disconnect delay rules
315
+ * - Updates connection state to `DISCONNECTED`
316
+ */
317
+ disconnect: () => void;
318
+ };
319
+ /**
320
+ * Data controls for the stream.
321
+ */
322
+ data: {
323
+ /**
324
+ * Resets the data state back to `INITIAL`.
325
+ *
326
+ * @remarks
327
+ * - Does not affect connection state
328
+ * - Useful for clearing stale or invalid data
329
+ */
330
+ reset: () => void;
331
+ };
332
+ /**
333
+ * Deletes the stream instance.
334
+ *
335
+ * @returns `true` if deleted, `false` otherwise
336
+ *
337
+ * @remarks
338
+ * - Cannot delete while there are active subscribers
339
+ * - Clears connection, state, and cached instance
340
+ */
341
+ delete: () => boolean;
342
+ };
343
+ export {};
@@ -0,0 +1,6 @@
1
+ import { useLayoutEffect } from "react";
2
+ /**
3
+ * Does exactly same as `useLayoutEffect`.\
4
+ * It will use `useEffect` in **server-side** to prevent warning when executed on server-side.
5
+ */
6
+ export declare const useIsomorphicLayoutEffect: typeof useLayoutEffect;
@@ -0,0 +1,84 @@
1
+ import { type MutationOptions, type MutationState } from "./create-mutation.mjs";
2
+ /**
3
+ * A hook for managing async mutation state.
4
+ *
5
+ * @param mutationFn - Async function that performs the mutation.
6
+ * Receives the input variable and the state snapshot before execution.
7
+ *
8
+ * @param options - Optional lifecycle callbacks:
9
+ * - `onSuccess(data, variable, stateBeforeExecute)`
10
+ * - `onError(error, variable, stateBeforeExecute)`
11
+ * - `onSettled(variable, stateBeforeExecute)`
12
+ *
13
+ * @returns A tuple containing:
14
+ * - state: The current mutation state (render snapshot)
15
+ * - controls: An object with mutation actions and helpers
16
+ *
17
+ * @remarks
18
+ * - No retry mechanism is provided by default.
19
+ * - The mutation always resolves (never throws): the result contains either `data` or `error`.
20
+ * - If multiple executions triggered at the same time:
21
+ * - Only the latest execution is allowed to update the state.
22
+ * - Results from previous executions are ignored if a newer one exists.
23
+ *
24
+ * @see https://yuustate.vercel.app/docs/mutation
25
+ */
26
+ export declare const useMutation: <TData, TVariable = undefined, TError = Error>(
27
+ /**
28
+ * Async function that performs the mutation.
29
+ *
30
+ * @remarks
31
+ * - Does NOT need to be memoized (e.g. `useCallback`).
32
+ * - The latest function reference is always used internally.
33
+ */
34
+ mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => Promise<TData>,
35
+ /**
36
+ * Optional lifecycle callbacks.
37
+ *
38
+ * @remarks
39
+ * - Callbacks do NOT need to be memoized.
40
+ * - The latest callbacks are always used internally.
41
+ */
42
+ options?: MutationOptions<TData, TVariable, TError>) => [MutationState<TData, TVariable, TError>, {
43
+ /**
44
+ * Executes the mutation.
45
+ *
46
+ * @param variable - Input passed to the mutation function
47
+ *
48
+ * @returns A promise that always resolves with:
49
+ * - `{ data, variable }` on success
50
+ * - `{ error, variable }` on failure
51
+ *
52
+ * @remarks
53
+ * - The promise never rejects to simplify async handling.
54
+ * - If a mutation is already in progress, a warning is logged.
55
+ * - When a new execution starts, all previous pending executions will resolve with the result of the latest execution.
56
+ */
57
+ execute: TVariable extends undefined ? () => Promise<{
58
+ variable: undefined;
59
+ data?: TData;
60
+ error?: TError;
61
+ }> : (variable: TVariable) => Promise<{
62
+ variable: TVariable;
63
+ data?: TData;
64
+ error?: TError;
65
+ }>;
66
+ /**
67
+ * Resets the mutation state back to its initial state.
68
+ *
69
+ * @remarks
70
+ * - Does not cancel any ongoing execution.
71
+ * - If an execution is still pending, its result may override the reset state.
72
+ */
73
+ reset: () => void;
74
+ /**
75
+ * Returns the latest mutation state directly from the internal ref.
76
+ *
77
+ * @returns The most up-to-date mutation state.
78
+ *
79
+ * @remarks
80
+ * - Unlike the `state` returned by the hook, this value is not tied to React render cycles.
81
+ * - Use this inside async flows or event handlers to avoid stale reads.
82
+ */
83
+ getLatestState: () => MutationState<TData, TVariable, TError>;
84
+ }];
@@ -0,0 +1,34 @@
1
+ import { type StoreApi } from "yuustate/vanilla";
2
+ type Path = Array<string | number | symbol>;
3
+ export declare const getValueByPath: (obj: any, path: Path) => any;
4
+ export declare const isPrefixPath: (candidatePrefix: Path, targetPath: Path) => boolean;
5
+ export declare const compressPaths: (paths: Path[]) => Path[];
6
+ export declare const useStoreStateProxy: <TState extends Record<string, any>>(storeState: TState) => readonly [TState, import("react").RefObject<Path[]>];
7
+ export declare const NO_INITIAL_VALUE: {};
8
+ export declare const useStoreStateWithInitializer: <TState extends Record<string, any>>(store: StoreApi<TState>, initialState?: Partial<TState>) => readonly [TState, import("react").RefObject<WeakMap<StoreApi<TState>, number>>];
9
+ /**
10
+ * React hook for subscribing to a store using automatic dependency tracking.
11
+ *
12
+ * @param store - The store instance to subscribe to
13
+ * @param options - Optional configuration
14
+ * @param options.initialState - Initial state used on first render (and will also update the store state right after that)
15
+ *
16
+ * @returns A proxied version of the store state
17
+ *
18
+ * @remarks
19
+ * - This hook uses a **Proxy-based tracking mechanism** to detect which parts of the state are accessed during render.
20
+ * - The component will only re-render when the **accessed values actually change**.
21
+ * - State must be treated as **immutable**:
22
+ * - Updates must replace objects rather than mutate them
23
+ * - Otherwise, changes may not be detected
24
+ * - If provided, `initialState` will be applied **once per store instance**
25
+ *
26
+ * @example
27
+ * const state = useStoreState(store);
28
+ * return <div>{state.user.name}</div>;
29
+ * // Component will only re-render if `user.name` changes
30
+ */
31
+ export declare const useStoreState: <TState extends Record<string, any>>(store: StoreApi<TState>, options?: {
32
+ initialState?: Partial<TState>;
33
+ }) => TState;
34
+ export {};
@@ -0,0 +1,8 @@
1
+ export * from "./react/use-isomorphic-layout-effect.mjs";
2
+ export { useStoreState } from "./react/use-store.mjs";
3
+ export * from "./react/create-store.mjs";
4
+ export * from "./react/create-stores.mjs";
5
+ export * from "./react/create-query.mjs";
6
+ export { createMutation, type MutationOptions, type MutationState, } from "./react/create-mutation.mjs";
7
+ export * from "./react/use-mutation.mjs";
8
+ export * from "./react/create-stream.mjs";