@fozy-labs/rx-toolkit 0.5.3-rc.1 → 0.5.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.
Files changed (207) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +143 -137
  3. package/dist/common/devtools/combineDevtools.js +3 -3
  4. package/dist/common/devtools/index.d.ts +3 -3
  5. package/dist/common/devtools/index.js +3 -3
  6. package/dist/common/devtools/reduxDevtools.d.ts +1 -1
  7. package/dist/common/devtools/reduxDevtools.js +17 -17
  8. package/dist/common/devtools/types.d.ts +0 -6
  9. package/dist/common/options/DefaultOptions.d.ts +1 -1
  10. package/dist/common/options/SharedOptions.d.ts +3 -2
  11. package/dist/common/options/SharedOptions.js +6 -0
  12. package/dist/common/options/index.d.ts +1 -1
  13. package/dist/common/options/index.js +1 -1
  14. package/dist/common/react/index.d.ts +2 -2
  15. package/dist/common/react/index.js +2 -2
  16. package/dist/common/react/useConstant.js +1 -1
  17. package/dist/common/utils/deepEqual.js +1 -1
  18. package/dist/common/utils/index.d.ts +3 -3
  19. package/dist/common/utils/index.js +3 -3
  20. package/dist/common/utils/shallowEqual.js +1 -1
  21. package/dist/index.d.ts +8 -7
  22. package/dist/index.js +8 -7
  23. package/dist/query/SKIP_TOKEN.js +1 -1
  24. package/dist/query/api/createCommand.d.ts +21 -0
  25. package/dist/query/api/createCommand.js +20 -0
  26. package/dist/query/api/createOperation.d.ts +5 -3
  27. package/dist/query/api/createOperation.js +6 -2
  28. package/dist/query/api/createResource.d.ts +2 -2
  29. package/dist/query/api/createResourceDuplicator.d.ts +2 -2
  30. package/dist/query/core/Command/Command.d.ts +35 -0
  31. package/dist/query/core/{Opertation/Operation.js → Command/Command.js} +13 -14
  32. package/dist/query/core/Command/CommandAgent.d.ts +19 -0
  33. package/dist/query/core/{Opertation/OperationAgent.js → Command/CommandAgent.js} +13 -13
  34. package/dist/query/core/Command/index.d.ts +2 -0
  35. package/dist/query/core/Command/index.js +2 -0
  36. package/dist/query/core/Operation/Operation.d.ts +8 -0
  37. package/dist/query/core/Operation/Operation.js +4 -0
  38. package/dist/query/core/Operation/OperationAgent.d.ts +4 -0
  39. package/dist/query/core/Operation/OperationAgent.js +4 -0
  40. package/dist/query/core/QueriesCache.d.ts +2 -2
  41. package/dist/query/core/QueriesCache.js +1 -1
  42. package/dist/query/core/QueriesLifetimeHooks.d.ts +1 -1
  43. package/dist/query/core/QueriesLifetimeHooks.js +7 -7
  44. package/dist/query/core/Resource/Resource.d.ts +16 -16
  45. package/dist/query/core/Resource/Resource.js +7 -7
  46. package/dist/query/core/Resource/ResourceAgent.d.ts +2 -2
  47. package/dist/query/core/Resource/ResourceAgent.js +3 -3
  48. package/dist/query/core/Resource/ResourceDuplicator.d.ts +17 -17
  49. package/dist/query/core/Resource/ResourceDuplicator.js +18 -20
  50. package/dist/query/core/Resource/ResourceDuplicatorAgent.d.ts +6 -6
  51. package/dist/query/core/Resource/ResourceDuplicatorAgent.js +3 -3
  52. package/dist/query/core/Resource/ResourceRef.d.ts +2 -2
  53. package/dist/query/core/Resource/ResourceRef.js +12 -12
  54. package/dist/query/index.d.ts +11 -8
  55. package/dist/query/index.js +14 -8
  56. package/dist/query/lib/IndirectMap.js +4 -4
  57. package/dist/query/lib/ReactiveCache.d.ts +1 -1
  58. package/dist/query/react/useCommandAgent.d.ts +24 -0
  59. package/dist/query/react/useCommandAgent.js +39 -0
  60. package/dist/query/react/useOperationAgent.d.ts +6 -8
  61. package/dist/query/react/useOperationAgent.js +6 -23
  62. package/dist/query/react/useResourceAgent.d.ts +4 -4
  63. package/dist/query/react/useResourceAgent.js +1 -1
  64. package/dist/query/react/useResourceRef.d.ts +3 -3
  65. package/dist/query/react/useResourceRef.js +7 -2
  66. package/dist/query/types/Command.types.d.ts +154 -0
  67. package/dist/query/types/Command.types.js +1 -0
  68. package/dist/query/types/Operation.types.d.ts +13 -154
  69. package/dist/query/types/Resource.types.d.ts +7 -5
  70. package/dist/query/types/index.d.ts +4 -3
  71. package/dist/query/types/index.js +5 -3
  72. package/dist/query-v2/api/createApi.d.ts +10 -0
  73. package/dist/query-v2/api/createApi.js +83 -0
  74. package/dist/query-v2/core/common/CacheEntry.d.ts +29 -0
  75. package/dist/query-v2/core/common/CacheEntry.js +71 -0
  76. package/dist/query-v2/core/common/CacheMap.d.ts +38 -0
  77. package/dist/query-v2/core/common/CacheMap.js +127 -0
  78. package/dist/query-v2/core/common/LifecycleHooks.d.ts +22 -0
  79. package/dist/query-v2/core/common/LifecycleHooks.js +104 -0
  80. package/dist/query-v2/core/common/index.d.ts +3 -0
  81. package/dist/query-v2/core/common/index.js +3 -0
  82. package/dist/query-v2/core/index.d.ts +3 -0
  83. package/dist/query-v2/core/index.js +3 -0
  84. package/dist/query-v2/core/machines/Machine.d.ts +14 -0
  85. package/dist/query-v2/core/machines/Machine.js +33 -0
  86. package/dist/query-v2/core/machines/MachineError.d.ts +11 -0
  87. package/dist/query-v2/core/machines/MachineError.js +26 -0
  88. package/dist/query-v2/core/machines/MachineIdle.d.ts +8 -0
  89. package/dist/query-v2/core/machines/MachineIdle.js +19 -0
  90. package/dist/query-v2/core/machines/MachinePending.d.ts +12 -0
  91. package/dist/query-v2/core/machines/MachinePending.js +29 -0
  92. package/dist/query-v2/core/machines/MachineRefreshing.d.ts +14 -0
  93. package/dist/query-v2/core/machines/MachineRefreshing.js +46 -0
  94. package/dist/query-v2/core/machines/MachineSuccess.d.ts +16 -0
  95. package/dist/query-v2/core/machines/MachineSuccess.js +42 -0
  96. package/dist/query-v2/core/machines/MachineWithData.d.ts +18 -0
  97. package/dist/query-v2/core/machines/MachineWithData.js +40 -0
  98. package/dist/query-v2/core/machines/Patcher.d.ts +20 -0
  99. package/dist/query-v2/core/machines/Patcher.js +104 -0
  100. package/dist/query-v2/core/machines/index.d.ts +8 -0
  101. package/dist/query-v2/core/machines/index.js +8 -0
  102. package/dist/query-v2/core/resource/ResourceV2.d.ts +120 -0
  103. package/dist/query-v2/core/resource/ResourceV2.js +464 -0
  104. package/dist/query-v2/core/resource/ResourceV2Agent.d.ts +26 -0
  105. package/dist/query-v2/core/resource/ResourceV2Agent.js +132 -0
  106. package/dist/query-v2/core/resource/index.d.ts +2 -0
  107. package/dist/query-v2/core/resource/index.js +2 -0
  108. package/dist/query-v2/index.d.ts +11 -0
  109. package/dist/query-v2/index.js +17 -0
  110. package/dist/query-v2/lib/NO_VALUE.d.ts +2 -0
  111. package/dist/query-v2/lib/NO_VALUE.js +1 -0
  112. package/dist/query-v2/lib/SKIP_TOKEN.d.ts +2 -0
  113. package/dist/query-v2/lib/SKIP_TOKEN.js +1 -0
  114. package/dist/query-v2/lib/index.d.ts +4 -0
  115. package/dist/query-v2/lib/index.js +3 -0
  116. package/dist/query-v2/lib/stableStringify.d.ts +8 -0
  117. package/dist/query-v2/lib/stableStringify.js +23 -0
  118. package/dist/query-v2/plugins/ReactHooksPlugin.d.ts +25 -0
  119. package/dist/query-v2/plugins/ReactHooksPlugin.js +19 -0
  120. package/dist/query-v2/plugins/types.d.ts +1 -0
  121. package/dist/query-v2/plugins/types.js +1 -0
  122. package/dist/query-v2/react/__tests__/helpers.d.ts +12 -0
  123. package/dist/query-v2/react/__tests__/helpers.js +33 -0
  124. package/dist/query-v2/react/index.d.ts +2 -0
  125. package/dist/query-v2/react/index.js +2 -0
  126. package/dist/query-v2/react/useResourceV2Agent.d.ts +12 -0
  127. package/dist/query-v2/react/useResourceV2Agent.js +36 -0
  128. package/dist/query-v2/react/useResourceV2Ref.d.ts +12 -0
  129. package/dist/query-v2/react/useResourceV2Ref.js +57 -0
  130. package/dist/query-v2/snapshot/Snapshot.d.ts +13 -0
  131. package/dist/query-v2/snapshot/Snapshot.js +76 -0
  132. package/dist/query-v2/types/agent.types.d.ts +54 -0
  133. package/dist/query-v2/types/agent.types.js +1 -0
  134. package/dist/query-v2/types/api.types.d.ts +22 -0
  135. package/dist/query-v2/types/api.types.js +1 -0
  136. package/dist/query-v2/types/cache.types.d.ts +37 -0
  137. package/dist/query-v2/types/cache.types.js +1 -0
  138. package/dist/query-v2/types/index.d.ts +9 -0
  139. package/dist/query-v2/types/index.js +9 -0
  140. package/dist/query-v2/types/lifecycle.types.d.ts +25 -0
  141. package/dist/query-v2/types/lifecycle.types.js +1 -0
  142. package/dist/query-v2/types/machine.types.d.ts +67 -0
  143. package/dist/query-v2/types/machine.types.js +1 -0
  144. package/dist/query-v2/types/plugin.types.d.ts +38 -0
  145. package/dist/query-v2/types/plugin.types.js +1 -0
  146. package/dist/query-v2/types/resource.types.d.ts +35 -0
  147. package/dist/query-v2/types/resource.types.js +1 -0
  148. package/dist/query-v2/types/shared.types.d.ts +20 -0
  149. package/dist/query-v2/types/shared.types.js +1 -0
  150. package/dist/query-v2/types/snapshot.types.d.ts +21 -0
  151. package/dist/query-v2/types/snapshot.types.js +1 -0
  152. package/dist/signals/base/Batcher.js +9 -5
  153. package/dist/signals/base/ComputeCache.js +3 -3
  154. package/dist/signals/base/DependencyTracker.js +1 -1
  155. package/dist/signals/base/Devtools.d.ts +3 -2
  156. package/dist/signals/base/Devtools.js +54 -27
  157. package/dist/signals/base/Indexer.js +1 -1
  158. package/dist/signals/base/ReadonlySignal.js +1 -1
  159. package/dist/signals/base/SyncObservable.d.ts +1 -2
  160. package/dist/signals/base/SyncObservable.js +2 -5
  161. package/dist/signals/base/index.d.ts +6 -6
  162. package/dist/signals/base/index.js +6 -6
  163. package/dist/signals/index.d.ts +5 -4
  164. package/dist/signals/index.js +5 -4
  165. package/dist/signals/operators/index.d.ts +1 -1
  166. package/dist/signals/operators/index.js +1 -1
  167. package/dist/signals/operators/signalize.d.ts +1 -1
  168. package/dist/signals/react/index.d.ts +1 -1
  169. package/dist/signals/react/index.js +1 -1
  170. package/dist/signals/signals/Computed.d.ts +3 -4
  171. package/dist/signals/signals/Computed.js +18 -10
  172. package/dist/signals/signals/Effect.js +2 -1
  173. package/dist/signals/signals/LocalState.d.ts +44 -0
  174. package/dist/signals/signals/{LocalSignal.js → LocalState.js} +62 -28
  175. package/dist/signals/signals/Signal.d.ts +8 -7
  176. package/dist/signals/signals/Signal.js +4 -1
  177. package/dist/signals/signals/State.d.ts +4 -5
  178. package/dist/signals/signals/State.js +23 -9
  179. package/dist/signals/signals/index.d.ts +5 -5
  180. package/dist/signals/signals/index.js +5 -6
  181. package/dist/signals/types/SignalOptions.d.ts +16 -0
  182. package/dist/signals/types/SignalOptions.js +1 -0
  183. package/dist/signals/types/index.d.ts +3 -1
  184. package/dist/signals/types/index.js +3 -1
  185. package/dist/signals/types/normalizeSignalOptions.d.ts +2 -0
  186. package/dist/signals/types/normalizeSignalOptions.js +10 -0
  187. package/dist/signals/types/signals.types.d.ts +6 -2
  188. package/docs/CHANGELOG.md +111 -32
  189. package/docs/CONTRIBUTING.md +230 -0
  190. package/docs/contributing/ai-assisted-development.md +47 -0
  191. package/docs/contributing/query-v2/README.md +379 -0
  192. package/docs/{release → contributing/release}/README.md +59 -59
  193. package/docs/devtools/README.md +228 -228
  194. package/docs/migrations/0.5.0.md +58 -58
  195. package/docs/migrations/query-v2.md +171 -0
  196. package/docs/options/README.md +92 -90
  197. package/docs/query/README.md +575 -571
  198. package/docs/query-v2/README.md +280 -0
  199. package/docs/query-v2/api-reference.md +235 -0
  200. package/docs/query-v2/optimistic-updates.md +148 -0
  201. package/docs/query-v2/ssr.md +130 -0
  202. package/docs/signals/README.md +300 -295
  203. package/docs/usage/react/README.md +309 -307
  204. package/package.json +86 -63
  205. package/dist/query/core/Opertation/Operation.d.ts +0 -35
  206. package/dist/query/core/Opertation/OperationAgent.d.ts +0 -19
  207. package/dist/signals/signals/LocalSignal.d.ts +0 -32
@@ -0,0 +1,132 @@
1
+ import { SKIP } from "../../../query-v2/lib/SKIP_TOKEN";
2
+ import { Signal } from "../../../signals";
3
+ /**
4
+ * Agent that tracks a single cache entry with reactive state, designed for React hook consumption.
5
+ * Provides SWR (stale-while-revalidate) semantics by keeping previous data while new data loads.
6
+ */
7
+ export class ResourceV2Agent {
8
+ _resource;
9
+ _tracking$;
10
+ _refreshError$;
11
+ _state$;
12
+ _unsubRefreshError;
13
+ _currentArgs = null;
14
+ constructor(resource) {
15
+ this._resource = resource;
16
+ // Agent signals are internal derived state for React hooks — only CacheEntry signals belong in devtools
17
+ this._tracking$ = Signal.state({ previous: null, current: null }, { isDisabled: true });
18
+ // Agent signal — excluded from devtools; only CacheEntry signals represent canonical cache state
19
+ this._refreshError$ = Signal.state(null, { isDisabled: true });
20
+ // Subscribe to refresh errors from the resource
21
+ this._unsubRefreshError = this._resource.onRefreshError((args, error) => {
22
+ if (this._currentArgs !== null && this._resource.compareArgs(this._currentArgs, args)) {
23
+ this._refreshError$.set(error);
24
+ }
25
+ });
26
+ // Agent signal — excluded from devtools; only CacheEntry signals represent canonical cache state
27
+ this._state$ = Signal.compute(() => {
28
+ const { previous, current } = this._tracking$();
29
+ if (!current) {
30
+ return {
31
+ status: "idle",
32
+ data: null,
33
+ error: null,
34
+ args: null,
35
+ isLoading: false,
36
+ isInitialLoading: false,
37
+ isRefreshing: false,
38
+ isSuccess: false,
39
+ isError: false,
40
+ refreshError: this._refreshError$(),
41
+ };
42
+ }
43
+ // Read machine$ reactively — this is the key reactive subscription
44
+ const machine = current.machine$();
45
+ const machineState = machine.state;
46
+ const status = machineState.status;
47
+ // Determine previous data for SWR
48
+ let previousData = null;
49
+ if (previous) {
50
+ const prevMachine = previous.machine$();
51
+ const prevState = prevMachine.state;
52
+ if (prevState.data != null) {
53
+ previousData = prevState.data;
54
+ }
55
+ }
56
+ const currentData = machineState.data;
57
+ const isLoading = status === "pending" || status === "refreshing";
58
+ const isRefreshing = status === "refreshing";
59
+ // SWR: use previous data if current is loading and has no data yet
60
+ const data = currentData ?? (isLoading ? previousData : null);
61
+ const hasPreviousData = previousData !== null;
62
+ const isInitialLoading = isLoading && !hasPreviousData && currentData === null;
63
+ const isSuccess = data !== null;
64
+ const isError = status === "error";
65
+ const error = isError ? machineState.error : null;
66
+ return {
67
+ status,
68
+ data,
69
+ error,
70
+ args: machineState.args ?? null,
71
+ isLoading,
72
+ isInitialLoading,
73
+ isRefreshing,
74
+ isSuccess,
75
+ isError,
76
+ refreshError: this._refreshError$(),
77
+ };
78
+ }, { isDisabled: true });
79
+ }
80
+ /** Computed reactive state signal — projects CacheEntry machine state into a flat agent state object. */
81
+ get state$() {
82
+ return this._state$;
83
+ }
84
+ /**
85
+ * Start (or re-start) the agent with new args. Skips if args are unchanged.
86
+ *
87
+ * @param args - Query arguments, or `SKIP_TOKEN` to do nothing.
88
+ */
89
+ async start(args) {
90
+ // SKIP: no-op
91
+ if (args === SKIP) {
92
+ return;
93
+ }
94
+ const typedArgs = args;
95
+ // Same args check — skip if unchanged
96
+ if (this._currentArgs !== null && this._resource.compareArgs(this._currentArgs, typedArgs)) {
97
+ return;
98
+ }
99
+ this._currentArgs = typedArgs;
100
+ // Query the resource — get a cache entry
101
+ const entryPromise = this._resource.query(typedArgs);
102
+ // Get the entry synchronously from the resource
103
+ const entry = this._resource.entry(typedArgs);
104
+ // Swap previous/current
105
+ const oldTracking = this._tracking$.peek();
106
+ this._tracking$.set({
107
+ previous: oldTracking.current,
108
+ current: entry,
109
+ });
110
+ // Wait for the query to resolve and then clear previous
111
+ try {
112
+ await entryPromise;
113
+ // Clear refreshError on successful query completion
114
+ this._refreshError$.set(null);
115
+ }
116
+ catch {
117
+ // query() shouldn't reject, but handle gracefully
118
+ }
119
+ // Clear previous after current resolves (success or error)
120
+ // Only if current hasn't changed (latest-wins)
121
+ const currentTracking = this._tracking$.peek();
122
+ if (currentTracking.current === entry) {
123
+ this._tracking$.set({
124
+ previous: null,
125
+ current: entry,
126
+ });
127
+ }
128
+ }
129
+ compareArgs(a, b) {
130
+ return this._resource.compareArgs(a, b);
131
+ }
132
+ }
@@ -0,0 +1,2 @@
1
+ export { ResourceV2, type ResourceV2Config } from "./ResourceV2";
2
+ export { ResourceV2Agent } from "./ResourceV2Agent";
@@ -0,0 +1,2 @@
1
+ export { ResourceV2 } from "./ResourceV2";
2
+ export { ResourceV2Agent } from "./ResourceV2Agent";
@@ -0,0 +1,11 @@
1
+ export { SKIP, type SKIP_TOKEN } from "./lib/SKIP_TOKEN";
2
+ export { NO_VALUE } from "./lib/NO_VALUE";
3
+ export type { NO_VALUE as NO_VALUE_TYPE } from "./lib/NO_VALUE";
4
+ export { stableStringify } from "./lib/stableStringify";
5
+ export { Machine, type TMachineInstance, MachineIdle, MachinePending, MachineSuccess, MachineError, MachineRefreshing, MachineWithData, Patcher, CacheEntry, type CacheEntryOptions, CacheMap, type TCacheMapInstance, LifecycleHooks, ResourceV2, type ResourceV2Config, ResourceV2Agent, } from "./core";
6
+ export { createApi } from "./api/createApi";
7
+ export { ReactHooksPlugin } from "./plugins/ReactHooksPlugin";
8
+ export type { IReactHooksPluginContributions } from "./plugins/ReactHooksPlugin";
9
+ export { useResourceV2Agent, useResourceV2Ref } from "./react";
10
+ export { getSnapshot, hydrateSnapshot, CURRENT_SNAPSHOT_VERSION } from "./snapshot/Snapshot";
11
+ export * from "./types";
@@ -0,0 +1,17 @@
1
+ // Sentinel tokens
2
+ export { SKIP } from "./lib/SKIP_TOKEN";
3
+ export { NO_VALUE } from "./lib/NO_VALUE";
4
+ // Utilities
5
+ export { stableStringify } from "./lib/stableStringify";
6
+ // Core — Machine classes
7
+ export { Machine, MachineIdle, MachinePending, MachineSuccess, MachineError, MachineRefreshing, MachineWithData, Patcher, CacheEntry, CacheMap, LifecycleHooks, ResourceV2, ResourceV2Agent, } from "./core";
8
+ // API factory
9
+ export { createApi } from "./api/createApi";
10
+ // Plugins
11
+ export { ReactHooksPlugin } from "./plugins/ReactHooksPlugin";
12
+ // React hooks (standalone)
13
+ export { useResourceV2Agent, useResourceV2Ref } from "./react";
14
+ // Snapshot
15
+ export { getSnapshot, hydrateSnapshot, CURRENT_SNAPSHOT_VERSION } from "./snapshot/Snapshot";
16
+ // Types
17
+ export * from "./types";
@@ -0,0 +1,2 @@
1
+ export declare const NO_VALUE: unique symbol;
2
+ export type NO_VALUE = typeof NO_VALUE;
@@ -0,0 +1 @@
1
+ export const NO_VALUE = Symbol("NO_VALUE");
@@ -0,0 +1,2 @@
1
+ export declare const SKIP: unique symbol;
2
+ export type SKIP_TOKEN = typeof SKIP;
@@ -0,0 +1 @@
1
+ export const SKIP = Symbol("SKIP");
@@ -0,0 +1,4 @@
1
+ export { SKIP, type SKIP_TOKEN } from "./SKIP_TOKEN";
2
+ export { NO_VALUE } from "./NO_VALUE";
3
+ export type { NO_VALUE as NO_VALUE_TYPE } from "./NO_VALUE";
4
+ export { stableStringify } from "./stableStringify";
@@ -0,0 +1,3 @@
1
+ export { SKIP } from "./SKIP_TOKEN";
2
+ export { NO_VALUE } from "./NO_VALUE";
3
+ export { stableStringify } from "./stableStringify";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Deterministic JSON.stringify with sorted object keys.
3
+ * Used as the default serializeArgs for the 'serialize' key strategy.
4
+ *
5
+ * Handles: plain objects, arrays, primitives, null, undefined, nested structures.
6
+ * Does NOT handle: Date, Map, Set, RegExp (documented limitation).
7
+ */
8
+ export declare function stableStringify(value: unknown): string;
@@ -0,0 +1,23 @@
1
+ function isPlainObject(value) {
2
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3
+ }
4
+ /**
5
+ * Deterministic JSON.stringify with sorted object keys.
6
+ * Used as the default serializeArgs for the 'serialize' key strategy.
7
+ *
8
+ * Handles: plain objects, arrays, primitives, null, undefined, nested structures.
9
+ * Does NOT handle: Date, Map, Set, RegExp (documented limitation).
10
+ */
11
+ export function stableStringify(value) {
12
+ return JSON.stringify(value, (_, val) => {
13
+ if (isPlainObject(val)) {
14
+ return Object.keys(val)
15
+ .sort()
16
+ .reduce((acc, key) => {
17
+ acc[key] = val[key];
18
+ return acc;
19
+ }, {});
20
+ }
21
+ return val;
22
+ });
23
+ }
@@ -0,0 +1,25 @@
1
+ import type { ResourceV2 } from "../../query-v2/core/resource/ResourceV2";
2
+ import type { SKIP_TOKEN } from "../../query-v2/lib/SKIP_TOKEN";
3
+ import type { IResourceV2AgentState, IResourceV2Ref } from "../../query-v2/types/agent.types";
4
+ import type { IPlugin, IPluginContext } from "../../query-v2/types/plugin.types";
5
+ import type { IResourceV2Options } from "../../query-v2/types/resource.types";
6
+ /** Contributions added by ReactHooksPlugin to resources */
7
+ export interface IReactHooksPluginContributions<TArgs, TData, TError = Error> {
8
+ useResourceV2Agent(args: TArgs | SKIP_TOKEN): IResourceV2AgentState<TArgs, TData, TError>;
9
+ useResourceV2Ref(args: TArgs | SKIP_TOKEN): IResourceV2Ref<TArgs, TData, TError>;
10
+ }
11
+ declare module "../../query-v2/types/plugin.types" {
12
+ interface PluginContributionMap<TArgs, TData, TError> {
13
+ ReactHooksPlugin: IReactHooksPluginContributions<TArgs, TData, TError>;
14
+ }
15
+ }
16
+ /**
17
+ * Plugin that attaches `useResourceV2Agent` and `useResourceV2Ref` as methods on resources via `augmentResource`.
18
+ * Standalone imports from `@/query-v2/react/` are available as an alternative without requiring this plugin.
19
+ */
20
+ export declare class ReactHooksPlugin implements IPlugin {
21
+ readonly name: "ReactHooksPlugin";
22
+ private _context;
23
+ install(context: IPluginContext): void;
24
+ augmentResource<TArgs, TData, TError>(res: ResourceV2<TArgs, TData, TError>, _options: IResourceV2Options<TArgs, TData, TError>): Record<string, unknown>;
25
+ }
@@ -0,0 +1,19 @@
1
+ import { useResourceV2Agent } from "../../query-v2/react/useResourceV2Agent";
2
+ import { useResourceV2Ref } from "../../query-v2/react/useResourceV2Ref";
3
+ /**
4
+ * Plugin that attaches `useResourceV2Agent` and `useResourceV2Ref` as methods on resources via `augmentResource`.
5
+ * Standalone imports from `@/query-v2/react/` are available as an alternative without requiring this plugin.
6
+ */
7
+ export class ReactHooksPlugin {
8
+ name = "ReactHooksPlugin";
9
+ _context = null;
10
+ install(context) {
11
+ this._context = context;
12
+ }
13
+ augmentResource(res, _options) {
14
+ return {
15
+ useResourceV2Agent: (args) => useResourceV2Agent(res, args),
16
+ useResourceV2Ref: (args) => useResourceV2Ref(res, args),
17
+ };
18
+ }
19
+ }
@@ -0,0 +1 @@
1
+ export type { IPlugin, IPluginContext, ExtractPluginContributions, PluginAugmentations, } from "../../query-v2/types/plugin.types";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { ResourceV2, type ResourceV2Config } from "../../../query-v2/core/resource/ResourceV2";
2
+ export declare function controllableQueryFn<TArgs = unknown, TData = unknown>(): {
3
+ fn: import("vitest").Mock<(args: TArgs, { abortSignal }: {
4
+ abortSignal: AbortSignal;
5
+ }) => Promise<TData>>;
6
+ calls: {
7
+ args: TArgs;
8
+ resolve: (data: TData) => void;
9
+ reject: (error: Error) => void;
10
+ }[];
11
+ };
12
+ export declare function createTestResource<TArgs = unknown, TData = unknown, TError = Error>(config: Partial<ResourceV2Config<TArgs, TData, TError>> & Pick<ResourceV2Config<TArgs, TData, TError>, "queryFn">): ResourceV2<TArgs, TData, TError>;
@@ -0,0 +1,33 @@
1
+ import { vi } from "vitest";
2
+ import { ResourceV2 } from "../../../query-v2/core/resource/ResourceV2";
3
+ export function controllableQueryFn() {
4
+ const calls = [];
5
+ const fn = vi.fn((args, { abortSignal }) => {
6
+ return new Promise((resolve, reject) => {
7
+ let settled = false;
8
+ const wrappedResolve = (data) => {
9
+ if (!settled) {
10
+ settled = true;
11
+ resolve(data);
12
+ }
13
+ };
14
+ const wrappedReject = (error) => {
15
+ if (!settled) {
16
+ settled = true;
17
+ reject(error);
18
+ }
19
+ };
20
+ calls.push({ args, resolve: wrappedResolve, reject: wrappedReject });
21
+ abortSignal.addEventListener("abort", () => {
22
+ wrappedReject(new DOMException("Aborted", "AbortError"));
23
+ });
24
+ });
25
+ });
26
+ return { fn, calls };
27
+ }
28
+ export function createTestResource(config) {
29
+ return new ResourceV2({
30
+ key: "test-resource",
31
+ ...config,
32
+ });
33
+ }
@@ -0,0 +1,2 @@
1
+ export { useResourceV2Agent } from "./useResourceV2Agent";
2
+ export { useResourceV2Ref } from "./useResourceV2Ref";
@@ -0,0 +1,2 @@
1
+ export { useResourceV2Agent } from "./useResourceV2Agent";
2
+ export { useResourceV2Ref } from "./useResourceV2Ref";
@@ -0,0 +1,12 @@
1
+ import type { ResourceV2 } from "../../query-v2/core/resource/ResourceV2";
2
+ import { type SKIP_TOKEN } from "../../query-v2/lib/SKIP_TOKEN";
3
+ import type { IResourceV2AgentState } from "../../query-v2/types/agent.types";
4
+ /**
5
+ * React hook that creates a ResourceV2Agent and returns its reactive state (SWR).
6
+ *
7
+ * @param resource - The resource to observe.
8
+ * @param args - Query arguments, or `SKIP_TOKEN` to skip the query.
9
+ * @returns Reactive agent state with `data`, `error`, status flags, etc.
10
+ * @see docs/query-v2/README.md
11
+ */
12
+ export declare function useResourceV2Agent<TArgs, TData, TError>(resource: ResourceV2<TArgs, TData, TError>, args: TArgs | SKIP_TOKEN): IResourceV2AgentState<TArgs, TData, TError>;
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { useConstant } from "../../common/react/useConstant";
3
+ import { SKIP } from "../../query-v2/lib/SKIP_TOKEN";
4
+ import { useSignal } from "../../signals/react/useSignal";
5
+ /**
6
+ * React hook that creates a ResourceV2Agent and returns its reactive state (SWR).
7
+ *
8
+ * @param resource - The resource to observe.
9
+ * @param args - Query arguments, or `SKIP_TOKEN` to skip the query.
10
+ * @returns Reactive agent state with `data`, `error`, status flags, etc.
11
+ * @see docs/query-v2/README.md
12
+ */
13
+ export function useResourceV2Agent(resource, args) {
14
+ const prevArgsRef = React.useRef(SKIP);
15
+ const agent = useConstant(() => {
16
+ const agent = resource.createAgent();
17
+ if (args !== SKIP) {
18
+ agent.start(args);
19
+ }
20
+ return agent;
21
+ });
22
+ if (!compareArgs(args, prevArgsRef.current, resource)) {
23
+ prevArgsRef.current = args;
24
+ if (args !== SKIP) {
25
+ agent.start(args);
26
+ }
27
+ }
28
+ return useSignal(agent.state$);
29
+ }
30
+ function compareArgs(args, prevArgs, resource) {
31
+ if (args === SKIP && prevArgs === SKIP)
32
+ return true;
33
+ if (args === SKIP || prevArgs === SKIP)
34
+ return false;
35
+ return resource.compareArgs(args, prevArgs);
36
+ }
@@ -0,0 +1,12 @@
1
+ import type { ResourceV2 } from "../../query-v2/core/resource/ResourceV2";
2
+ import { type SKIP_TOKEN } from "../../query-v2/lib/SKIP_TOKEN";
3
+ import type { IResourceV2Ref } from "../../query-v2/types/agent.types";
4
+ /**
5
+ * React hook that provides an imperative ref handle for a cache entry (lock, invalidate, patch, create).
6
+ *
7
+ * @param resource - The resource to access.
8
+ * @param args - Query arguments, or `SKIP_TOKEN` to return a no-op ref.
9
+ * @returns Imperative ref with `has`, `lock`, `invalidate`, `createPatch`, `create`.
10
+ * @see docs/query-v2/optimistic-updates.md
11
+ */
12
+ export declare function useResourceV2Ref<TArgs, TData, TError>(resource: ResourceV2<TArgs, TData, TError>, args: TArgs | SKIP_TOKEN): IResourceV2Ref<TArgs, TData, TError>;
@@ -0,0 +1,57 @@
1
+ import React from "react";
2
+ import { shallowEqual } from "../../common/utils/shallowEqual";
3
+ import { SKIP } from "../../query-v2/lib/SKIP_TOKEN";
4
+ /**
5
+ * React hook that provides an imperative ref handle for a cache entry (lock, invalidate, patch, create).
6
+ *
7
+ * @param resource - The resource to access.
8
+ * @param args - Query arguments, or `SKIP_TOKEN` to return a no-op ref.
9
+ * @returns Imperative ref with `has`, `lock`, `invalidate`, `createPatch`, `create`.
10
+ * @see docs/query-v2/optimistic-updates.md
11
+ */
12
+ export function useResourceV2Ref(resource, args) {
13
+ const stableArgsRef = React.useRef(args);
14
+ if (!shallowEqual(stableArgsRef.current, args)) {
15
+ stableArgsRef.current = args;
16
+ }
17
+ return React.useMemo(() => {
18
+ if (stableArgsRef.current === SKIP) {
19
+ return createSkippedRef();
20
+ }
21
+ return createRefHandle(resource, stableArgsRef.current);
22
+ }, [stableArgsRef.current]);
23
+ }
24
+ function createRefHandle(resource, args) {
25
+ return {
26
+ get has() {
27
+ return resource.hasEntry(args);
28
+ },
29
+ lock() {
30
+ return resource.lockEntry(args);
31
+ },
32
+ invalidate() {
33
+ resource.invalidate(args);
34
+ },
35
+ createPatch(patchFn) {
36
+ return resource.createEntryPatch(args, patchFn);
37
+ },
38
+ create(data) {
39
+ resource.populateEntry(args, data);
40
+ },
41
+ };
42
+ }
43
+ function createSkippedRef() {
44
+ return {
45
+ get has() {
46
+ return false;
47
+ },
48
+ lock() {
49
+ return { unlock: () => { } };
50
+ },
51
+ invalidate() { },
52
+ createPatch() {
53
+ return null;
54
+ },
55
+ create() { },
56
+ };
57
+ }
@@ -0,0 +1,13 @@
1
+ import type { ResourceV2 } from "../../query-v2/core/resource/ResourceV2";
2
+ import type { TApiSnapshot } from "../../query-v2/types/snapshot.types";
3
+ export declare const CURRENT_SNAPSHOT_VERSION = 1;
4
+ /**
5
+ * Capture snapshot from all registered resources.
6
+ * Only `MachineSuccess` entries are included (per design §5).
7
+ */
8
+ export declare function getSnapshot(resources: Map<string, ResourceV2<any, any, any>>, keyPrefix: string | null, keyStrategy: "serialize" | "compare"): TApiSnapshot;
9
+ /**
10
+ * Hydrate resources from a snapshot.
11
+ * Validates version and keyPrefix before applying.
12
+ */
13
+ export declare function hydrateSnapshot(snapshot: TApiSnapshot, resources: Map<string, ResourceV2<any, any, any>>, apiKeyPrefix: string | null, maxSnapshotDataAge: number): void;
@@ -0,0 +1,76 @@
1
+ import { Machine } from "../../query-v2/core/machines/Machine";
2
+ import { MachineSuccess } from "../../query-v2/core/machines/MachineSuccess";
3
+ export const CURRENT_SNAPSHOT_VERSION = 1;
4
+ /**
5
+ * Capture snapshot from all registered resources.
6
+ * Only `MachineSuccess` entries are included (per design §5).
7
+ */
8
+ export function getSnapshot(resources, keyPrefix, keyStrategy) {
9
+ if (keyStrategy === "compare") {
10
+ throw new Error('getSnapshot() is not supported with keyStrategy "compare". ' +
11
+ 'SSR snapshots require keyStrategy "serialize" so that cache keys are serializable strings.');
12
+ }
13
+ const resourceSnapshots = {};
14
+ for (const [resourceKey, resource] of resources) {
15
+ const entries = {};
16
+ let hasEntries = false;
17
+ for (const [key, cacheEntry] of resource.cacheEntries()) {
18
+ const machine = cacheEntry.peek();
19
+ if (!(machine instanceof MachineSuccess))
20
+ continue;
21
+ const state = machine.state;
22
+ entries[key] = {
23
+ status: "success",
24
+ args: state.args,
25
+ data: state.data,
26
+ updatedAt: state.updatedAt,
27
+ };
28
+ hasEntries = true;
29
+ }
30
+ if (hasEntries) {
31
+ resourceSnapshots[resourceKey] = { entries };
32
+ }
33
+ }
34
+ return {
35
+ version: CURRENT_SNAPSHOT_VERSION,
36
+ keyPrefix,
37
+ resources: resourceSnapshots,
38
+ };
39
+ }
40
+ /**
41
+ * Hydrate resources from a snapshot.
42
+ * Validates version and keyPrefix before applying.
43
+ */
44
+ export function hydrateSnapshot(snapshot, resources, apiKeyPrefix, maxSnapshotDataAge) {
45
+ // Fatal: snapshot format incompatibility — the serialization schema has changed between versions.
46
+ // This cannot be recovered from; the snapshot must be regenerated.
47
+ if (snapshot.version !== CURRENT_SNAPSHOT_VERSION) {
48
+ throw new Error(`Snapshot version mismatch: expected ${CURRENT_SNAPSHOT_VERSION}, got ${snapshot.version}. ` +
49
+ `The snapshot format is incompatible with the current version of query-v2.`);
50
+ }
51
+ // Fatal: wrong API instance — the snapshot was created by a different keyPrefix configuration.
52
+ // Applying it would pollute the cache with data from an unrelated API.
53
+ if (snapshot.keyPrefix !== apiKeyPrefix) {
54
+ throw new Error(`Snapshot keyPrefix mismatch: expected "${apiKeyPrefix}", got "${snapshot.keyPrefix}". ` +
55
+ `Ensure the snapshot was created by the same API instance configuration.`);
56
+ }
57
+ const now = Date.now();
58
+ for (const [resourceKey, resourceSnapshot] of Object.entries(snapshot.resources)) {
59
+ const resource = resources.get(resourceKey);
60
+ if (!resource) {
61
+ // Non-fatal: a resource may have been removed between versions — skip gracefully.
62
+ console.warn(`[rx-toolkit] hydrateSnapshot: unknown resource key "${resourceKey}", skipping.`);
63
+ continue;
64
+ }
65
+ for (const [, slice] of Object.entries(resourceSnapshot.entries)) {
66
+ const machine = Machine.fromSnapshot(slice);
67
+ // Reconstruct args from the slice
68
+ const args = slice.args;
69
+ resource.hydrateEntry(args, machine);
70
+ // S3: stale entries trigger invalidation
71
+ if (now - slice.updatedAt > maxSnapshotDataAge) {
72
+ resource.invalidate(args);
73
+ }
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,54 @@
1
+ import type { TMachineStatus, TPatchFn } from "./machine.types";
2
+ /** SKIP_TOKEN type alias for agent args */
3
+ type SKIP_TOKEN = typeof import("../lib/SKIP_TOKEN").SKIP;
4
+ /** Agent — observer with stale-while-revalidate */
5
+ export interface IResourceV2Agent<TArgs, TData, TError = Error> {
6
+ /** Reactive state (computed signal) */
7
+ readonly state$: () => IResourceV2AgentState<TArgs, TData, TError>;
8
+ /** Start query with new args (returns promise) */
9
+ start(args: TArgs | SKIP_TOKEN): Promise<void>;
10
+ /** Compare previous and new args */
11
+ compareArgs(a: TArgs, b: TArgs): boolean;
12
+ }
13
+ /** Agent's computed state shape */
14
+ export interface IResourceV2AgentState<TArgs, TData, TError = Error> {
15
+ /** Current machine status */
16
+ status: TMachineStatus;
17
+ /** Current data (may be stale during loading) */
18
+ data: TData | null;
19
+ /** Current error */
20
+ error: TError | null;
21
+ /** Current args (fresh) */
22
+ args: TArgs | null;
23
+ /** Loading indicator */
24
+ isLoading: boolean;
25
+ /** True only on first load (no previous data) */
26
+ isInitialLoading: boolean;
27
+ /** True when refreshing existing data */
28
+ isRefreshing: boolean;
29
+ /** True when data is available */
30
+ isSuccess: boolean;
31
+ /** True when in error state */
32
+ isError: boolean;
33
+ /** Error from a failed background refresh (stale data preserved) */
34
+ refreshError: TError | null;
35
+ }
36
+ /** Ref — imperative access to a specific cache entry by args */
37
+ export interface IResourceV2Ref<_TArgs, TData, _TError = Error> {
38
+ /** Check if cache entry exists */
39
+ readonly has: boolean;
40
+ /** Lock cache entry (prevent eviction) */
41
+ lock(): {
42
+ unlock: () => void;
43
+ };
44
+ /** Invalidate (force re-fetch) */
45
+ invalidate(): void;
46
+ /** Create optimistic patch */
47
+ createPatch(patchFn: TPatchFn<TData>): {
48
+ commit: () => void;
49
+ abort: () => void;
50
+ } | null;
51
+ /** Pre-populate cache with data */
52
+ create(data: TData): void;
53
+ }
54
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import type { IPlugin, PluginAugmentations } from "./plugin.types";
2
+ import type { IResourceV2, IResourceV2Options } from "./resource.types";
3
+ import type { TCompareArgsFn, TSerializeArgsFn } from "./shared.types";
4
+ import type { TApiSnapshot } from "./snapshot.types";
5
+ /** Options for createApi factory */
6
+ export interface ICreateApiOptions<TPlugins extends IPlugin[] = []> {
7
+ keyPrefix?: string | null;
8
+ keyStrategy?: "serialize" | "compare";
9
+ serializeArgs?: TSerializeArgsFn;
10
+ compareArg?: TCompareArgsFn;
11
+ initialSnapshot?: TApiSnapshot | null;
12
+ cacheLifetime?: number;
13
+ plugins?: TPlugins;
14
+ maxSnapshotDataAge?: number;
15
+ doCacheArgs?: boolean;
16
+ }
17
+ /** API instance returned by createApi */
18
+ export interface IApi<TPlugins extends IPlugin[] = []> {
19
+ createResource<TArgs, TData, TError = Error>(options: IResourceV2Options<TArgs, TData, TError>): IResourceV2<TArgs, TData, TError> & PluginAugmentations<TPlugins, TArgs, TData, TError>;
20
+ resetAll(): void;
21
+ getSnapshot(): TApiSnapshot;
22
+ }
@@ -0,0 +1 @@
1
+ export {};